diff --git a/.gitignore b/.gitignore index c95816f..cf3b56b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ *.terraform.lock.hcl .terraform.lock.hcl *src/.env +app-logs.zip +LogFiles +deploy.log # .tfstate files *.tfstate diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index eab7d02..cf2502c 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -347,7 +347,7 @@ terraform apply
- Total views -

Refresh Date: 2025-11-24

+ Total views +

Refresh Date: 2025-11-25

diff --git a/src/DATA_PIPELINE.md b/src/DATA_PIPELINE.md index 69a2ff8..54b5805 100644 --- a/src/DATA_PIPELINE.md +++ b/src/DATA_PIPELINE.md @@ -277,7 +277,7 @@ az search index show-statistics \
- Total views -

Refresh Date: 2025-11-24

+ Total views +

Refresh Date: 2025-11-25

diff --git a/src/Dockerfile b/src/Dockerfile new file mode 100644 index 0000000..3d1f25e --- /dev/null +++ b/src/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Copy requirements first for better caching +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Expose port +EXPOSE 8000 + +# Run the application +CMD ["uvicorn", "chat_app:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/src/app/__init__.py b/src/app/__init__.py new file mode 100644 index 0000000..3ddc6fa --- /dev/null +++ b/src/app/__init__.py @@ -0,0 +1 @@ +# Zava AI Shopping Assistant Application diff --git a/src/app/static/.gitkeep b/src/app/static/.gitkeep new file mode 100644 index 0000000..b794710 --- /dev/null +++ b/src/app/static/.gitkeep @@ -0,0 +1,2 @@ +# Placeholder file to preserve the static directory in git +# Add static assets like CSS, JavaScript, or images here as needed diff --git a/src/app/templates/index.html b/src/app/templates/index.html new file mode 100644 index 0000000..ee9de39 --- /dev/null +++ b/src/app/templates/index.html @@ -0,0 +1,306 @@ + + + + + + Zava AI Shopping Assistant + + + +
+
+

🏠 Zava AI Shopping Assistant

+

Your DIY Project Helper

