diff --git a/agentic_ai/agents/agent_framework/__init__.py b/agentic_ai/agents/agent_framework/__init__.py new file mode 100644 index 000000000..7a035dac1 --- /dev/null +++ b/agentic_ai/agents/agent_framework/__init__.py @@ -0,0 +1,6 @@ +# Agent Framework module +# This package contains agent implementations built using the agent_framework SDK + +from .single_agent import Agent + +__all__ = ["Agent"] diff --git a/agentic_ai/applications/Dockerfile b/agentic_ai/applications/Dockerfile index ff39a2f98..fed9c28e8 100644 --- a/agentic_ai/applications/Dockerfile +++ b/agentic_ai/applications/Dockerfile @@ -37,9 +37,13 @@ RUN --mount=type=cache,target=/root/.cache/uv \ COPY applications/backend.py applications/utils.py ./ # Copy ONLY agent_framework directory (not all agents) +COPY agents/__init__.py /app/agents/__init__.py COPY agents/agent_framework /app/agents/agent_framework COPY agents/base_agent.py /app/agents/base_agent.py +# Copy observability module +COPY observability /app/observability + # Copy built frontend from previous stage (Vite outputs to 'dist') COPY --from=frontend-builder /app/frontend/dist ./static diff --git a/agentic_ai/applications/backend.py b/agentic_ai/applications/backend.py index 45083b94d..718177b0b 100644 --- a/agentic_ai/applications/backend.py +++ b/agentic_ai/applications/backend.py @@ -449,6 +449,45 @@ async def set_active_agent(req: SetAgentRequest, token: str = Depends(verify_tok "message": "Failed to load agent." } +# ────────────────────────────────────────────────────────────── +# Diagnostic: check observability status +# ────────────────────────────────────────────────────────────── +@app.get("/api/diagnostics/observability") +async def diagnostics_observability(): + """Check if observability is configured and working.""" + import importlib + diag: Dict[str, Any] = { + "observability_enabled": _observability_enabled, + "connection_string_set": bool(os.environ.get("APPLICATIONINSIGHTS_CONNECTION_STRING")), + } + # Check key imports + for mod_name in ["azure.monitor.opentelemetry", "agent_framework.observability", "opentelemetry"]: + try: + importlib.import_module(mod_name) + diag[f"import_{mod_name}"] = "ok" + except Exception as e: + diag[f"import_{mod_name}"] = str(e) + # Check OTel tracer provider + try: + from opentelemetry import trace + tp = trace.get_tracer_provider() + diag["tracer_provider"] = type(tp).__name__ + if hasattr(tp, '_active_span_processor'): + proc = tp._active_span_processor + diag["span_processors"] = type(proc).__name__ + if hasattr(proc, '_span_processors'): + diag["span_processor_list"] = [type(sp).__name__ for sp in proc._span_processors] + except Exception as e: + diag["tracer_provider_error"] = str(e) + # Check OTel meter provider + try: + from opentelemetry import metrics + mp = metrics.get_meter_provider() + diag["meter_provider"] = type(mp).__name__ + except Exception as e: + diag["meter_provider_error"] = str(e) + return diag + # ────────────────────────────────────────────────────────────── # Root route to serve React app # ────────────────────────────────────────────────────────────── diff --git a/agentic_ai/applications/requirements.txt b/agentic_ai/applications/requirements.txt index dddda8770..01d01b186 100644 --- a/agentic_ai/applications/requirements.txt +++ b/agentic_ai/applications/requirements.txt @@ -2,99 +2,150 @@ # uv pip compile pyproject.toml -o requirements.txt a2a-sdk==0.3.12 # via agent-framework-a2a -agent-framework==1.0.0b251028 +ag-ui-protocol==0.1.10 + # via agent-framework-ag-ui +agent-framework==1.0.0b260130 # via applications (pyproject.toml) agent-framework-a2a==1.0.0b251114 - # via agent-framework + # via agent-framework-core +agent-framework-ag-ui==1.0.0b260130 + # via agent-framework-core +agent-framework-anthropic==1.0.0b260130 + # via agent-framework-core agent-framework-azure-ai==1.0.0b251114 - # via agent-framework + # via agent-framework-core +agent-framework-azure-ai-search==1.0.0b260130 + # via agent-framework-core +agent-framework-azurefunctions==1.0.0b260130 + # via agent-framework-core +agent-framework-chatkit==1.0.0b260130 + # via agent-framework-core agent-framework-copilotstudio==1.0.0b251114 - # via agent-framework -agent-framework-core==1.0.0b251114 + # via agent-framework-core +agent-framework-core==1.0.0b260130 # via # agent-framework # agent-framework-a2a + # agent-framework-ag-ui + # agent-framework-anthropic # agent-framework-azure-ai + # agent-framework-azure-ai-search + # agent-framework-azurefunctions + # agent-framework-chatkit # agent-framework-copilotstudio + # agent-framework-declarative # agent-framework-devui + # agent-framework-durabletask + # agent-framework-github-copilot # agent-framework-lab # agent-framework-mem0 + # agent-framework-ollama # agent-framework-purview # agent-framework-redis +agent-framework-declarative==1.0.0b260130 + # via agent-framework-core agent-framework-devui==1.0.0b251114 - # via agent-framework + # via agent-framework-core +agent-framework-durabletask==1.0.0b260130 + # via + # agent-framework-azurefunctions + # agent-framework-core +agent-framework-github-copilot==1.0.0b260130 + # via agent-framework-core agent-framework-lab==1.0.0b251024 - # via agent-framework + # via agent-framework-core agent-framework-mem0==1.0.0b251114 - # via agent-framework + # via agent-framework-core +agent-framework-ollama==1.0.0b260130 + # via agent-framework-core agent-framework-purview==1.0.0b251114 - # via agent-framework + # via agent-framework-core agent-framework-redis==1.0.0b251114 - # via agent-framework + # via agent-framework-core aiohappyeyeballs==2.6.1 # via aiohttp aiohttp==3.13.2 # via # agent-framework-azure-ai - # semantic-kernel -aioice==0.10.1 - # via aiortc -aiortc==1.14.0 - # via semantic-kernel + # azure-ai-evaluation + # azure-functions-durable aiosignal==1.4.0 # via aiohttp altair==5.6.0.dev20251110 # via streamlit annotated-types==0.7.0 # via pydantic +anthropic==0.78.0 + # via agent-framework-anthropic anyio==4.11.0 # via + # anthropic # httpx # mcp # openai # sse-starlette # starlette # watchfiles +asgiref==3.11.1 + # via opentelemetry-instrumentation-asgi +asyncio==4.0.0 + # via durabletask attrs==25.4.0 # via # aiohttp # jsonschema # referencing -autogen-agentchat==0.7.1 - # via applications (pyproject.toml) -autogen-core==0.7.1 - # via - # autogen-agentchat - # autogen-ext -autogen-ext==0.7.1 - # via applications (pyproject.toml) -av==16.0.1 - # via aiortc azure-ai-agents==1.2.0b5 - # via - # agent-framework-azure-ai - # semantic-kernel + # via agent-framework-azure-ai +azure-ai-evaluation==1.14.0 + # via applications (pyproject.toml) azure-ai-projects==2.0.0b2 # via + # applications (pyproject.toml) # agent-framework-azure-ai - # semantic-kernel +azure-common==1.1.28 + # via azure-search-documents azure-core==1.36.0 # via # agent-framework-purview # azure-ai-agents + # azure-ai-evaluation # azure-ai-projects + # azure-core-tracing-opentelemetry # azure-cosmos # azure-identity + # azure-monitor-opentelemetry + # azure-monitor-opentelemetry-exporter + # azure-search-documents # azure-storage-blob # microsoft-agents-hosting-core + # msrest +azure-core-tracing-opentelemetry==1.0.0b12 + # via azure-monitor-opentelemetry azure-cosmos==4.9.0 # via applications (pyproject.toml) +azure-functions==1.25.0b3.dev3 + # via + # agent-framework-azurefunctions + # azure-functions-durable +azure-functions-durable==1.4.0 + # via agent-framework-azurefunctions azure-identity==1.26.0b1 # via # agent-framework-core - # semantic-kernel + # azure-ai-evaluation + # azure-monitor-opentelemetry-exporter + # durabletask-azuremanaged +azure-monitor-opentelemetry==1.8.6 + # via applications (pyproject.toml) +azure-monitor-opentelemetry-exporter==1.0.0b48 + # via azure-monitor-opentelemetry +azure-search-documents==11.7.0b2 + # via agent-framework-azure-ai-search azure-storage-blob==12.27.1 - # via azure-ai-projects + # via + # azure-ai-evaluation + # azure-ai-projects backoff==2.2.1 # via posthog blinker==1.9.0 @@ -109,48 +160,52 @@ certifi==2025.11.12 # via # httpcore # httpx + # msrest # requests cffi==2.0.0 # via + # clr-loader # cryptography - # pylibsrtp -chardet==5.2.0 - # via prance + # powerfx charset-normalizer==3.4.4 # via requests click==8.3.1 # via # flask + # nltk # streamlit # uvicorn -cloudevents==1.12.0 - # via semantic-kernel +clr-loader==0.2.10 + # via pythonnet colorama==0.4.6 # via # click + # griffe # tqdm # uvicorn cryptography==45.0.7 # via - # aiortc # azure-identity # azure-storage-blob # msal # pyjwt - # pyopenssl -defusedxml==0.8.0rc2 - # via semantic-kernel -deprecation==2.1.0 - # via cloudevents distro==1.9.0 # via + # anthropic # openai # posthog -dnspython==2.8.0 - # via aioice +docstring-parser==0.17.0 + # via anthropic +durabletask==1.3.0 + # via + # agent-framework-durabletask + # durabletask-azuremanaged +durabletask-azuremanaged==1.3.0 + # via agent-framework-durabletask fastapi==0.115.12 # via # applications (pyproject.toml) + # agent-framework-ag-ui # agent-framework-devui flasgger==0.9.7.1 # via applications (pyproject.toml) @@ -162,25 +217,27 @@ frozenlist==1.8.0 # via # aiohttp # aiosignal +furl==2.1.4 + # via azure-functions-durable gitdb==4.0.12 # via gitpython +github-copilot-sdk==0.1.22 + # via agent-framework-github-copilot gitpython==3.1.45 # via streamlit google-api-core==2.28.1 # via a2a-sdk google-auth==2.43.0 # via google-api-core -google-crc32c==1.7.1 - # via aiortc googleapis-common-protos==1.72.0 - # via - # google-api-core - # opentelemetry-exporter-otlp-proto-grpc + # via google-api-core greenlet==3.2.4 # via sqlalchemy +griffe==1.15.0 + # via openai-agents grpcio==1.76.0 # via - # opentelemetry-exporter-otlp-proto-grpc + # durabletask # qdrant-client h11==0.16.0 # via @@ -199,7 +256,10 @@ httpx==0.28.1 # applications (pyproject.toml) # a2a-sdk # agent-framework-purview + # anthropic + # azure-ai-evaluation # mcp + # ollama # openai # qdrant-client httpx-sse==0.4.3 @@ -214,57 +274,48 @@ idna==3.11 # httpx # requests # yarl -ifaddr==0.2.0 - # via aioice importlib-metadata==8.7.0 # via opentelemetry-api isodate==0.7.2 # via # azure-ai-agents # azure-ai-projects + # azure-search-documents # azure-storage-blob # microsoft-agents-hosting-core - # openapi-core + # msrest itsdangerous==2.2.0 # via flask jinja2==3.1.6 # via # altair + # azure-ai-evaluation # flask + # openai-chatkit # pydeck - # semantic-kernel jiter==0.12.0 - # via openai + # via + # anthropic + # openai +joblib==1.5.3 + # via nltk jsonpath-ng==1.7.0 # via redisvl -jsonref==1.1.0 - # via autogen-core jsonschema==4.25.1 # via # altair # flasgger # mcp - # openapi-core - # openapi-schema-validator - # openapi-spec-validator -jsonschema-path==0.3.4 - # via - # openapi-core - # openapi-spec-validator jsonschema-specifications==2025.9.1 - # via - # jsonschema - # openapi-schema-validator -lazy-object-proxy==1.12.0 - # via openapi-spec-validator + # via jsonschema markupsafe==3.0.3 # via # jinja2 # werkzeug -mcp==1.21.1 +mcp==1.26.0 # via # agent-framework-core - # autogen-ext + # openai-agents mem0ai==1.0.1 # via agent-framework-mem0 microsoft-agents-activity==0.6.0.dev17 @@ -277,8 +328,6 @@ mistune==3.1.4 # via flasgger ml-dtypes==0.5.3 # via redisvl -more-itertools==10.8.0 - # via openapi-core msal==1.31.0 # via # applications (pyproject.toml) @@ -286,14 +335,18 @@ msal==1.31.0 # msal-extensions msal-extensions==1.3.1 # via azure-identity +msrest==0.7.1 + # via + # azure-ai-evaluation + # azure-monitor-opentelemetry-exporter multidict==6.7.0 # via # aiohttp # yarl narwhals==2.11.0 # via altair -nest-asyncio==1.6.0 - # via semantic-kernel +nltk==3.9.2 + # via azure-ai-evaluation numpy==2.3.5 # via # agent-framework-redis @@ -302,74 +355,135 @@ numpy==2.3.5 # pydeck # qdrant-client # redisvl - # scipy - # semantic-kernel # streamlit +oauthlib==3.3.1 + # via requests-oauthlib +ollama==0.6.1 + # via agent-framework-ollama openai==2.8.0 # via # applications (pyproject.toml) # agent-framework-core + # azure-ai-evaluation # mem0ai - # semantic-kernel -openapi-core==0.19.4 - # via semantic-kernel -openapi-schema-validator==0.6.3 - # via - # openapi-core - # openapi-spec-validator -openapi-spec-validator==0.7.2 - # via openapi-core -opentelemetry-api==1.38.0 + # openai-agents + # openai-chatkit +openai-agents==0.4.2 + # via openai-chatkit +openai-chatkit==1.6.0 + # via agent-framework-chatkit +opentelemetry-api==1.39.0 # via # agent-framework-core - # autogen-core - # opentelemetry-exporter-otlp-proto-grpc + # azure-core-tracing-opentelemetry + # azure-functions-durable + # azure-monitor-opentelemetry-exporter + # opentelemetry-instrumentation + # opentelemetry-instrumentation-asgi + # opentelemetry-instrumentation-dbapi + # opentelemetry-instrumentation-django + # opentelemetry-instrumentation-fastapi + # opentelemetry-instrumentation-flask + # opentelemetry-instrumentation-psycopg2 + # opentelemetry-instrumentation-requests + # opentelemetry-instrumentation-urllib + # opentelemetry-instrumentation-urllib3 + # opentelemetry-instrumentation-wsgi # opentelemetry-sdk # opentelemetry-semantic-conventions - # semantic-kernel -opentelemetry-exporter-otlp-proto-common==1.38.0 - # via opentelemetry-exporter-otlp-proto-grpc -opentelemetry-exporter-otlp-proto-grpc==1.38.0 - # via agent-framework-core -opentelemetry-proto==1.38.0 - # via - # opentelemetry-exporter-otlp-proto-common - # opentelemetry-exporter-otlp-proto-grpc -opentelemetry-sdk==1.38.0 +opentelemetry-instrumentation==0.60b0 + # via + # opentelemetry-instrumentation-asgi + # opentelemetry-instrumentation-dbapi + # opentelemetry-instrumentation-django + # opentelemetry-instrumentation-fastapi + # opentelemetry-instrumentation-flask + # opentelemetry-instrumentation-psycopg2 + # opentelemetry-instrumentation-requests + # opentelemetry-instrumentation-urllib + # opentelemetry-instrumentation-urllib3 + # opentelemetry-instrumentation-wsgi +opentelemetry-instrumentation-asgi==0.60b0 + # via opentelemetry-instrumentation-fastapi +opentelemetry-instrumentation-dbapi==0.60b0 + # via opentelemetry-instrumentation-psycopg2 +opentelemetry-instrumentation-django==0.60b0 + # via azure-monitor-opentelemetry +opentelemetry-instrumentation-fastapi==0.60b0 + # via azure-monitor-opentelemetry +opentelemetry-instrumentation-flask==0.60b0 + # via azure-monitor-opentelemetry +opentelemetry-instrumentation-psycopg2==0.60b0 + # via azure-monitor-opentelemetry +opentelemetry-instrumentation-requests==0.60b0 + # via azure-monitor-opentelemetry +opentelemetry-instrumentation-urllib==0.60b0 + # via azure-monitor-opentelemetry +opentelemetry-instrumentation-urllib3==0.60b0 + # via azure-monitor-opentelemetry +opentelemetry-instrumentation-wsgi==0.60b0 + # via + # opentelemetry-instrumentation-django + # opentelemetry-instrumentation-flask +opentelemetry-resource-detector-azure==0.1.5 + # via azure-monitor-opentelemetry +opentelemetry-sdk==1.39.0 # via # agent-framework-core - # opentelemetry-exporter-otlp-proto-grpc - # semantic-kernel -opentelemetry-semantic-conventions==0.59b0 - # via opentelemetry-sdk + # azure-functions-durable + # azure-monitor-opentelemetry + # azure-monitor-opentelemetry-exporter + # opentelemetry-resource-detector-azure +opentelemetry-semantic-conventions==0.60b0 + # via + # opentelemetry-instrumentation + # opentelemetry-instrumentation-asgi + # opentelemetry-instrumentation-dbapi + # opentelemetry-instrumentation-django + # opentelemetry-instrumentation-fastapi + # opentelemetry-instrumentation-flask + # opentelemetry-instrumentation-requests + # opentelemetry-instrumentation-urllib + # opentelemetry-instrumentation-urllib3 + # opentelemetry-instrumentation-wsgi + # opentelemetry-sdk opentelemetry-semantic-conventions-ai==0.4.13 # via agent-framework-core +opentelemetry-util-http==0.60b0 + # via + # opentelemetry-instrumentation-asgi + # opentelemetry-instrumentation-django + # opentelemetry-instrumentation-fastapi + # opentelemetry-instrumentation-flask + # opentelemetry-instrumentation-requests + # opentelemetry-instrumentation-urllib + # opentelemetry-instrumentation-urllib3 + # opentelemetry-instrumentation-wsgi +orderedmultidict==1.0.2 + # via furl packaging==24.2 # via # agent-framework-core # altair - # deprecation + # durabletask # flasgger - # prance + # opentelemetry-instrumentation + # opentelemetry-instrumentation-flask # streamlit pandas==2.3.3 - # via streamlit -parse==1.20.2 - # via openapi-core -pathable==0.4.4 - # via jsonschema-path -pillow==11.3.0 # via - # autogen-core + # azure-ai-evaluation # streamlit +pillow==11.3.0 + # via streamlit ply==3.11 # via jsonpath-ng portalocker==3.2.0 # via qdrant-client posthog==7.0.1 # via mem0ai -prance==25.4.8.0 - # via semantic-kernel +powerfx==0.0.34 + # via agent-framework-declarative propcache==0.4.1 # via # aiohttp @@ -379,15 +493,15 @@ proto-plus==1.26.1 protobuf==5.29.5 # via # a2a-sdk - # autogen-core + # durabletask # google-api-core # googleapis-common-protos # mem0ai - # opentelemetry-proto # proto-plus # qdrant-client - # semantic-kernel # streamlit +psutil==7.2.2 + # via azure-monitor-opentelemetry-exporter pyarrow==22.0.0 # via streamlit pyasn1==0.6.1 @@ -396,49 +510,46 @@ pyasn1==0.6.1 # rsa pyasn1-modules==0.4.2 # via google-auth -pybars4==0.9.13 - # via semantic-kernel pycparser==2.23 # via cffi pydantic==2.11.4 # via # applications (pyproject.toml) # a2a-sdk + # ag-ui-protocol # agent-framework-core - # autogen-core + # anthropic # fastapi + # github-copilot-sdk # mcp # mem0ai # microsoft-agents-activity + # ollama # openai + # openai-agents + # openai-chatkit # pydantic-settings # qdrant-client # redisvl - # semantic-kernel pydantic-core==2.33.2 # via pydantic pydantic-settings==2.12.0 # via # agent-framework-core # mcp - # semantic-kernel pydeck==0.9.1 # via streamlit -pyee==13.0.0 - # via aiortc pyjwt==2.10.1 # via + # azure-ai-evaluation # mcp # microsoft-agents-hosting-core # msal -pylibsrtp==1.0.0 - # via aiortc -pymeta3==0.5.1 - # via pybars4 -pyopenssl==25.3.0 - # via aiortc python-dateutil==2.9.0.post0 # via + # agent-framework-durabletask + # azure-functions-durable + # github-copilot-sdk # pandas # posthog python-dotenv==1.2.1 @@ -452,6 +563,8 @@ python-multipart==0.0.20 # via mcp python-ulid==3.1.0 # via redisvl +pythonnet==3.0.5 + # via powerfx pytz==2025.2 # via # mem0ai @@ -462,8 +575,8 @@ pywin32==311 # portalocker pyyaml==6.0.3 # via + # agent-framework-declarative # flasgger - # jsonschema-path # redisvl # uvicorn qdrant-client==1.15.1 @@ -477,20 +590,23 @@ redisvl==0.11.0 referencing==0.36.2 # via # jsonschema - # jsonschema-path # jsonschema-specifications +regex==2026.1.15 + # via nltk requests==2.32.4 # via # applications (pyproject.toml) # azure-core + # azure-functions-durable # google-api-core - # jsonschema-path # msal + # msrest + # openai-agents # posthog - # prance + # requests-oauthlib # streamlit -rfc3339-validator==0.1.4 - # via openapi-schema-validator +requests-oauthlib==2.0.0 + # via msrest rpds-py==0.29.0 # via # jsonschema @@ -498,23 +614,21 @@ rpds-py==0.29.0 rsa==4.9.1 # via google-auth ruamel-yaml==0.18.16 - # via prance + # via azure-ai-evaluation ruamel-yaml-clib==0.2.15 # via ruamel-yaml -scipy==1.16.3 - # via semantic-kernel -semantic-kernel==1.35.0 - # via applications (pyproject.toml) six==1.17.0 # via # flasgger + # furl + # orderedmultidict # posthog # python-dateutil - # rfc3339-validator smmap==5.0.2 # via gitdb sniffio==1.3.1 # via + # anthropic # anyio # openai sqlalchemy==2.0.44 @@ -537,35 +651,38 @@ toml==0.10.2 tornado==6.5.2 # via streamlit tqdm==4.67.1 - # via openai + # via + # nltk + # openai +types-requests==2.32.4.20260107 + # via openai-agents typing-extensions==4.15.0 # via # agent-framework-core # aiosignal # altair + # anthropic # anyio - # autogen-core # azure-ai-agents # azure-ai-projects # azure-core # azure-cosmos # azure-identity + # azure-search-documents # azure-storage-blob # fastapi + # github-copilot-sdk # grpcio # mcp # openai + # openai-agents # opentelemetry-api - # opentelemetry-exporter-otlp-proto-grpc # opentelemetry-sdk # opentelemetry-semantic-conventions # posthog # pydantic # pydantic-core - # pyee - # pyopenssl # referencing - # semantic-kernel # sqlalchemy # streamlit # typing-inspection @@ -580,11 +697,14 @@ urllib3==2.5.0 # via # qdrant-client # requests + # types-requests uvicorn==0.38.0 # via # applications (pyproject.toml) + # agent-framework-ag-ui # agent-framework-devui # mcp + # openai-chatkit watchdog==6.0.0 # via streamlit watchfiles==1.1.1 @@ -593,12 +713,16 @@ websockets==15.0.1 # via # applications (pyproject.toml) # mcp - # semantic-kernel # uvicorn werkzeug==3.1.3 # via + # azure-functions # flask - # openapi-core +wrapt==1.17.3 + # via + # opentelemetry-instrumentation + # opentelemetry-instrumentation-dbapi + # opentelemetry-instrumentation-urllib3 yarl==1.22.0 # via aiohttp zipp==3.23.0 diff --git a/agentic_ai/observability/setup.py b/agentic_ai/observability/setup.py index d91e6ab17..e2cbe063c 100644 --- a/agentic_ai/observability/setup.py +++ b/agentic_ai/observability/setup.py @@ -76,13 +76,16 @@ def setup_observability( enable_instrumentation(enable_sensitive_data=enable_sensitive_data) _initialized = True + print(f"✅ Application Insights observability enabled (service: {service_name})") logger.info(f"✅ Application Insights observability enabled (service: {service_name})") return True except ImportError as e: + print(f"❌ Observability dependencies not installed: {e}") logger.warning(f"Observability dependencies not installed: {e}") return False except Exception as e: + print(f"❌ Failed to configure observability: {e}") logger.warning(f"Failed to configure observability: {e}") return False diff --git a/agentic_ai/workflow/fraud_detection_durable/Dockerfile b/agentic_ai/workflow/fraud_detection_durable/Dockerfile new file mode 100644 index 000000000..6ecdc816f --- /dev/null +++ b/agentic_ai/workflow/fraud_detection_durable/Dockerfile @@ -0,0 +1,69 @@ +# Fraud Detection Durable Workflow +# Multi-stage build: React UI + Python Worker/Backend +# DTS runs as sidecar container in Container Apps + +# ============================================================================ +# Stage 1: Build React Frontend +# ============================================================================ +FROM node:20-alpine AS frontend-builder + +WORKDIR /app/frontend + +# Copy frontend package files +COPY ui/package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy frontend source +COPY ui/ ./ + +# Build React app (Vite outputs to 'dist/') +RUN npm run build + +# ============================================================================ +# Stage 2: Python Backend with Frontend Assets +# ============================================================================ +FROM python:3.12-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Install uv for fast Python package management +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv + +WORKDIR /app + +# Copy dependency files first for caching +COPY pyproject.toml ./ + +# Install Python dependencies +RUN uv pip install --system -e . + +# Copy application code +COPY *.py ./ +COPY .env.sample ./ +COPY start.sh ./ +RUN chmod +x /app/start.sh + +# Copy built frontend from previous stage (Vite outputs to 'dist') +COPY --from=frontend-builder /app/frontend/dist ./static + +# Expose ports +# 8002 - FastAPI Backend (serves both API and UI) +EXPOSE 8002 + +# Environment variables (can be overridden) +# DTS_ENDPOINT should point to the sidecar container +ENV DTS_ENDPOINT="http://localhost:8080" +ENV DTS_TASKHUB="default" +ENV PYTHONUNBUFFERED=1 +ENV BACKEND_PORT=8002 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ + CMD curl -f http://localhost:8002/health || exit 1 + +CMD ["/app/start.sh"] diff --git a/agentic_ai/workflow/fraud_detection_durable/backend.py b/agentic_ai/workflow/fraud_detection_durable/backend.py index 0b7fd926d..195e604d5 100644 --- a/agentic_ai/workflow/fraud_detection_durable/backend.py +++ b/agentic_ai/workflow/fraud_detection_durable/backend.py @@ -17,12 +17,16 @@ from datetime import datetime from typing import Any +from pathlib import Path + from azure.identity import DefaultAzureCredential from dotenv import load_dotenv from durabletask.azuremanaged.client import DurableTaskSchedulerClient from durabletask.client import OrchestrationState from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse from pydantic import BaseModel # Load environment @@ -39,15 +43,40 @@ version="1.0.0", ) -# CORS +# CORS - allow localhost for dev and Azure Container Apps for prod +CORS_ORIGINS = [ + "http://localhost:3000", + "http://localhost:5173", + "http://localhost:8002", +] +# Add Azure Container Apps URL if set +if os.getenv("CONTAINER_APP_URL"): + CORS_ORIGINS.append(os.getenv("CONTAINER_APP_URL")) + app.add_middleware( CORSMiddleware, - allow_origins=["http://localhost:3000", "http://localhost:5173"], + allow_origins=CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) +# ============================================================================ +# Static Files (React UI) +# ============================================================================ + +# Serve static files from React build (production mode) +# Vite outputs to 'dist/' with assets in 'dist/assets/' +STATIC_DIR = Path(__file__).parent / "static" +STATIC_ASSET_DIR = STATIC_DIR / "assets" # Vite structure + +if STATIC_ASSET_DIR.exists(): + app.mount("/assets", StaticFiles(directory=str(STATIC_ASSET_DIR)), name="assets") + logger.info(f"Serving static assets from {STATIC_ASSET_DIR}") +elif STATIC_DIR.exists(): + # Fallback: mount entire static dir + logger.info(f"Static assets dir not found, mounting {STATIC_DIR}") + # Constants ANALYST_APPROVAL_EVENT = "AnalystDecision" ORCHESTRATION_NAME = "fraud_detection_orchestration" @@ -301,6 +330,19 @@ def start_status_polling(instance_id: str): # ============================================================================ +@app.get("/") +async def read_root(): + """Serve the React frontend index.html.""" + index_path = STATIC_DIR / "index.html" + if index_path.exists(): + return FileResponse(str(index_path)) + return { + "message": "Durable Fraud Detection API", + "version": "1.0.0", + "docs": "/docs", + } + + @app.get("/health") async def health(): """Health check endpoint.""" @@ -499,9 +541,10 @@ async def shutdown(): if __name__ == "__main__": import uvicorn + port = int(os.environ.get("BACKEND_PORT", "8002")) uvicorn.run( app, host="0.0.0.0", - port=8001, + port=port, log_level="info", ) diff --git a/agentic_ai/workflow/fraud_detection_durable/start.sh b/agentic_ai/workflow/fraud_detection_durable/start.sh new file mode 100644 index 000000000..e4f4472b7 --- /dev/null +++ b/agentic_ai/workflow/fraud_detection_durable/start.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# Start both worker and backend processes + +echo "Starting Fraud Detection Durable Workflow..." +echo "DTS Endpoint: $DTS_ENDPOINT" + +# Start worker in background +echo "Starting worker..." +python worker.py & +WORKER_PID=$! + +# Wait a bit for worker to connect to DTS +sleep 5 + +# Start backend in foreground +echo "Starting backend on port ${BACKEND_PORT:-8002}..." +python backend.py + +# If backend exits, kill worker +kill $WORKER_PID 2>/dev/null diff --git a/agentic_ai/workflow/fraud_detection_durable/ui/Dockerfile b/agentic_ai/workflow/fraud_detection_durable/ui/Dockerfile index f613068f8..70e074f86 100644 --- a/agentic_ai/workflow/fraud_detection_durable/ui/Dockerfile +++ b/agentic_ai/workflow/fraud_detection_durable/ui/Dockerfile @@ -40,9 +40,17 @@ RUN npm install -g serve # Copy built assets from builder stage COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist +# Copy startup script that injects runtime config +COPY --chown=nodejs:nodejs docker-entrypoint.sh /app/docker-entrypoint.sh +RUN chmod +x /app/docker-entrypoint.sh + # Switch to non-root user USER nodejs +# Environment variables for runtime configuration +ENV API_BASE_URL="" +ENV WS_URL="" + # Expose port EXPOSE 3000 @@ -53,5 +61,5 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ # Use dumb-init to handle signals properly ENTRYPOINT ["dumb-init", "--"] -# Serve the application -CMD ["serve", "-s", "dist", "-l", "3000"] +# Run startup script that injects config, then serves the app +CMD ["/app/docker-entrypoint.sh"] diff --git a/agentic_ai/workflow/fraud_detection_durable/ui/docker-entrypoint.sh b/agentic_ai/workflow/fraud_detection_durable/ui/docker-entrypoint.sh new file mode 100644 index 000000000..62e9d52f5 --- /dev/null +++ b/agentic_ai/workflow/fraud_detection_durable/ui/docker-entrypoint.sh @@ -0,0 +1,30 @@ +#!/bin/sh +# Docker entrypoint script that injects runtime configuration into the React app + +set -e + +# Generate runtime config script +CONFIG_SCRIPT="window.__CONFIG__ = {" + +if [ -n "$API_BASE_URL" ]; then + CONFIG_SCRIPT="${CONFIG_SCRIPT} API_BASE_URL: '${API_BASE_URL}'," +fi + +if [ -n "$WS_URL" ]; then + CONFIG_SCRIPT="${CONFIG_SCRIPT} WS_URL: '${WS_URL}'," +fi + +CONFIG_SCRIPT="${CONFIG_SCRIPT} };" + +echo "Injecting runtime config: $CONFIG_SCRIPT" + +# Create a config.js file in the dist folder +echo "$CONFIG_SCRIPT" > /app/dist/config.js + +# Inject script tag into index.html if not already present +if ! grep -q 'config.js' /app/dist/index.html; then + sed -i 's|||' /app/dist/index.html +fi + +# Start the server +exec serve -s dist -l 3000 diff --git a/agentic_ai/workflow/fraud_detection_durable/ui/package-lock.json b/agentic_ai/workflow/fraud_detection_durable/ui/package-lock.json index 0264ec278..40cfd0e81 100644 --- a/agentic_ai/workflow/fraud_detection_durable/ui/package-lock.json +++ b/agentic_ai/workflow/fraud_detection_durable/ui/package-lock.json @@ -5825,22 +5825,6 @@ "dev": true, "license": "ISC" }, - "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "extraneous": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/agentic_ai/workflow/fraud_detection_durable/ui/src/App.jsx b/agentic_ai/workflow/fraud_detection_durable/ui/src/App.jsx index af279a829..1c8421430 100644 --- a/agentic_ai/workflow/fraud_detection_durable/ui/src/App.jsx +++ b/agentic_ai/workflow/fraud_detection_durable/ui/src/App.jsx @@ -15,6 +15,7 @@ import { import SecurityIcon from '@mui/icons-material/Security'; import CloudSyncIcon from '@mui/icons-material/CloudSync'; import WorkflowVisualizer from './components/WorkflowVisualizer'; +import { API_CONFIG } from './constants/config'; import ControlPanel from './components/ControlPanel'; import AnalystDecisionPanel from './components/AnalystDecisionPanel'; @@ -57,7 +58,7 @@ function App() { // Load sample alerts on mount useEffect(() => { - fetch('http://localhost:8001/api/alerts') + fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.ALERTS}`) .then((res) => res.json()) .then((data) => setAlerts(data)) .catch((err) => console.error('Error loading alerts:', err)); @@ -71,7 +72,7 @@ function App() { useEffect(() => { if (!instanceId) return; - const wsUrl = `ws://localhost:8001/ws/${instanceId}`; + const wsUrl = `${API_CONFIG.WS_URL}/${instanceId}`; console.log('Connecting to WebSocket:', wsUrl); ws.current = new WebSocket(wsUrl); @@ -257,7 +258,7 @@ function App() { setStepDetails({}); // Reset step details for new workflow try { - const response = await fetch('http://localhost:8001/api/workflow/start', { + const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.WORKFLOW_START}`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -290,7 +291,7 @@ function App() { console.log('Submitting decision:', decision); try { - const response = await fetch('http://localhost:8001/api/workflow/decision', { + const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.WORKFLOW_DECISION}`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -345,7 +346,7 @@ function App() { {/* Left Column - Controls and Decision Panel */} - + {/* Center Column - Workflow Visualization */} - + Workflow Graph @@ -373,8 +374,10 @@ function App() { {orchestrationStatus && ` | Status: ${orchestrationStatus}`} - - + +
+ +
diff --git a/agentic_ai/workflow/fraud_detection_durable/ui/src/components/WorkflowVisualizer.jsx b/agentic_ai/workflow/fraud_detection_durable/ui/src/components/WorkflowVisualizer.jsx index e2d399ae3..0d4312302 100644 --- a/agentic_ai/workflow/fraud_detection_durable/ui/src/components/WorkflowVisualizer.jsx +++ b/agentic_ai/workflow/fraud_detection_durable/ui/src/components/WorkflowVisualizer.jsx @@ -204,6 +204,8 @@ function WorkflowVisualizer({ executorStates = {}, stepDetails = {} }) { onNodeClick={handleNodeClick} nodeTypes={nodeTypes} fitView + fitViewOptions={{ padding: 0.2 }} + style={{ width: '100%', height: '100%' }} attributionPosition="bottom-left" > diff --git a/agentic_ai/workflow/fraud_detection_durable/ui/src/constants/config.js b/agentic_ai/workflow/fraud_detection_durable/ui/src/constants/config.js index b5bbabc03..617845f48 100644 --- a/agentic_ai/workflow/fraud_detection_durable/ui/src/constants/config.js +++ b/agentic_ai/workflow/fraud_detection_durable/ui/src/constants/config.js @@ -1,9 +1,29 @@ +/** + * Runtime configuration support + * Priority: window.__CONFIG__ (runtime) > import.meta.env (build-time) > defaults + * + * When served from the same origin as the API (production), use relative URLs. + * For local dev with separate frontend server, use localhost URLs. + */ +const runtimeConfig = typeof window !== 'undefined' ? window.__CONFIG__ || {} : {}; + +// Determine if we're running from same origin as API (production mode) +const isSameOrigin = typeof window !== 'undefined' && + !window.location.port.includes('5173') && // Not Vite dev server + !window.location.port.includes('3000'); // Not React dev server + +// In production (same origin), use relative URLs. In dev, use localhost. +const defaultBaseUrl = isSameOrigin ? '' : 'http://localhost:8002'; +const defaultWsUrl = isSameOrigin + ? `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws` + : 'ws://localhost:8002/ws'; + /** * API configuration constants */ export const API_CONFIG = { - BASE_URL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8001', - WS_URL: import.meta.env.VITE_WS_URL || 'ws://localhost:8001/ws', + BASE_URL: runtimeConfig.API_BASE_URL || import.meta.env.VITE_API_BASE_URL || defaultBaseUrl, + WS_URL: runtimeConfig.WS_URL || import.meta.env.VITE_WS_URL || defaultWsUrl, ENDPOINTS: { ALERTS: '/api/alerts', WORKFLOW_START: '/api/workflow/start', diff --git a/agentic_ai/workflow/fraud_detection_durable/worker.py b/agentic_ai/workflow/fraud_detection_durable/worker.py index 30b04c06d..c9a6d59ef 100644 --- a/agentic_ai/workflow/fraud_detection_durable/worker.py +++ b/agentic_ai/workflow/fraud_detection_durable/worker.py @@ -24,7 +24,7 @@ from datetime import timedelta from typing import Any -from azure.identity import AzureCliCredential, DefaultAzureCredential +from azure.identity import DefaultAzureCredential, ManagedIdentityCredential from dotenv import load_dotenv from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker from durabletask.task import ActivityContext, OrchestrationContext, Task, when_any, when_all @@ -102,8 +102,18 @@ async def _ensure_resources(): logger.info(f"✓ MCP tool initialized at {mcp_uri}") if _chat_client is None: + # Use managed identity if AZURE_CLIENT_ID is set, otherwise DefaultAzureCredential + azure_client_id = os.getenv("AZURE_CLIENT_ID") + if azure_client_id: + from azure.identity import ManagedIdentityCredential + credential = ManagedIdentityCredential(client_id=azure_client_id) + logger.info(f"Using ManagedIdentityCredential with client_id: {azure_client_id}") + else: + credential = DefaultAzureCredential() + logger.info("Using DefaultAzureCredential") + _chat_client = AzureOpenAIChatClient( - credential=AzureCliCredential(), + credential=credential, deployment_name=os.getenv("AZURE_OPENAI_CHAT_DEPLOYMENT", "gpt-4o"), ) logger.info("✓ Azure OpenAI client initialized") diff --git a/infra/deploy.ps1 b/infra/deploy.ps1 index 4fad8dd30..e0c218c05 100644 --- a/infra/deploy.ps1 +++ b/infra/deploy.ps1 @@ -100,7 +100,7 @@ if (-not $SkipBuild) { # Step 4: Build and Push Application Image if (-not $SkipBuild) { - Write-Host "`n[4/5] Building and pushing Application image..." -ForegroundColor Green + Write-Host "`n[4/6] Building and pushing Application image..." -ForegroundColor Green Push-Location agentic_ai/applications try { @@ -118,14 +118,38 @@ if (-not $SkipBuild) { Write-Host "Application image built and pushed successfully!" -ForegroundColor Green } else { - Write-Host "`n[4/5] Skipping Application build (--SkipBuild)" -ForegroundColor Yellow + Write-Host "`n[4/6] Skipping Application build (--SkipBuild)" -ForegroundColor Yellow } -# Step 5: Restart Container Apps to pull new images -Write-Host "`n[5/5] Restarting Container Apps..." -ForegroundColor Green +# Step 5: Build and Push Fraud Workflow Image (includes DTS + Worker + Backend) +if (-not $SkipBuild) { + Write-Host "`n[5/6] Building and pushing Fraud Workflow image..." -ForegroundColor Green + + Push-Location agentic_ai/workflow/fraud_detection_durable + try { + docker build -t "$AcrLoginServer/fraud-workflow:latest" -f Dockerfile . + docker push "$AcrLoginServer/fraud-workflow:latest" + + if ($LASTEXITCODE -ne 0) { + Write-Error "Fraud Workflow image build/push failed!" + exit 1 + } + } + finally { + Pop-Location + } + + Write-Host "Fraud Workflow image built and pushed successfully!" -ForegroundColor Green +} else { + Write-Host "`n[5/6] Skipping Fraud Workflow build (--SkipBuild)" -ForegroundColor Yellow +} + +# Step 6: Restart Container Apps to pull new images +Write-Host "`n[6/6] Restarting Container Apps..." -ForegroundColor Green $McpServiceName = "$BaseName-$Environment-mcp" $AppName = "$BaseName-$Environment-app" +$FraudWorkflowName = "$BaseName-$Environment-fraud-wf" Write-Host "Restarting MCP Service: $McpServiceName" -ForegroundColor Gray az containerapp revision restart ` @@ -139,11 +163,19 @@ az containerapp revision restart ` --name $AppName ` --revision latest +Write-Host "Restarting Fraud Workflow: $FraudWorkflowName" -ForegroundColor Gray +az containerapp revision restart ` + --resource-group $ResourceGroupName ` + --name $FraudWorkflowName ` + --revision latest + Write-Host "`n======================================" -ForegroundColor Cyan Write-Host "Deployment Complete!" -ForegroundColor Green Write-Host "======================================" -ForegroundColor Cyan Write-Host "`nAccess your application at:" -ForegroundColor Yellow Write-Host " $($outputs.applicationUrl.value)" -ForegroundColor Cyan +Write-Host "`nFraud Detection Workflow:" -ForegroundColor Yellow +Write-Host " $($outputs.fraudWorkflowUrl.value)" -ForegroundColor Cyan Write-Host "`nMCP Service URL:" -ForegroundColor Yellow Write-Host " $($outputs.mcpServiceUrl.value)" -ForegroundColor Cyan Write-Host "`nResource Group:" -ForegroundColor Yellow diff --git a/infra/main.bicep b/infra/main.bicep index b1fbd9760..64423579c 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -158,6 +158,26 @@ module application 'modules/application.bicep' = { } } +// Fraud Detection Durable Workflow Container App (includes DTS + Worker + Backend) +module fraudWorkflow 'modules/fraud-workflow.bicep' = { + scope: rg + name: 'fraud-workflow-deployment' + params: { + location: location + baseName: '${baseName}-${environmentName}' + containerAppsEnvironmentId: containerAppsEnv.outputs.environmentId + containerRegistryName: acr.outputs.registryName + azureOpenAIEndpoint: openai.outputs.endpoint + azureOpenAIKey: openai.outputs.key + azureOpenAIDeploymentName: openai.outputs.chatDeploymentName + mcpServiceUrl: mcpService.outputs.internalUrl + userAssignedIdentityResourceId: useCosmosManagedIdentity ? containerAppsIdentity.outputs.resourceId : '' + userAssignedIdentityClientId: useCosmosManagedIdentity ? containerAppsIdentity.outputs.clientId : '' + applicationInsightsConnectionString: logAnalytics.outputs.applicationInsightsConnectionString + tags: tags + } +} + // Outputs output resourceGroupName string = rg.name output location string = location @@ -166,4 +186,5 @@ output cosmosDbEndpoint string = cosmosdb.outputs.endpoint output containerRegistryName string = acr.outputs.registryName output mcpServiceUrl string = mcpService.outputs.serviceUrl output applicationUrl string = application.outputs.applicationUrl +output fraudWorkflowUrl string = fraudWorkflow.outputs.url output containerAppsEnvironmentId string = containerAppsEnv.outputs.environmentId diff --git a/infra/modules/fraud-workflow.bicep b/infra/modules/fraud-workflow.bicep new file mode 100644 index 000000000..32a54a2ec --- /dev/null +++ b/infra/modules/fraud-workflow.bicep @@ -0,0 +1,211 @@ +// Fraud Detection Durable Workflow Container App +// Includes DTS Emulator + Worker + Backend in one container + +@description('Azure region for deployment') +param location string + +@description('Base name for resources') +param baseName string + +@description('Container Apps Environment resource ID') +param containerAppsEnvironmentId string + +@description('Container Registry name') +param containerRegistryName string + +@description('Azure OpenAI endpoint URL') +param azureOpenAIEndpoint string + +@description('Azure OpenAI API key') +@secure() +param azureOpenAIKey string + +@description('Azure OpenAI deployment name') +param azureOpenAIDeploymentName string + +@description('MCP service URL (internal)') +param mcpServiceUrl string + +@description('Optional user-assigned managed identity resource ID') +param userAssignedIdentityResourceId string = '' + +@description('Client ID for the user-assigned managed identity') +param userAssignedIdentityClientId string = '' + +@description('Application Insights connection string for observability') +param applicationInsightsConnectionString string = '' + +@description('Resource tags') +param tags object + +@description('Container image tag') +param imageTag string = 'latest' + +@description('Full container image name from azd') +param imageName string = '' + +var appName = '${baseName}-fraud-wf' +var containerImage = !empty(imageName) ? imageName : '${containerRegistryName}.azurecr.io/fraud-workflow:${imageTag}' +var azdTags = union(tags, { + 'azd-service-name': 'fraud-workflow' + 'azd-service-type': 'containerapp' +}) + +var managedIdentityEnv = !empty(userAssignedIdentityClientId) ? [ + { + name: 'AZURE_CLIENT_ID' + value: userAssignedIdentityClientId + } +] : [] + +var observabilityEnv = !empty(applicationInsightsConnectionString) ? [ + { + name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' + value: applicationInsightsConnectionString + } +] : [] + +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' existing = { + name: containerRegistryName +} + +resource fraudWorkflow 'Microsoft.App/containerApps@2023-05-01' = { + name: appName + location: location + identity: empty(userAssignedIdentityResourceId) ? null : { + type: 'UserAssigned' + userAssignedIdentities: { + '${userAssignedIdentityResourceId}': {} + } + } + properties: { + managedEnvironmentId: containerAppsEnvironmentId + configuration: { + ingress: { + external: true + targetPort: 8002 + transport: 'http' + allowInsecure: false + corsPolicy: { + allowedOrigins: ['*'] + allowedMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'] + allowedHeaders: ['*'] + allowCredentials: true + } + } + registries: [ + { + server: '${containerRegistryName}.azurecr.io' + identity: !empty(userAssignedIdentityResourceId) ? userAssignedIdentityResourceId : 'system' + } + ] + secrets: [ + { + name: 'azure-openai-key' + value: azureOpenAIKey + } + ] + } + template: { + containers: [ + { + name: 'fraud-workflow' + image: containerImage + resources: { + cpu: json('1.0') + memory: '2Gi' + } + env: concat([ + { + name: 'AZURE_OPENAI_ENDPOINT' + value: azureOpenAIEndpoint + } + { + name: 'AZURE_OPENAI_API_KEY' + secretRef: 'azure-openai-key' + } + { + name: 'AZURE_OPENAI_CHAT_DEPLOYMENT' + value: azureOpenAIDeploymentName + } + { + name: 'MCP_SERVER_URI' + value: mcpServiceUrl + } + { + name: 'DTS_ENDPOINT' + value: 'http://localhost:8080' + } + { + name: 'DTS_TASKHUB' + value: 'default' + } + { + name: 'BACKEND_PORT' + value: '8002' + } + ], managedIdentityEnv, observabilityEnv) + probes: [ + { + type: 'Liveness' + httpGet: { + path: '/health' + port: 8002 + } + initialDelaySeconds: 60 + periodSeconds: 30 + } + { + type: 'Readiness' + httpGet: { + path: '/health' + port: 8002 + } + initialDelaySeconds: 30 + periodSeconds: 10 + } + ] + } + // DTS Emulator sidecar container + { + name: 'dts-emulator' + image: 'mcr.microsoft.com/dts/dts-emulator:latest' + resources: { + cpu: json('0.5') + memory: '1Gi' + } + env: [ + { + name: 'DTS_PORT' + value: '8080' + } + ] + } + ] + scale: { + minReplicas: 1 + maxReplicas: 3 + rules: [ + { + name: 'http-scaling' + http: { + metadata: { + concurrentRequests: '50' + } + } + } + ] + } + } + } + tags: azdTags +} + +@description('Fraud workflow FQDN') +output fqdn string = fraudWorkflow.properties.configuration.ingress.fqdn + +@description('Fraud workflow URL') +output url string = 'https://${fraudWorkflow.properties.configuration.ingress.fqdn}' + +@description('Fraud workflow resource name') +output name string = fraudWorkflow.name diff --git a/infra/modules/log-analytics.bicep b/infra/modules/log-analytics.bicep index b5607f135..a6644716a 100644 --- a/infra/modules/log-analytics.bicep +++ b/infra/modules/log-analytics.bicep @@ -32,6 +32,22 @@ resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { tags: tags } +// Application Insights for observability +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { + name: '${baseName}-${environmentName}-appinsights' + location: location + kind: 'web' + properties: { + Application_Type: 'web' + WorkspaceResourceId: logAnalytics.id + publicNetworkAccessForIngestion: 'Enabled' + publicNetworkAccessForQuery: 'Enabled' + } + tags: tags +} + output workspaceId string = logAnalytics.id output customerId string = logAnalytics.properties.customerId output workspaceName string = logAnalytics.name +output applicationInsightsConnectionString string = applicationInsights.properties.ConnectionString +output applicationInsightsInstrumentationKey string = applicationInsights.properties.InstrumentationKey diff --git a/infra/modules/mcp-service.bicep b/infra/modules/mcp-service.bicep index c9328b2ec..41bb4d230 100644 --- a/infra/modules/mcp-service.bicep +++ b/infra/modules/mcp-service.bicep @@ -139,5 +139,6 @@ resource mcpService 'Microsoft.App/containerApps@2023-05-01' = { } output serviceUrl string = 'https://${mcpService.properties.configuration.ingress.fqdn}/mcp' +output internalUrl string = 'http://${mcpService.name}/mcp' output serviceName string = mcpService.name output fqdn string = mcpService.properties.configuration.ingress.fqdn