diff --git a/eks/agents/weather/src/agent_server_a2a.py b/eks/agents/weather/src/agent_server_a2a.py index 2874e01..404f78e 100644 --- a/eks/agents/weather/src/agent_server_a2a.py +++ b/eks/agents/weather/src/agent_server_a2a.py @@ -52,13 +52,15 @@ def a2a_agent(): logger.info("Agent instance created successfully") port = os.getenv("A2A_PORT", "9000") - hosting_http_url = os.getenv("A2A_URL", "0.0.0.0") + host_ip = os.getenv("A2A_HOST", "0.0.0.0") + hosting_url=os.getenv("A2A_URL", "http://weather-agent:9000") strands_a2a_agent = A2AServer( agent=strands_agent, + host=host_ip, port=int(port), - http_url=hosting_http_url + http_url=hosting_url ) logger.info("A2A Server wrapper created successfully") diff --git a/eks/infrastructure/terraform/eks.tf b/eks/infrastructure/terraform/eks.tf index 9ab52a5..6bf4b59 100644 --- a/eks/infrastructure/terraform/eks.tf +++ b/eks/infrastructure/terraform/eks.tf @@ -56,7 +56,12 @@ module "eks_blueprints_addons" { } # Add-ons - enable_cert_manager = true + enable_cert_manager = true + enable_ingress_nginx = true + + ingress_nginx = { + values = [templatefile("${path.module}/ingress_nginx_values.yaml", {}),] + } tags = local.tags diff --git a/eks/infrastructure/terraform/ingress_nginx_values.yaml b/eks/infrastructure/terraform/ingress_nginx_values.yaml new file mode 100644 index 0000000..24e3649 --- /dev/null +++ b/eks/infrastructure/terraform/ingress_nginx_values.yaml @@ -0,0 +1,10 @@ +controller: + replicaCount: 2 + service: + type: LoadBalancer + annotations: + service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: "*" + service.beta.kubernetes.io/aws-load-balancer-type: "external" + service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "ip" + service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing + service.beta.kubernetes.io/aws-load-balancer-backend-protocol: tcp diff --git a/eks/manifests/helm/agent/templates/deployment.yaml b/eks/manifests/helm/agent/templates/deployment.yaml index 7c76414..70bb83d 100644 --- a/eks/manifests/helm/agent/templates/deployment.yaml +++ b/eks/manifests/helm/agent/templates/deployment.yaml @@ -47,6 +47,8 @@ spec: value: "{{ .Values.mcp.port }}" - name: A2A_PORT value: "{{ .Values.a2a.port }}" + - name: A2A_HOST + value: "{{ .Values.a2a.host }}" - name: A2A_URL value: "{{ .Values.a2a.http_url }}" - name: FASTAPI_PORT diff --git a/eks/manifests/helm/agent/values.yaml b/eks/manifests/helm/agent/values.yaml index 1d7f9e2..4c99fc1 100644 --- a/eks/manifests/helm/agent/values.yaml +++ b/eks/manifests/helm/agent/values.yaml @@ -74,6 +74,8 @@ fastapi: enabled: false a2a: port: 9000 + host: "0.0.0.0" + http_url: "http://weather-agent:9000" exposedPort: 9000 ingress: enabled: false diff --git a/eks/manifests/helm/ui/templates/ingress-fastapi.yaml b/eks/manifests/helm/ui/templates/ingress-fastapi.yaml index f6a05bc..3273e10 100644 --- a/eks/manifests/helm/ui/templates/ingress-fastapi.yaml +++ b/eks/manifests/helm/ui/templates/ingress-fastapi.yaml @@ -24,6 +24,17 @@ spec: {{- end }} {{- end }} rules: + {{- if .Values.ingress.defaultRule.enabled }} + - http: + paths: + - path: {{ .Values.ingress.defaultRule.path | default "/" }} + pathType: {{ .Values.ingress.defaultRule.pathType | default "Prefix" }} + backend: + service: + name: {{ include "ui.fullname" . }} + port: + name: fastapi + {{- end }} {{- range .Values.ingress.hosts }} - host: {{ .host | quote }} http: diff --git a/eks/manifests/helm/ui/values.yaml b/eks/manifests/helm/ui/values.yaml index 864cffe..5aeaa8e 100644 --- a/eks/manifests/helm/ui/values.yaml +++ b/eks/manifests/helm/ui/values.yaml @@ -89,6 +89,10 @@ ingress: annotations: {} # kubernetes.io/ingress.class: nginx # kubernetes.io/tls-acme: "true" + defaultRule: + enabled: false + path: "/" + pathType: "Prefix" hosts: - host: chart-example.local paths: diff --git a/eks/scripts/terraform-prep-env-travel-agent.sh b/eks/scripts/terraform-prep-env-travel-agent.sh index 9b292df..0a021e4 100755 --- a/eks/scripts/terraform-prep-env-travel-agent.sh +++ b/eks/scripts/terraform-prep-env-travel-agent.sh @@ -156,5 +156,7 @@ image: env: OAUTH_JWKS_URL: $OAUTH_JWKS_URL SESSION_STORE_BUCKET_NAME: $SESSION_STORE_BUCKET_NAME + DISABLE_AUTH: "1" + EOF diff --git a/eks/scripts/terraform-prep-env-weather-agent.sh b/eks/scripts/terraform-prep-env-weather-agent.sh index 0c1c6ac..cb92032 100755 --- a/eks/scripts/terraform-prep-env-weather-agent.sh +++ b/eks/scripts/terraform-prep-env-weather-agent.sh @@ -78,5 +78,6 @@ image: env: OAUTH_JWKS_URL: $OAUTH_JWKS_URL SESSION_STORE_BUCKET_NAME: $SESSION_STORE_BUCKET_NAME + DISABLE_AUTH: "1" EOF diff --git a/eks/scripts/terraform-prep-env-weather-ui.sh b/eks/scripts/terraform-prep-env-weather-ui.sh index fea9992..a3a6282 100755 --- a/eks/scripts/terraform-prep-env-weather-ui.sh +++ b/eks/scripts/terraform-prep-env-weather-ui.sh @@ -40,17 +40,26 @@ echo "OAUTH_LOGOUT_URL=$OAUTH_LOGOUT_URL" >> $UI_AGENT_DST_FILE_NAME echo "OAUTH_WELL_KNOWN_URL=$OAUTH_WELL_KNOWN_URL" >> $UI_AGENT_DST_FILE_NAME echo "OAUTH_JWKS_URL=$OAUTH_JWKS_URL" >> $UI_AGENT_DST_FILE_NAME - +BASE_URL=$(kubectl get svc -n ingress-nginx ingress-nginx-controller -o jsonpath={.status.loadBalancer.ingress[0].hostname}) ECR_REPO_AGENT_UI_URI=$(terraform -chdir="$TERRAFORM_DIRECTORY" output -json ecr_agent_ui_repository_url) cat < $UI_AGENT_HELM_VALUES image: repository: $ECR_REPO_AGENT_UI_URI -env: env: AGENT_UI_ENDPOINT_URL_1: "http://weather-agent.agents/prompt" AGENT_UI_ENDPOINT_URL_2: "http://travel-agent.agents/prompt" - BASE_PATH: "${IDE_URL:+/proxy/8000}" - BASE_URL: "${IDE_URL:-http://localhost:8000}" + BASE_PATH: "/" + BASE_URL: "http://${BASE_URL:-localhost:8000}" + AUTH_ENABLED: "false" +fastapi: + ingress: + enabled: true +ingress: + enabled: true + className: nginx + defaultRule: + enabled: true + EOF diff --git a/eks/ui/app.py b/eks/ui/app.py index f279a16..384f20a 100644 --- a/eks/ui/app.py +++ b/eks/ui/app.py @@ -6,9 +6,6 @@ import gradio as gr import httpx import oauth -import uuid -import asyncio -from typing import Dict from dotenv import load_dotenv load_dotenv() # take environment variables @@ -21,6 +18,7 @@ if BASE_PATH == "": BASE_PATH = "/" CHAT_PATH = os.getenv("CHAT_PATH", "/chat") +AUTH_ENABLED = os.getenv("AUTH_ENABLED", "true").lower() == "true" # the following urls if BASE_PATH is / then another / after BASE_PATH is not need it CHAT_UI_URL = f"{BASE_URL}{BASE_PATH}{'chat/' if BASE_PATH.endswith('/') else '/chat/'}" #important this url need to end with / @@ -39,21 +37,21 @@ print(f"LOGIN_URL:{LOGIN_URL}") print(f"LOGOUT_URL:{LOGOUT_URL}") print(f"OAUTH_CALLBACK_URI:{OAUTH_CALLBACK_URI}") +print(f"AUTH_ENABLED:{AUTH_ENABLED}") user_avatar = "https://cdn-icons-png.flaticon.com/512/149/149071.png" bot_avatar = "https://cdn-icons-png.flaticon.com/512/4712/4712042.png" -# Store for background tasks -background_tasks: Dict[str, Dict] = {} - fastapi_app = FastAPI() fastapi_app.add_middleware(SessionMiddleware, secret_key="secret") -oauth.add_oauth_routes( - fastapi_app, - OAUTH_CALLBACK_URI=OAUTH_CALLBACK_URI, - UI_URL=UI_URL + +if AUTH_ENABLED: + oauth.add_oauth_routes( + fastapi_app, + OAUTH_CALLBACK_URI=OAUTH_CALLBACK_URI, + UI_URL=UI_URL ) @fastapi_app.get("/") @@ -62,197 +60,50 @@ async def root(): return RedirectResponse(url=CHAT_UI_URL) def check_auth(req: Request): + if not AUTH_ENABLED: + return "anonymous" + if not "access_token" in req.session or not "username" in req.session: print(f"check_auth:: access_token not found or username not found, redirecting to {LOGIN_URL}") raise HTTPException(status_code=302, detail="Redirecting to login", headers={"Location": LOGIN_URL}) username = req.session["username"] - print(f"check_auth::auth found username: {username}") return username -# Background task processing functions -@fastapi_app.post("/start-chat") -async def start_chat_task(request: Request): - # Check authentication first - if not "access_token" in request.session or not "username" in request.session: - raise HTTPException(status_code=401, detail="Not authenticated") - - data = await request.json() - task_id = str(uuid.uuid4()) - - # Store task info - background_tasks[task_id] = { - "status": "processing", - "result": None, - "error": None - } - - # Start background task - asyncio.create_task(process_chat_background(task_id, data)) - - return {"task_id": task_id} - -@fastapi_app.get("/chat-status/{task_id}") -async def get_chat_status(task_id: str): - if task_id not in background_tasks: - return {"status": "not_found"} - return background_tasks[task_id] - -@fastapi_app.delete("/cleanup-old-tasks") -async def cleanup_old_tasks(): - """Clean up tasks older than 1 hour to prevent memory leaks""" - import time - current_time = time.time() - tasks_to_remove = [] - - for task_id, task_data in background_tasks.items(): - # If task has been around for more than 1 hour, remove it - # This is a simple cleanup - in production you'd want timestamps - if len(background_tasks) > 100: # Simple cleanup when too many tasks - tasks_to_remove.append(task_id) - - for task_id in tasks_to_remove[:50]: # Remove oldest 50 tasks - del background_tasks[task_id] - - return {"cleaned_up": len(tasks_to_remove)} +def chat(message, history, agent_mode, request: gr.Request): + username = request.username + token = request.request.session.get("access_token") if AUTH_ENABLED else None + print(f"username={username}, message={message}, agent_mode={agent_mode}") -async def process_chat_background(task_id: str, data: dict): try: - message = data["message"] - agent_mode = data["agent_mode"] - token = data["token"] - # Select endpoint based on agent mode - if agent_mode == "Single Agent(Weather)": - endpoint_url = AGENT_UI_ENDPOINT_URL_1 - print(f"Background task {task_id}: Using Single Agent(Weather) endpoint: {endpoint_url}") - else: # Multi-Agent(Travel) - endpoint_url = AGENT_UI_ENDPOINT_URL_2 - print(f"Background task {task_id}: Using Multi-Agent(Travel) endpoint: {endpoint_url}") + endpoint_url = AGENT_UI_ENDPOINT_URL_1 if agent_mode == "Single Agent(Weather)" else AGENT_UI_ENDPOINT_URL_2 + print(f"Using endpoint: {endpoint_url}") - async with httpx.AsyncClient(timeout=httpx.Timeout(600.0, connect=30.0)) as client: - agent_response = await client.post( - endpoint_url, - headers={"Authorization": f"Bearer {token}"}, - json={"text": message} - ) + headers = {"Authorization": f"Bearer {token}"} if token else {} - if agent_response.status_code == 401 or agent_response.status_code == 403: - background_tasks[task_id] = { - "status": "failed", - "result": None, - "error": f"Agent returned authorization error. Status code: {agent_response.status_code}" - } - return + with httpx.Client(timeout=httpx.Timeout(600.0, connect=30.0)) as client: + response = client.post(endpoint_url, headers=headers, json={"text": message}) - if agent_response.status_code != 200: - background_tasks[task_id] = { - "status": "failed", - "result": None, - "error": f"Failed to communicate with Agent. Status code: {agent_response.status_code}" - } - return + if response.status_code in [401, 403]: + return f"❌ Agent returned authorization error. Status code: {response.status_code}" + if response.status_code != 200: + return f"❌ Failed to communicate with Agent. Status code: {response.status_code}" - response_text = agent_response.json()['text'] - print(f"Background task {task_id} got response: {response_text[:100]}..." if len(response_text) > 100 else f"Background task {task_id} got response: {response_text}") - background_tasks[task_id] = { - "status": "completed", - "result": response_text, - "error": None - } - print(f"Background task {task_id} marked as completed") + return response.json()['text'] except httpx.TimeoutException: - background_tasks[task_id] = { - "status": "failed", - "result": None, - "error": "Request timed out. The agent is taking longer than expected to respond." - } + return "⏰ Request timed out. The agent is taking longer than expected to respond." except httpx.ConnectError: - background_tasks[task_id] = { - "status": "failed", - "result": None, - "error": "Failed to connect to the agent. Please check if the agent service is running." - } + return "❌ Failed to connect to the agent. Please check if the agent service is running." except Exception as e: - print(f"Error in background task {task_id}: {e}") - background_tasks[task_id] = { - "status": "failed", - "result": None, - "error": f"An error occurred while communicating with the agent: {str(e)}" - } - -async def chat(message, history, agent_mode, request: gr.Request): - username = request.username - token = request.request.session["access_token"] - print(f"username={username}, message={message}, agent_mode={agent_mode}") - - try: - # Start background task - use internal call instead of HTTP - task_id = str(uuid.uuid4()) - - # Store task info - background_tasks[task_id] = { - "status": "processing", - "result": None, - "error": None - } - - # Start background task directly - task_data = { - "message": message, - "agent_mode": agent_mode, - "token": token, - "username": username - } - asyncio.create_task(process_chat_background(task_id, task_data)) - - print(f"Started background task {task_id} for user {username}") - - # Poll for completion every 5 seconds (faster for better UX) - max_polls = 120 # 10 minutes max (5 sec intervals) - for i in range(max_polls): - await asyncio.sleep(5) - - # Check task status directly from memory - if task_id not in background_tasks: - return "Task not found. Please try again." - - task_status = background_tasks[task_id] - - if task_status["status"] == "completed": - print(f"Task {task_id} completed after {(i+1)*5} seconds") - result = task_status["result"] - # Clean up completed task - del background_tasks[task_id] - return result - elif task_status["status"] == "failed": - print(f"Task {task_id} failed after {(i+1)*5} seconds") - error = task_status["error"] - # Clean up failed task - del background_tasks[task_id] - return f"❌ Error: {error}" - - # Show progress in logs every 30 seconds (every 6 polls) - if (i + 1) % 6 == 0: - elapsed_minutes = ((i + 1) * 5) // 60 - elapsed_seconds = ((i + 1) * 5) % 60 - print(f"Task {task_id} still processing... ({elapsed_minutes}m {elapsed_seconds}s elapsed)") - - # Clean up timed out task - if task_id in background_tasks: - del background_tasks[task_id] - return "⏰ Request timed out after 10 minutes. Please try again with a simpler question." - - except Exception as e: - print(f"Error in chat polling: {e}") - return f"❌ An error occurred while processing your request: {str(e)}" + return f"❌ An error occurred while communicating with the agent: {str(e)}" def on_gradio_app_load(request: gr.Request): # if request.username not present set username if not request.username: - request.username = "Alice" + request.username = "anonymous" if not AUTH_ENABLED else "Alice" return f"Logout ({request.username})", [gr.ChatMessage( role="assistant", content=f"Hi {request.username}, I'm your friendly corporate agent. Tell me how I can help. " @@ -285,17 +136,18 @@ def logout_click(request: gr.Request): ) ) - logout_button = gr.Button(value="Logout", variant="secondary") - logout_button.click( - fn=None, - js=f"() => {{ console.log('Logout Button clicked!'); console.log('Redirecting to: {LOGOUT_URL}'); window.location.href='{LOGOUT_URL}'; }}" - ) + logout_button = gr.Button(value="Logout", variant="secondary", visible=AUTH_ENABLED) + if AUTH_ENABLED: + logout_button.click( + fn=None, + js=f"() => {{ console.log('Logout Button clicked!'); console.log('Redirecting to: {LOGOUT_URL}'); window.location.href='{LOGOUT_URL}'; }}" + ) # js=f"() => {{ console.log('Logout Button clicked!'); console.log('Redirecting to: {LOGOUT_URL}'); window.location.href='{LOGOUT_URL}'; }}" gradio_app.load(on_gradio_app_load, inputs=None, outputs=[logout_button, chat.chatbot, agent_mode]) -gr.mount_gradio_app(fastapi_app, gradio_app, path=CHAT_PATH, auth_dependency=check_auth) +gr.mount_gradio_app(fastapi_app, gradio_app, path=CHAT_PATH, auth_dependency=check_auth if AUTH_ENABLED else None) def main(): uvicorn.run(