+
+ +
+
+
+ Welcome to Zava! I'm here to help you with your DIY projects. Ask me about our products, get recommendations, or find store locations. +
+
+
+ +
+ + +
+
+ + + + diff --git a/src/app/tools/__init__.py b/src/app/tools/__init__.py new file mode 100644 index 0000000..c02a9b1 --- /dev/null +++ b/src/app/tools/__init__.py @@ -0,0 +1 @@ +# Agent tools module diff --git a/src/app/tools/singleAgentExample.py b/src/app/tools/singleAgentExample.py new file mode 100644 index 0000000..8c392d7 --- /dev/null +++ b/src/app/tools/singleAgentExample.py @@ -0,0 +1,114 @@ +import os +import time +from azure.ai.inference import ChatCompletionsClient +from azure.core.credentials import AzureKeyCredential +from dotenv import load_dotenv + +# Load environment variables (Azure endpoint, deployment, keys, etc.) +load_dotenv() + +# Retrieve credentials from .env file or environment +endpoint = os.getenv("gpt_endpoint") +api_key = os.getenv("gpt_api_key") +deployment = os.getenv("gpt_deployment") + +# Global client instance +client = None + +def get_client(): + """Lazily initialize and return the Azure AI Foundry client""" + global client + if client is None: + if not all([endpoint, api_key]): + raise ValueError( + f"Missing required environment variables. " + f"endpoint={bool(endpoint)}, " + f"api_key={bool(api_key)}" + ) + # Use .services.ai.azure.com/models endpoint for Azure AI Foundry + # Convert cognitiveservices to services.ai if needed + foundry_endpoint = endpoint.replace('.cognitiveservices.', '.services.ai.') + + # Ensure it has .ai. in the domain + if '.services.azure.com' in foundry_endpoint and '.services.ai.azure.com' not in foundry_endpoint: + foundry_endpoint = foundry_endpoint.replace('.services.azure.com', '.services.ai.azure.com') + + # Add /models path if not present + if not foundry_endpoint.endswith('/models'): + foundry_endpoint = f"{foundry_endpoint.rstrip('/')}/models" + + client = ChatCompletionsClient( + endpoint=foundry_endpoint, + credential=AzureKeyCredential(api_key) + ) + return client + +def generate_response(text_input): + start_time = time.time() + """ + Input: + text_input (str): The user's chat input. + + Output: + response (str): A Markdown-formatted response from the agent. + """ + + # Get initialized client + client = get_client() + + # Prepare the messages for Azure AI Foundry + messages = [ + { + "role": "system", + "content": """You are an AI assistant for Zava, a leading home improvement and DIY products company. + +Your capabilities include: +- Providing expert advice on DIY projects, home improvement, repairs, and renovations +- Recommending products from Zava's extensive catalog (tools, materials, paint, hardware, etc.) +- Offering step-by-step guidance for various home projects +- Answering general questions about home maintenance, safety, and best practices +- Discussing design ideas, project planning, and cost estimation +- Providing information about Zava stores and services + +Product Guidelines: +- For paint colors, we feature: blue, green, and white (but can discuss other options available) +- Recommend appropriate tools and materials for each project +- Suggest safety equipment when relevant + +Store Information: +- Zava has locations nationwide +- For specific store availability, direct customers to our Miami flagship store +- Mention online ordering options when appropriate + +Tone & Style: +- Be friendly, helpful, and encouraging +- Provide detailed, practical advice +- Ask clarifying questions when needed +- Be enthusiastic about DIY projects while emphasizing safety +- Feel free to engage in broader conversations about home improvement topics + +You can discuss a wide range of topics related to home improvement, construction, design, and general DIY advice. Don't limit yourself to just product recommendations - provide comprehensive assistance! + """ + }, + { + "role": "user", + "content": text_input + } + ] + + # Call Azure AI Foundry chat API + response = client.complete( + model=deployment, + messages=messages, + max_tokens=10000, + temperature=1.0, + top_p=1.0, + frequency_penalty=0, + presence_penalty=0 + ) + + end_sum = time.time() + print(f"generate_response Execution Time: {end_sum - start_time} seconds") + + # Return response content + return response.choices[0].message.content diff --git a/src/chat_app.py b/src/chat_app.py new file mode 100644 index 0000000..d81e431 --- /dev/null +++ b/src/chat_app.py @@ -0,0 +1,76 @@ +import os +import logging +from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request +from fastapi.responses import HTMLResponse +from fastapi.templating import Jinja2Templates +from dotenv import load_dotenv +import orjson +from app.tools.singleAgentExample import generate_response + +# Load environment variables +load_dotenv() + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +# Initialize FastAPI app +app = FastAPI(title="Zava AI Shopping Assistant") + +# Mount templates +templates = Jinja2Templates(directory="app/templates") + +# Fast JSON serialization +def fast_json_dumps(obj): + return orjson.dumps(obj).decode("utf-8") + +@app.get("/", response_class=HTMLResponse) +async def read_root(request: Request): + """Serve the main chat interface""" + return templates.TemplateResponse("index.html", {"request": request}) + +@app.websocket("/ws") +async def websocket_endpoint(websocket: WebSocket): + """WebSocket endpoint for real-time chat""" + await websocket.accept() + logger.info("WebSocket connection established") + + # Initialize persistent cart for the session + persistent_cart = [] + + try: + while True: + # Receive message from client + data = await websocket.receive_text() + user_message = data.strip() + + if not user_message: + continue + + logger.info(f"Received message: {user_message}") + + # Single-agent example + try: + response = generate_response(user_message) + await websocket.send_text(fast_json_dumps({"answer": response, "agent": "single", "cart": persistent_cart})) + logger.info("Response sent successfully") + except Exception as e: + logger.error("Error during single-agent response generation", exc_info=True) + await websocket.send_text(fast_json_dumps({"answer": "Error during single-agent response generation", "error": str(e), "cart": persistent_cart})) + + except WebSocketDisconnect: + logger.info("WebSocket connection closed") + except Exception as e: + logger.error(f"WebSocket error: {e}", exc_info=True) + +@app.get("/health") +async def health_check(): + """Health check endpoint""" + return {"status": "healthy", "service": "Zava AI Shopping Assistant"} + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/src/requirements.txt b/src/requirements.txt index 8f79d8d..b794980 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -4,5 +4,10 @@ pandas>=2.2.2 azure-cosmos==4.9.0 azure-identity==1.19.0 azure-search-documents==11.6.0 -openai==1.54.5 azure-ai-inference==1.0.0b6 +fastapi==0.115.0 +uvicorn[standard]==0.32.0 +websockets==13.1 +jinja2==3.1.4 +python-multipart==0.0.12 +orjson==3.10.7 diff --git a/terraform-infrastructure/README.md b/terraform-infrastructure/README.md index 39c766e..99a27c3 100644 --- a/terraform-infrastructure/README.md +++ b/terraform-infrastructure/README.md @@ -119,7 +119,7 @@ graph TD;
- Total views -

Refresh Date: 2025-11-24

+ Total views +

Refresh Date: 2025-11-25

