diff --git a/.github/codeql/typescript-config.yml b/.github/codeql/typescript-config.yml index 7eb6200e..7841f676 100644 --- a/.github/codeql/typescript-config.yml +++ b/.github/codeql/typescript-config.yml @@ -1,8 +1,6 @@ name: TypeScript CodeQL Configuration paths: - - packages/tools/code-evaluation/** - - packages/tools/web-search/** - packages/api/** - packages/ui/** diff --git a/.github/workflows/codeql-typescript.yml b/.github/workflows/codeql-typescript.yml index f0d242dd..a2a8ebba 100644 --- a/.github/workflows/codeql-typescript.yml +++ b/.github/workflows/codeql-typescript.yml @@ -8,10 +8,6 @@ on: - 'packages/api/*.js' - 'packages/ui/*.ts' - 'packages/ui/*.js' - - 'packages/tools/code-evaluation/**/*.ts' - - 'packages/tools/code-evaluation/**/*.js' - - 'packages/tools/web-search/**/*.ts' - - 'packages/tools/web-search/**/*.js' - 'packages/tools/echo-ping/**/*.ts' - 'packages/tools/echo-ping/**/*.js' - 'src/shared/**/*.ts' @@ -25,10 +21,6 @@ on: - 'packages/api/*.js' - 'packages/ui/*.ts' - 'packages/ui/*.js' - - 'packages/tools/code-evaluation/**/*.ts' - - 'packages/tools/code-evaluation/**/*.js' - - 'packages/tools/web-search/**/*.ts' - - 'packages/tools/web-search/**/*.js' - 'packages/tools/echo-ping/**/*.ts' - 'packages/tools/echo-ping/**/*.js' - 'src/shared/**/*.ts' @@ -59,7 +51,7 @@ jobs: - name: Install dependencies run: | - for dir in packages/tools/code-evaluation packages/tools/web-search packages/tools/echo-ping src/api src/ui; do + for dir in packages/tools/echo-ping src/api src/ui; do if [ -f "$dir/package.json" ]; then cd $dir && npm install && cd - fi diff --git a/.vscode/mcp.json b/.vscode/mcp.json index 2dc9cadc..baefa90d 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -13,18 +13,6 @@ "type": "http", "url": "http://localhost:5007/mcp" }, - "tool-web-search": { - "type": "sse", - "url": "http://localhost:5006/sse" - }, - "tool-model-inference": { - "type": "sse", - "url": "http://localhost:5005/sse" - }, - "tool-code-evaluation": { - "type": "sse", - "url": "http://localhost:5004/sse" - }, "tool-itinerary-planning": { "type": "http", "url": "http://localhost:5003/mcp" diff --git a/AGENTS.md b/AGENTS.md index 598b6cb3..5d187dd5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,10 +29,8 @@ All orchestrators communicate with the same MCP tool servers. Each component is │ │ ├── echo-ping/ # TypeScript/Node.js (testing tool) │ │ ├── customer-query/ # C#/.NET (customer inquiry processing) │ │ ├── destination-recommendation/ # Java (travel destination suggestions) -│ │ ├── itinerary-planning/ # Python (detailed itinerary creation) -│ │ ├── code-evaluation/ # Python (code interpreter integration) -│ │ ├── model-inference/ # Python (local LLM with GPU support) -│ │ └── web-search/ # TypeScript (Bing API integration) +│ │ └── itinerary-planning/ # Python (detailed itinerary creation) +│ │ │ ├── shared/ # Common utilities and types │ └── docker-compose.yml # Local development environment ├── azure.yaml # Azure Developer CLI configuration @@ -60,9 +58,6 @@ The system implements specialized agents coordinated by orchestration layers. Al - **Customer Query Agent**: Extracts preferences from customer inquiries (via customer-query MCP) - **Destination Recommendation Agent**: Suggests destinations (via destination-recommendation MCP) - **Itinerary Planning Agent**: Creates detailed travel plans (via itinerary-planning MCP) - - **Web Search Agent**: Fetches live travel data via Bing API (via web-search MCP) - - **Code Evaluation Agent**: Executes custom logic (via code-evaluation MCP) - - **Model Inference Agent**: Runs local LLMs with ONNX/vLLM (via model-inference MCP) - **Echo Agent**: Testing and validation (via echo-ping MCP) **Option 2: LlamaIndex.TS Orchestration** (`packages/api/src/orchestrator/llamaindex/`) @@ -74,9 +69,6 @@ The system implements specialized agents coordinated by orchestration layers. Al - **Customer Query Agent**: Extracts preferences from customer inquiries (via customer-query MCP) - **Destination Recommendation Agent**: Suggests destinations (via destination-recommendation MCP) - **Itinerary Planning Agent**: Creates detailed travel plans (via itinerary-planning MCP) - - **Web Search Agent**: Fetches live travel data via Bing API (via web-search MCP) - - **Code Evaluation Agent**: Executes custom logic (via code-evaluation MCP) - - **Model Inference Agent**: Runs local LLMs with ONNX/vLLM (via model-inference MCP) - **Echo Agent**: Testing and validation (via echo-ping MCP) **Option 3: Microsoft Agent Framework Orchestration** (`packages/api-python/`) @@ -89,9 +81,6 @@ The system implements specialized agents coordinated by orchestration layers. Al - **CustomerQueryAgent**: Processes customer inquiries with MCP tools - **DestinationRecommendationAgent**: Provides destination suggestions - **ItineraryPlanningAgent**: Creates detailed itineraries with MCP tools - - **CodeEvaluationAgent**: Executes code via code-evaluation MCP server - - **ModelInferenceAgent**: Performs local model inference via MCP - - **WebSearchAgent**: Searches web using Bing API via MCP - **EchoAgent**: Testing and validation via echo-ping MCP #### MCP Tool Servers (Shared by All Orchestrations) @@ -101,9 +90,6 @@ All three orchestration implementations communicate with these MCP servers: - **customer-query** (.NET/C#) - Port 5001 - Customer inquiry processing - **destination-recommendation** (Java) - Port 5002 - Travel destination suggestions - **itinerary-planning** (Python) - Port 5003 - Detailed itinerary creation -- **code-evaluation** (Python) - Port 5004 - Code interpreter integration -- **model-inference** (Python) - Port 5005 - Local LLM with GPU support -- **web-search** (TypeScript) - Port 5006 - Bing API integration - **echo-ping** (TypeScript) - Port 5007 - Testing and validation ### Service Communication @@ -234,7 +220,7 @@ For detailed comparison, see `docs/orchestration.md`. ### TypeScript/Node.js Standards -**Location**: `packages/api/`, `packages/ui/`, `packages/tools/echo-ping/`, `packages/tools/web-search/` +**Location**: `packages/api/`, `packages/ui/`, `packages/tools/echo-ping/` **Key Conventions**: - Use ES modules (`"type": "module"` in package.json) @@ -529,7 +515,6 @@ chore(deps): update dependencies cd packages/tools/itinerary-planning && pip install . cd packages/tools/code-evaluation && pip install . cd packages/tools/model-inference && pip install . - cd packages/tools/web-search && npm run build ``` 2. **Test Coverage**: Run existing tests and add new tests for new functionality diff --git a/docs/advanced-setup.md b/docs/advanced-setup.md index 3a6e0307..ef04a21d 100644 --- a/docs/advanced-setup.md +++ b/docs/advanced-setup.md @@ -203,12 +203,6 @@ The application uses environment variables to configure the services. You can se - `packages/tools/destination-recommendation/.env.docker` - `packages/tools/itinerary-planning/.env` - `packages/tools/itinerary-planning/.env.docker` - - `packages/tools/code-evaluation/.env` - - `packages/tools/code-evaluation/.env.docker` - - `packages/tools/model-inference/.env` - - `packages/tools/model-inference/.env.docker` - - `packages/tools/web-search/.env` - - `packages/tools/web-search/.env.docker` - `packages/tools/echo-ping/.env` - `packages/tools/echo-ping/.env.docker` diff --git a/docs/deployment-architecture.md b/docs/deployment-architecture.md index fc2ffbf4..212d6080 100644 --- a/docs/deployment-architecture.md +++ b/docs/deployment-architecture.md @@ -41,7 +41,6 @@ graph TB EP[Echo Ping] CQ[Cust Query] DR[Dest Rec] - WS[Web Search] OTHER[...] end end @@ -136,18 +135,11 @@ AZURE_OPENAI_ENDPOINT=https://your-openai.openai.azure.com/ AZURE_OPENAI_API_KEY=your-api-key AZURE_OPENAI_DEPLOYMENT_NAME=gpt-4o-mini -# Bing Search -BING_SEARCH_API_KEY=your-bing-key -BING_SEARCH_ENDPOINT=https://api.bing.microsoft.com/ - # MCP Server URLs (local development) MCP_ECHO_PING_URL=http://localhost:5007 MCP_CUSTOMER_QUERY_URL=http://localhost:5001 MCP_DESTINATION_RECOMMENDATION_URL=http://localhost:5002 MCP_ITINERARY_PLANNING_URL=http://localhost:5003 -MCP_CODE_EVALUATION_URL=http://localhost:5004 -MCP_MODEL_INFERENCE_URL=http://localhost:5005 -MCP_WEB_SEARCH_URL=http://localhost:5006 # Monitoring OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:18889 @@ -219,9 +211,6 @@ services: tool-customer-query: tool-destination-recommendation: tool-itinerary-planning: - tool-code-evaluation: - tool-model-inference: - tool-web-search: web-api: # Express API server web-ui: # Angular UI ``` @@ -243,14 +232,10 @@ MCP_ECHO_PING_URL=http://tool-echo-ping:3000 MCP_CUSTOMER_QUERY_URL=http://tool-customer-query:8080 MCP_DESTINATION_RECOMMENDATION_URL=http://tool-destination-recommendation:8080 MCP_ITINERARY_PLANNING_URL=http://tool-itinerary-planning:8000 -MCP_CODE_EVALUATION_URL=http://tool-code-evaluation:5000 -MCP_MODEL_INFERENCE_URL=http://tool-model-inference:5000 -MCP_WEB_SEARCH_URL=http://tool-web-search:5000 # External services (from azd provision) AZURE_OPENAI_ENDPOINT=https://your-openai.openai.azure.com/ AZURE_OPENAI_API_KEY=your-api-key -BING_SEARCH_API_KEY=your-bing-key # Docker-specific settings IS_LOCAL_DOCKER_ENV=true @@ -545,11 +530,6 @@ resource apiApp 'Microsoft.App/containerApps@2023-05-01' = { keyVaultUrl: '${keyVault.properties.vaultUri}secrets/azure-openai-key' identity: managedIdentity.id } - { - name: 'bing-search-key' - keyVaultUrl: '${keyVault.properties.vaultUri}secrets/bing-search-key' - identity: managedIdentity.id - } ] } template: { @@ -570,10 +550,6 @@ resource apiApp 'Microsoft.App/containerApps@2023-05-01' = { name: 'AZURE_OPENAI_API_KEY' secretRef: 'azure-openai-key' } - { - name: 'BING_SEARCH_API_KEY' - secretRef: 'bing-search-key' - } // MCP server URLs { name: 'MCP_ECHO_PING_URL' @@ -942,7 +918,7 @@ az acr build \ packages/api # MCP Server Images -for server in echo-ping customer-query destination-recommendation itinerary-planning code-evaluation model-inference web-search; do +for server in echo-ping customer-query destination-recommendation itinerary-planning; do echo "Building MCP server: $server" az acr build \ --registry $ACR_NAME \ diff --git a/docs/development-guide.md b/docs/development-guide.md index 8299f45c..11069ffb 100644 --- a/docs/development-guide.md +++ b/docs/development-guide.md @@ -273,10 +273,7 @@ packages/tools/ │ ├── AITravelAgent.sln # Solution file │ └── Dockerfile ├── destination-recommendation/ # Java/Spring Boot -├── itinerary-planning/ # Python/FastAPI -├── code-evaluation/ # Python -├── model-inference/ # Python with ONNX/vLLM -└── web-search/ # TypeScript/Node.js +└── itinerary-planning/ # Python/FastAPI ``` ## Development Workflow @@ -568,9 +565,6 @@ tool-my-new-server: export type McpServerName = | "echo-ping" | "customer-query" - | "web-search" - | "itinerary-planning" - | "model-inference" | "code-evaluation" | "destination-recommendation" | "my-new-server"; // Add new server @@ -997,7 +991,6 @@ test.describe('Travel Planning User Journey', () => { // Select tools await page.check('[data-testid="tool-echo-ping"]'); - await page.check('[data-testid="tool-web-search"]'); // Verify selection const newSelectedTools = await page.$$('[data-testid^="tool-"]:checked'); diff --git a/docs/flow-diagrams.md b/docs/flow-diagrams.md index 8bce7495..f53fffda 100644 --- a/docs/flow-diagrams.md +++ b/docs/flow-diagrams.md @@ -69,17 +69,14 @@ sequenceDiagram participant U as User participant UI as Angular UI participant API as Express API - participant LI as LlamaIndex + participant LI as (LlamaIndex, Lanchain.js, or MS Agent Framework) participant TA as Triage Agent participant CQA as Customer Query Agent participant DRA as Destination Rec Agent participant IPA as Itinerary Planning Agent - participant WSA as Web Search Agent participant MCPCQ as MCP Customer Query participant MCPDR as MCP Destination Rec participant MCPIP as MCP Itinerary Plan - participant MCPWS as MCP Web Search - participant BING as Bing Search API participant AOAI as Azure OpenAI U->>UI: Enter travel query "Plan 7-day Japan trip" @@ -93,7 +90,6 @@ sequenceDiagram LI->>CQA: Initialize Customer Query Agent LI->>DRA: Initialize Destination Rec Agent LI->>IPA: Initialize Itinerary Planning Agent - LI->>WSA: Initialize Web Search Agent API->>TA: process("Plan 7-day Japan trip") API-->>UI: SSE: AgentSetup event @@ -117,14 +113,6 @@ sequenceDiagram DRA-->>TA: return recommendations API-->>UI: SSE: AgentStream event (partial results) - TA->>WSA: handoff(search current travel info) - WSA->>MCPWS: callTool("search", "Japan travel 2024") - MCPWS->>BING: Search API request - BING-->>MCPWS: Current travel information - MCPWS-->>WSA: Processed search results - WSA-->>TA: return current info - API-->>UI: SSE: AgentStream event - TA->>IPA: handoff(create itinerary, destinations + preferences) IPA->>MCPIP: callTool("plan_itinerary", full_context) MCPIP->>AOAI: Generate detailed itinerary @@ -313,21 +301,12 @@ graph TD C -->|Destination| D[Destination Recommendation] C -->|Planning| E[Itinerary Planning] - C -->|Information| F[Web Search] - C -->|Complex Logic| G[Code Evaluation] - C -->|Custom Model| H[Model Inference] D --> I[MCP Destination Server] E --> J[MCP Itinerary Server] - F --> K[MCP Web Search Server] - G --> L[MCP Code Eval Server] - H --> M[MCP Model Server] I --> N[Response Synthesis] J --> N - K --> N - L --> N - M --> N N --> O[Final Response] ``` diff --git a/docs/maf-implementation-guide.md b/docs/maf-implementation-guide.md index b7ba171b..3f9f8a1d 100644 --- a/docs/maf-implementation-guide.md +++ b/docs/maf-implementation-guide.md @@ -188,9 +188,6 @@ class Settings(BaseSettings): mcp_customer_query_url: str mcp_destination_recommendation_url: str mcp_itinerary_planning_url: str - mcp_code_evaluation_url: str - mcp_model_inference_url: str - mcp_web_search_url: str mcp_echo_ping_url: str mcp_echo_ping_access_token: Optional[str] = None @@ -294,21 +291,6 @@ class ToolRegistry: settings.mcp_itinerary_planning_url ) - # Code Evaluation - self.clients["code-evaluation"] = HTTPMCPClient( - settings.mcp_code_evaluation_url - ) - - # Model Inference - self.clients["model-inference"] = HTTPMCPClient( - settings.mcp_model_inference_url - ) - - # Web Search - self.clients["web-search"] = HTTPMCPClient( - settings.mcp_web_search_url - ) - # Echo Ping self.clients["echo-ping"] = HTTPMCPClient( settings.mcp_echo_ping_url, @@ -385,9 +367,6 @@ class TriageAgent(BaseAgent): - CustomerQueryAgent: For understanding customer preferences - DestinationRecommendationAgent: For suggesting destinations - ItineraryPlanningAgent: For creating travel itineraries - - WebSearchAgent: For finding current travel information - - CodeEvaluationAgent: For calculations and data processing - - ModelInferenceAgent: For specialized AI tasks Always choose the most appropriate agent(s) for the query. You may coordinate multiple agents for complex queries. diff --git a/docs/maf-migration-plan.md b/docs/maf-migration-plan.md index 2466e794..0f8a58c2 100644 --- a/docs/maf-migration-plan.md +++ b/docs/maf-migration-plan.md @@ -32,10 +32,7 @@ This document outlines the migration strategy from the current LlamaIndex.TS orc 2. CustomerQueryAgent 3. DestinationRecommendationAgent 4. ItineraryPlanningAgent -5. CodeEvaluationAgent -6. ModelInferenceAgent -7. WebSearchAgent -8. EchoAgent +5. EchoAgent **Endpoints**: - `GET /api/health` - Health check @@ -113,9 +110,6 @@ We will use a parallel deployment strategy to minimize risk: - [ ] Implement CustomerQueryAgent - [ ] Implement DestinationRecommendationAgent - [ ] Implement ItineraryPlanningAgent -- [ ] Implement CodeEvaluationAgent -- [ ] Implement ModelInferenceAgent -- [ ] Implement WebSearchAgent - [ ] Implement EchoAgent - [ ] Create tool wrappers for MCP calls diff --git a/docs/maf-orchestration-design.md b/docs/maf-orchestration-design.md index f8a3f654..7a2cc918 100644 --- a/docs/maf-orchestration-design.md +++ b/docs/maf-orchestration-design.md @@ -7,14 +7,18 @@ This document outlines the design for reimplementing the orchestration layer of ## Background ### Current Implementation + The current orchestration layer uses: + - **Framework**: LlamaIndex.TS - **Language**: TypeScript/Node.js - **Location**: `packages/api/src/orchestrator/llamaindex/` - **Pattern**: Multi-agent workflow with triage agent as root ### New Implementation Goals + Migrate to Microsoft Agent Framework to: + - Leverage MAF's native agent orchestration capabilities - Use Python ecosystem for AI/ML workflows - Utilize MAF's workflow patterns and best practices @@ -47,9 +51,6 @@ Migrate to Microsoft Agent Framework to: │ │ │ - Customer Query Agent │ │ │ │ │ │ - Destination Recommendation Agent │ │ │ │ │ │ - Itinerary Planning Agent │ │ │ -│ │ │ - Code Evaluation Agent │ │ │ -│ │ │ - Model Inference Agent │ │ │ -│ │ │ - Web Search Agent │ │ │ │ │ │ - Echo Ping Agent │ │ │ │ │ └──────────────────────────────────────┘ │ │ │ │ │ │ @@ -71,6 +72,7 @@ Migrate to Microsoft Agent Framework to: ### Component Design #### 1. MAF Workflow Engine + - **Purpose**: Orchestrate multi-agent workflows - **Responsibilities**: - Coordinate agent execution @@ -80,7 +82,9 @@ Migrate to Microsoft Agent Framework to: - Error handling and recovery #### 2. MAF Agents + Each agent will be implemented as a MAF agent with: + - **Configuration**: Agent name, system prompt, capabilities - **Tools**: MCP tools specific to the agent's domain - **LLM Integration**: Azure OpenAI connection @@ -89,52 +93,47 @@ Each agent will be implemented as a MAF agent with: **Agent Definitions**: 1. **Triage Agent** (Root Agent) + - Role: Route queries to appropriate specialized agents - Tools: Access to all available tools for context - Handoff: Can delegate to any specialized agent 2. **Customer Query Agent** + - Role: Understand customer preferences and requirements - Tools: Customer query analysis tools (MCP server) 3. **Destination Recommendation Agent** + - Role: Suggest travel destinations - Tools: Destination recommendation tools (MCP server) 4. **Itinerary Planning Agent** + - Role: Create detailed travel itineraries - Tools: Itinerary planning tools (MCP server) -5. **Code Evaluation Agent** - - Role: Execute code and calculations - - Tools: Code evaluation tools (MCP server) - -6. **Model Inference Agent** - - Role: Perform specialized model inference - - Tools: Model inference tools (MCP server) - -7. **Web Search Agent** - - Role: Search web for travel information - - Tools: Web search tools (MCP server) - -8. **Echo Ping Agent** +5. **Echo Ping Agent** - Role: Echo back input for testing - Tools: Echo ping tools (MCP server) #### 3. MCP Client Integration **HTTP MCP Client**: + - Support for HTTP-based MCP tool servers - Request/response handling - Retry logic with exponential backoff - Error handling **SSE MCP Client**: + - Support for Server-Sent Events based MCP servers - Streaming response handling - Connection management **Tool Registry**: + - Centralized tool configuration - Dynamic tool loading based on selected tools - Tool metadata and capabilities @@ -142,34 +141,35 @@ Each agent will be implemented as a MAF agent with: ### Workflow Patterns #### 1. Sequential Workflow + ```python # Example: Customer Query → Destination → Itinerary async def sequential_travel_planning(user_query: str): # Step 1: Understand customer preferences preferences = await customer_query_agent.process(user_query) - + # Step 2: Get destination recommendations destinations = await destination_agent.process(preferences) - + # Step 3: Create itinerary itinerary = await itinerary_agent.process({ "destinations": destinations, "preferences": preferences }) - + return itinerary ``` #### 2. Parallel Workflow + ```python # Example: Get destination recommendations and current travel data in parallel async def parallel_travel_research(preferences: dict): # Execute multiple agents in parallel results = await asyncio.gather( destination_agent.process(preferences), - web_search_agent.process({"query": "current travel conditions"}) ) - + return { "recommendations": results[0], "current_data": results[1] @@ -177,12 +177,13 @@ async def parallel_travel_research(preferences: dict): ``` #### 3. Conditional Workflow + ```python # Example: Route based on query type async def conditional_routing(user_query: str): # Triage agent determines the workflow intent = await triage_agent.analyze_intent(user_query) - + if intent.type == "destination_search": return await destination_agent.process(user_query) elif intent.type == "itinerary_planning": @@ -194,6 +195,7 @@ async def conditional_routing(user_query: str): ## Implementation Plan ### Directory Structure + ``` src/ ├── api/ # Existing TypeScript API (to be maintained or replaced) @@ -215,9 +217,6 @@ src/ │ │ │ ├── customer_query_agent.py │ │ │ ├── destination_agent.py │ │ │ ├── itinerary_agent.py - │ │ │ ├── code_eval_agent.py - │ │ │ ├── model_inference_agent.py - │ │ │ ├── web_search_agent.py │ │ │ └── echo_agent.py │ │ └── tools/ # MCP tool integration │ │ ├── __init__.py @@ -244,6 +243,7 @@ src/ ### Technology Stack **Core Dependencies**: + - `agent-framework` - Microsoft Agent Framework Python SDK - `fastapi` - Web framework for API - `uvicorn` - ASGI server @@ -259,6 +259,7 @@ src/ ### Configuration **Environment Variables**: + ```txt # Azure OpenAI Configuration AZURE_OPENAI_ENDPOINT= @@ -270,9 +271,6 @@ AZURE_OPENAI_API_VERSION= MCP_CUSTOMER_QUERY_URL= MCP_DESTINATION_RECOMMENDATION_URL= MCP_ITINERARY_PLANNING_URL= -MCP_CODE_EVALUATION_URL= -MCP_MODEL_INFERENCE_URL= -MCP_WEB_SEARCH_URL= MCP_ECHO_PING_URL= MCP_ECHO_PING_ACCESS_TOKEN= @@ -288,17 +286,20 @@ OTEL_SERVICE_NAME=api-python ## Migration Strategy ### Phase 1: Parallel Deployment + 1. Deploy new Python API alongside existing TypeScript API 2. Use feature flags or separate endpoints for testing 3. Gradually migrate traffic to Python API ### Phase 2: Integration Testing + 1. Validate all agent workflows 2. Test MCP tool integration 3. Performance benchmarking 4. End-to-end testing with UI ### Phase 3: Full Migration + 1. Update UI to point to new Python API 2. Update Docker Compose configuration 3. Update Azure deployment configurations @@ -307,30 +308,35 @@ OTEL_SERVICE_NAME=api-python ## Best Practices from MAF ### 1. Agent Design + - Keep agents focused on specific tasks - Use clear system prompts - Implement proper error handling - Add telemetry and logging ### 2. Workflow Design + - Design workflows for observability - Implement retry and fallback strategies - Use async/await for concurrent operations - Maintain conversation context ### 3. Tool Integration + - Validate tool inputs and outputs - Implement timeout handling - Add circuit breaker patterns for external services - Cache responses when appropriate ### 4. State Management + - Use MAF's built-in state management - Implement proper session handling - Track conversation history - Manage workflow context ### 5. Observability + - Instrument all agents and workflows - Use structured logging - Implement distributed tracing @@ -339,16 +345,19 @@ OTEL_SERVICE_NAME=api-python ## Testing Strategy ### Unit Tests + - Test individual agent implementations - Test MCP client implementations - Test workflow logic ### Integration Tests + - Test agent-to-agent handoffs - Test MCP tool integration - Test workflow orchestration ### End-to-End Tests + - Test complete user workflows - Test streaming responses - Test error scenarios @@ -357,6 +366,7 @@ OTEL_SERVICE_NAME=api-python ## Performance Considerations ### Optimization Strategies + 1. **Connection Pooling**: Reuse HTTP connections to MCP servers 2. **Caching**: Cache tool responses and agent outputs where appropriate 3. **Parallel Execution**: Use asyncio for concurrent agent operations @@ -364,6 +374,7 @@ OTEL_SERVICE_NAME=api-python 5. **Resource Management**: Proper cleanup of connections and resources ### Monitoring Metrics + - Request latency - Agent execution time - MCP tool call latency @@ -373,12 +384,14 @@ OTEL_SERVICE_NAME=api-python ## Security Considerations ### Authentication & Authorization + - Secure API endpoints - Validate MCP tool access - Implement rate limiting - Use Azure Managed Identity where possible ### Data Protection + - Sanitize user inputs - Validate tool outputs - Implement proper error messages (no sensitive data) diff --git a/docs/maf-quick-reference.md b/docs/maf-quick-reference.md index 20f64f77..dbb27fe5 100644 --- a/docs/maf-quick-reference.md +++ b/docs/maf-quick-reference.md @@ -219,7 +219,7 @@ async def parallel_agent_execution(agents: list, query: str): # Usage results = await parallel_agent_execution( - [destination_agent, web_search_agent], + [destination_agent, itinerary_agent], "Find destinations in Europe" ) ``` diff --git a/docs/maf-visual-guide.md b/docs/maf-visual-guide.md index 778571ce..4ee01efe 100644 --- a/docs/maf-visual-guide.md +++ b/docs/maf-visual-guide.md @@ -26,9 +26,6 @@ flowchart TB CQA[CustomerQueryAgent] DRA[DestinationRecommendationAgent] IPA[ItineraryPlanningAgent] - CEA[CodeEvaluationAgent] - MIA[ModelInferenceAgent] - WSA[WebSearchAgent] EA[EchoAgent] end subgraph MCP_CLIENT["MCP Client Integration"] @@ -42,9 +39,6 @@ flowchart TB CQ[Customer Query
C#/.NET
Port 5001] DR[Destination Recommend
Java
Port 5002] IP[Itinerary Planning
Python
Port 5003] - CE[Code Eval
Python
Port 5004] - MI[Model Inference
Python
Port 5005] - WS[Web Search
TypeScript
Port 5006] end UI -->|HTTP/SSE| API @@ -72,9 +66,6 @@ flowchart TB CQA[CustomerQueryAgent] DRA[DestinationRecommendationAgent] IPA[ItineraryPlanningAgent] - CEA[CodeEvaluationAgent] - MIA[ModelInferenceAgent] - WSA[WebSearchAgent] EA[EchoAgent] end subgraph MCP_CLIENT["MCP Client Integration - Python"] @@ -89,9 +80,6 @@ flowchart TB CQ[Customer Query
C#/.NET
Port 5001] DR[Destination Recommend
Java
Port 5002] IP[Itinerary Planning
Python
Port 5003] - CE[Code Eval
Python
Port 5004] - MI[Model Inference
Python
Port 5005] - WS[Web Search
TypeScript
Port 5006] end UI -->|HTTP/SSE
Same API contract| API @@ -120,9 +108,6 @@ flowchart TB CQ_A[customer_query_agent.py] DEST_A[destination_agent.py] ITIN_A[itinerary_agent.py] - CODE_A[code_eval_agent.py] - MODEL_A[model_inference_agent.py] - WEB_A[web_search_agent.py] ECHO_A[echo_agent.py] end @@ -181,10 +166,6 @@ flowchart TD DRA[Destination Recommendation Agent
Returns:
• Santorini
• Amalfi Coast
• Algarve] - WSA[Web Search Agent
Search for:
• Current weather
• Travel deals
• Events] - - CEA[Code Evaluation Agent
Calculate:
• Best time
• Price ranges] - COMBINE[Combine Results
• Destinations
• Current info
• Price analysis] FR[Final Response] @@ -194,8 +175,6 @@ flowchart TD TA --> WSA TA --> CEA DRA --> COMBINE - WSA --> COMBINE - CEA --> COMBINE COMBINE --> FR ``` @@ -292,9 +271,6 @@ src/ │ ├── customer-query/ # C#/.NET │ ├── destination-recommendation/ # Java │ ├── itinerary-planning/ # Python -│ ├── code-evaluation/ # Python -│ ├── model-inference/ # Python -│ ├── web-search/ # TypeScript │ └── echo-ping/ # TypeScript └── ui/ # Angular frontend ``` diff --git a/docs/mcp-servers.md b/docs/mcp-servers.md index 5492795f..e20ea015 100644 --- a/docs/mcp-servers.md +++ b/docs/mcp-servers.md @@ -971,167 +971,6 @@ class ModelInferenceService: | `generate_embeddings` | Generate text embeddings | `{texts: string[]}` | `{embeddings: number[][]}` | | `custom_inference` | Run custom model inference | `{model_name: string, inputs: object}` | `{outputs: object}` | -### 7. Web Search Server (TypeScript) - -**Purpose**: Real-time web search for travel information using Bing Search API -**Port**: 5006 (5000 internal) -**Technology**: TypeScript, Express.js, Bing Search API, Azure AI Grounding - -#### Architecture - -```typescript -import express from 'express'; -import { BingSearchClient } from '@azure/cognitiveservices-websearch'; -import { DefaultAzureCredential } from '@azure/identity'; -import { trace, metrics } from '@opentelemetry/api'; - -const app = express(); -const tracer = trace.getTracer('web-search-server'); -const meter = metrics.getMeter('web-search-server'); - -class WebSearchService { - private bingClient: BingSearchClient; - private searchCounter = meter.createCounter('web_searches_total'); - private searchDuration = meter.createHistogram('web_search_duration_ms'); - - constructor() { - this.bingClient = new BingSearchClient( - new DefaultAzureCredential(), - process.env.BING_SEARCH_ENDPOINT! - ); - } - - async searchTravel(query: string, options: SearchOptions = {}): Promise { - const span = tracer.startSpan('search_travel'); - const startTime = Date.now(); - - try { - // Enhance query with travel context - const enhancedQuery = this.enhanceQueryForTravel(query); - - // Perform Bing search - const searchResponse = await this.bingClient.web.search(enhancedQuery, { - count: options.count || 10, - offset: options.offset || 0, - market: options.market || 'en-US', - safeSearch: 'Moderate', - freshness: options.freshness || 'Month' - }); - - // Process and filter results - const processedResults = this.processSearchResults(searchResponse, query); - - // Apply travel-specific scoring - const scoredResults = this.scoreForTravelRelevance(processedResults); - - this.searchCounter.add(1, { status: 'success', type: 'travel' }); - span.setStatus({ code: SpanStatusCode.OK }); - - return { - query: enhancedQuery, - results: scoredResults, - totalEstimatedMatches: searchResponse.webPages?.totalEstimatedMatches || 0, - metadata: { - searchTime: Date.now() - startTime, - market: options.market, - freshness: options.freshness - } - }; - - } catch (error) { - this.searchCounter.add(1, { status: 'error', type: 'travel' }); - span.setStatus({ code: SpanStatusCode.ERROR, message: error.message }); - throw error; - } finally { - this.searchDuration.record(Date.now() - startTime); - span.end(); - } - } - - private enhanceQueryForTravel(query: string): string { - // Add travel-specific context to improve results - const travelKeywords = [ - 'travel', 'tourism', 'vacation', 'trip', 'visit', - 'destination', 'attractions', 'hotels', 'flights' - ]; - - const hasTravel = travelKeywords.some(keyword => - query.toLowerCase().includes(keyword) - ); - - if (!hasTravel) { - return `${query} travel tourism vacation`; - } - - return query; - } - - private processSearchResults(searchResponse: any, originalQuery: string): ProcessedResult[] { - const results = searchResponse.webPages?.value || []; - - return results.map((result: any) => ({ - title: result.name, - url: result.url, - snippet: result.snippet, - dateLastCrawled: result.dateLastCrawled, - displayUrl: result.displayUrl, - travelRelevance: this.calculateTravelRelevance(result, originalQuery), - extractedInfo: this.extractTravelInfo(result) - })); - } - - private calculateTravelRelevance(result: any, query: string): number { - let score = 0; - - // Travel domain indicators - const travelDomains = [ - 'tripadvisor', 'booking.com', 'expedia', 'airbnb', - 'hotels.com', 'kayak', 'skyscanner', 'lonelyplanet' - ]; - - if (travelDomains.some(domain => result.url.includes(domain))) { - score += 0.3; - } - - // Travel keywords in title/snippet - const travelTerms = [ - 'hotel', 'flight', 'destination', 'attraction', 'restaurant', - 'review', 'guide', 'itinerary', 'activity', 'tour' - ]; - - const text = `${result.name} ${result.snippet}`.toLowerCase(); - const matchingTerms = travelTerms.filter(term => text.includes(term)); - score += (matchingTerms.length / travelTerms.length) * 0.4; - - // Query term matching - const queryTerms = query.toLowerCase().split(' '); - const matchingQuery = queryTerms.filter(term => text.includes(term)); - score += (matchingQuery.length / queryTerms.length) * 0.3; - - return Math.min(score, 1.0); - } - - private extractTravelInfo(result: any): TravelInfo { - const snippet = result.snippet.toLowerCase(); - - return { - priceIndicators: this.extractPrices(snippet), - locationMentions: this.extractLocations(snippet), - activityTypes: this.extractActivities(snippet), - ratings: this.extractRatings(snippet) - }; - } -} -``` - -#### Available Tools - -| Tool Name | Description | Input Schema | Output | -|-----------|-------------|--------------|---------| -| `search_travel_info` | Search for travel-related information | `{query: string, options?: SearchOptions}` | `{results: SearchResult[], metadata: object}` | -| `search_destinations` | Search for destination information | `{destination: string, type?: string}` | `{destination_info: DestinationInfo}` | -| `search_accommodations` | Search for accommodation options | `{location: string, dates?: DateRange, filters?: object}` | `{accommodations: AccommodationResult[]}` | -| `search_activities` | Search for activities and attractions | `{location: string, activity_types?: string[]}` | `{activities: ActivityResult[]}` | ## Communication Protocols diff --git a/docs/orchestration.md b/docs/orchestration.md index 941fcbc6..7ada6082 100644 --- a/docs/orchestration.md +++ b/docs/orchestration.md @@ -237,9 +237,6 @@ All three orchestration approaches share: - Customer Query Server (C#/.NET) - Destination Recommendation (Java) - Itinerary Planning (Python) -- Travel Restrictions (TypeScript) -- Web Search (TypeScript) -- Code Interpreter (Python) - Echo Ping (TypeScript) ### Same Core Capabilities diff --git a/docs/overview.md b/docs/overview.md index 659b4a51..4903012f 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -59,7 +59,7 @@ Located in `packages/api-python/`, this is a complete, production-ready alternat **Key Features**: - ✅ Native Azure OpenAI integration (plus GitHub Models, Ollama, Docker Models) -- ✅ 7 specialized agents (Customer Query, Itinerary Planning, Destination, Code Eval, Model Inference, Web Search, Echo) +- ✅ 4 specialized agents (Customer Query, Itinerary Planning, Destination, Echo) - ✅ Graceful degradation when MCP servers are unavailable - ✅ OpenTelemetry observability ready - ✅ Proper async lifecycle management @@ -83,7 +83,7 @@ The Azure AI Travel Agents system is built on a microservices architecture with - **Current**: Express.js with LangChain.js orchestration (TypeScript) - **Alternative 1**: Express.js with LlamaIndex.TS orchestration (TypeScript) - **Alternative 2**: FastAPI with Microsoft Agent Framework (Python) -- **MCP Servers**: 7 specialized services in TypeScript, C#, Java, and Python +- **MCP Servers**: 4 specialized services in TypeScript, C#, Java, and Python - **AI Services**: Azure OpenAI and custom model inference - **Monitoring**: OpenTelemetry with Aspire Dashboard - **Deployment**: Docker containers on Azure Container Apps @@ -205,9 +205,6 @@ The system provides two orchestration approaches, each with distinct advantages: - CustomerQueryAgent - DestinationRecommendationAgent - ItineraryPlanningAgent -- CodeEvaluationAgent -- ModelInferenceAgent -- WebSearchAgent - EchoAgent (testing) **Best For**: Python-first teams, teams wanting native MAF SDK, projects leveraging Python's AI ecosystem diff --git a/docs/technical-architecture.md b/docs/technical-architecture.md index 958bdcc8..82e75c13 100644 --- a/docs/technical-architecture.md +++ b/docs/technical-architecture.md @@ -34,9 +34,6 @@ The Azure AI Travel Agents is a sophisticated microservices-based AI application - **Natural Language Processing**: Understanding and extracting user preferences from conversational input - **Intelligent Routing**: Triage agent determines which specialized agents to engage -- **Real-time Data Access**: Live web search integration for up-to-date travel information -- **Code Execution**: Dynamic code evaluation for complex logic and calculations -- **Local AI Inference**: On-demand model inference using ONNX and vLLM - **Comprehensive Planning**: End-to-end itinerary creation and destination recommendations ## Architecture Components @@ -112,24 +109,6 @@ The system includes seven specialized MCP servers, each serving a specific domai - **Technology**: Python, FastAPI - **Features**: Multi-day planning, activity scheduling -#### 3.5 Code Evaluation Server (Python) -- **Purpose**: Dynamic code execution and evaluation -- **Port**: 5004 (5000 internal) -- **Technology**: Python -- **Features**: Secure code execution, result processing - -#### 3.6 Model Inference Server (Python) -- **Purpose**: Local LLM inference using ONNX/vLLM -- **Port**: 5005 (5000 internal) -- **Technology**: Python, ONNX Runtime, vLLM -- **Features**: GPU acceleration, model optimization - -#### 3.7 Web Search Server (TypeScript) -- **Purpose**: Real-time web search for travel information -- **Port**: 5006 (5000 internal) -- **Technology**: TypeScript, Bing Search API -- **Features**: Grounding with Bing Search, travel data aggregation - ### 4. Monitoring Layer (Aspire Dashboard) **Technology Stack:** @@ -183,7 +162,7 @@ User Input → Angular UI → API Server → LangChain.js Orchestrator → Super 6. **MCP Server Communication** - HTTP/SSE connections to relevant MCP servers - Tool-specific processing (search, recommendation, planning, etc.) - - External API calls as needed (Bing Search, Azure OpenAI, etc.) + - External API calls as needed (Azure OpenAI, etc.) 7. **Response Aggregation** - Results collected from all engaged agents @@ -340,13 +319,13 @@ const result = await mcpClient.callTool("echo", { input: "Hello" }); // Client configuration const mcpClient = new MCPSSEClient( "llamaindex-sse-client", - "http://tool-web-search:5000/sse", + "http://tool-customer-query:8080/mcp", accessToken ); // Streaming tool invocation const tools = await mcpClient.listTools(); -const result = await mcpClient.callTool("search", { query: "hotels in Tokyo" }); +const result = await mcpClient.callTool("extract_preferences", { query: "I want to visit Japan" }); ``` ### Tool Discovery and Registration @@ -512,30 +491,6 @@ See [Orchestration Options](./orchestration.md) for detailed comparison and migr - Handle logistics and scheduling - **Tools**: Itinerary algorithms, scheduling optimization -#### Web Search Agent -- **Role**: Real-time information gatherer -- **Responsibilities**: - - Search for current travel information - - Validate recommendations with live data - - Provide up-to-date pricing and availability -- **Tools**: Bing Search API, data aggregation - -#### Code Evaluation Agent -- **Role**: Dynamic computation specialist -- **Responsibilities**: - - Execute custom logic and calculations - - Handle complex data processing - - Provide algorithmic solutions -- **Tools**: Python code execution, mathematical computation - -#### Model Inference Agent -- **Role**: Local AI processing -- **Responsibilities**: - - Provide specialized model inference - - Handle GPU-accelerated computations - - Support custom model deployments -- **Tools**: ONNX runtime, vLLM, GPU acceleration - ### Agent Handoff Patterns ```typescript @@ -550,7 +505,7 @@ if (needsDestinationRecommendation) { // Parallel execution const [recommendations, currentData] = await Promise.all([ callAgent("DestinationRecommendationAgent", preferences), - callAgent("WebSearchAgent", { query: "current travel conditions" }) + callAgent("CustomerQueryAgent", userInput) ]); // Sequential handoff chain @@ -574,12 +529,10 @@ sequenceDiagram participant Orchestrator as Agent Orchestrator participant TriageAgent as Triage/Supervisor Agent participant DestAgent as Destination Agent - participant WebAgent as Web Search Agent participant ItinAgent as Itinerary Agent participant MCPDest as MCP Destination Server participant MCPWeb as MCP Web Server participant MCPItin as MCP Itinerary Server - participant BingAPI as Bing Search API User->>UI: Submit travel query UI->>API: POST /api/chat @@ -596,8 +549,6 @@ sequenceDiagram TriageAgent->>WebAgent: handoff(destinationQuery) WebAgent->>MCPWeb: callTool("search", query) - MCPWeb->>BingAPI: search request - BingAPI-->>MCPWeb: search results MCPWeb-->>WebAgent: processed results WebAgent-->>TriageAgent: current travel data @@ -748,16 +699,12 @@ NODE_ENV=development # MCP Server URLs (local) MCP_ECHO_PING_URL=http://localhost:5007 MCP_CUSTOMER_QUERY_URL=http://localhost:5001 -MCP_WEB_SEARCH_URL=http://localhost:5006 MCP_ITINERARY_PLANNING_URL=http://localhost:5003 -MCP_MODEL_INFERENCE_URL=http://localhost:5005 -MCP_CODE_EVALUATION_URL=http://localhost:5004 MCP_DESTINATION_RECOMMENDATION_URL=http://localhost:5002 # Azure Services AZURE_OPENAI_ENDPOINT=... AZURE_OPENAI_API_KEY=... -BING_SEARCH_API_KEY=... # Monitoring OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:18889 @@ -768,10 +715,7 @@ OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:18889 # MCP Server URLs (Docker internal) MCP_ECHO_PING_URL=http://tool-echo-ping:3000 MCP_CUSTOMER_QUERY_URL=http://tool-customer-query:8080 -MCP_WEB_SEARCH_URL=http://tool-web-search:5000 MCP_ITINERARY_PLANNING_URL=http://tool-itinerary-planning:8000 -MCP_MODEL_INFERENCE_URL=http://tool-model-inference:5000 -MCP_CODE_EVALUATION_URL=http://tool-code-evaluation:5000 MCP_DESTINATION_RECOMMENDATION_URL=http://tool-destination-recommendation:8080 # Docker-specific settings @@ -797,9 +741,6 @@ OTEL_EXPORTER_OTLP_ENDPOINT=http://aspire-dashboard:18889 - **UI**: Minimal resources (static serving) - **API**: CPU-intensive for agent orchestration - **MCP Servers**: Varies by function - - Code evaluation: High CPU/memory - - Model inference: GPU requirements - - Web search: Network-intensive - Others: Moderate resource usage #### Performance Optimization diff --git a/infra/hooks/mcp/setup.ps1 b/infra/hooks/mcp/setup.ps1 index 9b2abedf..665ebcf8 100755 --- a/infra/hooks/mcp/setup.ps1 +++ b/infra/hooks/mcp/setup.ps1 @@ -3,7 +3,7 @@ ########################################################################## # MCP Tools ########################################################################## -$tools = @('echo-ping', 'customer-query', 'destination-recommendation', 'itinerary-planning', 'code-evaluation', 'model-inference', 'web-search') +$tools = @('echo-ping', 'customer-query', 'destination-recommendation', 'itinerary-planning') Write-Host '>> Creating .env file for the MCP servers...' foreach ($tool in $tools) { diff --git a/infra/hooks/mcp/setup.sh b/infra/hooks/mcp/setup.sh index c2d03408..78ad9055 100755 --- a/infra/hooks/mcp/setup.sh +++ b/infra/hooks/mcp/setup.sh @@ -5,7 +5,7 @@ ########################################################################## # MCP Tools ########################################################################## -tools="echo-ping customer-query destination-recommendation itinerary-planning code-evaluation model-inference web-search" +tools="echo-ping customer-query destination-recommendation itinerary-planning" printf ">> Creating .env file for the MCP servers...\n" # for each tool copy the .env.sample (if it exists) to .env and .env.docker (dont overwrite existing .env files) diff --git a/infra/hooks/postprovision.ps1 b/infra/hooks/postprovision.ps1 index ed3efa49..02fd08ea 100755 --- a/infra/hooks/postprovision.ps1 +++ b/infra/hooks/postprovision.ps1 @@ -26,9 +26,6 @@ if (-not (Test-Path $apiEnvPath)) { "MCP_CUSTOMER_QUERY_URL=http://localhost:8080" | Add-Content $apiEnvPath "MCP_DESTINATION_RECOMMENDATION_URL=http://localhost:5002" | Add-Content $apiEnvPath "MCP_ITINERARY_PLANNING_URL=http://localhost:5003" | Add-Content $apiEnvPath - "MCP_CODE_EVALUATION_URL=http://localhost:5004" | Add-Content $apiEnvPath - "MCP_MODEL_INFERENCE_URL=http://localhost:5005" | Add-Content $apiEnvPath - "MCP_WEB_SEARCH_URL=http://localhost:5006" | Add-Content $apiEnvPath "MCP_ECHO_PING_URL=http://localhost:5007" | Add-Content $apiEnvPath "MCP_ECHO_PING_ACCESS_TOKEN=123-this-is-a-fake-token-please-use-a-token-provider" | Add-Content $apiEnvPath "" | Add-Content $apiEnvPath @@ -46,9 +43,6 @@ if (-not (Test-Path $apiEnvDockerPath)) { "MCP_CUSTOMER_QUERY_URL=http://tool-customer-query:8080" | Add-Content $apiEnvDockerPath "MCP_DESTINATION_RECOMMENDATION_URL=http://tool-destination-recommendation:5002" | Add-Content $apiEnvDockerPath "MCP_ITINERARY_PLANNING_URL=http://tool-itinerary-planning:5003" | Add-Content $apiEnvDockerPath - "MCP_CODE_EVALUATION_URL=http://tool-code-evaluation:5004" | Add-Content $apiEnvDockerPath - "MCP_MODEL_INFERENCE_URL=http://tool-model-inference:5005" | Add-Content $apiEnvDockerPath - "MCP_WEB_SEARCH_URL=http://tool-web-search:5006" | Add-Content $apiEnvDockerPath "MCP_ECHO_PING_URL=http://tool-echo-ping:5007" | Add-Content $apiEnvDockerPath } @@ -90,7 +84,7 @@ if (-not (Test-Path "./packages/ui/node_modules")) { ########################################################################## # MCP Tools ########################################################################## -$tools = @('echo-ping', 'customer-query', 'destination-recommendation', 'itinerary-planning', 'code-evaluation', 'model-inference', 'web-search') +$tools = @('echo-ping', 'customer-query', 'destination-recommendation', 'itinerary-planning') Write-Host ">> Creating .env file for the MCP servers..." foreach ($tool in $tools) { diff --git a/infra/hooks/postprovision.sh b/infra/hooks/postprovision.sh index ecbb0e31..4809fc65 100755 --- a/infra/hooks/postprovision.sh +++ b/infra/hooks/postprovision.sh @@ -27,9 +27,6 @@ if [ ! -f ./packages/api/.env ]; then echo "MCP_CUSTOMER_QUERY_URL=http://localhost:8080" >> ./packages/api/.env echo "MCP_DESTINATION_RECOMMENDATION_URL=http://localhost:5002" >> ./packages/api/.env echo "MCP_ITINERARY_PLANNING_URL=http://localhost:5003" >> ./packages/api/.env - echo "MCP_CODE_EVALUATION_URL=http://localhost:5004" >> ./packages/api/.env - echo "MCP_MODEL_INFERENCE_URL=http://localhost:5005" >> ./packages/api/.env - echo "MCP_WEB_SEARCH_URL=http://localhost:5006" >> ./packages/api/.env echo "MCP_ECHO_PING_URL=http://localhost:5007" >> ./packages/api/.env echo "MCP_ECHO_PING_ACCESS_TOKEN=123-this-is-a-fake-token-please-use-a-token-provider" >> ./packages/api/.env echo "" @@ -46,9 +43,6 @@ if [ ! -f ./packages/api/.env.docker ]; then echo "MCP_CUSTOMER_QUERY_URL=http://tool-customer-query:8080" >> ./packages/api/.env.docker echo "MCP_DESTINATION_RECOMMENDATION_URL=http://tool-destination-recommendation:5002" >> ./packages/api/.env.docker echo "MCP_ITINERARY_PLANNING_URL=http://tool-itinerary-planning:5003" >> ./packages/api/.env.docker - echo "MCP_CODE_EVALUATION_URL=http://tool-code-evaluation:5004" >> ./packages/api/.env.docker - echo "MCP_MODEL_INFERENCE_URL=http://tool-model-inference:5005" >> ./packages/api/.env.docker - echo "MCP_WEB_SEARCH_URL=http://tool-web-search:5006" >> ./packages/api/.env.docker echo "MCP_ECHO_PING_URL=http://tool-echo-ping:5007" >> ./packages/api/.env.docker fi diff --git a/infra/resources.bicep b/infra/resources.bicep index e76c6830..e888a4d8 100644 --- a/infra/resources.bicep +++ b/infra/resources.bicep @@ -201,18 +201,6 @@ module api 'br/public:avm/res/app/container-app:0.8.0' = { name: 'MCP_ECHO_PING_URL' value: 'https://echo-ping.internal.${containerAppsEnvironment.outputs.defaultDomain}' } - { - name: 'MCP_WEB_SEARCH_URL' - value: 'https://web-search.internal.${containerAppsEnvironment.outputs.defaultDomain}' - } - { - name: 'MCP_MODEL_INFERENCE_URL' - value: 'https://model-inference.internal.${containerAppsEnvironment.outputs.defaultDomain}' - } - { - name: 'MCP_CODE_EVALUATION_URL' - value: 'https://code-evaluation.internal.${containerAppsEnvironment.outputs.defaultDomain}' - } { name: 'MCP_ECHO_PING_ACCESS_TOKEN' value: llamaIndexConfig.sampleAccessTokens.echo diff --git a/llms.txt b/llms.txt index c86f8939..4f415b83 100644 --- a/llms.txt +++ b/llms.txt @@ -715,18 +715,6 @@ module api 'br/public:avm/res/app/container-app:0.8.0' = { name: 'MCP_ECHO_PING_URL' value: 'https://echo-ping.internal.${containerAppsEnvironment.outputs.defaultDomain}' } - { - name: 'MCP_WEB_SEARCH_URL' - value: 'https://web-search.internal.${containerAppsEnvironment.outputs.defaultDomain}' - } - { - name: 'MCP_MODEL_INFERENCE_URL' - value: 'https://model-inference.internal.${containerAppsEnvironment.outputs.defaultDomain}' - } - { - name: 'MCP_CODE_EVALUATION_URL' - value: 'https://code-evaluation.internal.${containerAppsEnvironment.outputs.defaultDomain}' - } { name: 'MCP_ECHO_PING_ACCESS_TOKEN' value: llamaIndexConfig.sampleAccessTokens.echo @@ -1270,7 +1258,7 @@ output AZURE_CLIENT_ID string = apiIdentity.outputs.clientId ########################################################################## # MCP Tools ########################################################################## -$tools = @('echo-ping', 'customer-query', 'destination-recommendation', 'itinerary-planning', 'code-evaluation', 'model-inference', 'web-search') +$tools = @('echo-ping', 'customer-query', 'destination-recommendation', 'itinerary-planning') Write-Host '>> Creating .env file for the MCP servers...' foreach ($tool in $tools) { @@ -1380,9 +1368,6 @@ if (-not (Test-Path $apiEnvPath)) { "MCP_CUSTOMER_QUERY_URL=http://localhost:8080" | Add-Content $apiEnvPath "MCP_DESTINATION_RECOMMENDATION_URL=http://localhost:5002" | Add-Content $apiEnvPath "MCP_ITINERARY_PLANNING_URL=http://localhost:5003" | Add-Content $apiEnvPath - "MCP_CODE_EVALUATION_URL=http://localhost:5004" | Add-Content $apiEnvPath - "MCP_MODEL_INFERENCE_URL=http://localhost:5005" | Add-Content $apiEnvPath - "MCP_WEB_SEARCH_URL=http://localhost:5006" | Add-Content $apiEnvPath "MCP_ECHO_PING_URL=http://localhost:5007" | Add-Content $apiEnvPath "MCP_ECHO_PING_ACCESS_TOKEN=123-this-is-a-fake-token-please-use-a-token-provider" | Add-Content $apiEnvPath "" | Add-Content $apiEnvPath @@ -1400,9 +1385,6 @@ if (-not (Test-Path $apiEnvDockerPath)) { "MCP_CUSTOMER_QUERY_URL=http://tool-customer-query:8080" | Add-Content $apiEnvDockerPath "MCP_DESTINATION_RECOMMENDATION_URL=http://tool-destination-recommendation:5002" | Add-Content $apiEnvDockerPath "MCP_ITINERARY_PLANNING_URL=http://tool-itinerary-planning:5003" | Add-Content $apiEnvDockerPath - "MCP_CODE_EVALUATION_URL=http://tool-code-evaluation:5004" | Add-Content $apiEnvDockerPath - "MCP_MODEL_INFERENCE_URL=http://tool-model-inference:5005" | Add-Content $apiEnvDockerPath - "MCP_WEB_SEARCH_URL=http://tool-web-search:5006" | Add-Content $apiEnvDockerPath "MCP_ECHO_PING_URL=http://tool-echo-ping:5007" | Add-Content $apiEnvDockerPath } @@ -1444,7 +1426,7 @@ if (-not (Test-Path "./packages/ui/node_modules")) { ########################################################################## # MCP Tools ########################################################################## -$tools = @('echo-ping', 'customer-query', 'destination-recommendation', 'itinerary-planning', 'code-evaluation', 'model-inference', 'web-search') +$tools = @('echo-ping', 'customer-query', 'destination-recommendation', 'itinerary-planning') Write-Host ">> Creating .env file for the MCP servers..." foreach ($tool in $tools) { @@ -1516,9 +1498,6 @@ if [ ! -f ./packages/api/.env ]; then echo "MCP_CUSTOMER_QUERY_URL=http://localhost:8080" >> ./packages/api/.env echo "MCP_DESTINATION_RECOMMENDATION_URL=http://localhost:5002" >> ./packages/api/.env echo "MCP_ITINERARY_PLANNING_URL=http://localhost:5003" >> ./packages/api/.env - echo "MCP_CODE_EVALUATION_URL=http://localhost:5004" >> ./packages/api/.env - echo "MCP_MODEL_INFERENCE_URL=http://localhost:5005" >> ./packages/api/.env - echo "MCP_WEB_SEARCH_URL=http://localhost:5006" >> ./packages/api/.env echo "MCP_ECHO_PING_URL=http://localhost:5007" >> ./packages/api/.env echo "MCP_ECHO_PING_ACCESS_TOKEN=123-this-is-a-fake-token-please-use-a-token-provider" >> ./packages/api/.env echo "" @@ -1535,9 +1514,6 @@ if [ ! -f ./packages/api/.env.docker ]; then echo "MCP_CUSTOMER_QUERY_URL=http://tool-customer-query:8080" >> ./packages/api/.env.docker echo "MCP_DESTINATION_RECOMMENDATION_URL=http://tool-destination-recommendation:5002" >> ./packages/api/.env.docker echo "MCP_ITINERARY_PLANNING_URL=http://tool-itinerary-planning:5003" >> ./packages/api/.env.docker - echo "MCP_CODE_EVALUATION_URL=http://tool-code-evaluation:5004" >> ./packages/api/.env.docker - echo "MCP_MODEL_INFERENCE_URL=http://tool-model-inference:5005" >> ./packages/api/.env.docker - echo "MCP_WEB_SEARCH_URL=http://tool-web-search:5006" >> ./packages/api/.env.docker echo "MCP_ECHO_PING_URL=http://tool-echo-ping:5007" >> ./packages/api/.env.docker fi @@ -1606,7 +1582,7 @@ fi ########################################################################## # MCP Tools ########################################################################## -tools="echo-ping customer-query destination-recommendation itinerary-planning code-evaluation model-inference web-search" +tools="echo-ping customer-query destination-recommendation itinerary-planning" printf ">> Creating .env file for the MCP servers...\n" # for each tool copy the .env.sample (if it exists) to .env and .env.docker (dont overwrite existing .env files) diff --git a/packages/api-python/.env.sample b/packages/api-python/.env.sample index 21fdee3c..c93416f3 100644 --- a/packages/api-python/.env.sample +++ b/packages/api-python/.env.sample @@ -32,9 +32,6 @@ AZURE_OPENAI_API_VERSION=2024-02-15-preview MCP_CUSTOMER_QUERY_URL=http://tool-customer-query:8080 MCP_DESTINATION_RECOMMENDATION_URL=http://tool-destination-recommendation:5002 MCP_ITINERARY_PLANNING_URL=http://tool-itinerary-planning:5003 -MCP_CODE_EVALUATION_URL=http://tool-code-evaluation:5004 -MCP_MODEL_INFERENCE_URL=http://tool-model-inference:5005 -MCP_WEB_SEARCH_URL=http://tool-web-search:5006 MCP_ECHO_PING_URL=http://tool-echo-ping:5000 MCP_ECHO_PING_ACCESS_TOKEN=123-this-is-a-fake-token-please-use-a-token-provider diff --git a/packages/api-python/README.md b/packages/api-python/README.md index 4cbc688e..cf859da3 100644 --- a/packages/api-python/README.md +++ b/packages/api-python/README.md @@ -104,7 +104,7 @@ LLM_PROVIDER=azure-openai # Azure OpenAI AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/ AZURE_OPENAI_API_KEY=your-api-key -AZURE_OPENAI_DEPLOYMENT_NAME=gpt-4 +AZURE_OPENAI_DEPLOYMENT_NAME=gpt-5 # MCP Servers (Docker containers) MCP_CUSTOMER_QUERY_URL=http://localhost:5001 @@ -115,11 +115,8 @@ MCP_ECHO_PING_URL=http://localhost:5003 ### Run ```bash -# Start MCP servers (from repo root) -./run.sh - # Start API server (from src/api-python) -python -m src.main +uvicorn src.main:app --reload --port 4000 --log-level=debug ``` Server starts at `http://localhost:8000` @@ -140,7 +137,7 @@ Health check with MCP server status. "llm_provider": "azure-openai", "mcp": { "total_servers": 3, - "configured_servers": ["echo-ping", "customer-query", "itinerary-planning"] + "configured_servers": ["echo-ping", "customer-query", "itinerary-planning", "destination-recommendation"], } } ``` @@ -252,6 +249,7 @@ workflow = ( CustomerQueryAgent=customer_agent, ItineraryAgent=itinerary_agent, DestinationAgent=destination_agent, + EchoAgent=echo_agent, ) .on_event(event_handler, mode=MagenticCallbackMode.STREAMING) .with_standard_manager( @@ -312,37 +310,6 @@ async for event in workflow.run_stream(message): └─────────────────────────────────┘ ``` - -## Project Structure - -``` -packages/api-python/ -├── src/ -│ ├── main.py # FastAPI application & endpoints -│ ├── config.py # Configuration management -│ ├── orchestrator/ -│ │ ├── magentic_workflow.py # Magentic orchestration (MAF) -│ │ ├── providers/ # LLM provider adapters -│ │ │ └── __init__.py # get_llm_client() -│ │ └── tools/ -│ │ ├── tool_config.py # MCP server configuration -│ │ └── tool_registry.py # Tool metadata registry -│ ├── utils/ # Utility modules -│ └── tests/ # Test files -├── MCP_SIMPLIFIED_ARCHITECTURE.md # Architecture documentation -├── MCP_QUICK_REFERENCE.md # Quick reference guide -├── pyproject.toml # Project configuration -├── .env.sample # Environment template -└── README.md # This file -``` - -## Documentation - -### Main Documentation - -- **[MCP Simplified Architecture](./MCP_SIMPLIFIED_ARCHITECTURE.md)** - Complete architecture overview -- **[MCP Quick Reference](./MCP_QUICK_REFERENCE.md)** - Developer quick reference - ### Additional Resources - [Microsoft Agent Framework Docs](https://learn.microsoft.com/en-us/agent-framework/) @@ -364,13 +331,13 @@ LLM_PROVIDER=azure-openai # or github-models, ollama-models, docker-models ```txt AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/ AZURE_OPENAI_API_KEY=your-api-key -AZURE_OPENAI_DEPLOYMENT_NAME=gpt-4 +AZURE_OPENAI_DEPLOYMENT_NAME=gpt-5 ``` **GitHub Models:** ```txt GITHUB_TOKEN=your-github-token -GITHUB_MODEL=openai/gpt-4o +GITHUB_MODEL=openai/gpt-5o ``` **Ollama:** @@ -383,16 +350,16 @@ OLLAMA_MODEL=llama3 ```bash # Linting -ruff check src/ +uvx ruff check src/ # Auto-fix -ruff check --fix src/ +uvx ruff check --fix src/ # Format -ruff format src/ +uvx ruff format src/ # Type checking -mypy src/ +uvx mypy src/ ``` ### Testing diff --git a/packages/api-python/src/config.py b/packages/api-python/src/config.py index ed04e008..5abde3cc 100644 --- a/packages/api-python/src/config.py +++ b/packages/api-python/src/config.py @@ -54,9 +54,6 @@ class Settings(BaseSettings): mcp_customer_query_url: str = "http://tool-customer-query:8080" mcp_destination_recommendation_url: str = "http://tool-destination-recommendation:5002" mcp_itinerary_planning_url: str = "http://tool-itinerary-planning:5003" - mcp_code_evaluation_url: str = "http://tool-code-evaluation:5004" - mcp_model_inference_url: str = "http://tool-model-inference:5005" - mcp_web_search_url: str = "http://tool-web-search:5006" mcp_echo_ping_url: str = "http://tool-echo-ping:5000" mcp_echo_ping_access_token: Optional[str] = "123-this-is-a-fake-token-please-use-a-token-provider" diff --git a/packages/api-python/src/main.py b/packages/api-python/src/main.py index dffe6f67..f8beea6d 100644 --- a/packages/api-python/src/main.py +++ b/packages/api-python/src/main.py @@ -16,6 +16,7 @@ from .orchestrator.tools.tool_registry import tool_registry from agent_framework.observability import setup_observability + setup_observability(enable_sensitive_data=True) # Configure logging @@ -34,7 +35,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: logger.info(f"Service: {settings.otel_service_name}") logger.info(f"Port: {settings.port}") logger.info(f"LLM Provider: {settings.llm_provider}") - + # Initialize Magentic workflow orchestrator logger.info("Initializing Magentic workflow orchestrator...") try: @@ -75,12 +76,14 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: class ChatRequest(BaseModel): """Request model for chat endpoint.""" + message: str context: dict = {} class ChatResponse(BaseModel): """Response model for chat endpoint.""" + response: str agent: str = "TravelPlanningWorkflow" @@ -97,7 +100,7 @@ async def health() -> dict: "total_servers": len(tool_registry._server_metadata), "configured_servers": list(tool_registry._server_metadata.keys()), } - + return { "status": "OK", "service": settings.otel_service_name, @@ -110,7 +113,7 @@ async def health() -> dict: @app.get("/api/tools") async def list_tools() -> dict: """List all available MCP tools. - + Mirrors the TypeScript mcpToolsList implementation by: - Connecting to each configured MCP server - Listing the actual tools available on each server @@ -137,10 +140,7 @@ async def list_tools() -> dict: return tools_info except Exception as e: logger.error(f"Error listing tools: {e}", exc_info=True) - return { - "tools": [], - "error": str(e) - } + return {"tools": [], "error": str(e)} @app.post("/api/chat") @@ -156,9 +156,10 @@ async def chat(request: ChatRequest) -> StreamingResponse: Raises: HTTPException: If processing fails """ + async def event_generator() -> AsyncGenerator[str, None]: """Generate Server-Sent Events for the chat response. - + Format matches UI ChatStreamState: { type: 'START' | 'END' | 'ERROR' | 'MESSAGE', @@ -168,55 +169,46 @@ async def event_generator() -> AsyncGenerator[str, None]: """ try: logger.info(f"Processing chat request with Magentic: {request.message[:100]}...") - + # Send START event start_event = { "type": "metadata", "event": "WorkflowStarted", - "data": { - "agent": "Orchestrator", - "message": "Starting workflow" - } + "data": {"agent": "Orchestrator", "message": "Starting workflow"}, } yield f"data: {json.dumps(start_event)}\n\n" - + # Process through Magentic workflow with streaming async for internal_event in magentic_orchestrator.process_request_stream( - user_message=request.message, - conversation_history=request.context + user_message=request.message, conversation_history=request.context ): # Wrap internal event in ChatStreamState format if internal_event.get("type") == "error": # Error event - extract message and statusCode from the event error_message = internal_event.get("message", "An error occurred") error_status_code = internal_event.get("statusCode", 500) - + stream_state = { "type": "metadata", "event": internal_event, - "error": { - "message": error_message, - "statusCode": error_status_code - } + "error": {"message": error_message, "statusCode": error_status_code}, } else: # Regular message/metadata event stream_state = internal_event - + yield f"data: {json.dumps(stream_state)}\n\n" - + # Send END event end_event = { "type": "metadata", "agent": "TravelPlanningWorkflow", "event": "Complete", - "data": { - "message": "Request processed successfully" - } + "data": {"message": "Request processed successfully"}, } yield f"data: {json.dumps(end_event)}\n\n" logger.info("Request processed successfully") - + except Exception as e: logger.error(f"Error processing chat request: {e}", exc_info=True) error_stream_state = { @@ -229,17 +221,13 @@ async def event_generator() -> AsyncGenerator[str, None]: "agent": None, "error": str(e), "message": f"An error occurred: {str(e)}", - "statusCode": 500 - } + "statusCode": 500, + }, }, - "error": { - "type": "general", - "message": f"An error occurred: {str(e)}", - "statusCode": 500 - } + "error": {"type": "general", "message": f"An error occurred: {str(e)}", "statusCode": 500}, } yield f"data: {json.dumps(error_stream_state)}\n\n" - + return StreamingResponse( event_generator(), media_type="text/event-stream", @@ -247,7 +235,7 @@ async def event_generator() -> AsyncGenerator[str, None]: "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no", # Disable nginx buffering - } + }, ) diff --git a/packages/api-python/src/orchestrator/agents/__init__.py b/packages/api-python/src/orchestrator/agents/__init__.py index 5a0d8973..0e1c35d9 100644 --- a/packages/api-python/src/orchestrator/agents/__init__.py +++ b/packages/api-python/src/orchestrator/agents/__init__.py @@ -13,8 +13,5 @@ "CustomerQueryAgent", "DestinationRecommendationAgent", "ItineraryPlanningAgent", - "CodeEvaluationAgent", - "ModelInferenceAgent", - "WebSearchAgent", "EchoAgent", ] diff --git a/packages/api-python/src/orchestrator/agents/triage_agent/__init__.py b/packages/api-python/src/orchestrator/agents/triage_agent/__init__.py index 11e87fdb..a25d0c92 100644 --- a/packages/api-python/src/orchestrator/agents/triage_agent/__init__.py +++ b/packages/api-python/src/orchestrator/agents/triage_agent/__init__.py @@ -16,9 +16,6 @@ - CustomerQueryAgent: Analyzes customer preferences and requirements - DestinationRecommendationAgent: Suggests travel destinations - ItineraryPlanningAgent: Creates detailed travel itineraries -- CodeEvaluationAgent: Evaluates code snippets and calculations -- ModelInferenceAgent: Performs specialized AI model inference -- WebSearchAgent: Searches for current travel information - EchoAgent: Simple echo tool for testing Your task: diff --git a/packages/api-python/src/orchestrator/magentic_workflow.py b/packages/api-python/src/orchestrator/magentic_workflow.py index 96be0a60..cab773d3 100644 --- a/packages/api-python/src/orchestrator/magentic_workflow.py +++ b/packages/api-python/src/orchestrator/magentic_workflow.py @@ -35,16 +35,16 @@ class MagenticTravelOrchestrator: """Magentic-based travel planning orchestrator using Microsoft Agent Framework. - + Completely simplified implementation strictly following MAF best practices. Each workflow run creates fresh agents with their own MCP tool instances, properly managed using async context managers exactly as shown in MAF samples. - + Architecture: - CustomerQueryAgent: Handles customer inquiries with customer-query MCP tools - ItineraryAgent: Plans itineraries with itinerary-planning MCP tools - DestinationAgent: Recommends destinations (no MCP tools, uses LLM knowledge) - + Reference: - Magentic: https://github.com/microsoft/agent-framework/blob/main/python/samples/getting_started/workflows/orchestration/magentic.py - MCP tools: https://github.com/microsoft/agent-framework/blob/main/python/samples/getting_started/agents/openai/openai_responses_client_with_hosted_mcp.py @@ -58,7 +58,7 @@ def __init__(self): async def initialize(self) -> None: """Initialize the chat client for the workflow.""" logger.info("Initializing Magentic travel planning workflow...") - + # Get the chat client from Microsoft Agent Framework self.chat_client = await get_llm_client() logger.info(f"✓ Chat client initialized for provider: {settings.llm_provider}") @@ -70,38 +70,38 @@ async def process_request_stream( conversation_history: Optional[List[Dict[str, str]]] = None, ) -> AsyncGenerator[Dict[str, Any], None]: """Process a user request using the Magentic workflow with true streaming. - + Creates a fresh workflow for each request following MAF best practices. MCP tools are created once and passed to agents at creation time - MAF handles the connection lifecycle automatically through the workflow's async context manager. - + Args: user_message: The user's message/request conversation_history: Optional conversation history (not used in current implementation) - + Yields: Event dictionaries with type, agent, event, and data for UI consumption """ if not self.chat_client: raise RuntimeError("Chat client not initialized. Call initialize() first.") - + logger.info(f"Processing request with Magentic workflow: {user_message[:100]}...") - + # Get MCP server metadata customer_query_metadata = tool_registry.get_server_metadata("customer-query") itinerary_metadata = tool_registry.get_server_metadata("itinerary-planning") - + # Helper function to safely create MCP tool def create_mcp_tool(metadata: Optional[Dict[str, Any]]) -> Optional[MCPStreamableHTTPTool]: """Create MCP tool from metadata with error handling.""" if not metadata: return None - + try: headers = {} - if metadata.get('access_token'): + if metadata.get("access_token"): headers["Authorization"] = f"Bearer {metadata['access_token']}" - + # Create tool exactly as shown in MAF samples return MCPStreamableHTTPTool( name=metadata["name"], @@ -115,28 +115,28 @@ def create_mcp_tool(metadata: Optional[Dict[str, Any]]) -> Optional[MCPStreamabl except Exception as e: logger.warning(f"⚠ Could not create MCP tool for {metadata.get('name')}: {e}") return None - + # Create MCP tool instances - will be passed to agents at creation customer_query_tool = create_mcp_tool(customer_query_metadata) itinerary_tool = create_mcp_tool(itinerary_metadata) - + # Log MCP tool availability if customer_query_tool: logger.info("✓ Customer Query MCP tool configured") else: logger.warning("⚠ Customer Query agent will run without MCP tools") - + if itinerary_tool: logger.info("✓ Itinerary MCP tool configured") else: logger.warning("⚠ Itinerary agent will run without MCP tools") - + try: # Create event queue for streaming event_queue: asyncio.Queue[Dict[str, Any]] = asyncio.Queue() workflow_done = False workflow_error: Optional[Exception] = None - + # Define streaming callback async def on_event(event: MagenticCallbackEvent) -> None: """Stream workflow events to UI in real-time.""" @@ -144,7 +144,7 @@ async def on_event(event: MagenticCallbackEvent) -> None: if event_data: await event_queue.put(event_data) logger.debug(f"→ Event: {event_data.get('event')} from {event_data.get('agent')}") - + # Build workflow with agents and tools # Following exact pattern from MAF sample - tools passed at agent creation workflow = ( @@ -156,8 +156,12 @@ async def on_event(event: MagenticCallbackEvent) -> None: instructions=( "You are a Customer Query Agent for a travel planning system. " "Answer customer questions about destinations, hotels, and travel logistics. " - + ("Use the MCP tools to retrieve accurate information. " if customer_query_tool else "Use your knowledge. ") + - "Be helpful and customer-focused." + + ( + "Use the MCP tools to retrieve accurate information. " + if customer_query_tool + else "Use your knowledge. " + ) + + "Be helpful and customer-focused." ), chat_client=self.chat_client, tools=customer_query_tool, # Tool passed - MAF manages lifecycle @@ -168,8 +172,8 @@ async def on_event(event: MagenticCallbackEvent) -> None: instructions=( "You are an Itinerary Planning Agent for a travel planning system. " "Create detailed day-by-day travel itineraries. " - + ("Use the MCP tools to plan itineraries. " if itinerary_tool else "Use your knowledge. ") + - "Be thorough and organized." + + ("Use the MCP tools to plan itineraries. " if itinerary_tool else "Use your knowledge. ") + + "Be thorough and organized." ), chat_client=self.chat_client, tools=itinerary_tool, # Tool passed - MAF manages lifecycle @@ -195,7 +199,7 @@ async def on_event(event: MagenticCallbackEvent) -> None: ) .build() ) - + # Run workflow in background task async def run_workflow(): """Execute workflow and mark completion.""" @@ -218,8 +222,8 @@ async def run_workflow(): "statusCode": 504, "reason": { "message": str(e), - } - } + }, + }, } await event_queue.put(error_event) except Exception as e: @@ -234,28 +238,28 @@ async def run_workflow(): "data": { "agent": None, "error": str(e), - } + }, } await event_queue.put(error_event) finally: workflow_done = True await event_queue.put(None) # Signal completion - + # Start workflow workflow_task = asyncio.create_task(run_workflow()) - + # Stream events as they arrive try: while True: try: event_data = await asyncio.wait_for(event_queue.get(), timeout=0.1) - + if event_data is None: # Completion signal break - + yield event_data - + except asyncio.TimeoutError: if workflow_done: # Drain remaining events @@ -265,16 +269,16 @@ async def run_workflow(): yield event_data break continue - + # Wait for workflow task await workflow_task - + # If there was an error, it's already been sent if workflow_error: logger.error(f"✗ Workflow completed with error: {workflow_error}") else: logger.info("✓ Workflow completed successfully") - + except Exception as e: logger.error(f"Error streaming workflow events: {e}", exc_info=True) workflow_task.cancel() @@ -283,7 +287,7 @@ async def run_workflow(): except asyncio.CancelledError: pass raise - + except ServiceResponseException as e: # Handle timeout and service errors specially logger.error(f"Service error in Magentic workflow: {e}", exc_info=True) @@ -296,7 +300,7 @@ async def run_workflow(): "data": { "agent": None, "error": str(e), - } + }, } except Exception as e: logger.error(f"Error in Magentic workflow: {e}", exc_info=True) @@ -309,12 +313,12 @@ async def run_workflow(): "data": { "agent": None, "error": str(e), - } + }, } def _convert_workflow_event(self, event: Any) -> Optional[Dict[str, Any]]: """Convert a Magentic workflow event to our API event format. - + Expected UI format: { type: "metadata", @@ -322,18 +326,18 @@ def _convert_workflow_event(self, event: Any) -> Optional[Dict[str, Any]]: event: event display name, data: { agent, ...event data } } - + Args: event: Workflow event from Magentic - + Returns: Event dictionary in our API format, or None if not relevant for UI """ # Handle different event types from Microsoft Agent Framework if isinstance(event, MagenticOrchestratorMessageEvent): # Orchestrator planning messages - message_text = getattr(event.message, 'text', '') if event.message else "" - + message_text = getattr(event.message, "text", "") if event.message else "" + return { "type": "metadata", "agent": "Orchestrator", @@ -342,13 +346,13 @@ def _convert_workflow_event(self, event: Any) -> Optional[Dict[str, Any]]: "agent": "Orchestrator", "message": message_text, "kind": event.kind, - } + }, } - + elif isinstance(event, MagenticAgentDeltaEvent): # Token-by-token streaming from agents agent_id = event.agent_id or "UnknownAgent" - + return { "type": "metadata", "agent": agent_id, @@ -356,14 +360,14 @@ def _convert_workflow_event(self, event: Any) -> Optional[Dict[str, Any]]: "data": { "agent": agent_id, "delta": event.text, - } + }, } - + elif isinstance(event, MagenticAgentMessageEvent): # Complete agent messages agent_id = event.agent_id or "UnknownAgent" - message_text = getattr(event.message, 'text', '') if event.message else "" - + message_text = getattr(event.message, "text", "") if event.message else "" + return { "type": "metadata", "agent": agent_id, @@ -371,14 +375,14 @@ def _convert_workflow_event(self, event: Any) -> Optional[Dict[str, Any]]: "data": { "agent": agent_id, "message": message_text, - "role": getattr(event.message, 'role', None) if event.message else None, - } + "role": getattr(event.message, "role", None) if event.message else None, + }, } - + elif isinstance(event, MagenticFinalResultEvent): # Final result from workflow - result_text = getattr(event.message, 'text', '') if event.message else "Task completed" - + result_text = getattr(event.message, "text", "") if event.message else "Task completed" + return { "type": "metadata", "agent": None, @@ -387,13 +391,13 @@ def _convert_workflow_event(self, event: Any) -> Optional[Dict[str, Any]]: "agent": None, "message": result_text, "completed": True, - } + }, } - + elif isinstance(event, WorkflowOutputEvent): # Final workflow output output_data = str(event.data) if event.data else "Workflow completed" - + return { "type": "metadata", "agent": None, @@ -402,9 +406,9 @@ def _convert_workflow_event(self, event: Any) -> Optional[Dict[str, Any]]: "agent": None, "output": output_data, "completed": True, - } + }, } - + # Return None for events we don't need to surface to UI return None diff --git a/packages/api-python/src/orchestrator/providers/azure_openai.py b/packages/api-python/src/orchestrator/providers/azure_openai.py index 6f529656..d5b85839 100644 --- a/packages/api-python/src/orchestrator/providers/azure_openai.py +++ b/packages/api-python/src/orchestrator/providers/azure_openai.py @@ -37,7 +37,7 @@ async def get_client(self) -> Any: # Create the underlying Azure OpenAI async client async_client: AsyncAzureOpenAI - + # If API key is provided, use it (local development or explicit configuration) if settings.azure_openai_api_key: logger.info("Using API key authentication") @@ -70,6 +70,6 @@ async def get_client(self) -> Any: model_id=settings.azure_openai_deployment_name, async_client=async_client, ) - + logger.info(f"Created MAF OpenAIChatClient with model: {settings.azure_openai_deployment_name}") return maf_client diff --git a/packages/api-python/src/orchestrator/providers/docker_models.py b/packages/api-python/src/orchestrator/providers/docker_models.py index 3886df87..1d10c562 100644 --- a/packages/api-python/src/orchestrator/providers/docker_models.py +++ b/packages/api-python/src/orchestrator/providers/docker_models.py @@ -37,12 +37,12 @@ async def get_client(self) -> Any: base_url=settings.docker_model_endpoint, api_key="DOCKER_API_KEY", # Placeholder API key for Docker models ) - + # Wrap with MAF's OpenAIChatClient maf_client = OpenAIChatClient( model_id=settings.docker_model, async_client=async_client, ) - + logger.info(f"Created MAF OpenAIChatClient with model: {settings.docker_model}") return maf_client diff --git a/packages/api-python/src/orchestrator/providers/foundry_local.py b/packages/api-python/src/orchestrator/providers/foundry_local.py index 81cdaba2..e4852187 100644 --- a/packages/api-python/src/orchestrator/providers/foundry_local.py +++ b/packages/api-python/src/orchestrator/providers/foundry_local.py @@ -26,10 +26,7 @@ async def get_client(self) -> Any: NotImplementedError: Foundry Local Python SDK not yet available """ logger.info("Using Azure Foundry Local") - logger.warning( - "Foundry Local Python SDK is not yet available. " - "This is a placeholder implementation." - ) + logger.warning("Foundry Local Python SDK is not yet available. This is a placeholder implementation.") # Placeholder implementation # TODO: Update when Foundry Local Python SDK becomes available diff --git a/packages/api-python/src/orchestrator/providers/github_models.py b/packages/api-python/src/orchestrator/providers/github_models.py index 29bd9883..ce9b553a 100644 --- a/packages/api-python/src/orchestrator/providers/github_models.py +++ b/packages/api-python/src/orchestrator/providers/github_models.py @@ -37,12 +37,12 @@ async def get_client(self) -> Any: base_url="https://models.inference.ai.azure.com", api_key=settings.github_token, ) - + # Wrap with MAF's OpenAIChatClient maf_client = OpenAIChatClient( model_id=settings.github_model, async_client=async_client, ) - + logger.info(f"Created MAF OpenAIChatClient with model: {settings.github_model}") return maf_client diff --git a/packages/api-python/src/orchestrator/providers/ollama_models.py b/packages/api-python/src/orchestrator/providers/ollama_models.py index d7b0360d..bf27f5f6 100644 --- a/packages/api-python/src/orchestrator/providers/ollama_models.py +++ b/packages/api-python/src/orchestrator/providers/ollama_models.py @@ -37,12 +37,12 @@ async def get_client(self) -> Any: base_url=settings.ollama_model_endpoint, api_key="OLLAMA_API_KEY", # Placeholder API key for Ollama models ) - + # Wrap with MAF's OpenAIChatClient maf_client = OpenAIChatClient( model_id=settings.ollama_model, async_client=async_client, ) - + logger.info(f"Created MAF OpenAIChatClient with model: {settings.ollama_model}") return maf_client diff --git a/packages/api-python/src/orchestrator/tools/examples.py b/packages/api-python/src/orchestrator/tools/examples.py index 7c6b2020..2d164d96 100644 --- a/packages/api-python/src/orchestrator/tools/examples.py +++ b/packages/api-python/src/orchestrator/tools/examples.py @@ -22,22 +22,22 @@ async def example_1_basic_usage(): """Example 1: Basic MCP tools usage with ChatAgent. - + Demonstrates: - Loading all MCP tools - Creating a ChatAgent with MCP tools - Processing user queries """ logger.info("=== Example 1: Basic MCP Tools Usage ===") - + # Load all available MCP tools mcp_tools = await tool_registry.get_all_tools() logger.info(f"Loaded {len(mcp_tools)} MCP tools") - + # Create a ChatAgent with MCP tools # Note: Requires an actual LLM client (Azure OpenAI, GitHub Models, etc.) # For this example, we'll just show the tool loading - + logger.info("MCP tools ready for use with ChatAgent") for tool in mcp_tools[:5]: # Show first 5 tools logger.info(f" - {tool.name}: {tool.description}") @@ -45,18 +45,16 @@ async def example_1_basic_usage(): async def example_2_specific_servers(): """Example 2: Load tools from specific MCP servers. - + Demonstrates: - Selective tool loading - Server filtering """ logger.info("=== Example 2: Load Specific MCP Servers ===") - + # Load tools from specific servers only - travel_tools = await tool_registry.get_all_tools( - servers=["itinerary-planning", "destination-recommendation"] - ) - + travel_tools = await tool_registry.get_all_tools(servers=["itinerary-planning", "destination-recommendation"]) + logger.info(f"Loaded {len(travel_tools)} travel-related tools") for tool in travel_tools: logger.info(f" - {tool.name}") @@ -64,93 +62,84 @@ async def example_2_specific_servers(): async def example_3_tool_discovery(): """Example 3: Discover and inspect MCP tools. - + Demonstrates: - Tool discovery - Schema inspection - Server capabilities """ logger.info("=== Example 3: Tool Discovery ===") - + # List all tools from all servers all_tools = await tool_registry.list_tools() - + for server_name, tools in all_tools.items(): if isinstance(tools, dict) and "error" in tools: logger.error(f"Server '{server_name}' error: {tools['error']}") continue - + logger.info(f"\nServer: {server_name}") logger.info(f" Tools: {len(tools)}") - + for tool in tools[:2]: # Show first 2 tools per server logger.info(f" - {tool.get('name', 'unknown')}") logger.info(f" Description: {tool.get('description', 'N/A')}") - + # Show input schema - input_schema = tool.get('inputSchema', {}) - properties = input_schema.get('properties', {}) + input_schema = tool.get("inputSchema", {}) + properties = input_schema.get("properties", {}) if properties: logger.info(f" Parameters: {list(properties.keys())}") async def example_4_direct_tool_call(): """Example 4: Direct MCP tool invocation. - + Demonstrates: - Bypassing MAF wrapper for direct calls - Low-level MCP protocol usage """ logger.info("=== Example 4: Direct Tool Call ===") - + try: # Call MCP tool directly (without going through agent) result = await tool_registry.call_tool( - server="echo-ping", - tool_name="ping", - arguments={"message": "Hello from MCP!"} + server="echo-ping", tool_name="ping", arguments={"message": "Hello from MCP!"} ) - + logger.info(f"Direct call result: {result}") - + except Exception as e: logger.error(f"Direct call failed: {e}") async def example_5_custom_wrapper(): """Example 5: Create custom MCP tool wrapper. - + Demonstrates: - Custom server configuration - Manual wrapper creation - Tool conversion """ logger.info("=== Example 5: Custom MCP Wrapper ===") - + # Create custom MCP server config - custom_config: MCPServerConfig = { - "url": "http://localhost:8001/mcp", - "type": "http", - "verbose": True - } - + custom_config: MCPServerConfig = {"url": "http://localhost:8001/mcp", "type": "http", "verbose": True} + # Create loader for custom server - loader = MCPToolLoader( - server_config=custom_config, - server_name="Custom MCP Server" - ) - + loader = MCPToolLoader(server_config=custom_config, server_name="Custom MCP Server") + try: # Get tools from custom server tools = await loader.get_tools() logger.info(f"Custom server has {len(tools)} tools") - + for tool in tools: logger.info(f" - {tool.name}: {tool.description}") - + except Exception as e: logger.error(f"Failed to load custom server tools: {e}") - + finally: # Cleanup await loader.close() @@ -158,29 +147,27 @@ async def example_5_custom_wrapper(): async def example_6_agent_with_tools(): """Example 6: Complete agent setup with MCP tools. - + Demonstrates: - Full agent initialization - MCP tools integration - Agent execution flow - + Note: This requires a valid LLM client configuration. """ logger.info("=== Example 6: Agent with MCP Tools ===") - + # Load MCP tools - mcp_tools = await tool_registry.get_all_tools( - servers=["itinerary-planning", "customer-query", "echo-ping"] - ) - + mcp_tools = await tool_registry.get_all_tools(servers=["itinerary-planning", "customer-query", "echo-ping"]) + logger.info(f"Loaded {len(mcp_tools)} tools for agent") - + # The agent would be initialized like this: # from orchestrator.agents.base_agent import BaseAgent # from orchestrator.providers.azure_openai import get_azure_openai_client # # llm_client = await get_azure_openai_client() - # + # # agent = BaseAgent( # name="travel_assistant", # description="AI-powered travel planning assistant", @@ -188,45 +175,41 @@ async def example_6_agent_with_tools(): # "Use available tools to help users plan their trips.", # tools=mcp_tools # ) - # + # # await agent.initialize(llm_client) - # + # # # Process user request # response = await agent.process( # "I need a 3-day itinerary for Paris with hotel recommendations" # ) - # + # # logger.info(f"Agent response: {response}") - + logger.info("Agent setup complete (LLM client required for execution)") async def example_7_error_handling(): """Example 7: Error handling with MCP tools. - + Demonstrates: - Graceful error handling - Server unavailability - Tool call failures """ logger.info("=== Example 7: Error Handling ===") - + # Try to load tools - some servers might be unavailable mcp_tools = await tool_registry.get_all_tools() - + # Tools with errors will be skipped, successful ones loaded logger.info(f"Successfully loaded {len(mcp_tools)} tools") - + # Try direct call to potentially unavailable server try: - result = await tool_registry.call_tool( - server="nonexistent-server", - tool_name="test", - arguments={} - ) + result = await tool_registry.call_tool(server="nonexistent-server", tool_name="test", arguments={}) except ValueError as e: logger.info(f"Expected error for unknown server: {e}") - + # MCP tool wrappers return errors as strings instead of raising # This allows agents to handle errors gracefully @@ -242,14 +225,14 @@ async def main(): example_6_agent_with_tools, example_7_error_handling, ] - + for example in examples: try: await example() print() # Blank line between examples except Exception as e: logger.error(f"Example failed: {e}", exc_info=True) - + # Cleanup logger.info("=== Cleanup ===") await tool_registry.close_all() diff --git a/packages/api-python/src/orchestrator/tools/mcp_tool_wrapper.py b/packages/api-python/src/orchestrator/tools/mcp_tool_wrapper.py index eebde1d9..987d7e3f 100644 --- a/packages/api-python/src/orchestrator/tools/mcp_tool_wrapper.py +++ b/packages/api-python/src/orchestrator/tools/mcp_tool_wrapper.py @@ -13,6 +13,4 @@ logger = logging.getLogger(__name__) -logger.warning( - "mcp_tool_wrapper is deprecated. Use tool_registry.create_mcp_tool() instead." -) +logger.warning("mcp_tool_wrapper is deprecated. Use tool_registry.create_mcp_tool() instead.") diff --git a/packages/api-python/src/orchestrator/tools/tool_config.py b/packages/api-python/src/orchestrator/tools/tool_config.py index 2b855964..5bb98b8f 100644 --- a/packages/api-python/src/orchestrator/tools/tool_config.py +++ b/packages/api-python/src/orchestrator/tools/tool_config.py @@ -36,14 +36,14 @@ class MCPServerDefinition(TypedDict): def get_mcp_tools_config() -> dict[McpServerName, MCPServerDefinition]: """ Get MCP tools configuration following TypeScript implementation pattern. - + Mirrors packages/api/src/orchestrator/*/tools/index.ts - + Returns: Dictionary mapping server names to their configurations """ settings = Settings() - + return { "echo-ping": { "config": { diff --git a/packages/api-python/src/orchestrator/tools/tool_registry.py b/packages/api-python/src/orchestrator/tools/tool_registry.py index abf72483..6f044753 100644 --- a/packages/api-python/src/orchestrator/tools/tool_registry.py +++ b/packages/api-python/src/orchestrator/tools/tool_registry.py @@ -14,8 +14,7 @@ from agent_framework import MCPStreamableHTTPTool except ImportError: raise ImportError( - "Microsoft Agent Framework SDK is required. " - "Install with: pip install agent-framework>=1.0.0b251001" + "Microsoft Agent Framework SDK is required. Install with: pip install agent-framework>=1.0.0b251001" ) from .tool_config import MCP_TOOLS_CONFIG, McpServerName @@ -25,17 +24,17 @@ class ToolRegistry: """Registry for managing MCP tool server metadata. - + Simplified implementation that only stores metadata about MCP servers. Each agent creates its own MCPStreamableHTTPTool instances and manages their lifecycle using async context managers, following Microsoft Agent Framework best practices. - + This avoids issues with: - Shared async context managers across different tasks - Cancel scope violations - Persistent connection management - + Reference: https://github.com/microsoft/agent-framework/blob/main/python/samples/getting_started/agents/openai/openai_chat_client_with_local_mcp.py """ @@ -50,7 +49,7 @@ def _initialize_metadata(self) -> None: for server_id, server_def in MCP_TOOLS_CONFIG.items(): config = server_def["config"] name = server_def["name"] - + # Store only metadata - no actual connections self._server_metadata[server_id] = { "id": server_id, @@ -60,24 +59,24 @@ def _initialize_metadata(self) -> None: "selected": server_id != "echo-ping", "access_token": config.get("accessToken"), } - + logger.info(f"Registered MCP server '{name}' ({server_id}) at {config['url']}") logger.info(f"Tool registry ready with {len(self._server_metadata)} MCP servers") def create_mcp_tool(self, server_id: McpServerName) -> Optional[MCPStreamableHTTPTool]: """Create a new MCP tool instance for a server. - + Each call creates a fresh MCPStreamableHTTPTool instance that should be used within an async context manager. This follows the pattern from Microsoft Agent Framework samples. - + Args: server_id: The ID of the MCP server - + Returns: New MCPStreamableHTTPTool instance or None if server not found - + Example: ```python tool = registry.create_mcp_tool("customer-query") @@ -91,13 +90,13 @@ def create_mcp_tool(self, server_id: McpServerName) -> Optional[MCPStreamableHTT if not metadata: logger.warning(f"MCP server '{server_id}' not found in registry") return None - + # Build headers if access token provided headers = None access_token = metadata.get("access_token") if access_token: headers = {"Authorization": f"Bearer {access_token}"} - + # Create a new MCPStreamableHTTPTool instance # The caller is responsible for using it in an async context manager return MCPStreamableHTTPTool( @@ -111,10 +110,10 @@ def create_mcp_tool(self, server_id: McpServerName) -> Optional[MCPStreamableHTT def get_server_metadata(self, server_id: McpServerName) -> Optional[Dict[str, Any]]: """Get metadata for a server without creating a connection. - + Args: server_id: The ID of the MCP server - + Returns: Server metadata dictionary or None if not found """ @@ -122,12 +121,12 @@ def get_server_metadata(self, server_id: McpServerName) -> Optional[Dict[str, An async def list_tools(self) -> Dict[str, Any]: """List all available MCP tools with reachability checks. - + Ports the TypeScript mcpToolsList implementation to: 1. Connect to each MCP server 2. List the actual tools available on each server 3. Return detailed information including tool definitions - + Returns response in the format expected by the frontend: { "tools": [ @@ -143,6 +142,7 @@ async def list_tools(self) -> Dict[str, Any]: ] } """ + async def check_server_and_list_tools(server_id: str, metadata: Dict[str, Any]) -> Dict[str, Any]: """Connect to MCP server and list its tools, mirroring TS mcpToolsList behavior.""" server_info = { @@ -152,58 +152,58 @@ async def check_server_and_list_tools(server_id: str, metadata: Dict[str, Any]) "type": metadata["type"], "reachable": False, "selected": metadata["selected"], - "tools": [] + "tools": [], } - + # Create MCP tool instance to connect and list tools try: logger.info(f"Connecting to MCP server {metadata['name']} at {metadata['url']}") mcp_tool = self.create_mcp_tool(server_id) - + if not mcp_tool: logger.warning(f"Could not create MCP tool for server '{server_id}'") return server_info - + # Use the tool in async context manager to connect async with mcp_tool: logger.info(f"MCP server {metadata['name']} is reachable") server_info["reachable"] = True - + # List tools from the server # The MCPStreamableHTTPTool loads tools on connection # Access them via the tool's internal state - if hasattr(mcp_tool, '_tools') and mcp_tool._tools: + if hasattr(mcp_tool, "_tools") and mcp_tool._tools: tools_list = [] for tool in mcp_tool._tools: # Convert tool to dict format tool_info = { - "name": tool.metadata.name if hasattr(tool, 'metadata') else str(tool), - "description": tool.metadata.description if hasattr(tool, 'metadata') else "" + "name": tool.metadata.name if hasattr(tool, "metadata") else str(tool), + "description": tool.metadata.description if hasattr(tool, "metadata") else "", } tools_list.append(tool_info) - + server_info["tools"] = tools_list logger.info(f"MCP server {metadata['name']} has {len(tools_list)} tools") else: logger.info(f"MCP server {metadata['name']} has 0 tools") - + except Exception as error: logger.error(f"MCP server {metadata['name']} is not reachable: {str(error)}") server_info["error"] = str(error) - + return server_info - + # Check all servers concurrently, matching TS Promise.all pattern tasks = [] for server_id, metadata in self._server_metadata.items(): task = asyncio.create_task(check_server_and_list_tools(server_id, metadata)) tasks.append(task) - + # Wait for all checks with overall timeout try: results = await asyncio.wait_for( asyncio.gather(*tasks, return_exceptions=True), - timeout=10.0 # Increased timeout for actual MCP connections + timeout=10.0, # Increased timeout for actual MCP connections ) except asyncio.TimeoutError: logger.warning("Tool list overall timeout - returning partial results") @@ -216,7 +216,7 @@ async def check_server_and_list_tools(server_id: str, metadata: Dict[str, Any]) logger.debug(f"Error getting task result: {e}") else: task.cancel() - + # Build tools list from results tools_list = [] for result in results: @@ -224,12 +224,12 @@ async def check_server_and_list_tools(server_id: str, metadata: Dict[str, Any]) tools_list.append(result) elif isinstance(result, Exception): logger.debug(f"Error checking server: {result}") - + return {"tools": tools_list} async def close_all(self) -> None: """Cleanup resources. - + Since we don't maintain persistent connections, this just clears metadata. """ logger.info("Cleaning up tool registry...") diff --git a/packages/api-python/src/orchestrator/workflow.py b/packages/api-python/src/orchestrator/workflow.py index abc188b4..2a99f9c7 100644 --- a/packages/api-python/src/orchestrator/workflow.py +++ b/packages/api-python/src/orchestrator/workflow.py @@ -6,15 +6,10 @@ from ..config import settings from .providers import get_llm_client from .agents.triage_agent import TriageAgent -from .agents.specialized_agents import ( - CustomerQueryAgent, - DestinationRecommendationAgent, - ItineraryPlanningAgent, - CodeEvaluationAgent, - ModelInferenceAgent, - WebSearchAgent, - EchoAgent, -) +from .agents.customer_query_agent import CustomerQueryAgent +from .agents.destination_recommendation_agent import DestinationRecommendationAgent +from .agents.itinerary_planning_agent import ItineraryPlanningAgent +from .agents.echo_agent import EchoAgent from .tools import MCP_TOOLS_CONFIG, tool_registry logger = logging.getLogger(__name__) @@ -22,7 +17,7 @@ class TravelWorkflowOrchestrator: """Orchestrates multi-agent workflow for travel planning using MAF. - + This class manages the initialization and coordination of all agents in the travel planning system using Microsoft Agent Framework with simplified MCP integration using MAF's built-in MCP support. @@ -32,303 +27,175 @@ def __init__(self): """Initialize the workflow orchestrator.""" self.chat_client: Optional[Any] = None self.all_tools: List[Any] = [] - + # Initialize agents (will be configured with tools during initialize()) self.triage_agent: Optional[TriageAgent] = None self.customer_query_agent: Optional[CustomerQueryAgent] = None self.destination_agent: Optional[DestinationRecommendationAgent] = None self.itinerary_agent: Optional[ItineraryPlanningAgent] = None - self.code_eval_agent: Optional[CodeEvaluationAgent] = None - self.model_inference_agent: Optional[ModelInferenceAgent] = None - self.web_search_agent: Optional[WebSearchAgent] = None self.echo_agent: Optional[EchoAgent] = None - + logger.info("Workflow orchestrator initialized") async def initialize(self, enabled_tools: Optional[List[str]] = None) -> None: """Initialize the workflow with LLM client, MCP tools, and all agents. - + Uses Microsoft Agent Framework's built-in MCP support via MCPStreamableHTTPTool. - + Args: enabled_tools: List of enabled tool IDs. If None, all tools are enabled. """ logger.info("Initializing MAF workflow with simplified MCP integration...") - + # Get the chat client from Microsoft Agent Framework self.chat_client = await get_llm_client() logger.info(f"Chat client initialized for provider: {settings.llm_provider}") - + # Determine which tools to enable (default: all except echo-ping for production) if enabled_tools is None: enabled_tools = [ "customer-query", - "web-search", "itinerary-planning", - "model-inference", - "code-evaluation", "destination-recommendation", + "echo-ping", ] - + # Load MCP tools using the tool registry (which uses MAF's built-in MCP support) # This will continue even if some servers are unavailable self.all_tools = await tool_registry.get_all_tools(servers=enabled_tools) - + if self.all_tools: - logger.info( - f"✓ Loaded {len(self.all_tools)} tools - agents will have MCP capabilities" - ) + logger.info(f"✓ Loaded {len(self.all_tools)} tools - agents will have MCP capabilities") else: - logger.warning( - f"⚠ No MCP tools loaded - agents will run without MCP capabilities" - ) - logger.warning( - f"⚠ Check if MCP servers are running and accessible" - ) - + logger.warning(f"⚠ No MCP tools loaded - agents will run without MCP capabilities") + logger.warning(f"⚠ Check if MCP servers are running and accessible") + # Initialize specialized agents with their specific tools # Each agent gets the full tool list - the agent's system prompt determines usage - + # Triage Agent (orchestrator) self.triage_agent = TriageAgent(tools=self.all_tools) await self.triage_agent.initialize(self.chat_client) logger.info("TriageAgent initialized") - + # Customer Query Agent self.customer_query_agent = CustomerQueryAgent(tools=self.all_tools) await self.customer_query_agent.initialize(self.chat_client) logger.info("CustomerQueryAgent initialized") - + # Destination Recommendation Agent self.destination_agent = DestinationRecommendationAgent(tools=self.all_tools) await self.destination_agent.initialize(self.chat_client) logger.info("DestinationRecommendationAgent initialized") - + # Itinerary Planning Agent self.itinerary_agent = ItineraryPlanningAgent(tools=self.all_tools) await self.itinerary_agent.initialize(self.chat_client) logger.info("ItineraryPlanningAgent initialized") - - # Code Evaluation Agent - self.code_eval_agent = CodeEvaluationAgent(tools=self.all_tools) - await self.code_eval_agent.initialize(self.chat_client) - logger.info("CodeEvaluationAgent initialized") - - # Model Inference Agent - self.model_inference_agent = ModelInferenceAgent(tools=self.all_tools) - await self.model_inference_agent.initialize(self.chat_client) - logger.info("ModelInferenceAgent initialized") - - # Web Search Agent - self.web_search_agent = WebSearchAgent(tools=self.all_tools) - await self.web_search_agent.initialize(self.chat_client) - logger.info("WebSearchAgent initialized") - + # Echo Agent (for testing) self.echo_agent = EchoAgent(tools=self.all_tools) await self.echo_agent.initialize(self.chat_client) logger.info("EchoAgent initialized") - - logger.info( - f"MAF workflow fully initialized with {len(self.all_tools)} total tools" - ) + + logger.info(f"MAF workflow fully initialized with {len(self.all_tools)} total tools") @property def agents(self) -> List[Any]: """Get list of all initialized agents.""" return [ - agent for agent in [ + agent + for agent in [ self.triage_agent, self.customer_query_agent, self.destination_agent, self.itinerary_agent, - self.code_eval_agent, - self.model_inference_agent, - self.web_search_agent, self.echo_agent, ] if agent is not None ] - async def process( - self, - message: str, - context: Optional[Dict[str, Any]] = None - ) -> str: + async def process(self, message: str, context: Optional[Dict[str, Any]] = None) -> str: """Process a message through the multi-agent workflow. - + Args: message: User message to process context: Optional context information - + Returns: Response from the triage agent """ if not self.triage_agent: raise RuntimeError("Workflow not initialized. Call initialize() first.") - + logger.info(f"Processing message through MAF workflow: {message[:100]}...") - + # Use the triage agent to process the message # It will coordinate with other agents as needed through its tools response = await self.triage_agent.process(message, context) - + logger.info("Message processing complete") return response async def process_stream( - self, - message: str, - context: Optional[Dict[str, Any]] = None + self, message: str, context: Optional[Dict[str, Any]] = None ) -> AsyncGenerator[Dict[str, Any], None]: """Process a message with streaming response. - + Args: message: User message to process context: Optional context information - + Yields: Streaming response chunks """ if not self.triage_agent: raise RuntimeError("Workflow not initialized. Call initialize() first.") - + logger.info(f"Processing message with streaming: {message[:100]}...") - + # For now, use non-streaming and yield as single chunk # TODO: Implement true streaming with MAF's streaming capabilities response = await self.triage_agent.process(message, context) - - yield { - "type": "response", - "agent": self.triage_agent.name, - "data": {"message": response} - } - + + yield {"type": "response", "agent": self.triage_agent.name, "data": {"message": response}} + logger.info("Streaming message processing complete") async def cleanup(self) -> None: """Clean up workflow resources.""" logger.info("Cleaning up workflow resources...") - + try: # Close all MCP tool connections await tool_registry.close_all() logger.info("MCP tool connections closed") except Exception as e: logger.error(f"Error closing MCP connections: {e}") - + logger.info("Workflow cleanup complete") - - # Initialize agents with their specific tools - # Following TypeScript pattern: each agent gets tools from its MCP server - - # Echo Agent (for testing) - if "echo-ping" in tools_by_server: - self.echo_agent = EchoAgent(tools=tools_by_server.get("echo-ping", [])) - await self.echo_agent.initialize(self.chat_client) - logger.info("EchoAgent initialized with tools") - else: - self.echo_agent = EchoAgent() - await self.echo_agent.initialize(self.chat_client) - - # Customer Query Agent - if "customer-query" in tools_by_server: - self.customer_query_agent = CustomerQueryAgent( - tools=tools_by_server.get("customer-query", []) - ) - await self.customer_query_agent.initialize(self.chat_client) - logger.info("CustomerQueryAgent initialized with tools") - else: - self.customer_query_agent = CustomerQueryAgent() - await self.customer_query_agent.initialize(self.chat_client) - - # Web Search Agent - if "web-search" in tools_by_server: - self.web_search_agent = WebSearchAgent( - tools=tools_by_server.get("web-search", []) - ) - await self.web_search_agent.initialize(self.chat_client) - logger.info("WebSearchAgent initialized with tools") - else: - self.web_search_agent = WebSearchAgent() - await self.web_search_agent.initialize(self.chat_client) - - # Itinerary Planning Agent - if "itinerary-planning" in tools_by_server: - self.itinerary_agent = ItineraryPlanningAgent( - tools=tools_by_server.get("itinerary-planning", []) - ) - await self.itinerary_agent.initialize(self.chat_client) - logger.info("ItineraryPlanningAgent initialized with tools") - else: - self.itinerary_agent = ItineraryPlanningAgent() - await self.itinerary_agent.initialize(self.chat_client) - - # Model Inference Agent - if "model-inference" in tools_by_server: - self.model_inference_agent = ModelInferenceAgent( - tools=tools_by_server.get("model-inference", []) - ) - await self.model_inference_agent.initialize(self.chat_client) - logger.info("ModelInferenceAgent initialized with tools") - else: - self.model_inference_agent = ModelInferenceAgent() - await self.model_inference_agent.initialize(self.chat_client) - - # Code Evaluation Agent - if "code-evaluation" in tools_by_server: - self.code_eval_agent = CodeEvaluationAgent( - tools=tools_by_server.get("code-evaluation", []) - ) - await self.code_eval_agent.initialize(self.chat_client) - logger.info("CodeEvaluationAgent initialized with tools") - else: - self.code_eval_agent = CodeEvaluationAgent() - await self.code_eval_agent.initialize(self.chat_client) - - # Destination Recommendation Agent - if "destination-recommendation" in tools_by_server: - self.destination_agent = DestinationRecommendationAgent( - tools=tools_by_server.get("destination-recommendation", []) - ) - await self.destination_agent.initialize(self.chat_client) - logger.info("DestinationRecommendationAgent initialized with tools") - else: - self.destination_agent = DestinationRecommendationAgent() - await self.destination_agent.initialize(self.chat_client) - - # Triage Agent gets all tools (like TypeScript TravelAgent) - self.triage_agent = TriageAgent(tools=self.all_tools) - await self.triage_agent.initialize(self.chat_client) - logger.info("TriageAgent initialized with all tools") - - logger.info( - f"MAF workflow fully initialized with {len(self.all_tools)} total tools" - ) + self.triage_agent = None + self.customer_query_agent = None + self.destination_agent = None + self.itinerary_agent = None + self.echo_agent = None @property def agents(self) -> List[Any]: """Get list of all initialized agents.""" return [ - agent for agent in [ + agent + for agent in [ self.triage_agent, self.customer_query_agent, self.destination_agent, self.itinerary_agent, - self.code_eval_agent, - self.model_inference_agent, - self.web_search_agent, self.echo_agent, ] if agent is not None ] - async def process_request( - self, - message: str, - context: Optional[Dict[str, Any]] = None - ) -> str: + async def process_request(self, message: str, context: Optional[Dict[str, Any]] = None) -> str: """Process a travel planning request through the workflow. Args: @@ -345,23 +212,21 @@ async def process_request( raise RuntimeError("Workflow not initialized. Call initialize() first.") logger.info(f"Processing request: {message[:100]}...") - + try: # Use the triage agent to process the request # The triage agent will coordinate with other agents as needed result = await self.triage_agent.process(message, context) - + logger.info("Request processed successfully") return result - + except Exception as e: logger.error(f"Error processing request: {e}", exc_info=True) raise async def process_request_stream( - self, - message: str, - context: Optional[Dict[str, Any]] = None + self, message: str, context: Optional[Dict[str, Any]] = None ) -> AsyncGenerator[Dict[str, Any], None]: """Process a travel planning request through the workflow with streaming. @@ -384,7 +249,7 @@ async def process_request_stream( raise RuntimeError("Workflow not initialized. Call initialize() first.") logger.info(f"Processing streaming request: {message[:100]}...") - + try: # Send agent setup event yield { @@ -393,10 +258,10 @@ async def process_request_stream( "data": { "message": "Initializing travel planning workflow with MCP tools", "tool_count": len(self.all_tools), - "timestamp": None - } + "timestamp": None, + }, } - + # Send agent tool call event yield { "agent": "TriageAgent", @@ -404,50 +269,32 @@ async def process_request_stream( "data": { "message": "Processing travel request with specialized agents", "toolName": "triage_agent_process", - "timestamp": None - } + "timestamp": None, + }, } - + # Process through triage agent result = await self.triage_agent.process(message, context) - + # Send streaming chunks of the response # Split the response into chunks for streaming effect chunk_size = 50 for i in range(0, len(result), chunk_size): - chunk = result[i:i + chunk_size] - yield { - "agent": "TriageAgent", - "event": "AgentStream", - "data": { - "delta": chunk, - "timestamp": None - } - } - + chunk = result[i : i + chunk_size] + yield {"agent": "TriageAgent", "event": "AgentStream", "data": {"delta": chunk, "timestamp": None}} + # Send completion event yield { "agent": "TriageAgent", "event": "AgentComplete", - "data": { - "message": "Request processed successfully", - "result": result, - "timestamp": None - } + "data": {"message": "Request processed successfully", "result": result, "timestamp": None}, } - + logger.info("Streaming request processed successfully") - + except Exception as e: logger.error(f"Error processing streaming request: {e}", exc_info=True) - yield { - "agent": None, - "event": "Error", - "data": { - "error": str(e), - "timestamp": None - } - } + yield {"agent": None, "event": "Error", "data": {"error": str(e), "timestamp": None}} raise async def get_agent_by_name(self, name: str) -> Optional[Any]: @@ -464,12 +311,7 @@ async def get_agent_by_name(self, name: str) -> Optional[Any]: return agent return None - async def handoff_to_agent( - self, - agent_name: str, - message: str, - context: Optional[Dict[str, Any]] = None - ) -> str: + async def handoff_to_agent(self, agent_name: str, message: str, context: Optional[Dict[str, Any]] = None) -> str: """Handoff request to a specific agent. Args: @@ -489,7 +331,7 @@ async def handoff_to_agent( logger.info(f"Handing off to {agent_name}") return await agent.process(message, context) - + async def close(self) -> None: """Clean up MCP client resources.""" for loader in self.mcp_loaders.values(): diff --git a/packages/api-python/src/tests/test_agents.py b/packages/api-python/src/tests/test_agents.py index 7568baac..f3555d1f 100644 --- a/packages/api-python/src/tests/test_agents.py +++ b/packages/api-python/src/tests/test_agents.py @@ -14,12 +14,8 @@ @pytest.mark.asyncio async def test_base_agent_initialization(): """Test base agent initialization.""" - agent = BaseAgent( - name="TestAgent", - description="Test agent", - system_prompt="Test prompt" - ) - + agent = BaseAgent(name="TestAgent", description="Test agent", system_prompt="Test prompt") + assert agent.name == "TestAgent" assert agent.description == "Test agent" assert agent.system_prompt == "Test prompt" @@ -29,17 +25,13 @@ async def test_base_agent_initialization(): @pytest.mark.asyncio async def test_base_agent_initialize_with_llm(): """Test base agent initialization with LLM client.""" - agent = BaseAgent( - name="TestAgent", - description="Test agent", - system_prompt="Test prompt" - ) - + agent = BaseAgent(name="TestAgent", description="Test agent", system_prompt="Test prompt") + mock_llm_client = MagicMock() - + with patch("src.orchestrator.agents.base_agent.Agent") as mock_agent_class: await agent.initialize(mock_llm_client) - + assert agent.agent is not None mock_agent_class.assert_called_once() @@ -47,12 +39,8 @@ async def test_base_agent_initialize_with_llm(): @pytest.mark.asyncio async def test_base_agent_process_without_initialization(): """Test that processing without initialization raises error.""" - agent = BaseAgent( - name="TestAgent", - description="Test agent", - system_prompt="Test prompt" - ) - + agent = BaseAgent(name="TestAgent", description="Test agent", system_prompt="Test prompt") + with pytest.raises(RuntimeError, match="not initialized"): await agent.process("test message") @@ -61,7 +49,7 @@ async def test_base_agent_process_without_initialization(): async def test_triage_agent_initialization(): """Test triage agent initialization.""" agent = TriageAgent() - + assert agent.name == "TriageAgent" assert "triage" in agent.description.lower() assert agent.system_prompt is not None @@ -71,7 +59,7 @@ async def test_triage_agent_initialization(): async def test_customer_query_agent_initialization(): """Test customer query agent initialization.""" agent = CustomerQueryAgent() - + assert agent.name == "CustomerQueryAgent" assert "customer" in agent.description.lower() assert agent.system_prompt is not None @@ -81,7 +69,7 @@ async def test_customer_query_agent_initialization(): async def test_destination_recommendation_agent_initialization(): """Test destination recommendation agent initialization.""" agent = DestinationRecommendationAgent() - + assert agent.name == "DestinationRecommendationAgent" assert "destination" in agent.description.lower() assert agent.system_prompt is not None @@ -92,6 +80,6 @@ async def test_destination_agent_with_tools(): """Test destination agent with tools.""" mock_tools = [MagicMock(), MagicMock()] agent = DestinationRecommendationAgent(tools=mock_tools) - + assert agent.tools == mock_tools assert len(agent.tools) == 2 diff --git a/packages/api-python/src/tests/test_config.py b/packages/api-python/src/tests/test_config.py index 8506256d..e014ece5 100644 --- a/packages/api-python/src/tests/test_config.py +++ b/packages/api-python/src/tests/test_config.py @@ -15,9 +15,6 @@ def test_settings_defaults(): mcp_customer_query_url="http://localhost:5001", mcp_destination_recommendation_url="http://localhost:5002", mcp_itinerary_planning_url="http://localhost:5003", - mcp_code_evaluation_url="http://localhost:5004", - mcp_model_inference_url="http://localhost:5005", - mcp_web_search_url="http://localhost:5006", mcp_echo_ping_url="http://localhost:5007", ) @@ -36,9 +33,6 @@ def test_settings_custom_port(): mcp_customer_query_url="http://localhost:5001", mcp_destination_recommendation_url="http://localhost:5002", mcp_itinerary_planning_url="http://localhost:5003", - mcp_code_evaluation_url="http://localhost:5004", - mcp_model_inference_url="http://localhost:5005", - mcp_web_search_url="http://localhost:5006", mcp_echo_ping_url="http://localhost:5007", port=5000, ) diff --git a/packages/api-python/src/tests/test_mcp_client.py b/packages/api-python/src/tests/test_mcp_client.py index 5246fc38..93876e62 100644 --- a/packages/api-python/src/tests/test_mcp_client.py +++ b/packages/api-python/src/tests/test_mcp_client.py @@ -8,19 +8,15 @@ async def test_mcp_tool_loader_initialization(): """Test MCPToolLoader initialization with Microsoft Agent Framework SDK.""" from orchestrator.tools.mcp_tool_wrapper import MCPToolLoader - - config = { - "url": "http://localhost:8001/mcp", - "type": "http", - "verbose": True - } - + + config = {"url": "http://localhost:8001/mcp", "type": "http", "verbose": True} + loader = MCPToolLoader(config, "Test Server") - + assert loader.server_name == "Test Server" assert loader.base_url == "http://localhost:8001/mcp" assert loader._tools == [] - + await loader.close() @@ -28,54 +24,50 @@ async def test_mcp_tool_loader_initialization(): async def test_tool_registry_initialization(): """Test ToolRegistry initialization.""" from orchestrator.tools.tool_registry import ToolRegistry - + # Create a new registry instance registry = ToolRegistry() - + # Should have loaders for configured servers assert len(registry.loaders) > 0 - + await registry.close_all() -@pytest.mark.asyncio +@pytest.mark.asyncio async def test_get_tools_with_maf_sdk(): """Test loading tools using Microsoft Agent Framework's MCPStreamableHTTPTool.""" from orchestrator.tools.mcp_tool_wrapper import MCPToolLoader from agent_framework import MCPStreamableHTTPTool - - config = { - "url": "http://localhost:8001/mcp", - "type": "http", - "verbose": True - } - + + config = {"url": "http://localhost:8001/mcp", "type": "http", "verbose": True} + loader = MCPToolLoader(config, "Test Server") - + # Mock the MCPStreamableHTTPTool context manager mock_tool = MagicMock() mock_tool.functions = [MagicMock(name="test_tool")] - - with patch('orchestrator.tools.mcp_tool_wrapper.MCPStreamableHTTPTool') as mock_tool_class: + + with patch("orchestrator.tools.mcp_tool_wrapper.MCPStreamableHTTPTool") as mock_tool_class: # Make the mock return an async context manager mock_context = AsyncMock() mock_context.__aenter__.return_value = mock_tool mock_context.__aexit__.return_value = None mock_tool_class.return_value = mock_context - + tools = await loader.get_tools() - + # Should have called MCPStreamableHTTPTool with correct parameters mock_tool_class.assert_called_once() call_kwargs = mock_tool_class.call_args[1] - assert call_kwargs['name'] == "Test Server" - assert call_kwargs['url'] == "http://localhost:8001/mcp" - assert call_kwargs['load_tools'] == True - + assert call_kwargs["name"] == "Test Server" + assert call_kwargs["url"] == "http://localhost:8001/mcp" + assert call_kwargs["load_tools"] == True + # Should return the tools assert len(tools) == 1 assert tools[0].name == "test_tool" - + await loader.close() @@ -83,25 +75,25 @@ async def test_get_tools_with_maf_sdk(): async def test_tool_registry_get_all_tools(): """Test getting all tools from registry.""" from orchestrator.tools.tool_registry import tool_registry - + # Mock the tool loading - with patch.object(tool_registry, 'loaders') as mock_loaders: + with patch.object(tool_registry, "loaders") as mock_loaders: # Create a mock loader mock_loader = AsyncMock() mock_tool = MagicMock() mock_tool.name = "test_tool" mock_loader.get_tools.return_value = [mock_tool] - + mock_loaders.items.return_value = [("test-server", mock_loader)] mock_loaders.keys.return_value = ["test-server"] mock_loaders.__contains__ = lambda self, key: key == "test-server" - + # Get tools tools = await tool_registry.get_all_tools() - + # Should have called the loader mock_loader.get_tools.assert_called_once() - + # Should return the tools assert len(tools) == 1 assert tools[0] == mock_tool @@ -112,6 +104,7 @@ async def test_maf_sdk_import(): """Test that Microsoft Agent Framework SDK imports work correctly.""" try: from agent_framework import MCPStreamableHTTPTool + assert MCPStreamableHTTPTool is not None except ImportError as e: # Expected if SDK not installed @@ -122,20 +115,16 @@ async def test_maf_sdk_import(): async def test_mcp_tool_with_auth_header(): """Test MCPToolLoader with authentication header.""" from orchestrator.tools.mcp_tool_wrapper import MCPToolLoader - - config = { - "url": "http://localhost:8001/mcp", - "type": "http", - "accessToken": "test-token-123" - } - + + config = {"url": "http://localhost:8001/mcp", "type": "http", "accessToken": "test-token-123"} + loader = MCPToolLoader(config, "Authenticated Server") - + # Verify the authentication header is configured assert loader.access_token == "test-token-123" assert "Authorization" in loader.headers assert loader.headers["Authorization"] == "Bearer test-token-123" - + await loader.close() @@ -144,26 +133,23 @@ async def test_error_handling_on_connection_failure(): """Test error handling when MCP server connection fails.""" from orchestrator.tools.mcp_tool_wrapper import MCPToolLoader from agent_framework import MCPStreamableHTTPTool - - config = { - "url": "http://localhost:8001/mcp", - "type": "http" - } - + + config = {"url": "http://localhost:8001/mcp", "type": "http"} + loader = MCPToolLoader(config, "Test Server") - + # Mock connection failure - with patch('orchestrator.tools.mcp_tool_wrapper.MCPStreamableHTTPTool') as mock_tool_class: + with patch("orchestrator.tools.mcp_tool_wrapper.MCPStreamableHTTPTool") as mock_tool_class: # Make the context manager raise an exception mock_context = AsyncMock() mock_context.__aenter__.side_effect = Exception("Connection failed") mock_tool_class.return_value = mock_context - + tools = await loader.get_tools() - + # Should return empty list on error assert tools == [] - + await loader.close() @@ -172,29 +158,26 @@ async def test_context_manager_cleanup(): """Test that async context manager properly cleans up resources.""" from orchestrator.tools.mcp_tool_wrapper import MCPToolLoader from agent_framework import MCPStreamableHTTPTool - - config = { - "url": "http://localhost:8001/mcp", - "type": "http" - } - + + config = {"url": "http://localhost:8001/mcp", "type": "http"} + loader = MCPToolLoader(config, "Test Server") - + # Mock the MCPStreamableHTTPTool mock_tool = MagicMock() mock_tool.functions = [MagicMock(name="tool1")] - + mock_exit = AsyncMock() - - with patch('orchestrator.tools.mcp_tool_wrapper.MCPStreamableHTTPTool') as mock_tool_class: + + with patch("orchestrator.tools.mcp_tool_wrapper.MCPStreamableHTTPTool") as mock_tool_class: mock_context = AsyncMock() mock_context.__aenter__.return_value = mock_tool mock_context.__aexit__ = mock_exit mock_tool_class.return_value = mock_context - + await loader.get_tools() - + # Should have called __aexit__ for cleanup mock_exit.assert_called_once() - + await loader.close() diff --git a/packages/api-python/src/tests/test_mcp_graceful_degradation.py b/packages/api-python/src/tests/test_mcp_graceful_degradation.py index a2b8759e..842f501c 100644 --- a/packages/api-python/src/tests/test_mcp_graceful_degradation.py +++ b/packages/api-python/src/tests/test_mcp_graceful_degradation.py @@ -9,23 +9,20 @@ async def test_mcp_server_unavailable_graceful_degradation(): """Test that unavailable MCP servers are handled gracefully with warnings.""" from orchestrator.tools.mcp_tool_wrapper import MCPToolLoader - - config = { - "url": "http://unavailable-server:9999/mcp", - "type": "http" - } - + + config = {"url": "http://unavailable-server:9999/mcp", "type": "http"} + loader = MCPToolLoader(config, "Unavailable Server") - + # Mock connection failure - with patch('orchestrator.tools.mcp_tool_wrapper.MCPStreamableHTTPTool') as mock_tool_class: + with patch("orchestrator.tools.mcp_tool_wrapper.MCPStreamableHTTPTool") as mock_tool_class: mock_context = AsyncMock() mock_context.__aenter__.side_effect = ConnectionError("Server not responding") mock_tool_class.return_value = mock_context - + # Should return empty list, not raise exception tools = await loader.get_tools() - + assert tools == [] # No exception should be raised @@ -34,35 +31,32 @@ async def test_mcp_server_unavailable_graceful_degradation(): async def test_tool_registry_continues_with_failed_servers(caplog): """Test that tool registry continues loading from available servers when some fail.""" from orchestrator.tools.tool_registry import ToolRegistry - + registry = ToolRegistry() - + # Mock loaders: one succeeds, one fails - with patch.object(registry, 'loaders') as mock_loaders: + with patch.object(registry, "loaders") as mock_loaders: # Successful loader success_loader = AsyncMock() success_tool = MagicMock(name="working_tool") success_loader.get_tools.return_value = [success_tool] - + # Failed loader failed_loader = AsyncMock() failed_loader.get_tools.return_value = [] # Returns empty on failure - - mock_loaders.items.return_value = [ - ("working-server", success_loader), - ("failed-server", failed_loader) - ] + + mock_loaders.items.return_value = [("working-server", success_loader), ("failed-server", failed_loader)] mock_loaders.keys.return_value = ["working-server", "failed-server"] mock_loaders.__contains__ = lambda self, key: key in ["working-server", "failed-server"] - + # Get tools from all servers with caplog.at_level(logging.WARNING): tools = await registry.get_all_tools() - + # Should have tools from successful server assert len(tools) == 1 assert tools[0] == success_tool - + # Should log warning about failed server warning_logs = [record for record in caplog.records if record.levelname == "WARNING"] assert any("failed-server" in record.message for record in warning_logs) @@ -72,30 +66,27 @@ async def test_tool_registry_continues_with_failed_servers(caplog): async def test_all_servers_unavailable_no_exception(caplog): """Test that when all MCP servers are unavailable, system continues without tools.""" from orchestrator.tools.tool_registry import ToolRegistry - + registry = ToolRegistry() - + # Mock all loaders to fail - with patch.object(registry, 'loaders') as mock_loaders: + with patch.object(registry, "loaders") as mock_loaders: failed_loader1 = AsyncMock() failed_loader1.get_tools.return_value = [] - + failed_loader2 = AsyncMock() failed_loader2.get_tools.return_value = [] - - mock_loaders.items.return_value = [ - ("server1", failed_loader1), - ("server2", failed_loader2) - ] + + mock_loaders.items.return_value = [("server1", failed_loader1), ("server2", failed_loader2)] mock_loaders.keys.return_value = ["server1", "server2"] mock_loaders.__contains__ = lambda self, key: key in ["server1", "server2"] - + # Should return empty list, not raise exception with caplog.at_level(logging.WARNING): tools = await registry.get_all_tools() - + assert tools == [] - + # Should log warning about no tools warning_logs = [record for record in caplog.records if record.levelname == "WARNING"] assert any("No tools loaded" in record.message for record in warning_logs) @@ -105,36 +96,32 @@ async def test_all_servers_unavailable_no_exception(caplog): async def test_partial_server_failure_continues(caplog): """Test that partial server failures don't stop the workflow.""" from orchestrator.tools.tool_registry import ToolRegistry - + registry = ToolRegistry() - - with patch.object(registry, 'loaders') as mock_loaders: + + with patch.object(registry, "loaders") as mock_loaders: # Create 3 loaders: 2 succeed, 1 fails loader1 = AsyncMock() tool1 = MagicMock(name="tool1") loader1.get_tools.return_value = [tool1] - + loader2 = AsyncMock() loader2.get_tools.return_value = [] # Failed - + loader3 = AsyncMock() tool3 = MagicMock(name="tool3") loader3.get_tools.return_value = [tool3] - - mock_loaders.items.return_value = [ - ("server1", loader1), - ("server2", loader2), - ("server3", loader3) - ] + + mock_loaders.items.return_value = [("server1", loader1), ("server2", loader2), ("server3", loader3)] mock_loaders.keys.return_value = ["server1", "server2", "server3"] mock_loaders.__contains__ = lambda self, key: key in ["server1", "server2", "server3"] - + with caplog.at_level(logging.INFO): tools = await registry.get_all_tools() - + # Should have tools from successful servers assert len(tools) == 2 - + # Should log success for working servers info_logs = [record for record in caplog.records if record.levelname == "INFO"] assert any("server1" in record.message for record in info_logs) @@ -146,23 +133,23 @@ async def test_workflow_initialization_with_no_tools(): """Test that workflow can initialize even when no MCP tools are available.""" from orchestrator.workflow import TravelWorkflowOrchestrator from orchestrator.tools.tool_registry import tool_registry - + orchestrator = TravelWorkflowOrchestrator() - + # Mock tool registry to return empty list - with patch.object(tool_registry, 'get_all_tools', new_callable=AsyncMock) as mock_get_tools: + with patch.object(tool_registry, "get_all_tools", new_callable=AsyncMock) as mock_get_tools: mock_get_tools.return_value = [] - + # Mock LLM client - with patch('orchestrator.workflow.get_llm_client', new_callable=AsyncMock) as mock_llm: + with patch("orchestrator.workflow.get_llm_client", new_callable=AsyncMock) as mock_llm: mock_llm.return_value = MagicMock() - + # Should initialize without error await orchestrator.initialize() - + # Should have empty tools list assert orchestrator.all_tools == [] - + # Agents should still be initialized (with no tools) assert orchestrator.triage_agent is not None assert orchestrator.customer_query_agent is not None @@ -172,29 +159,26 @@ async def test_workflow_initialization_with_no_tools(): async def test_connection_timeout_handled_gracefully(caplog): """Test that connection timeouts are handled gracefully.""" from orchestrator.tools.mcp_tool_wrapper import MCPToolLoader - - config = { - "url": "http://slow-server:8000/mcp", - "type": "http" - } - + + config = {"url": "http://slow-server:8000/mcp", "type": "http"} + loader = MCPToolLoader(config, "Slow Server") - + # Mock timeout error - with patch('orchestrator.tools.mcp_tool_wrapper.MCPStreamableHTTPTool') as mock_tool_class: + with patch("orchestrator.tools.mcp_tool_wrapper.MCPStreamableHTTPTool") as mock_tool_class: mock_context = AsyncMock() mock_context.__aenter__.side_effect = TimeoutError("Connection timeout") mock_tool_class.return_value = mock_context - + with caplog.at_level(logging.WARNING): tools = await loader.get_tools() - + assert tools == [] - + # Should log warning, not error warning_logs = [record for record in caplog.records if record.levelname == "WARNING"] assert any("unavailable or not responding" in record.message for record in warning_logs) - + # Should not have any ERROR logs error_logs = [record for record in caplog.records if record.levelname == "ERROR"] assert len(error_logs) == 0 diff --git a/packages/api-python/src/tests/test_workflow.py b/packages/api-python/src/tests/test_workflow.py index 983d28eb..dbf822d1 100644 --- a/packages/api-python/src/tests/test_workflow.py +++ b/packages/api-python/src/tests/test_workflow.py @@ -10,7 +10,7 @@ async def test_workflow_orchestrator_initialization(): """Test workflow orchestrator initialization.""" orchestrator = TravelWorkflowOrchestrator() - + assert orchestrator.triage_agent is not None assert orchestrator.customer_query_agent is not None assert orchestrator.destination_agent is not None @@ -22,14 +22,14 @@ async def test_workflow_orchestrator_initialization(): async def test_workflow_orchestrator_initialize(): """Test workflow orchestrator full initialization.""" orchestrator = TravelWorkflowOrchestrator() - + mock_llm_client = MagicMock() - + with patch("src.orchestrator.workflow.get_llm_client", return_value=mock_llm_client): with patch.object(orchestrator.triage_agent, "initialize", new_callable=AsyncMock): with patch("src.orchestrator.workflow.Workflow") as mock_workflow_class: await orchestrator.initialize() - + assert orchestrator.llm_client is not None assert orchestrator.workflow is not None mock_workflow_class.assert_called_once() @@ -39,7 +39,7 @@ async def test_workflow_orchestrator_initialize(): async def test_workflow_process_request_without_initialization(): """Test that processing without initialization raises error.""" orchestrator = TravelWorkflowOrchestrator() - + with pytest.raises(RuntimeError, match="not initialized"): await orchestrator.process_request("test message") @@ -48,11 +48,11 @@ async def test_workflow_process_request_without_initialization(): async def test_workflow_get_agent_by_name(): """Test getting agent by name.""" orchestrator = TravelWorkflowOrchestrator() - + agent = await orchestrator.get_agent_by_name("TriageAgent") assert agent is not None assert agent.name == "TriageAgent" - + agent = await orchestrator.get_agent_by_name("NonExistentAgent") assert agent is None @@ -61,24 +61,21 @@ async def test_workflow_get_agent_by_name(): async def test_workflow_handoff_to_agent(): """Test handoff to specific agent.""" orchestrator = TravelWorkflowOrchestrator() - + mock_llm_client = MagicMock() - + with patch("src.orchestrator.workflow.get_llm_client", return_value=mock_llm_client): with patch.object(orchestrator.triage_agent, "initialize", new_callable=AsyncMock): with patch.object(orchestrator.triage_agent, "process", new_callable=AsyncMock) as mock_process: mock_process.return_value = "Test response" - + # Initialize agents first for agent in orchestrator.agents: with patch.object(agent, "initialize", new_callable=AsyncMock): await agent.initialize(mock_llm_client) - - response = await orchestrator.handoff_to_agent( - "TriageAgent", - "test message" - ) - + + response = await orchestrator.handoff_to_agent("TriageAgent", "test message") + assert response == "Test response" mock_process.assert_called_once() @@ -87,9 +84,6 @@ async def test_workflow_handoff_to_agent(): async def test_workflow_handoff_to_nonexistent_agent(): """Test handoff to nonexistent agent raises error.""" orchestrator = TravelWorkflowOrchestrator() - + with pytest.raises(ValueError, match="not found"): - await orchestrator.handoff_to_agent( - "NonExistentAgent", - "test message" - ) + await orchestrator.handoff_to_agent("NonExistentAgent", "test message") diff --git a/packages/api-python/test_magentic_simplified.py b/packages/api-python/test_magentic_simplified.py deleted file mode 100755 index 58a86e1e..00000000 --- a/packages/api-python/test_magentic_simplified.py +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env python3 -"""Quick test script for simplified Magentic implementation.""" - -import asyncio -import logging -import sys -from pathlib import Path - -# Add src to path -sys.path.insert(0, str(Path(__file__).parent / "src")) - -from src.orchestrator.magentic_workflow import magentic_orchestrator - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - - -async def test_initialization(): - """Test that orchestrator can initialize.""" - logger.info("=" * 60) - logger.info("Testing Magentic Orchestrator Initialization") - logger.info("=" * 60) - - try: - await magentic_orchestrator.initialize() - logger.info("✓ Orchestrator initialized successfully") - return True - except Exception as e: - logger.error(f"✗ Initialization failed: {e}", exc_info=True) - return False - - -async def test_simple_request(): - """Test a simple request through the workflow.""" - logger.info("\n" + "=" * 60) - logger.info("Testing Simple Request") - logger.info("=" * 60) - - test_query = "What are some popular destinations for a beach vacation?" - logger.info(f"Query: {test_query}") - - try: - event_count = 0 - async for event in magentic_orchestrator.process_request_stream(test_query): - event_count += 1 - event_type = event.get("type") - agent = event.get("agent") - event_name = event.get("event") - - logger.info(f"Event #{event_count}: type={event_type}, agent={agent}, event={event_name}") - - # Limit output for testing - if event_count >= 10: - logger.info("Received 10 events, stopping...") - break - - logger.info(f"✓ Request completed with {event_count} events") - return True - - except Exception as e: - logger.error(f"✗ Request failed: {e}", exc_info=True) - return False - - -async def main(): - """Run all tests.""" - logger.info("\n" + "=" * 60) - logger.info("MAGENTIC ORCHESTRATOR TEST SUITE") - logger.info("=" * 60 + "\n") - - # Test 1: Initialization - init_ok = await test_initialization() - - if not init_ok: - logger.error("\n❌ Initialization failed - skipping request test") - return False - - # Test 2: Simple request - request_ok = await test_simple_request() - - # Summary - logger.info("\n" + "=" * 60) - logger.info("TEST SUMMARY") - logger.info("=" * 60) - logger.info(f"Initialization: {'✓ PASS' if init_ok else '✗ FAIL'}") - logger.info(f"Simple Request: {'✓ PASS' if request_ok else '✗ FAIL'}") - logger.info("=" * 60 + "\n") - - return init_ok and request_ok - - -if __name__ == "__main__": - success = asyncio.run(main()) - sys.exit(0 if success else 1) diff --git a/packages/api-python/test_simplification.py b/packages/api-python/test_simplification.py deleted file mode 100755 index 5c93bef9..00000000 --- a/packages/api-python/test_simplification.py +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/env python3 -"""Simple test script to verify MCP integration works correctly.""" - -import asyncio -import logging -import sys -import os - -# Add src to path for imports -sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) - -logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') -logger = logging.getLogger(__name__) - - -async def test_tool_registry(): - """Test the simplified tool registry.""" - logger.info("Testing Tool Registry...") - - from orchestrator.tools.tool_registry import tool_registry - - # Test metadata retrieval - metadata = tool_registry.get_server_metadata("customer-query") - if metadata: - logger.info(f"✓ Got metadata for customer-query: {metadata['name']} at {metadata['url']}") - else: - logger.error("✗ Failed to get metadata for customer-query") - return False - - # Test tool creation - tool = tool_registry.create_mcp_tool("customer-query") - if tool: - logger.info(f"✓ Created MCPStreamableHTTPTool instance: {type(tool).__name__}") - else: - logger.error("✗ Failed to create tool") - return False - - # Test list_tools - try: - tools_info = await tool_registry.list_tools() - logger.info(f"✓ list_tools() returned {len(tools_info['tools'])} tools") - for tool_info in tools_info['tools']: - status = "✓ reachable" if tool_info['reachable'] else "✗ not reachable" - logger.info(f" - {tool_info['name']}: {status}") - except Exception as e: - logger.error(f"✗ list_tools() failed: {e}") - return False - - return True - - -async def test_orchestrator_init(): - """Test the Magentic orchestrator initialization.""" - logger.info("\nTesting Magentic Orchestrator...") - - from orchestrator.magentic_workflow import magentic_orchestrator - - # Test initialization - try: - await magentic_orchestrator.initialize() - logger.info("✓ Orchestrator initialized successfully") - except Exception as e: - logger.error(f"✗ Orchestrator initialization failed: {e}") - return False - - # Check chat client is set - if magentic_orchestrator.chat_client: - logger.info(f"✓ Chat client configured: {type(magentic_orchestrator.chat_client).__name__}") - else: - logger.error("✗ Chat client not configured") - return False - - return True - - -async def test_workflow_streaming(): - """Test workflow event streaming (basic structure test).""" - logger.info("\nTesting Workflow Streaming Structure...") - - from orchestrator.magentic_workflow import magentic_orchestrator - - # This tests the structure without actually making LLM calls - # which would require a running LLM service - try: - # Get one event to verify streaming works - event_count = 0 - async for event in magentic_orchestrator.process_request_stream("test message"): - event_count += 1 - logger.info(f"✓ Received event: type={event.get('type')}, agent={event.get('agent')}") - if event_count >= 1: - # Got at least one event, structure is working - break - - if event_count > 0: - logger.info("✓ Event streaming structure verified") - return True - else: - logger.warning("⚠ No events received (this may require LLM service)") - return True # Still pass - structure is correct - - except Exception as e: - logger.error(f"✗ Workflow streaming failed: {e}") - import traceback - traceback.print_exc() - return False - - -async def main(): - """Run all tests.""" - logger.info("="*60) - logger.info("MCP Integration Simplification - Test Suite") - logger.info("="*60) - - tests = [ - ("Tool Registry", test_tool_registry), - ("Orchestrator Init", test_orchestrator_init), - ("Workflow Streaming", test_workflow_streaming), - ] - - results = [] - for name, test_func in tests: - try: - result = await test_func() - results.append((name, result)) - except Exception as e: - logger.error(f"✗ {name} crashed: {e}") - results.append((name, False)) - - logger.info("\n" + "="*60) - logger.info("Test Results Summary") - logger.info("="*60) - - for name, result in results: - status = "✓ PASS" if result else "✗ FAIL" - logger.info(f"{status}: {name}") - - all_passed = all(result for _, result in results) - if all_passed: - logger.info("\n✅ All tests passed!") - return 0 - else: - logger.error("\n❌ Some tests failed") - return 1 - - -if __name__ == "__main__": - sys.exit(asyncio.run(main())) diff --git a/packages/api/.env.sample b/packages/api/.env.sample index c2774424..59ccc10a 100644 --- a/packages/api/.env.sample +++ b/packages/api/.env.sample @@ -43,9 +43,6 @@ OLLAMA_MODEL=llama3.1 MCP_CUSTOMER_QUERY_URL=http://tool-customer-query:8080 MCP_DESTINATION_RECOMMENDATION_URL=http://tool-destination-recommendation:5002 MCP_ITINERARY_PLANNING_URL=http://tool-itinerary-planning:5003 -MCP_CODE_EVALUATION_URL=http://tool-code-evaluation:5004 -MCP_MODEL_INFERENCE_URL=http://tool-model-inference:5005 -MCP_WEB_SEARCH_URL=http://tool-web-search:5006 MCP_ECHO_PING_URL=http://tool-echo-ping:5000 MCP_ECHO_PING_ACCESS_TOKEN=123-this-is-a-fake-token-please-use-a-token-provider diff --git a/packages/api/readme.md b/packages/api/readme.md index 8ca406c5..84478aad 100644 --- a/packages/api/readme.md +++ b/packages/api/readme.md @@ -1,9 +1,7 @@ ## MCP Host This package represents the MCP (Model Context Protocol). It contains the following MCP Clients: -- **Model Inference**: This tool runs an LLM using ONNX on Azure Container Apps' serverless GPU for AI-powered responses. - **Echo MCP**: This tool echoes back any received input and is used as an example. - **Customer Query**: This tool handles customer queries and provides relevant information. - **Destination Recommendation**: This tool recommends travel destinations based on user preferences. - **Itinerary Planning**: This tool creates detailed travel itineraries based on user preferences. -- **Web Search**: This tool uses Grounding with Bing Search to fetch live travel data. \ No newline at end of file diff --git a/packages/api/src/orchestrator/langchain/tools/index.ts b/packages/api/src/orchestrator/langchain/tools/index.ts index 1e6926dc..0b5e18d0 100644 --- a/packages/api/src/orchestrator/langchain/tools/index.ts +++ b/packages/api/src/orchestrator/langchain/tools/index.ts @@ -3,13 +3,9 @@ import { McpServerDefinition } from "../../../mcp/mcp-tools.js"; export type McpServerName = | "echo-ping" | "customer-query" - | "web-search" | "itinerary-planning" - | "model-inference" - | "code-evaluation" | "destination-recommendation"; -const MCP_API_SSE_PATH = "/sse"; const MCP_API_HTTP_PATH = "/mcp"; export const McpToolsConfig = (): { @@ -40,16 +36,6 @@ export const McpToolsConfig = (): { id: "customer-query", name: "Customer Query", }, - "web-search": { - config: { - url: process.env["MCP_WEB_SEARCH_URL"] + MCP_API_SSE_PATH, - type: "sse", - verbose: true, - useSSETransport: true - }, - id: "web-search", - name: "Web Search", - }, "itinerary-planning": { config: { url: process.env["MCP_ITINERARY_PLANNING_URL"] + MCP_API_HTTP_PATH, @@ -60,26 +46,6 @@ export const McpToolsConfig = (): { id: "itinerary-planning", name: "Itinerary Planning", }, - "model-inference": { - config: { - url: process.env["MCP_MODEL_INFERENCE_URL"] + MCP_API_SSE_PATH, - type: "sse", - verbose: true, - useSSETransport: true - }, - id: "model-inference", - name: "Model Inference", - }, - "code-evaluation": { - config: { - url: process.env["MCP_CODE_EVALUATION_URL"] + MCP_API_SSE_PATH, - type: "sse", - verbose: true, - useSSETransport: true - }, - id: "code-evaluation", - name: "Code Evaluation", - }, "destination-recommendation": { config: { url: process.env["MCP_DESTINATION_RECOMMENDATION_URL"] + MCP_API_HTTP_PATH, diff --git a/packages/api/src/orchestrator/llamaindex/index.ts b/packages/api/src/orchestrator/llamaindex/index.ts index f97b28a8..1a571e13 100644 --- a/packages/api/src/orchestrator/llamaindex/index.ts +++ b/packages/api/src/orchestrator/llamaindex/index.ts @@ -59,23 +59,6 @@ export async function setupAgents(filteredTools: McpServerDefinition[] = []) { toolsList.push(...tools); } - if (tools["web-search"]) { - const mcpServerConfig = mcpToolsConfig["web-search"]; - const tools = await mcp(mcpServerConfig.config).tools(); - console.log("Including Web Search Agent in the workflow"); - const webSearchAgent = agent({ - name: "WebSearchAgent", - systemPrompt: - "Searches the web for up-to-date travel information using Bing Search.", - tools, - llm, - verbose, - }); - agentsList.push(webSearchAgent); - handoffTargets.push(webSearchAgent); - toolsList.push(...tools); - } - if (tools["itinerary-planning"]) { const mcpServerConfig = mcpToolsConfig["itinerary-planning"]; const tools = await mcp(mcpServerConfig.config).tools(); @@ -92,38 +75,6 @@ export async function setupAgents(filteredTools: McpServerDefinition[] = []) { toolsList.push(...tools); } - if (tools["model-inference"]) { - const mcpServerConfig = mcpToolsConfig["model-inference"]; - const tools = await mcp(mcpServerConfig.config).tools(); - const modelInferenceAgent = agent({ - name: "ModelInferenceAgent", - systemPrompt: - "Performs model inference tasks based on user input and requirements.", - tools, - llm, - verbose, - }); - agentsList.push(modelInferenceAgent); - handoffTargets.push(modelInferenceAgent); - toolsList.push(...tools); - } - - if (tools["code-evaluation"]) { - const mcpServerConfig = mcpToolsConfig["code-evaluation"]; - const tools = await mcp(mcpServerConfig.config).tools(); - const codeEvaluationAgent = agent({ - name: "CodeEvaluationAgent", - systemPrompt: - "Evaluates code snippets and provides feedback or suggestions.", - tools, - llm, - verbose, - }); - agentsList.push(codeEvaluationAgent); - handoffTargets.push(codeEvaluationAgent); - toolsList.push(...tools); - } - if (tools["destination-recommendation"]) { const mcpServerConfig = mcpToolsConfig["destination-recommendation"]; const tools = await mcp(mcpServerConfig.config).tools(); diff --git a/packages/api/src/orchestrator/llamaindex/tools/index.ts b/packages/api/src/orchestrator/llamaindex/tools/index.ts index 3c2ad9de..cea7a71f 100644 --- a/packages/api/src/orchestrator/llamaindex/tools/index.ts +++ b/packages/api/src/orchestrator/llamaindex/tools/index.ts @@ -3,10 +3,7 @@ import { McpServerDefinition } from "../../../mcp/mcp-tools.js"; export type McpServerName = | "echo-ping" | "customer-query" - | "web-search" | "itinerary-planning" - | "model-inference" - | "code-evaluation" | "destination-recommendation"; const MCP_API_SSE_PATH = "/sse"; @@ -40,16 +37,6 @@ export const McpToolsConfig = (): { id: "customer-query", name: "Customer Query", }, - "web-search": { - config: { - url: process.env["MCP_WEB_SEARCH_URL"] + MCP_API_SSE_PATH, - type: "sse", - verbose: true, - useSSETransport: true - }, - id: "web-search", - name: "Web Search", - }, "itinerary-planning": { config: { url: process.env["MCP_ITINERARY_PLANNING_URL"] + MCP_API_HTTP_PATH, @@ -60,26 +47,6 @@ export const McpToolsConfig = (): { id: "itinerary-planning", name: "Itinerary Planning", }, - "model-inference": { - config: { - url: process.env["MCP_MODEL_INFERENCE_URL"] + MCP_API_SSE_PATH, - type: "sse", - verbose: true, - useSSETransport: true - }, - id: "model-inference", - name: "Model Inference", - }, - "code-evaluation": { - config: { - url: process.env["MCP_CODE_EVALUATION_URL"] + MCP_API_SSE_PATH, - type: "sse", - verbose: true, - useSSETransport: true - }, - id: "code-evaluation", - name: "Code Evaluation", - }, "destination-recommendation": { config: { url: process.env["MCP_DESTINATION_RECOMMENDATION_URL"] + MCP_API_HTTP_PATH, diff --git a/packages/docker-compose.yml b/packages/docker-compose.yml index e2abc638..5f4a93e7 100644 --- a/packages/docker-compose.yml +++ b/packages/docker-compose.yml @@ -21,24 +21,6 @@ services: ports: - "5003:8000" - tool-code-evaluation: - container_name: tool-code-evaluation - build: ./tools/code-evaluation - ports: - - "5004:5000" - - tool-model-inference: - container_name: tool-model-inference - build: ./tools/model-inference - ports: - - "5005:5000" - - tool-web-search: - container_name: tool-web-search - build: ./tools/web-search - ports: - - "5006:5000" - tool-echo-ping: container_name: tool-echo-ping build: ./tools/echo-ping @@ -59,9 +41,6 @@ services: - tool-customer-query - tool-destination-recommendation - tool-itinerary-planning - - tool-code-evaluation - - tool-model-inference - - tool-web-search - tool-echo-ping env_file: - "./api/.env" @@ -79,9 +58,6 @@ services: - tool-customer-query - tool-destination-recommendation - tool-itinerary-planning - - tool-code-evaluation - - tool-model-inference - - tool-web-search - tool-echo-ping env_file: - "./api-python/.env" diff --git a/packages/tools/README.md b/packages/tools/README.md index 32b19d0e..19ddc143 100644 --- a/packages/tools/README.md +++ b/packages/tools/README.md @@ -4,15 +4,12 @@ This directory contains various AI agent tools that serve different purposes wit ## Overview of Tools -| Tool Name | Technology | Containerized ? | OpenTelemetry ? | Code Interpreter | GPU ? | AI Foundry Access | -| ---------------------------------------------------------- | ---------- | --------------- | --------------- | ---------------- | ----- | ----------------- | -| [echo-ping](./echo-ping) (for testing) | TypeScript | ✅ | ✅ | | | | -| [code-evaluation](./code-evaluation) | Python | ✅ | ✅ | ✅ | | | -| [web-search](./web-search) | TypeScript | ✅ | ✅ | | | ✅ | -| [customer-query](./customer-query) | C# | ✅ | ✅ | | | ✅ | -| [destination-recommendation](./destination-recommendation) | Java | ✅ | ✅ | | | ✅ | -| [itinerary-planning](./itinerary-planning) | Python | ✅ | ✅ | | | | -| [model-inference](./model-inference) | Python | ✅ | ✅ | | ✅ | | +| Tool Name | Technology | Containerized ? | OpenTelemetry ? | AI Foundry Access | +| ---------------------------------------------------------- | ---------- | --------------- | --------------- | ----------------- | +| [echo-ping](./echo-ping) (for testing) | TypeScript | ✅ | ✅ | | +| [customer-query](./customer-query) | C# | ✅ | ✅ | ✅ | +| [destination-recommendation](./destination-recommendation) | Java | ✅ | ✅ | ✅ | +| [itinerary-planning](./itinerary-planning) | Python | ✅ | ✅ | | ## Tool Descriptions @@ -36,14 +33,6 @@ A Node.js MCP server that provides echo functionality for testing purposes. Feat A tool for planning detailed travel itineraries. -### model-inference - -A tool for performing AI model inference locally with GPU support. - -### web-search - -A tool for searching the web for travel-related information, using Bing Grounding API. - ## Setting Up Each tool is containerized using Docker and can be deployed independently or as part of the entire AI Travel Agents suite using Docker Compose (see [docker-compose.yml](../docker-compose.yml)). diff --git a/packages/tools/code-evaluation/Dockerfile b/packages/tools/code-evaluation/Dockerfile deleted file mode 100644 index 92933d05..00000000 --- a/packages/tools/code-evaluation/Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -# Use an ultra-lightweight base image -FROM alpine:latest - -# Default command to print "Hello World" -CMD ["echo", "Hello from code-evaluation!"] \ No newline at end of file diff --git a/packages/tools/model-inference/Dockerfile b/packages/tools/model-inference/Dockerfile deleted file mode 100644 index 4f3c2d8f..00000000 --- a/packages/tools/model-inference/Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -# Use an ultra-lightweight base image -FROM alpine:latest - -# Default command to print "Hello World" -CMD ["echo", "Hello from model-inference!"] \ No newline at end of file diff --git a/packages/tools/web-search/Dockerfile b/packages/tools/web-search/Dockerfile deleted file mode 100644 index b5328b5d..00000000 --- a/packages/tools/web-search/Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -# Use an ultra-lightweight base image -FROM alpine:latest - -# Default command to print "Hello World" -CMD ["echo", "Hello from web-search!"] \ No newline at end of file diff --git a/packages/ui/src/app/services/api.service.ts b/packages/ui/src/app/services/api.service.ts index 0c4ffdf6..e06f958a 100644 --- a/packages/ui/src/app/services/api.service.ts +++ b/packages/ui/src/app/services/api.service.ts @@ -11,10 +11,8 @@ import { environment } from '../../environments/environment'; export type ServerID = | 'echo-ping' | 'customer-query' - | 'web-search' | 'itinerary-planning' - | 'model-inference' - | 'code-evaluation'; + | 'destination-recommendation'; export type Tools = { id: ServerID; diff --git a/preview.ps1 b/preview.ps1 index 096b5bb4..ca4b6b41 100644 --- a/preview.ps1 +++ b/preview.ps1 @@ -112,9 +112,6 @@ DOCKER_MODEL=ai/phi4:14B-Q4_0 MCP_CUSTOMER_QUERY_URL=http://localhost:8080 MCP_DESTINATION_RECOMMENDATION_URL=http://localhost:5002 MCP_ITINERARY_PLANNING_URL=http://localhost:5003 -MCP_CODE_EVALUATION_URL=http://localhost:5004 -MCP_MODEL_INFERENCE_URL=http://localhost:5005 -MCP_WEB_SEARCH_URL=http://localhost:5006 MCP_ECHO_PING_URL=http://localhost:5007 MCP_ECHO_PING_ACCESS_TOKEN=123-this-is-a-fake-token-please-use-a-token-provider "@ diff --git a/preview.sh b/preview.sh index b8911c78..e450c84b 100755 --- a/preview.sh +++ b/preview.sh @@ -110,9 +110,6 @@ DOCKER_MODEL=ai/phi4:14B-Q4_0 MCP_CUSTOMER_QUERY_URL=http://localhost:8080 MCP_DESTINATION_RECOMMENDATION_URL=http://localhost:5002 MCP_ITINERARY_PLANNING_URL=http://localhost:5003 -MCP_CODE_EVALUATION_URL=http://localhost:5004 -MCP_MODEL_INFERENCE_URL=http://localhost:5005 -MCP_WEB_SEARCH_URL=http://localhost:5006 MCP_ECHO_PING_URL=http://localhost:5007 MCP_ECHO_PING_ACCESS_TOKEN=123-this-is-a-fake-token-please-use-a-token-provider EOM