diff --git a/terraform-infrastructure/main.tf b/terraform-infrastructure/main.tf index 652b2f0..cb14493 100644 --- a/terraform-infrastructure/main.tf +++ b/terraform-infrastructure/main.tf @@ -163,9 +163,9 @@ resource "azurerm_container_registry_webhook" "webhook" { registry_name = azurerm_container_registry.acr.name location = var.location - service_uri = "https://${local.web_app_name}.scm.azurewebsites.net/docker/hook" + service_uri = "https://${local.web_app_name}.scm.azurewebsites.net/api/registry/webhook" status = "enabled" - scope = "${local.suffix}/techworkshopl300/zava:latest" + scope = "zava-chat-app:latest" actions = ["push"] custom_headers = { @@ -191,20 +191,14 @@ resource "azurerm_linux_web_app" "app" { https_only = true site_config { - application_stack { - docker_image_name = "${local.registry_name}.azurecr.io/${local.suffix}/techworkshopl300/zava:latest" - docker_registry_url = "https://${local.registry_name}.azurecr.io" - } - http2_enabled = true + always_on = false + http2_enabled = true minimum_tls_version = "1.2" } app_settings = { WEBSITES_ENABLE_APP_SERVICE_STORAGE = "false" - DOCKER_REGISTRY_SERVER_URL = "https://${local.registry_name}.azurecr.io" - DOCKER_REGISTRY_SERVER_USERNAME = azurerm_container_registry.acr.name - DOCKER_REGISTRY_SERVER_PASSWORD = azurerm_container_registry.acr.admin_password - APPINSIGHTS_INSTRUMENTATIONKEY = azurerm_application_insights.appinsights.instrumentation_key + DOCKER_ENABLE_CI = "true" } depends_on = [azurerm_container_registry.acr] @@ -678,6 +672,12 @@ AZURE_OPENAI_ENDPOINT=$aiFoundryEndpoint AZURE_OPENAI_API_KEY=$aiFoundryKey AZURE_OPENAI_API_VERSION=2024-02-01 +# GPT Model Configuration (for single-agent chat) +gpt_endpoint=$aiFoundryEndpoint +gpt_deployment=gpt-4o-mini +gpt_api_key=$aiFoundryKey +gpt_api_version=2024-02-01 + # Azure Cosmos DB Configuration COSMOS_DB_ENDPOINT=${azurerm_cosmosdb_account.cosmos.endpoint} COSMOS_DB_KEY=$cosmosKey @@ -717,6 +717,12 @@ AZURE_OPENAI_ENDPOINT=$aiFoundryEndpoint AZURE_OPENAI_API_KEY=$aiFoundryKey AZURE_OPENAI_API_VERSION=2024-02-01 +# GPT Model Configuration (for single-agent chat) +gpt_endpoint=$aiFoundryEndpoint +gpt_deployment=gpt-4o-mini +gpt_api_key=$aiFoundryKey +gpt_api_version=2024-02-01 + # Azure Cosmos DB Configuration COSMOS_DB_ENDPOINT=${azurerm_cosmosdb_account.cosmos.endpoint} COSMOS_DB_KEY=$cosmosKey @@ -881,3 +887,237 @@ resource "null_resource" "data_pipeline" { env_file_id = null_resource.create_env_file[0].id } } + +# Single-Agent Application Verification - Verifies the chat application is ready +resource "null_resource" "verify_single_agent_app" { + count = var.enable_data_pipeline ? 1 : 0 + + depends_on = [ + null_resource.data_pipeline + ] + + provisioner "local-exec" { + command = <<-EOT + Write-Host "" + Write-Host "=== Verifying Single-Agent Chat Application ===" + Write-Host "" + + # Check if application files exist + $appFiles = @( + "../src/chat_app.py", + "../src/app/tools/singleAgentExample.py", + "../src/app/templates/index.html" + ) + + $allFilesExist = $true + foreach ($file in $appFiles) { + if (Test-Path $file) { + Write-Host "✓ Found: $file" + } else { + Write-Host "✗ Missing: $file" + $allFilesExist = $false + } + } + + Write-Host "" + if ($allFilesExist) { + Write-Host "✓ All single-agent application files are in place!" + Write-Host "" + Write-Host "Application structure:" + Write-Host " 📄 chat_app.py - FastAPI web application" + Write-Host " 🤖 app/tools/singleAgentExample.py - AI agent logic" + Write-Host " 🎨 app/templates/index.html - Chat interface" + Write-Host "" + Write-Host "🚀 To start the chat application:" + Write-Host " 1. cd ..\src" + Write-Host " 2. venv\Scripts\Activate.ps1" + Write-Host " 3. uvicorn chat_app:app --host 0.0.0.0 --port 8000" + Write-Host " 4. Open http://127.0.0.1:8000 in your browser" + Write-Host "" + } else { + Write-Host "⚠️ Some application files are missing!" + Write-Host "Please ensure all files are committed to the repository." + } + EOT + interpreter = ["PowerShell", "-Command"] + working_dir = path.module + } + + triggers = { + data_pipeline_id = null_resource.data_pipeline[0].id + env_file_id = null_resource.create_env_file[0].id + } +} + +# Install Docker Desktop if not present and deploy chat app using containers +resource "null_resource" "deploy_chat_app" { + count = var.enable_data_pipeline ? 1 : 0 + + depends_on = [ + null_resource.verify_single_agent_app, + null_resource.data_pipeline, + azurerm_linux_web_app.app + ] + + provisioner "local-exec" { + command = <<-EOT + Write-Host "" + Write-Host "=== Deploying Chat Application to Azure Web App ===" + Write-Host "" + + # Build container directly in ACR (no local Docker needed) + Write-Host "Building chat application container in Azure Container Registry..." + Write-Host "This includes the Azure AI Foundry SDK with corrected endpoint configuration" + Write-Host "Build time: approximately 2-3 minutes..." + Write-Host "" + + # Set UTF-8 encoding to prevent Azure CLI Unicode errors + [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + $env:PYTHONIOENCODING = "utf-8" + + # Calculate absolute path to src directory + $srcPath = Join-Path (Split-Path $PWD.Path -Parent) "src" + Write-Host "Source directory: $srcPath" + + # Verify source files exist + if (!(Test-Path "$srcPath\Dockerfile")) { + Write-Host "ERROR: Dockerfile not found at $srcPath\Dockerfile" + exit 1 + } + if (!(Test-Path "$srcPath\app\tools\singleAgentExample.py")) { + Write-Host "ERROR: singleAgentExample.py not found" + exit 1 + } + + Write-Host "✓ Source files verified" + Write-Host "" + Write-Host "Starting ACR cloud build..." + + # Build in ACR and capture output + $buildOutput = az acr build ` + --resource-group ${azurerm_resource_group.rg.name} ` + --registry ${local.registry_name} ` + --image zava-chat-app:latest ` + --file "$srcPath\Dockerfile" ` + "$srcPath" 2>&1 + + # Display selected output lines + $buildOutput | Select-String -Pattern "Successfully|Step|digest:|Run ID" | ForEach-Object { + Write-Host $_.Line + } + + # Check build result + if ($LASTEXITCODE -eq 0) { + Write-Host "" + Write-Host "✓ ACR build completed successfully" + } else { + Write-Host "" + Write-Host "⚠ Build may still be in progress. Check Azure Portal for status." + } + + Write-Host "" + Write-Host "Configuring Web App..." + + # Get AI Foundry endpoint (remains .cognitiveservices., code will convert it) + $aiFoundryEndpoint = az cognitiveservices account show ` + --resource-group ${azurerm_resource_group.rg.name} ` + --name ${local.ai_foundry_name} ` + --query "properties.endpoint" ` + --output tsv + + # Get Azure AI Foundry access key + $aiFoundryKey = az cognitiveservices account keys list ` + --resource-group ${azurerm_resource_group.rg.name} ` + --name ${local.ai_foundry_name} ` + --query "key1" ` + --output tsv + + # Configure environment variables for the app + Write-Host "Setting application environment variables..." + az webapp config appsettings set ` + --resource-group ${azurerm_resource_group.rg.name} ` + --name ${local.web_app_name} ` + --settings ` + WEBSITES_PORT=8000 ` + gpt_endpoint="$aiFoundryEndpoint" ` + gpt_deployment="gpt-4o-mini" ` + gpt_api_key="$aiFoundryKey" ` + gpt_api_version="2024-12-01-preview" | Out-Null + + if ($LASTEXITCODE -eq 0) { + Write-Host "✓ Environment variables configured" + } + + # Get ACR admin credentials + $acrUsername = az acr credential show ` + --name ${local.registry_name} ` + --query "username" ` + --output tsv + + $acrPassword = az acr credential show ` + --name ${local.registry_name} ` + --query "passwords[0].value" ` + --output tsv + + # Update container image with ACR credentials + Write-Host "Configuring container deployment..." + az webapp config container set ` + --resource-group ${azurerm_resource_group.rg.name} ` + --name ${local.web_app_name} ` + --docker-custom-image-name ${local.registry_name}.azurecr.io/zava-chat-app:latest ` + --docker-registry-server-url https://${local.registry_name}.azurecr.io ` + --docker-registry-server-user "$acrUsername" ` + --docker-registry-server-password "$acrPassword" ` + --enable-app-service-storage false | Out-Null + + if ($LASTEXITCODE -eq 0) { + Write-Host "✓ Container configuration updated" + } + + # Restart the web app + Write-Host "" + Write-Host "Restarting Web App to pull latest container..." + az webapp restart ` + --resource-group ${azurerm_resource_group.rg.name} ` + --name ${local.web_app_name} | Out-Null + + Write-Host "✓ Web App restarted" + Write-Host "" + Write-Host "Waiting for container to initialize (30 seconds)..." + Start-Sleep -Seconds 30 + + Write-Host "" + Write-Host "============================================================================" + Write-Host "" + Write-Host " 🎉 ZAVA AI SHOPPING ASSISTANT - DEPLOYED TO AZURE" + Write-Host "" + Write-Host " 🌐 Web App URL: https://${local.web_app_name}.azurewebsites.net" + Write-Host " ❤️ Health Check: https://${local.web_app_name}.azurewebsites.net/health" + Write-Host "" + Write-Host " ✓ Container: ${local.registry_name}.azurecr.io/zava-chat-app:latest" + Write-Host " ✓ SDK: azure-ai-inference (Azure AI Foundry)" + Write-Host " ✓ Model: gpt-4o-mini" + Write-Host " ✓ Endpoint: .services.ai.azure.com/models (auto-converted)" + Write-Host "" + Write-Host " Note: App may take 1-2 minutes to fully initialize on first start" + Write-Host "" + Write-Host "============================================================================" + Write-Host "" + + # Try to open browser to the deployed app + try { + Start-Process "https://${local.web_app_name}.azurewebsites.net" + } catch { + Write-Host "Could not auto-open browser. Please visit the URL above manually." + } + EOT + interpreter = ["PowerShell", "-Command"] + working_dir = path.module + } + + triggers = { + verify_app_id = null_resource.verify_single_agent_app[0].id + always_run = timestamp() + } +} + diff --git a/terraform-infrastructure/outputs.tf b/terraform-infrastructure/outputs.tf index d83c350..44d66ec 100644 --- a/terraform-infrastructure/outputs.tf +++ b/terraform-infrastructure/outputs.tf @@ -76,3 +76,51 @@ output "env_file_location" { value = var.enable_ai_automation ? "../src/.env" : "Not created (AI automation disabled)" description = "Location of the generated .env file" } + +output "chat_application_url" { + value = "http://127.0.0.1:8000" + description = "URL to access the Zava AI Shopping Assistant chat application" +} + +output "chat_application_health" { + value = "http://127.0.0.1:8000/health" + description = "Health check endpoint for the chat application" +} + +output "application_instructions" { + value = <<-EOT + + ============================================================================ + ZAVA AI SHOPPING ASSISTANT - DEPLOYMENT COMPLETE + ============================================================================ + + AZURE WEB APP: + - App Name: ${azurerm_linux_web_app.app.name} + - URL: https://${azurerm_linux_web_app.app.default_hostname} + - Health Check: https://${azurerm_linux_web_app.app.default_hostname}/health + + LOCAL TESTING: + - URL: http://127.0.0.1:8000 + - To run locally: + cd ../src + venv\Scripts\Activate.ps1 + uvicorn chat_app:app --host 0.0.0.0 --port 8000 + + TEST PROMPTS: + - "What colors of paint do you have available?" + - "Tell me about lattices" + - "Where can I find your store?" + - "Do you have history books?" (tests scope limits) + + AZURE RESOURCES: + - Resource Group: ${azurerm_resource_group.rg.name} + - AI Foundry: ${local.ai_foundry_name} + - Cosmos DB: ${local.cosmos_account_name} + - Search Service: ${local.search_service_name} + - Container Registry: ${local.registry_name} + + ============================================================================ + + EOT + description = "Deployment summary and usage instructions" +} diff --git a/terraform-infrastructure/terraform.tfvars b/terraform-infrastructure/terraform.tfvars index 2cc89f4..1aeae09 100644 --- a/terraform-infrastructure/terraform.tfvars +++ b/terraform-infrastructure/terraform.tfvars @@ -1,4 +1,4 @@ -resource_group_name = "RG-AI-retailbrw5" +resource_group_name = "RG-AI-retailbrw6" location = "westus3" name_prefix = "zava" # user_principal_id is optional - defaults to current Azure CLI user (az login)