SrikanthNagelli commited on
Commit
72282ab
Β·
1 Parent(s): dfa5b2c

inital commit

Browse files
Files changed (3) hide show
  1. README.md +297 -5
  2. app.py +1581 -0
  3. requirements.txt +15 -0
README.md CHANGED
@@ -1,12 +1,304 @@
1
  ---
2
- title: Test Mcp Client
3
- emoji: πŸ¦€
4
- colorFrom: purple
5
- colorTo: purple
6
  sdk: gradio
7
- sdk_version: 5.33.1
8
  app_file: app.py
9
  pinned: false
 
 
 
10
  ---
11
 
12
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: VOYAGER AI
3
+ emoji: 🌍
4
+ colorFrom: gray
5
+ colorTo: green
6
  sdk: gradio
7
+ sdk_version: 5.33.0
8
  app_file: app.py
9
  pinned: false
10
+ short_description: "AI MCP client: intelligent task decomposition & agents"
11
+ tags:
12
+ - agent-demo-track
13
  ---
14
 
15
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
16
+
17
+ # πŸ’» Enhanced MCP Client - AI-Powered Task Decomposition Interface
18
+
19
+ A sophisticated AI-enhanced client application that connects to MCP servers and provides intelligent task decomposition, agent coordination, and a beautiful Gradio web interface.
20
+
21
+ ## 🎯 **What is this?**
22
+
23
+ This is an **AI-powered MCP client** that transforms how you interact with MCP (Model Context Protocol) servers by providing:
24
+
25
+ - 🧠 **AI Task Decomposition** - Claude Sonnet 4 analyzes complex queries
26
+ - 🎯 **Smart Agent Routing** - Intelligent assignment of tasks to specialized agents
27
+ - πŸ“Š **Performance Monitoring** - Real-time tracking of agent usage and response times
28
+ - 🎨 **Modern Gradio UI** - Beautiful, intuitive web interface
29
+ - πŸ”„ **MCP Protocol Support** - Native connectivity to any MCP server
30
+ - πŸ’‘ **Query Insights** - See how your queries are analyzed and processed
31
+
32
+ ## ✨ **Key Features**
33
+
34
+ ### **🧠 AI-Powered Intelligence**
35
+ - **Claude Sonnet 4 Integration**: Advanced query understanding and decomposition
36
+ - **Dynamic Task Analysis**: Automatically breaks down complex multi-part queries
37
+ - **Context Awareness**: Understands intent, location, and complexity
38
+ - **Confidence Scoring**: Evaluates task assignment reliability
39
+
40
+ ### **πŸ€– Specialized Agent System**
41
+ - **Sentiment Agent**: Analyzes text emotion and sentiment
42
+ - **Place Agent**: Finds hotels, accommodations, and lodging
43
+ - **Restaurant Agent**: Discovers dining options and restaurants
44
+ - **Hiking Agent**: Locates trails and outdoor activities
45
+ - **Web Agent**: Searches the internet for information
46
+
47
+ ### **πŸ“Š Advanced Coordination**
48
+ - **Enhanced Coordinator**: Orchestrates multiple agents intelligently
49
+ - **Performance Tracking**: Monitors response times and success rates
50
+ - **Parallel Execution**: Handles multiple tasks simultaneously
51
+ - **Error Handling**: Graceful degradation and retry logic
52
+
53
+ ## πŸš€ **Quick Start**
54
+
55
+ ### 1. **Install Dependencies**
56
+ ```bash
57
+ pip install -r requirements.txt
58
+ ```
59
+
60
+ ### 2. **Set Up Environment Variables**
61
+ ```bash
62
+ # Required for AI features
63
+ export ANTHROPIC_API_KEY="your_anthropic_api_key"
64
+
65
+ # Optional for enhanced location services
66
+ export FOURSQUARE_API_KEY="your_foursquare_api_key"
67
+ ```
68
+
69
+ ### 3. **Start the Client**
70
+ ```bash
71
+ python client.py
72
+ ```
73
+
74
+ ### 4. **Access the Web Interface**
75
+ Open your browser to: `http://localhost:7860`
76
+
77
+ ## 🎨 **Web Interface**
78
+
79
+ ### **πŸ’¬ Enhanced Chat Tab**
80
+ - **Smart Query Processing**: Enter natural language queries
81
+ - **Real-time Analysis**: See how your query is decomposed
82
+ - **Rich Responses**: Beautiful formatted results from multiple agents
83
+ - **Example Queries**: Pre-built examples to get started
84
+
85
+ ### **πŸ› οΈ MCP Tools Tab**
86
+ - **Direct Tool Access**: Execute MCP tools directly
87
+ - **Tool Discovery**: Browse available MCP server tools
88
+ - **Parameter Input**: JSON-based tool parameter configuration
89
+ - **Real-time Results**: Immediate tool execution feedback
90
+
91
+ ### **πŸ“Š Performance Dashboard**
92
+ - **Agent Statistics**: Usage patterns and performance metrics
93
+ - **Response Times**: Average execution times per agent
94
+ - **Success Rates**: Reliability tracking across agents
95
+ - **MCP Connection Status**: Monitor server connectivity
96
+
97
+ ### **βš™οΈ Advanced Settings**
98
+ - **Model Configuration**: Select AI models (currently Anthropic)
99
+ - **MCP Server Setup**: Configure connection to any MCP server
100
+ - **Connection Management**: Reconnect and test connections
101
+
102
+ ## πŸ”§ **Configuration**
103
+
104
+ ### **Command Line Options**
105
+ ```bash
106
+ python client.py --help
107
+
108
+ Options:
109
+ --model {anthropic} AI model to use (default: anthropic)
110
+ --server-url TEXT MCP server URL (default: http://localhost:7861/gradio_api/mcp/sse)
111
+ ```
112
+
113
+ ### **Environment Variables**
114
+ - `ANTHROPIC_API_KEY` - **Required** for AI task decomposition
115
+ - `FOURSQUARE_API_KEY` - Optional for enhanced place search
116
+
117
+ ### **MCP Server Connection**
118
+ The client can connect to any MCP-compatible server:
119
+ ```bash
120
+ # Local server
121
+ python client.py --server-url "http://localhost:7861/gradio_api/mcp/sse"
122
+
123
+ # Remote server
124
+ python client.py --server-url "http://remote-host:7861/gradio_api/mcp/sse"
125
+
126
+ # Custom configuration
127
+ python client.py --model anthropic --server-url "http://custom-server:8080/mcp"
128
+ ```
129
+
130
+ ## πŸ—οΈ **Architecture**
131
+
132
+ ```
133
+ mcp_client/
134
+ β”œβ”€β”€ client.py # Main application with LLM integration
135
+ β”œβ”€β”€ app.py # HuggingFace Spaces entry point
136
+ β”œβ”€β”€ agents/ # AI Agent System
137
+ β”‚ β”œβ”€β”€ enhanced_coordinator.py # Smart coordination logic
138
+ β”‚ β”œβ”€β”€ task_analyzer.py # AI-powered query analysis
139
+ β”‚ β”œβ”€β”€ sentiment_agent.py # Sentiment analysis
140
+ β”‚ β”œβ”€β”€ place_agent.py # Place/hotel search
141
+ β”‚ β”œβ”€β”€ restaurant_agent.py # Restaurant search
142
+ β”‚ β”œβ”€β”€ hiking_agent.py # Hiking trail search
143
+ β”‚ β”œβ”€β”€ web_agent.py # Web search
144
+ β”‚ └── base_agent.py # Agent foundation
145
+ β”œβ”€β”€ services/ # Backend Services
146
+ β”‚ β”œβ”€β”€ place_service.py # Place search API
147
+ β”‚ β”œβ”€β”€ restaurant_service.py # Restaurant search API
148
+ β”‚ └── hiking_service.py # Hiking trail API
149
+ └── utils/
150
+ └── api_config.py # Configuration management
151
+ ```
152
+
153
+ ## πŸ’‘ **Usage Examples**
154
+
155
+ ### **Complex Multi-Agent Queries**
156
+ ```
157
+ "Find hotels and restaurants in Paris with hiking trails nearby"
158
+ ```
159
+ **What happens:**
160
+ 1. πŸ” **Claude Sonnet 4** breaks down the query into 3 components
161
+ 2. 🏨 **Place Agent** finds hotels in Paris
162
+ 3. 🍽️ **Restaurant Agent** discovers dining options
163
+ 4. πŸ₯Ύ **Hiking Agent** locates nearby trails
164
+ 5. 🧠 **Coordinator** combines and formats results
165
+
166
+ ### **Simple Single-Agent Tasks**
167
+ ```
168
+ "What's the weather like in Tokyo today?"
169
+ ```
170
+ **What happens:**
171
+ 1. πŸ” **Claude Sonnet 4** identifies this as a web search task
172
+ 2. 🌐 **Web Agent** searches for Tokyo weather information
173
+ 3. πŸ“Š **Coordinator** returns formatted weather data
174
+
175
+ ### **Sentiment Analysis**
176
+ ```
177
+ "Analyze sentiment: This product is absolutely amazing and exceeded all my expectations!"
178
+ ```
179
+ **What happens:**
180
+ 1. πŸ” **Claude Sonnet 4** identifies sentiment analysis task
181
+ 2. 😊 **Sentiment Agent** analyzes the emotional tone
182
+ 3. πŸ“ˆ **Returns** detailed sentiment breakdown with confidence scores
183
+
184
+ ## 🎯 **Smart Features**
185
+
186
+ ### **🧠 AI Query Analysis**
187
+ The task analyzer provides insights into:
188
+ - **Query Complexity**: Simple, moderate, or complex
189
+ - **Primary Intent**: Main goal of the query
190
+ - **Location Detection**: Automatic location extraction
191
+ - **Agent Assignment**: Which agents should handle which parts
192
+ - **Confidence Scores**: Reliability of task assignments
193
+
194
+ ### **πŸ“Š Performance Monitoring**
195
+ Track real-time metrics:
196
+ - **Total Queries Processed**: Overall usage statistics
197
+ - **Success/Failure Rates**: Reliability tracking
198
+ - **Average Response Times**: Performance monitoring
199
+ - **Agent Usage Patterns**: Most/least used agents
200
+ - **Error Analysis**: Common failure modes
201
+
202
+ ## πŸ› οΈ **Customization**
203
+
204
+ ### **Adding New Agents**
205
+ 1. Create a new agent class inheriting from `BaseAgent`
206
+ 2. Implement the required methods
207
+ 3. Add to the agent list in `client.py`
208
+
209
+ ### **Connecting to Different MCP Servers**
210
+ The client is designed to work with any MCP-compatible server:
211
+ - Standard MCP protocol support
212
+ - Automatic tool discovery
213
+ - Dynamic schema adaptation
214
+ - Error handling for server differences
215
+
216
+ ### **UI Customization**
217
+ The Gradio interface can be customized:
218
+ - Themes and styling
219
+ - Additional tabs and components
220
+ - Custom visualizations
221
+ - Branding and layout
222
+
223
+ ## πŸ› **Troubleshooting**
224
+
225
+ ### **Common Issues**
226
+
227
+ 1. **Client won't start**
228
+ - Check Python version (3.8+ required)
229
+ - Install dependencies: `pip install -r requirements.txt`
230
+ - Verify Anthropic API key is set
231
+
232
+ 2. **AI features not working**
233
+ - Ensure `ANTHROPIC_API_KEY` is configured
234
+ - Check internet connectivity
235
+ - Verify API key has sufficient credits
236
+
237
+ 3. **MCP server connection fails**
238
+ - Check MCP server is running
239
+ - Verify server command path
240
+ - Test with `--mcp-server` parameter
241
+
242
+ 4. **Agents return errors**
243
+ - Check individual API keys (Foursquare, etc.)
244
+ - Verify internet connection
245
+ - Review agent-specific error messages
246
+
247
+ ### **Debug Mode**
248
+ ```bash
249
+ # Start with verbose logging
250
+ python client.py --debug
251
+ ```
252
+
253
+ ## πŸ“¦ **Dependencies**
254
+
255
+ ### **Core Dependencies**
256
+ - `gradio` - Web interface framework
257
+ - `smolagents` - AI agent framework with MCP support
258
+ - `anthropic` - Claude AI integration
259
+ - `requests` - HTTP client for APIs
260
+ - `asyncio` - Async programming support
261
+
262
+ ### **Optional Dependencies**
263
+ - `foursquare` - Enhanced place search
264
+ - `beautifulsoup4` - Web scraping (for web agent)
265
+ - `selenium` - Advanced web automation
266
+
267
+ ## 🀝 **Integration**
268
+
269
+ ### **MCP Server Compatibility**
270
+ Works with any server implementing MCP protocol:
271
+ - βœ… Standard MCP servers
272
+ - βœ… Custom MCP implementations
273
+ - βœ… Third-party MCP services
274
+ - βœ… Cloud-hosted MCP servers
275
+
276
+ ### **AI Model Support**
277
+ Currently supports:
278
+ - βœ… **Anthropic Claude Sonnet 4** (primary)
279
+ - πŸ”„ **Additional models** (coming soon)
280
+
281
+ ### **Extension Points**
282
+ - Custom agent development
283
+ - Additional AI model backends
284
+ - Enhanced UI components
285
+ - Custom MCP server adapters
286
+
287
+ ## πŸ“„ **License**
288
+
289
+ MIT License - Free to use and modify!
290
+
291
+ ## πŸ”— **Related Resources**
292
+
293
+ - [Model Context Protocol Specification](https://spec.modelcontextprotocol.io/)
294
+ - [Anthropic AI Documentation](https://docs.anthropic.com/)
295
+ - [Gradio Documentation](https://gradio.app/docs/)
296
+ - [SmoLAgents Framework](https://github.com/huggingface/smolagents)
297
+
298
+ ## 🎬 **Demo & Videos**
299
+
300
+ - [πŸ“Ί VOYAGER AI Demo Video](https://www.youtube.com/watch?v=yrfhYyy0nIo) - Watch the application in action!
301
+
302
+ ---
303
+
304
+ **Experience the future of AI-powered task coordination with MCP!** πŸš€
app.py ADDED
@@ -0,0 +1,1581 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Enhanced MCP Client with LLM-based task decomposition, intelligent agent routing, and real MCP protocol.
4
+ This client uses AI for smart query analysis and agent coordination instead of hard-coded rules.
5
+ """
6
+ import gradio as gr
7
+ import asyncio
8
+ import json
9
+ import os
10
+ import sys
11
+ import argparse
12
+ from typing import Dict, List, Any, Optional
13
+ from dataclasses import dataclass
14
+ from enum import Enum
15
+ import uuid
16
+ from pathlib import Path
17
+ import requests
18
+ from smolagents import MCPClient, LiteLLMModel
19
+
20
+ ANTHROPIC_API_KEY = os.environ.get('ANTHROPIC_API_KEY')
21
+
22
+ def setup_environment():
23
+ """Set up environment variables and configuration."""
24
+ global ANTHROPIC_API_KEY
25
+
26
+ # Validate API keys
27
+ print("\nπŸ”‘ API Configuration:")
28
+ print(f"Anthropic API Key: {'βœ“ Configured' if ANTHROPIC_API_KEY else 'βœ— Missing'}")
29
+
30
+ if not ANTHROPIC_API_KEY:
31
+ print("⚠️ Warning: ANTHROPIC_API_KEY not found in environment")
32
+ print("πŸ’‘ Set environment variable: ANTHROPIC_API_KEY=your_anthropic_key")
33
+
34
+ class TaskType(Enum):
35
+ """Types of tasks that can be decomposed."""
36
+ SENTIMENT_ANALYSIS = "sentiment_analysis"
37
+ LOCATION_SEARCH = "place_search"
38
+ RESTAURANT_SEARCH = "restaurant_search"
39
+ HIKING_SEARCH = "hiking_search"
40
+ WEB_SEARCH = "web_search"
41
+ COMPLEX_QUERY = "complex_query"
42
+
43
+ @dataclass
44
+ class SubTask:
45
+ """Represents a sub-atomic task."""
46
+ id: str
47
+ task_type: TaskType
48
+ description: str
49
+ parameters: Dict[str, Any]
50
+ agent_id: str
51
+ confidence: float = 0.5
52
+ status: str = "pending"
53
+ result: Optional[Dict[str, Any]] = None
54
+
55
+ @dataclass
56
+ class Agent:
57
+ """Represents a dedicated agent for handling specific tools."""
58
+ id: str
59
+ name: str
60
+ tool_name: str
61
+ description: str
62
+ capabilities: List[str]
63
+ keywords: List[str]
64
+
65
+ class LLMTaskDecomposer:
66
+ """LLM-powered task decomposer using system prompts for intelligent query analysis."""
67
+
68
+ def __init__(self, model_name: str = "anthropic"):
69
+ """Initialize with support for multiple LLM providers."""
70
+ self.model_name = model_name.lower()
71
+ self.model = None
72
+
73
+ # Initialize the model based on selection
74
+ self._initialize_model()
75
+ self.agents = self._initialize_agents()
76
+
77
+ def _initialize_model(self):
78
+ """Initialize the selected model with proper error handling."""
79
+ try:
80
+ if self.model_name == "anthropic":
81
+ if not ANTHROPIC_API_KEY:
82
+ print("❌ ANTHROPIC_API_KEY environment variable is required for Anthropic model")
83
+ print("πŸ’‘ Model will fall back to keyword-based decomposition")
84
+ self.model = None
85
+ return
86
+
87
+ print(f"πŸ”§ Initializing Anthropic model...")
88
+ self.model = LiteLLMModel(
89
+ model_id="anthropic/claude-sonnet-4-20250514",
90
+ temperature=0.2,
91
+ api_key=ANTHROPIC_API_KEY
92
+ )
93
+
94
+ # Test the model with a simple call
95
+ try:
96
+ test_response = self.model([{"role": "user", "content": "Hello"}])
97
+ print(f"βœ… Anthropic model initialized and tested successfully")
98
+ print(f"🧠 Model response test: {str(test_response)[:50]}...")
99
+ except Exception as test_error:
100
+ print(f"⚠️ Model initialized but test call failed: {test_error}")
101
+ print(f"πŸ”„ Will attempt to use model anyway, with fallback to keywords")
102
+ else:
103
+ print(f"❌ Unknown model name: {self.model_name}")
104
+ self.model = None
105
+
106
+ except Exception as e:
107
+ print(f"❌ Model initialization failed: {e}")
108
+ print(f"πŸ”„ Falling back to keyword-based decomposition")
109
+ self.model = None
110
+
111
+ def get_model_info(self) -> Dict[str, str]:
112
+ """Get information about the current model."""
113
+ if self.model_name == "anthropic":
114
+ return {
115
+ "name": "Claude Sonnet 4",
116
+ "provider": "Anthropic",
117
+ "emoji": "πŸ€–",
118
+ "model_id": "anthropic/claude-sonnet-4-20250514",
119
+ "status": "initialized" if self.model else "failed"
120
+ }
121
+ else:
122
+ return {
123
+ "name": "Unknown Model",
124
+ "provider": "Unknown",
125
+ "emoji": "❓",
126
+ "model_id": "unknown",
127
+ "status": "failed"
128
+ }
129
+
130
+ def _initialize_agents(self) -> Dict[str, Agent]:
131
+ """Initialize specialized agents with their capabilities and keywords."""
132
+ agents = {
133
+ "sentiment_agent": Agent(
134
+ id="sentiment_agent",
135
+ name="Sentiment Analysis Agent",
136
+ tool_name="sentiment_analysis",
137
+ description="Analyzes text sentiment, emotions, and opinions",
138
+ capabilities=["text_analysis", "emotion_detection", "polarity_scoring", "opinion_mining"],
139
+ keywords=["sentiment", "feeling", "opinion", "review", "emotion", "mood", "analyze text", "positive", "negative", "happy", "sad", "angry", "excited"]
140
+ ),
141
+ "location_agent": Agent(
142
+ id="location_agent",
143
+ name="Location Search Agent",
144
+ tool_name="place_search",
145
+ description="Finds hotels, accommodations, and places to stay",
146
+ capabilities=["place_search", "hotel_finder", "accommodation_search", "lodging_recommendations"],
147
+ keywords=["hotel", "hotels", "stay", "accommodation", "lodging", "motel", "resort", "inn", "bed and breakfast", "airbnb", "place to stay"]
148
+ ),
149
+ "restaurant_agent": Agent(
150
+ id="restaurant_agent",
151
+ name="Restaurant Search Agent",
152
+ tool_name="restaurant_search",
153
+ description="Discovers restaurants, food places, and dining options",
154
+ capabilities=["restaurant_search", "cuisine_finder", "dining_recommendations", "food_discovery"],
155
+ keywords=["restaurant", "restaurants", "food", "dining", "eat", "dinner", "lunch", "breakfast", "cafe", "bar", "cuisine", "meal", "dining out"]
156
+ ),
157
+ "hiking_agent": Agent(
158
+ id="hiking_agent",
159
+ name="Hiking Search Agent",
160
+ tool_name="hiking_search",
161
+ description="Finds hiking trails, outdoor activities, and nature spots",
162
+ capabilities=["trail_finder", "outdoor_activities", "difficulty_assessment", "nature_exploration"],
163
+ keywords=["hike", "hiking", "trail", "trails", "trek", "trekking", "outdoor", "mountain", "nature", "walk", "walking", "climbing", "adventure"]
164
+ ),
165
+ "web_agent": Agent(
166
+ id="web_agent",
167
+ name="Web Search Agent",
168
+ tool_name="web_search",
169
+ description="Searches web for information, news, weather, finance, and general queries with intelligent ticker detection for financial data",
170
+ capabilities=["web_search", "information_retrieval", "real_time_data", "news_search", "weather_data", "financial_data", "ticker_detection"],
171
+ keywords=["search", "find", "lookup", "google", "web", "information", "weather", "news", "current", "latest", "what is", "definition", "stock", "price", "market", "finance"]
172
+ )
173
+ }
174
+ return agents
175
+
176
+ async def decompose_query(self, user_query: str) -> List[SubTask]:
177
+ """
178
+ Use LLM to analyze user query and decompose into actionable subtasks.
179
+ """
180
+ print(f"πŸ” Decomposing query: '{user_query}'")
181
+
182
+ try:
183
+ # Create decomposition prompt
184
+ system_prompt = self._create_decomposition_prompt()
185
+
186
+ # Prepare the user message
187
+ user_message = f"""Query to analyze: "{user_query}"
188
+
189
+ Please analyze this query and respond with a JSON object containing your analysis."""
190
+
191
+ print(f"🧠 Attempting LLM decomposition with model: {self.model_name}")
192
+
193
+ # Use Anthropic model (synchronous)
194
+ if self.model_name == "anthropic" and self.model is not None:
195
+ # Use LiteLLM model directly (synchronous)
196
+ try:
197
+ print(f"πŸ“‘ Calling LLM model...")
198
+ response = self.model([
199
+ {"role": "system", "content": system_prompt},
200
+ {"role": "user", "content": user_message}
201
+ ])
202
+ print(f"βœ… LLM response received: {str(response)[:200]}...")
203
+ print(f"πŸ” Response type: {type(response)}")
204
+ except Exception as e:
205
+ print(f"❌ Model call failed: {e}")
206
+ print(f"πŸ”„ Falling back to keyword-based decomposition")
207
+ return self._fallback_decomposition(user_query)
208
+ else:
209
+ print(f"❌ Model not available ({self.model_name}), using fallback")
210
+ return self._fallback_decomposition(user_query)
211
+
212
+ # Parse LLM response
213
+ print(f"πŸ” Parsing LLM response...")
214
+ analysis = self._parse_llm_response(response, user_query)
215
+ print(f"πŸ“‹ Analysis result: {analysis}")
216
+
217
+ # Convert analysis to subtasks
218
+ print(f"🎯 Creating subtasks from analysis...")
219
+ subtasks = self._create_subtasks(analysis, user_query)
220
+ print(f"βœ… Generated {len(subtasks)} subtasks")
221
+
222
+ if not subtasks:
223
+ print("⚠️ No subtasks generated, using fallback")
224
+ return self._fallback_decomposition(user_query)
225
+
226
+ # Debug: print subtask details
227
+ for i, subtask in enumerate(subtasks):
228
+ print(f" πŸ“ Subtask {i+1}: {subtask.agent_id} -> {subtask.description}")
229
+
230
+ return subtasks
231
+
232
+ except Exception as e:
233
+ print(f"❌ Task decomposition failed: {e}")
234
+ print(f"πŸ”„ Using fallback decomposition")
235
+ return self._fallback_decomposition(user_query)
236
+
237
+ def _create_decomposition_prompt(self) -> str:
238
+ """Create comprehensive system prompt for task decomposition."""
239
+ agent_descriptions = []
240
+ for agent_id, agent in self.agents.items():
241
+ agent_descriptions.append(f"""
242
+ **{agent.name}** ({agent_id}):
243
+ - Description: {agent.description}
244
+ - Tool: {agent.tool_name}
245
+ - Keywords: {', '.join(agent.keywords[:10])}
246
+ - Capabilities: {', '.join(agent.capabilities)}
247
+ """)
248
+
249
+ return f"""You are an intelligent task decomposer for a multi-agent system. Your job is to analyze user queries and route them to the most appropriate specialized agents.
250
+
251
+ AVAILABLE AGENTS:
252
+ {chr(10).join(agent_descriptions)}
253
+
254
+ TASK DECOMPOSITION RULES:
255
+ 1. **Analyze Intent**: Identify the primary purpose of the user's query
256
+ 2. **Extract Entities**: Find locations, keywords, parameters, and specific requirements
257
+ 3. **Route Intelligently**: Choose the most appropriate agent(s) based on intent and entities
258
+ 4. **Handle Complex Queries**: Break down multi-intent queries into separate tasks
259
+ 5. **Provide Fallbacks**: Use web_agent for ambiguous or unsupported queries
260
+
261
+ RESPONSE FORMAT:
262
+ Always respond with valid JSON in this exact format:
263
+ {{
264
+ "analysis": {{
265
+ "query_type": "simple|complex|ambiguous",
266
+ "primary_intent": "brief description of main intent",
267
+ "complexity_score": 0.0-1.0,
268
+ "location_extracted": "location if found or null",
269
+ "entities": ["entity1", "entity2"],
270
+ "reasoning": "brief explanation of your analysis"
271
+ }},
272
+ "tasks": [
273
+ {{
274
+ "task_id": "unique_id",
275
+ "agent_id": "agent_name",
276
+ "description": "clear task description",
277
+ "parameters": {{"param1": "value1"}},
278
+ "confidence": 0.0-1.0,
279
+ "priority": 1-5
280
+ }}
281
+ ]
282
+ }}
283
+
284
+ TOOL PARAMETER SPECIFICATIONS:
285
+ - **web_search**: {{"query": "search_terms", "max_results": 5}}
286
+ - **sentiment_analysis**: {{"text": "text_to_analyze"}}
287
+ - **place_search**: {{"query": "location", "max_distance": 20}}
288
+ - **restaurant_search**: {{"query": "location", "cuisine": "cuisine_type_or_null"}}
289
+ - **hiking_search**: {{"location": "location", "difficulty": "easy|moderate|hard|null", "max_distance": 30}}
290
+
291
+ COMPREHENSIVE EXAMPLES:
292
+
293
+ **Financial/Stock Queries (Enhanced with Ticker Detection):**
294
+ Query: "What's NVIDIA's current stock price?"
295
+ {{
296
+ "analysis": {{"query_type": "simple", "primary_intent": "get financial data", "complexity_score": 0.3, "location_extracted": null, "entities": ["NVIDIA", "stock price"], "reasoning": "Financial query for real-time stock data - ticker detection will enhance this"}},
297
+ "tasks": [{{"task_id": "web_001", "agent_id": "web_agent", "description": "Get current NVIDIA stock price with intelligent ticker detection", "parameters": {{"query": "NVIDIA stock price", "max_results": 5}}, "confidence": 0.95, "priority": 1}}]
298
+ }}
299
+
300
+ **General Web Search:**
301
+ Query: "Latest news about AI technology"
302
+ {{
303
+ "analysis": {{"query_type": "simple", "primary_intent": "search for news", "complexity_score": 0.3, "location_extracted": null, "entities": ["news", "AI", "technology"], "reasoning": "General web search for current information"}},
304
+ "tasks": [{{"task_id": "web_002", "agent_id": "web_agent", "description": "Search for latest AI technology news", "parameters": {{"query": "latest AI technology news", "max_results": 5}}, "confidence": 0.9, "priority": 1}}]
305
+ }}
306
+
307
+ **Hiking/Outdoor Queries:**
308
+ Query: "Find moderate hiking trails near Seattle within 30 miles"
309
+ {{
310
+ "analysis": {{"query_type": "simple", "primary_intent": "find hiking trails", "complexity_score": 0.4, "location_extracted": "Seattle", "entities": ["hiking", "trails", "moderate", "Seattle", "30 miles"], "reasoning": "Outdoor activity search with specific location and difficulty"}},
311
+ "tasks": [{{"task_id": "hiking_001", "agent_id": "hiking_agent", "description": "Find moderate hiking trails near Seattle", "parameters": {{"location": "Seattle", "difficulty": "moderate", "max_distance": 30}}, "confidence": 0.95, "priority": 1}}]
312
+ }}
313
+
314
+ **Hotel/Accommodation Queries:**
315
+ Query: "Best luxury hotels in Paris near Eiffel Tower"
316
+ {{
317
+ "analysis": {{"query_type": "simple", "primary_intent": "find accommodation", "complexity_score": 0.4, "location_extracted": "Paris", "entities": ["hotels", "luxury", "Paris", "Eiffel Tower"], "reasoning": "Accommodation search with location and luxury preference"}},
318
+ "tasks": [{{"task_id": "place_001", "agent_id": "location_agent", "description": "Find luxury hotels in Paris near Eiffel Tower", "parameters": {{"query": "luxury hotels Paris near Eiffel Tower", "max_distance": 20}}, "confidence": 0.9, "priority": 1}}]
319
+ }}
320
+
321
+ **Restaurant/Food Queries:**
322
+ Query: "Italian restaurants in New York with outdoor seating"
323
+ {{
324
+ "analysis": {{"query_type": "simple", "primary_intent": "find restaurants", "complexity_score": 0.4, "location_extracted": "New York", "entities": ["Italian", "restaurants", "New York", "outdoor seating"], "reasoning": "Restaurant search with cuisine and location preferences"}},
325
+ "tasks": [{{"task_id": "rest_001", "agent_id": "restaurant_agent", "description": "Find Italian restaurants in New York with outdoor seating", "parameters": {{"query": "New York", "cuisine": "Italian"}}, "confidence": 0.9, "priority": 1}}]
326
+ }}
327
+
328
+ **Sentiment Analysis Queries:**
329
+ Query: "Analyze sentiment: 'This product is amazing and exceeded my expectations!'"
330
+ {{
331
+ "analysis": {{"query_type": "simple", "primary_intent": "analyze text sentiment", "complexity_score": 0.2, "location_extracted": null, "entities": ["sentiment", "text analysis"], "reasoning": "Clear sentiment analysis request with provided text"}},
332
+ "tasks": [{{"task_id": "sent_001", "agent_id": "sentiment_agent", "description": "Analyze sentiment of product review", "parameters": {{"text": "This product is amazing and exceeded my expectations!"}}, "confidence": 0.95, "priority": 1}}]
333
+ }}
334
+
335
+ **Complex Multi-Intent Queries:**
336
+ Query: "I'm planning a trip to Tokyo - need hotels and restaurants"
337
+ {{
338
+ "analysis": {{"query_type": "complex", "primary_intent": "travel planning with accommodation and dining", "complexity_score": 0.7, "location_extracted": "Tokyo", "entities": ["trip", "Tokyo", "hotels", "restaurants"], "reasoning": "Multi-intent travel query requiring both accommodation and restaurant search"}},
339
+ "tasks": [
340
+ {{"task_id": "place_001", "agent_id": "location_agent", "description": "Find hotels in Tokyo", "parameters": {{"query": "Tokyo", "max_distance": 20}}, "confidence": 0.9, "priority": 1}},
341
+ {{"task_id": "rest_001", "agent_id": "restaurant_agent", "description": "Find restaurants in Tokyo", "parameters": {{"query": "Tokyo", "cuisine": null}}, "confidence": 0.9, "priority": 1}}
342
+ ]
343
+ }}
344
+
345
+ **Weather/News/General Web Queries:**
346
+ Query: "Latest news about artificial intelligence developments"
347
+ {{
348
+ "analysis": {{"query_type": "simple", "primary_intent": "get current news information", "complexity_score": 0.3, "location_extracted": null, "entities": ["news", "artificial intelligence"], "reasoning": "Information retrieval query requiring web search"}},
349
+ "tasks": [{{"task_id": "web_001", "agent_id": "web_agent", "description": "Get latest AI news", "parameters": {{"query": "latest news artificial intelligence developments"}}, "confidence": 0.9, "priority": 1}}]
350
+ }}
351
+
352
+ **Ambiguous Queries:**
353
+ Query: "Tell me about Paris"
354
+ {{
355
+ "analysis": {{"query_type": "ambiguous", "primary_intent": "get general information", "complexity_score": 0.5, "location_extracted": "Paris", "entities": ["Paris"], "reasoning": "Vague query - could be travel, history, or general info - use web search"}},
356
+ "tasks": [{{"task_id": "web_001", "agent_id": "web_agent", "description": "Get general information about Paris", "parameters": {{"query": "Paris information travel guide"}}, "confidence": 0.7, "priority": 1}}]
357
+ }}
358
+
359
+ INTELLIGENT ROUTING GUIDELINES:
360
+ - **Keywords for hiking_agent**: hiking, trails, trek, outdoor, mountain, nature, walk, climbing, adventure
361
+ - **Keywords for location_agent**: hotel, hotels, accommodation, stay, lodging, motel, resort, inn
362
+ - **Keywords for restaurant_agent**: restaurant, food, dining, eat, cuisine, meal, cafe, bar
363
+ - **Keywords for sentiment_agent**: sentiment, analyze, opinion, feeling, emotion, review, mood
364
+ - **Keywords for web_agent**: news, weather, stock, price, latest, current, information, what is
365
+
366
+ PARAMETER EXTRACTION RULES:
367
+ - **Locations**: Look for city names, landmarks, "in", "near", "at", "around"
368
+ - **Difficulties**: easy, moderate, hard, difficult, challenging, extreme
369
+ - **Distances**: "within X miles", "X km radius", "close to"
370
+ - **Cuisines**: Italian, Chinese, Mexican, etc.
371
+ - **Accommodations**: luxury, budget, boutique, business, etc.
372
+
373
+ IMPORTANT:
374
+ - Always provide valid JSON
375
+ - Use exact agent_id values from the list above
376
+ - Extract locations and parameters accurately
377
+ - Assign appropriate confidence scores based on query clarity
378
+ - For unclear queries, use web_agent as fallback
379
+ - Be specific in task descriptions and reasoning"""
380
+
381
+ def _parse_llm_response(self, response, original_query: str) -> Dict[str, Any]:
382
+ """Parse LLM response and extract structured analysis."""
383
+ try:
384
+ # Handle different response types
385
+ if hasattr(response, 'content'):
386
+ # ChatMessage object - extract content
387
+ response_text = response.content
388
+ elif hasattr(response, 'text'):
389
+ # Some other response object with text attribute
390
+ response_text = response.text
391
+ elif isinstance(response, str):
392
+ # Already a string
393
+ response_text = response
394
+ else:
395
+ # Try to convert to string
396
+ response_text = str(response)
397
+
398
+ print(f"πŸ” Raw response text: {response_text[:500]}...")
399
+
400
+ # Clean up markdown code blocks if present
401
+ import re
402
+
403
+ # Remove markdown code block markers
404
+ response_text = re.sub(r'```json\s*', '', response_text)
405
+ response_text = re.sub(r'```\s*$', '', response_text)
406
+ response_text = response_text.strip()
407
+
408
+ # Try to extract JSON from the response - more robust pattern
409
+ json_patterns = [
410
+ r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}', # Simple nested braces
411
+ r'\{.*\}', # Original fallback pattern
412
+ ]
413
+
414
+ analysis = None
415
+
416
+ # First try: Direct JSON parsing if the response looks like pure JSON
417
+ if response_text.strip().startswith('{') and response_text.strip().endswith('}'):
418
+ try:
419
+ analysis = json.loads(response_text.strip())
420
+ print(f"βœ… Successfully parsed JSON via direct parsing")
421
+ except json.JSONDecodeError:
422
+ print(f"⚠️ Direct JSON parsing failed, trying pattern matching")
423
+
424
+ # Second try: Pattern matching
425
+ if not analysis:
426
+ for pattern in json_patterns:
427
+ json_match = re.search(pattern, response_text, re.DOTALL)
428
+ if json_match:
429
+ try:
430
+ json_text = json_match.group().strip()
431
+ print(f"πŸ” Extracted JSON: {json_text[:200]}...")
432
+ analysis = json.loads(json_text)
433
+ print(f"βœ… Successfully parsed JSON analysis via pattern matching")
434
+ break
435
+ except json.JSONDecodeError as json_error:
436
+ print(f"❌ JSON decode error with pattern {pattern}: {json_error}")
437
+ continue
438
+
439
+ # Third try: Find balanced braces manually
440
+ if not analysis:
441
+ try:
442
+ analysis = self._extract_json_with_balanced_braces(response_text)
443
+ if analysis:
444
+ print(f"βœ… Successfully parsed JSON via balanced brace extraction")
445
+ except Exception as brace_error:
446
+ print(f"❌ Balanced brace extraction failed: {brace_error}")
447
+
448
+ if analysis:
449
+ return analysis
450
+ else:
451
+ raise ValueError("No valid JSON found in response")
452
+
453
+ except Exception as e:
454
+ print(f"❌ Error parsing LLM response: {e}")
455
+ print(f"πŸ”„ Falling back to keyword-based analysis")
456
+ # Return fallback analysis
457
+ return {
458
+ "analysis": {
459
+ "query_type": "simple",
460
+ "primary_intent": "general query",
461
+ "complexity_score": 0.5,
462
+ "location_extracted": None,
463
+ "entities": [],
464
+ "reasoning": f"LLM parsing failed: {str(e)}"
465
+ },
466
+ "tasks": [
467
+ {
468
+ "task_id": "web_fallback",
469
+ "agent_id": "web_agent",
470
+ "description": original_query,
471
+ "parameters": {"query": original_query, "category": None},
472
+ "confidence": 0.5,
473
+ "priority": 1
474
+ }
475
+ ]
476
+ }
477
+
478
+ def _extract_json_with_balanced_braces(self, text: str) -> Optional[Dict[str, Any]]:
479
+ """Extract JSON by finding balanced braces manually."""
480
+ import json
481
+
482
+ # Find the first opening brace
483
+ start_idx = text.find('{')
484
+ if start_idx == -1:
485
+ return None
486
+
487
+ # Count braces to find the matching closing brace
488
+ brace_count = 0
489
+ end_idx = start_idx
490
+ in_string = False
491
+ escape_next = False
492
+
493
+ for i, char in enumerate(text[start_idx:], start_idx):
494
+ if escape_next:
495
+ escape_next = False
496
+ continue
497
+
498
+ if char == '\\':
499
+ escape_next = True
500
+ continue
501
+
502
+ if char == '"' and not escape_next:
503
+ in_string = not in_string
504
+ continue
505
+
506
+ if not in_string:
507
+ if char == '{':
508
+ brace_count += 1
509
+ elif char == '}':
510
+ brace_count -= 1
511
+ if brace_count == 0:
512
+ end_idx = i
513
+ break
514
+
515
+ if brace_count == 0:
516
+ json_text = text[start_idx:end_idx + 1]
517
+ try:
518
+ return json.loads(json_text)
519
+ except json.JSONDecodeError:
520
+ return None
521
+
522
+ return None
523
+
524
+ def _create_subtasks(self, analysis: Dict[str, Any], original_query: str) -> List[SubTask]:
525
+ """Convert LLM analysis into SubTask objects."""
526
+ subtasks = []
527
+
528
+ tasks = analysis.get("tasks", [])
529
+ if not tasks:
530
+ # Fallback if no tasks generated
531
+ tasks = [{
532
+ "task_id": "fallback_001",
533
+ "agent_id": "web_agent",
534
+ "description": original_query,
535
+ "parameters": {"query": original_query},
536
+ "confidence": 0.5,
537
+ "priority": 1
538
+ }]
539
+
540
+ for task_data in tasks:
541
+ agent_id = task_data.get("agent_id", "web_agent")
542
+
543
+ # Map agent_id to task_type
544
+ task_type_mapping = {
545
+ "sentiment_agent": TaskType.SENTIMENT_ANALYSIS,
546
+ "location_agent": TaskType.LOCATION_SEARCH,
547
+ "restaurant_agent": TaskType.RESTAURANT_SEARCH,
548
+ "hiking_agent": TaskType.HIKING_SEARCH,
549
+ "web_agent": TaskType.WEB_SEARCH
550
+ }
551
+
552
+ task_type = task_type_mapping.get(agent_id, TaskType.WEB_SEARCH)
553
+
554
+ subtask = SubTask(
555
+ id=task_data.get("task_id", str(uuid.uuid4())),
556
+ task_type=task_type,
557
+ description=task_data.get("description", original_query),
558
+ parameters=task_data.get("parameters", {"query": original_query}),
559
+ agent_id=agent_id,
560
+ confidence=task_data.get("confidence", 0.5)
561
+ )
562
+
563
+ subtasks.append(subtask)
564
+
565
+ return subtasks
566
+
567
+ def _fallback_decomposition(self, user_query: str) -> List[SubTask]:
568
+ """Fallback decomposition using simple keyword matching."""
569
+ print(f"πŸ”„ Using fallback decomposition for: '{user_query}'")
570
+ query_lower = user_query.lower()
571
+
572
+ # Simple keyword-based classification
573
+ if any(word in query_lower for word in ["sentiment", "feeling", "opinion", "emotion", "analyze"]):
574
+ print(f"🎭 Detected sentiment analysis request")
575
+
576
+ # Extract text to analyze - look for text in quotes or after "analyze sentiment:"
577
+ import re
578
+ text_to_analyze = user_query
579
+
580
+ # Try to extract quoted text first
581
+ quote_pattern = r"['\"]([^'\"]+)['\"]"
582
+ quote_match = re.search(quote_pattern, user_query)
583
+ if quote_match:
584
+ text_to_analyze = quote_match.group(1)
585
+ print(f"πŸ“ Extracted quoted text: '{text_to_analyze}'")
586
+
587
+ # Try to extract text after "analyze sentiment:" or similar patterns
588
+ elif "analyze sentiment:" in query_lower:
589
+ parts = user_query.split(":", 1)
590
+ if len(parts) > 1:
591
+ text_to_analyze = parts[1].strip().strip("'\"")
592
+ print(f"πŸ“ Extracted text after colon: '{text_to_analyze}'")
593
+
594
+ # Try to extract text after "sentiment" keyword
595
+ elif "sentiment" in query_lower:
596
+ # Look for patterns like "sentiment of X" or "analyze X sentiment"
597
+ sentiment_patterns = [
598
+ r"sentiment[:\s]+['\"]?([^'\"]+)['\"]?",
599
+ r"analyze[:\s]+['\"]?([^'\"]+)['\"]?\s+sentiment",
600
+ r"['\"]([^'\"]+)['\"].*sentiment"
601
+ ]
602
+
603
+ for pattern in sentiment_patterns:
604
+ match = re.search(pattern, user_query, re.IGNORECASE)
605
+ if match:
606
+ text_to_analyze = match.group(1).strip()
607
+ print(f"πŸ“ Extracted text via pattern: '{text_to_analyze}'")
608
+ break
609
+
610
+ print(f"🎯 Final text for sentiment analysis: '{text_to_analyze}'")
611
+
612
+ return [SubTask(
613
+ id=str(uuid.uuid4()),
614
+ task_type=TaskType.SENTIMENT_ANALYSIS,
615
+ description=f"Analyze sentiment: {text_to_analyze}",
616
+ parameters={"text": text_to_analyze},
617
+ agent_id="sentiment_agent",
618
+ confidence=0.8
619
+ )]
620
+ elif any(word in query_lower for word in ["hiking", "trail", "trails", "trek", "trekking", "hike", "hikes"]):
621
+ # Extract location and difficulty for hiking
622
+ import re
623
+
624
+ # Extract location patterns
625
+ location_patterns = [
626
+ r"(?:in|at|near|around|close to)\s+([a-zA-Z\s,]+?)(?:\s+within|\s+\d|$|\.|,)",
627
+ r"([A-Z][a-zA-Z\s]+?)(?:\s+within|\s+\d|$)"
628
+ ]
629
+
630
+ location = None
631
+ for pattern in location_patterns:
632
+ location_match = re.search(pattern, user_query)
633
+ if location_match:
634
+ location = location_match.group(1).strip()
635
+ break
636
+
637
+ if not location:
638
+ location = user_query # Fallback to full query
639
+
640
+ # Extract difficulty
641
+ difficulty = None
642
+ if "easy" in query_lower:
643
+ difficulty = "easy"
644
+ elif "moderate" in query_lower:
645
+ difficulty = "moderate"
646
+ elif any(word in query_lower for word in ["hard", "difficult", "challenging"]):
647
+ difficulty = "hard"
648
+ elif any(word in query_lower for word in ["very hard", "extreme", "strenuous"]):
649
+ difficulty = "very_hard"
650
+
651
+ # Extract distance
652
+ max_distance = 30 # Default
653
+ distance_match = re.search(r"within\s+(\d+)\s*(?:mile|miles|mi)", query_lower)
654
+ if distance_match:
655
+ max_distance = int(distance_match.group(1))
656
+
657
+ return [SubTask(
658
+ id=str(uuid.uuid4()),
659
+ task_type=TaskType.HIKING_SEARCH,
660
+ description=f"Find hiking trails: {user_query}",
661
+ parameters={"location": location, "difficulty": difficulty, "max_distance": max_distance},
662
+ agent_id="hiking_agent",
663
+ confidence=0.8
664
+ )]
665
+ elif any(word in query_lower for word in ["hotel", "accommodation", "stay", "place"]):
666
+ return [SubTask(
667
+ id=str(uuid.uuid4()),
668
+ task_type=TaskType.LOCATION_SEARCH,
669
+ description=f"Find accommodations: {user_query}",
670
+ parameters={"query": user_query, "max_distance": 20},
671
+ agent_id="location_agent",
672
+ confidence=0.7
673
+ )]
674
+ elif any(word in query_lower for word in ["restaurant", "food", "dining", "eat"]):
675
+ return [SubTask(
676
+ id=str(uuid.uuid4()),
677
+ task_type=TaskType.RESTAURANT_SEARCH,
678
+ description=f"Find restaurants: {user_query}",
679
+ parameters={"query": user_query, "cuisine": None},
680
+ agent_id="restaurant_agent",
681
+ confidence=0.7
682
+ )]
683
+ else:
684
+ # Default to web search
685
+ return [SubTask(
686
+ id=str(uuid.uuid4()),
687
+ task_type=TaskType.WEB_SEARCH,
688
+ description=f"Web search: {user_query}",
689
+ parameters={"query": user_query, "category": None},
690
+ agent_id="web_agent",
691
+ confidence=0.6
692
+ )]
693
+
694
+ async def test_ticker_detection(self, test_queries: List[str] = None) -> Dict[str, str]:
695
+ """Test ticker detection on various queries to help debug issues."""
696
+ if test_queries is None:
697
+ test_queries = [
698
+ "What's the current stock price of NVDA?",
699
+ "NVDA stock price",
700
+ "Get NVIDIA stock price",
701
+ "What is TSLA trading at?",
702
+ "Apple stock price",
703
+ "AAPL current price"
704
+ ]
705
+
706
+ results = {}
707
+ print("πŸ§ͺ Testing ticker detection...")
708
+
709
+ for query in test_queries:
710
+ detected = await self.detect_ticker_symbol(query)
711
+ results[query] = detected
712
+ print(f" '{query}' β†’ '{detected}'")
713
+
714
+ return results
715
+
716
+ async def detect_ticker_symbol(self, user_query: str) -> str:
717
+ """
718
+ Use LLM to detect and extract ticker symbols from financial queries.
719
+ """
720
+ try:
721
+ # First check for obvious ticker symbols in the query
722
+ import re
723
+
724
+ print(f"πŸ” Analyzing query for ticker: '{user_query}'")
725
+
726
+ # Common ticker patterns - improved to catch more cases
727
+ ticker_patterns = [
728
+ r'\b([A-Z]{1,5})\b(?:\s+stock|\s+price|\s+quote)', # NVDA stock, AAPL price
729
+ r'\bof\s+([A-Z]{2,5})\b', # "price of NVDA"
730
+ r'\b([A-Z]{2,5})\s*\??\s*$', # NVDA at end of query
731
+ r'\b([A-Z]{2,5})\b(?=\s)', # Standalone uppercase 2-5 letters followed by space
732
+ r'\b([A-Z]{2,5})\b', # Any 2-5 letter uppercase sequence
733
+ ]
734
+
735
+ # Known ticker mappings for common companies
736
+ company_tickers = {
737
+ 'nvidia': 'NVDA',
738
+ 'apple': 'AAPL',
739
+ 'tesla': 'TSLA',
740
+ 'microsoft': 'MSFT',
741
+ 'google': 'GOOGL',
742
+ 'amazon': 'AMZN',
743
+ 'meta': 'META',
744
+ 'facebook': 'META',
745
+ 'spy': 'SPY',
746
+ 'qqq': 'QQQ'
747
+ }
748
+
749
+ query_lower = user_query.lower()
750
+
751
+ # Check for direct ticker matches first
752
+ for i, pattern in enumerate(ticker_patterns):
753
+ matches = re.findall(pattern, user_query, re.IGNORECASE)
754
+ print(f" Pattern {i+1} ('{pattern}'): {matches}")
755
+ for match in matches:
756
+ if len(match) >= 2 and match.upper() not in ['THE', 'AND', 'FOR', 'ARE', 'BUT', 'NOT', 'YOU', 'ALL', 'CAN', 'HER', 'WAS', 'ONE', 'OUR', 'HAD', 'BUT', 'WHAT', 'BEEN', 'THAT', 'WITH', 'THIS']:
757
+ print(f"🎯 Direct ticker pattern match found: {match.upper()}")
758
+ return match.upper()
759
+
760
+ # Check for company name matches
761
+ for company, ticker in company_tickers.items():
762
+ if company in query_lower:
763
+ print(f"🏒 Company name match found: {company} β†’ {ticker}")
764
+ return ticker
765
+
766
+ # Use LLM as fallback for complex cases
767
+ prompt = f"""
768
+ You are a financial assistant. Your task is to identify stock ticker symbols in queries.
769
+
770
+ Query: "{user_query}"
771
+
772
+ If this query mentions a company or stock, return ONLY the ticker symbol (e.g., "AAPL", "TSLA", "NVDA").
773
+ If no ticker can be identified, return "UNKNOWN".
774
+
775
+ Examples:
776
+ - "Apple stock price" β†’ AAPL
777
+ - "Tesla earnings" β†’ TSLA
778
+ - "NVIDIA performance" β†’ NVDA
779
+ - "Microsoft news" β†’ MSFT
780
+ - "What's the current stock price of NVDA?" β†’ NVDA
781
+ - "weather forecast" β†’ UNKNOWN
782
+
783
+ Response (ticker only):"""
784
+
785
+ if self.model_name == "anthropic" and self.model:
786
+ response = self.model([{"role": "user", "content": prompt}])
787
+ ticker = str(response).strip().upper()
788
+
789
+ # Validate ticker format
790
+ if ticker and ticker != "UNKNOWN" and len(ticker) <= 5 and ticker.isalpha():
791
+ print(f"πŸ€– LLM ticker detection: {ticker}")
792
+ return ticker
793
+
794
+ print(f"❓ No ticker detected for query: {user_query}")
795
+ return "UNKNOWN"
796
+
797
+ except Exception as e:
798
+ print(f"❌ Ticker detection failed: {e}")
799
+ return "UNKNOWN"
800
+
801
+ async def enhance_financial_query(self, user_query: str) -> str:
802
+ """Enhance financial queries with ticker symbol detection."""
803
+ ticker_result = await self.detect_ticker_symbol(user_query)
804
+
805
+ if ticker_result != "UNKNOWN":
806
+ # Specific ticker found - create focused financial query
807
+ enhanced_query = f"{ticker_result} stock price quote market data"
808
+ print(f"🎯 Enhanced financial query: '{user_query}' β†’ '{enhanced_query}' (ticker: {ticker_result})")
809
+ return enhanced_query
810
+ else:
811
+ print(f"πŸ” Using original query: '{user_query}' (no ticker detected)")
812
+ return user_query
813
+
814
+ class MCPClientManager:
815
+ """Enhanced MCP Client Manager with LLM-powered task decomposition."""
816
+
817
+ def __init__(self, server_url: str = "http://localhost:7861/gradio_api/mcp/sse", model_name: str = "anthropic"):
818
+ self.server_url = server_url
819
+ self.model_name = model_name
820
+ self.task_decomposer = LLMTaskDecomposer(model_name)
821
+ self.mcp_client = None
822
+ self.session_id = str(uuid.uuid4())
823
+ self.is_connected = False
824
+ self.available_tools = []
825
+
826
+ async def connect_to_server(self) -> bool:
827
+ """Connect to an already running MCP server using smolagents MCPClient."""
828
+ max_retries = 3 if "localhost" in self.server_url else 2
829
+ retry_delay = 2
830
+
831
+ for attempt in range(max_retries):
832
+ try:
833
+ if attempt > 0:
834
+ print(f"πŸ”„ Retry attempt {attempt + 1}/{max_retries}")
835
+ await asyncio.sleep(retry_delay)
836
+
837
+ print(f"πŸ”— Attempting to connect to MCP server at: {self.server_url}")
838
+
839
+ # Add timeout for remote connections
840
+ timeout = 15 if "localhost" in self.server_url else 45
841
+
842
+ # Create MCP client using smolagents with explicit transport
843
+ self.mcp_client = MCPClient({
844
+ "url": self.server_url,
845
+ "transport": "sse", # Explicitly specify SSE transport
846
+ "timeout": timeout
847
+ })
848
+
849
+ # Get available tools with timeout
850
+ try:
851
+ print("πŸ” Fetching available tools...")
852
+ self.available_tools = self.mcp_client.get_tools()
853
+ tool_names = [tool.name for tool in self.available_tools]
854
+ print(f"βœ… Connected to MCP server. Available tools: {tool_names}")
855
+
856
+ # Debug: Print detailed tool information
857
+ if self.available_tools:
858
+ print("πŸ“‹ Tool Details:")
859
+ for tool in self.available_tools:
860
+ print(f" β€’ {tool.name}")
861
+ else:
862
+ print("⚠️ Warning: No tools found on the server")
863
+
864
+ # Check if tools have prefixes and suggest mapping
865
+ if tool_names and any("_" in name for name in tool_names):
866
+ print("πŸ”§ Detected prefixed tool names - using flexible matching")
867
+
868
+ except Exception as tools_error:
869
+ print(f"⚠️ Warning: Connected but failed to get tools: {tools_error}")
870
+ self.available_tools = []
871
+
872
+ self.is_connected = True
873
+ return True
874
+
875
+ except Exception as e:
876
+ error_msg = str(e)
877
+ if "timeout" in error_msg.lower() or "connection" in error_msg.lower():
878
+ print(f"⏱️ Connection attempt {attempt + 1} failed: {error_msg}")
879
+ else:
880
+ print(f"❌ Connection attempt {attempt + 1} failed: {error_msg}")
881
+
882
+ if attempt == max_retries - 1:
883
+ print(f"❌ Failed to connect to MCP server after {max_retries} attempts")
884
+ print(f"πŸ’‘ Connection troubleshooting for: {self.server_url}")
885
+
886
+ if "localhost" in self.server_url:
887
+ print("🏠 LOCAL SERVER ISSUES:")
888
+ print(" β€’ Make sure the MCP server is running locally")
889
+ print(" β€’ Check if port 7861 is available")
890
+ print(" β€’ Try running: python server.py in the mcp_server directory")
891
+ else:
892
+ print("🌐 REMOTE SERVER ISSUES:")
893
+ if "hf.space" in self.server_url:
894
+ print(" β€’ The Hugging Face Space might be PRIVATE (not publicly accessible)")
895
+ print(" β€’ Make the Space PUBLIC in HF settings, or")
896
+ print(" β€’ Use a local server instead")
897
+ print(" β€’ Check your internet connection")
898
+ print(" β€’ Verify the server URL is correct and accessible")
899
+
900
+ await self._cleanup()
901
+
902
+ return False
903
+
904
+ async def execute_subtask(self, subtask: SubTask) -> Dict[str, Any]:
905
+ """Execute a subtask using the MCP tool."""
906
+ if not self.is_connected or not self.mcp_client:
907
+ return {"error": "Not connected to MCP server"}
908
+
909
+ try:
910
+ agent = self.task_decomposer.agents.get(subtask.agent_id)
911
+ if not agent:
912
+ return {"error": f"Agent {subtask.agent_id} not found"}
913
+
914
+ tool_name = agent.tool_name
915
+
916
+ # Debug: print available tools
917
+ available_tool_names = [tool.name for tool in self.available_tools]
918
+ print(f"πŸ” Looking for tool '{tool_name}' among available tools: {available_tool_names}")
919
+
920
+ # Find the tool - support both exact matches and suffix matches (for prefixed tools)
921
+ tool = None
922
+ for available_tool in self.available_tools:
923
+ # First try exact match
924
+ if available_tool.name == tool_name:
925
+ tool = available_tool
926
+ print(f"βœ… Found exact match: {available_tool.name}")
927
+ break
928
+ # Then try suffix match (for tools like "test_mcp_server_sentiment_analysis")
929
+ elif available_tool.name.endswith(f"_{tool_name}") or available_tool.name.endswith(tool_name):
930
+ tool = available_tool
931
+ print(f"βœ… Found suffix match: {available_tool.name} matches {tool_name}")
932
+ break
933
+
934
+ if not tool:
935
+ return {
936
+ "error": f"Tool {tool_name} not available on server",
937
+ "available_tools": available_tool_names,
938
+ "requested_tool": tool_name,
939
+ "agent_id": subtask.agent_id
940
+ }
941
+
942
+ # Special handling for web search with ticker detection
943
+ if tool_name == "web_search" and subtask.agent_id == "web_agent":
944
+ # Enhance query with ticker detection for financial queries
945
+ original_query = subtask.parameters.get("query", "")
946
+ enhanced_query = await self.task_decomposer.enhance_financial_query(original_query)
947
+ subtask.parameters["query"] = enhanced_query
948
+ print(f"πŸ’‘ Web search query enhanced: '{original_query}' β†’ '{enhanced_query}'")
949
+
950
+ # Map and filter parameters based on tool type
951
+ filtered_params = self._filter_tool_parameters(tool_name, subtask.parameters)
952
+ print(f"πŸ”§ Filtered parameters for {tool_name}: {filtered_params}")
953
+
954
+ # Execute the tool
955
+ try:
956
+ result = tool(**filtered_params)
957
+
958
+ # Handle the result - parse JSON string if needed
959
+ if isinstance(result, str):
960
+ try:
961
+ parsed_result = json.loads(result)
962
+ except json.JSONDecodeError:
963
+ parsed_result = {"result": result}
964
+ elif isinstance(result, dict):
965
+ parsed_result = result
966
+ elif hasattr(result, 'content'):
967
+ # If it's a tool result object
968
+ parsed_result = {"result": str(result.content)}
969
+ else:
970
+ parsed_result = {"result": str(result)}
971
+
972
+ subtask.status = "completed"
973
+ subtask.result = parsed_result
974
+ return parsed_result
975
+
976
+ except Exception as tool_error:
977
+ return {"error": f"Tool execution error: {str(tool_error)}", "tool_name": tool_name}
978
+
979
+ except Exception as e:
980
+ subtask.status = "failed"
981
+ return {"error": f"Subtask execution failed: {str(e)}", "subtask_id": subtask.id}
982
+
983
+ def _filter_tool_parameters(self, tool_name: str, parameters: Dict[str, Any]) -> Dict[str, Any]:
984
+ """Filter and map parameters based on tool requirements."""
985
+
986
+ # Parameter mappings for each tool
987
+ tool_param_mappings = {
988
+ "web_search": { # Updated to handle actual web_search tool
989
+ "allowed_params": ["query", "max_results"],
990
+ "param_mapping": {
991
+ "search_query": "query",
992
+ "search_term": "query",
993
+ "q": "query",
994
+ "data_type": None, # Remove this parameter
995
+ "category": None # Remove this parameter
996
+ }
997
+ },
998
+ "sentiment_analysis": {
999
+ "allowed_params": ["text"],
1000
+ "param_mapping": {
1001
+ "input_text": "text",
1002
+ "content": "text"
1003
+ }
1004
+ },
1005
+ "place_search": {
1006
+ "allowed_params": ["query", "max_distance"],
1007
+ "param_mapping": {
1008
+ "location": "query",
1009
+ "search_query": "query",
1010
+ "distance": "max_distance"
1011
+ }
1012
+ },
1013
+ "restaurant_search": {
1014
+ "allowed_params": ["query", "cuisine"],
1015
+ "param_mapping": {
1016
+ "location": "query",
1017
+ "search_query": "query",
1018
+ "cuisine_type": "cuisine"
1019
+ }
1020
+ },
1021
+ "hiking_search": {
1022
+ "allowed_params": ["location", "difficulty", "max_distance"],
1023
+ "param_mapping": {
1024
+ "query": "location",
1025
+ "search_query": "location",
1026
+ "skill_level": "difficulty"
1027
+ }
1028
+ }
1029
+ }
1030
+
1031
+ mapping_config = tool_param_mappings.get(tool_name, {
1032
+ "allowed_params": ["query"],
1033
+ "param_mapping": {}
1034
+ })
1035
+
1036
+ filtered_params = {}
1037
+
1038
+ for param_key, param_value in parameters.items():
1039
+ # Check if parameter should be mapped to a different name
1040
+ mapped_key = mapping_config["param_mapping"].get(param_key, param_key)
1041
+
1042
+ # Skip parameters that are mapped to None (should be removed)
1043
+ if mapped_key is None:
1044
+ continue
1045
+
1046
+ # Skip None or empty values
1047
+ if param_value is None or param_value == "":
1048
+ continue
1049
+
1050
+ # Only include allowed parameters
1051
+ if mapped_key in mapping_config["allowed_params"]:
1052
+ filtered_params[mapped_key] = param_value
1053
+
1054
+ # Ensure required parameters exist with defaults
1055
+ if tool_name in ["web_search"] and "query" not in filtered_params:
1056
+ # If no query parameter, use the first available parameter value
1057
+ if parameters:
1058
+ filtered_params["query"] = str(list(parameters.values())[0])
1059
+ elif tool_name == "hiking_search" and "location" not in filtered_params:
1060
+ # For hiking, ensure location is provided
1061
+ if "query" in parameters and parameters["query"]:
1062
+ filtered_params["location"] = str(parameters["query"])
1063
+ elif tool_name == "restaurant_search" and "query" not in filtered_params:
1064
+ # For restaurants, ensure query is provided
1065
+ if "location" in parameters and parameters["location"]:
1066
+ filtered_params["query"] = str(parameters["location"])
1067
+ elif tool_name == "place_search" and "query" not in filtered_params:
1068
+ # For places, ensure query is provided
1069
+ if "location" in parameters and parameters["location"]:
1070
+ filtered_params["query"] = str(parameters["location"])
1071
+
1072
+ return filtered_params
1073
+
1074
+ def test_tool_matching(self) -> str:
1075
+ """Test tool matching logic for debugging purposes."""
1076
+ if not self.available_tools:
1077
+ return "❌ No tools available to test"
1078
+
1079
+ results = []
1080
+ results.append("πŸ§ͺ Tool Matching Test Results:")
1081
+ results.append("")
1082
+
1083
+ # Test each agent's tool against available tools
1084
+ for agent_id, agent in self.task_decomposer.agents.items():
1085
+ tool_name = agent.tool_name
1086
+ results.append(f"πŸ” Testing agent '{agent_id}' looking for tool '{tool_name}':")
1087
+
1088
+ # Test exact match
1089
+ exact_match = None
1090
+ suffix_match = None
1091
+
1092
+ for available_tool in self.available_tools:
1093
+ if available_tool.name == tool_name:
1094
+ exact_match = available_tool.name
1095
+ break
1096
+ elif available_tool.name.endswith(f"_{tool_name}") or available_tool.name.endswith(tool_name):
1097
+ suffix_match = available_tool.name
1098
+
1099
+ if exact_match:
1100
+ results.append(f" βœ… Exact match found: {exact_match}")
1101
+ elif suffix_match:
1102
+ results.append(f" βœ… Suffix match found: {suffix_match}")
1103
+ else:
1104
+ results.append(f" ❌ No match found")
1105
+
1106
+ results.append("")
1107
+
1108
+ return "\n".join(results)
1109
+
1110
+ async def process_query(self, user_query: str) -> Dict[str, Any]:
1111
+ """Process user query through LLM-powered task decomposition and agent routing."""
1112
+ try:
1113
+ # Ensure connection
1114
+ if not self.is_connected:
1115
+ success = await self.connect_to_server()
1116
+ if not success:
1117
+ return {
1118
+ "query": user_query,
1119
+ "error": "Failed to connect to MCP server. Please ensure the server is running separately.",
1120
+ "status": "failed"
1121
+ }
1122
+
1123
+ # Use LLM to decompose query into subtasks
1124
+ subtasks = await self.task_decomposer.decompose_query(user_query)
1125
+
1126
+ # Execute subtasks
1127
+ results = []
1128
+ for subtask in subtasks:
1129
+ result = await self.execute_subtask(subtask)
1130
+ results.append({
1131
+ "subtask_id": subtask.id,
1132
+ "task_type": subtask.task_type.value,
1133
+ "agent": subtask.agent_id,
1134
+ "description": subtask.description,
1135
+ "confidence": subtask.confidence,
1136
+ "result": result
1137
+ })
1138
+
1139
+ # Aggregate results
1140
+ response = {
1141
+ "query": user_query,
1142
+ "subtasks_count": len(subtasks),
1143
+ "subtasks": results,
1144
+ "status": "completed",
1145
+ "summary": self._generate_summary(user_query, results)
1146
+ }
1147
+
1148
+ return response
1149
+
1150
+ except Exception as e:
1151
+ print(f"❌ Failed to process query: {e}")
1152
+ return {
1153
+ "query": user_query,
1154
+ "error": f"Query processing failed: {str(e)}",
1155
+ "status": "failed"
1156
+ }
1157
+
1158
+ def _generate_summary(self, query: str, results: List[Dict[str, Any]]) -> str:
1159
+ """Generate summary of all subtask results - now simplified since server handles formatting."""
1160
+ try:
1161
+ if not results:
1162
+ return f"# πŸ€” No Results\n\nNo results available for your query. Please try a different search term.\n\n---\n*🧠 Powered by AI Task Decomposition*"
1163
+
1164
+ # Check if we have a pre-formatted summary from server
1165
+ for result in results:
1166
+ try:
1167
+ if isinstance(result, dict) and isinstance(result.get("result"), dict):
1168
+ # Check for direct summary first
1169
+ if result["result"].get("summary"):
1170
+ return result["result"]["summary"]
1171
+ # Check for formatted result content
1172
+ elif result["result"].get("result"):
1173
+ formatted_content = result["result"]["result"]
1174
+ if isinstance(formatted_content, str) and "🎭" in formatted_content:
1175
+ # This is pre-formatted content from server - return it directly
1176
+ return formatted_content
1177
+ except (KeyError, TypeError, AttributeError) as e:
1178
+ print(f"⚠️ Warning: Error accessing result summary: {e}")
1179
+ continue
1180
+
1181
+ # Generate custom formatted summary
1182
+ summary_parts = [f"# οΏ½οΏ½οΏ½ Results for: *{query}*", ""]
1183
+
1184
+ for i, result in enumerate(results, 1):
1185
+ try:
1186
+ task_type = result.get("task_type", "unknown")
1187
+ description = result.get("description", "No description")
1188
+ confidence = result.get("confidence", 0.5)
1189
+
1190
+ result_data = result.get("result", {})
1191
+
1192
+ if isinstance(result_data, dict) and "error" not in result_data:
1193
+ summary_parts.append(f"## 🌐 Task {i}: {task_type.replace('_', ' ').title()}")
1194
+ summary_parts.append(f"**Confidence:** {confidence:.1%}")
1195
+ summary_parts.append("")
1196
+
1197
+ # Extract and format the actual content
1198
+ if result_data.get("summary"):
1199
+ # Direct summary
1200
+ summary_parts.append(result_data["summary"])
1201
+ elif result_data.get("result"):
1202
+ # Extract formatted content from nested result
1203
+ content = result_data["result"]
1204
+ if isinstance(content, str):
1205
+ # Clean up any escaped newlines and display formatted content
1206
+ formatted_content = content.replace('\\n', '\n').replace('\\t', '\t')
1207
+ summary_parts.append(formatted_content)
1208
+ else:
1209
+ # Handle other data types
1210
+ summary_parts.append(self._format_result_content(content))
1211
+ else:
1212
+ # Fallback - format the entire result_data
1213
+ summary_parts.append(self._format_result_content(result_data))
1214
+
1215
+ summary_parts.append("")
1216
+ else:
1217
+ # Handle errors
1218
+ summary_parts.append(f"## ❌ {task_type.replace('_', ' ').title()} - Error")
1219
+ if isinstance(result_data, dict):
1220
+ error_msg = result_data.get('error', 'Unknown error occurred')
1221
+ else:
1222
+ error_msg = str(result_data)
1223
+ summary_parts.append(f"**Issue:** {error_msg}")
1224
+ summary_parts.append("")
1225
+
1226
+ except Exception as e:
1227
+ print(f"⚠️ Warning: Error processing result {i}: {e}")
1228
+ summary_parts.append(f"## ❌ Task {i} - Processing Error")
1229
+ summary_parts.append(f"**Issue:** {str(e)}")
1230
+ summary_parts.append("")
1231
+
1232
+ summary_parts.append("---")
1233
+
1234
+ return "\n".join(summary_parts)
1235
+
1236
+ except Exception as e:
1237
+ print(f"❌ Error in _generate_summary: {e}")
1238
+ return f"# ❌ Summary Generation Error\n\nFailed to generate summary: {str(e)}\n\n---\n*🧠 Powered by AI Task Decomposition*"
1239
+
1240
+ async def _cleanup(self):
1241
+ """Clean up resources."""
1242
+ if self.mcp_client:
1243
+ try:
1244
+ self.mcp_client.disconnect()
1245
+ except:
1246
+ pass
1247
+ self.mcp_client = None
1248
+
1249
+ self.is_connected = False
1250
+
1251
+ async def disconnect(self):
1252
+ """Disconnect from MCP server."""
1253
+ await self._cleanup()
1254
+ print("πŸ”Œ Disconnected from MCP server")
1255
+
1256
+ def create_mcp_client_interface(server_url: str = "http://localhost:7861/gradio_api/mcp/sse", model_name: str = "anthropic"):
1257
+ """Create the Gradio interface for MCP Client with LLM-powered task decomposition."""
1258
+
1259
+ # Create the client manager
1260
+ async def process_query(query: str):
1261
+ """Process user query through MCP with LLM decomposition."""
1262
+ if not query.strip():
1263
+ return "Please enter a query to process."
1264
+
1265
+ try:
1266
+ # Create a new client manager for each query to ensure fresh connection
1267
+ client_manager = MCPClientManager(server_url, model_name)
1268
+
1269
+ # Process the query
1270
+ result = await client_manager.process_query(query)
1271
+
1272
+ # Clean up
1273
+ await client_manager.disconnect()
1274
+
1275
+ if result and 'summary' in result:
1276
+ return result['summary']
1277
+ else:
1278
+ return "❌ No results found or error occurred during processing."
1279
+
1280
+ except Exception as e:
1281
+ return f"❌ Error processing query: {str(e)}"
1282
+
1283
+ # Check available models and API keys
1284
+ available_models = []
1285
+
1286
+ if ANTHROPIC_API_KEY:
1287
+ available_models.append(("πŸ€– Claude Sonnet 4 via Anthropic", "anthropic"))
1288
+
1289
+ if not available_models:
1290
+ available_models.append(("❌ No API Keys Configured", "none"))
1291
+
1292
+ # Custom CSS for better UI
1293
+ css = """
1294
+ .gradio-container {
1295
+ max-width: 1200px !important;
1296
+ margin: auto !important;
1297
+ }
1298
+ .header-container {
1299
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1300
+ padding: 2rem;
1301
+ border-radius: 15px;
1302
+ margin-bottom: 2rem;
1303
+ color: white;
1304
+ text-align: center;
1305
+ }
1306
+ .model-info {
1307
+ background: #f8fafc;
1308
+ border: 1px solid #e2e8f0;
1309
+ border-radius: 10px;
1310
+ padding: 1rem;
1311
+ margin: 1rem 0;
1312
+ }
1313
+ .example-btn {
1314
+ margin: 0.25rem !important;
1315
+ background: linear-gradient(45deg, #4f46e5, #7c3aed) !important;
1316
+ border: none !important;
1317
+ color: white !important;
1318
+ }
1319
+ .example-btn:hover {
1320
+ transform: translateY(-2px);
1321
+ box-shadow: 0 4px 12px rgba(79, 70, 229, 0.4) !important;
1322
+ }
1323
+ .input-section {
1324
+ background: #ffffff;
1325
+ border: 1px solid #e5e7eb;
1326
+ border-radius: 12px;
1327
+ padding: 1.5rem;
1328
+ margin: 1rem 0;
1329
+ box-shadow: 0 2px 8px rgba(0,0,0,0.05);
1330
+ }
1331
+ .results-container {
1332
+ background: #ffffff;
1333
+ border: 1px solid #e5e7eb;
1334
+ border-radius: 12px;
1335
+ padding: 1.5rem;
1336
+ margin: 1rem 0;
1337
+ box-shadow: 0 2px 8px rgba(0,0,0,0.05);
1338
+ }
1339
+ .control-buttons {
1340
+ margin-top: 1rem;
1341
+ gap: 1rem;
1342
+ }
1343
+ .markdown-content {
1344
+ line-height: 1.6;
1345
+ }
1346
+ """
1347
+
1348
+ with gr.Blocks(css=css, title="πŸš€ VOYAGER AI") as demo:
1349
+ # Header Section
1350
+ with gr.Column(elem_classes="header-container"):
1351
+ gr.HTML("""
1352
+ <h1 style="margin: 0; font-size: 2.5rem; font-weight: bold;">
1353
+ πŸš€ VOYAGER AI
1354
+ </h1>
1355
+ <p style="margin: 0.5rem 0 0 0; font-size: 1.2rem; opacity: 0.9;">
1356
+ Intelligent AI Assistant with Multi-Agent Coordination
1357
+ </p>
1358
+ """)
1359
+
1360
+ # Main Interface
1361
+ with gr.Column():
1362
+ # Examples Section
1363
+ with gr.Column():
1364
+ gr.HTML("""
1365
+ <h3 style="color: #1f2937; margin: 20px 0 15px 0; font-size: 18px; font-weight: 600;">
1366
+ πŸ’‘ Quick Start - Try These Examples:
1367
+ </h3>
1368
+ """)
1369
+
1370
+ with gr.Row():
1371
+ with gr.Column(scale=1):
1372
+ sentiment_btn = gr.Button("😊 Analyze Sentiment", elem_classes=["example-btn"])
1373
+ hiking_btn = gr.Button("πŸ”οΈ Hiking trails near Denver", elem_classes=["example-btn"])
1374
+ with gr.Column(scale=1):
1375
+ stock_btn = gr.Button("πŸ“ˆ Stock Prices", elem_classes=["example-btn"])
1376
+ news_btn = gr.Button("πŸ“° Latest News", elem_classes=["example-btn"])
1377
+ with gr.Column(scale=1):
1378
+ hotel_btn = gr.Button("🏨 Find Hotels", elem_classes=["example-btn"])
1379
+ restaurant_btn = gr.Button("🍽️ Find Restaurants", elem_classes=["example-btn"])
1380
+
1381
+ # Input Section
1382
+ with gr.Column(elem_classes="input-section"):
1383
+ # Query Input
1384
+ query_input = gr.Textbox(
1385
+ placeholder="πŸ’¬ Ask me anything... (e.g., 'What's NVIDIA's stock price?' or 'Find hotels and restaurants in New york')",
1386
+ lines=3,
1387
+ label="Your Query",
1388
+ show_label=False,
1389
+ container=False
1390
+ )
1391
+
1392
+ # Control Buttons
1393
+ with gr.Row(elem_classes="control-buttons"):
1394
+ submit_btn = gr.Button(
1395
+ "🧠 Analyze & Execute Query",
1396
+ variant="primary",
1397
+ size="lg",
1398
+ scale=2
1399
+ )
1400
+ clear_btn = gr.Button(
1401
+ "πŸ”„ Clear All",
1402
+ variant="secondary",
1403
+ size="lg",
1404
+ scale=1
1405
+ )
1406
+
1407
+ # Results Display
1408
+ with gr.Column(elem_classes="results-container"):
1409
+ gr.HTML("""
1410
+ <h3 style="color: #1f2937; margin: 0 0 20px 0; font-size: 18px; font-weight: 600;">
1411
+ 🎯 AI Results & Analysis
1412
+ </h3>
1413
+ """)
1414
+
1415
+ results_output = gr.Markdown(
1416
+ value=f"""**Welcome to VOYAGER AI!** 🧠
1417
+
1418
+ **🎯 How it works:**
1419
+ 1. **Try one of the examples above** or type your question naturally
1420
+ 2. **Click "Analyze & Execute"** to get intelligent results
1421
+
1422
+ **✨ Features:**
1423
+ β€’ πŸ” **Smart Query Analysis** - AI understands your intent
1424
+ β€’ πŸ“‹ **Task Decomposition** - Complex queries broken down into subtasks
1425
+ β€’ πŸ€– **Agent Routing** - Specialized agents for different tasks
1426
+ β€’ ⚑ **Real-time Data** - Live web search and current information
1427
+ β€’ 🎨 **Professional Results** - Clean, formatted responses
1428
+
1429
+ **πŸš€ Ready to start?** Try one of the example buttons above or type your own query!
1430
+
1431
+ ---
1432
+ πŸ€– **Current Model:** {available_models[0][0] if available_models[0][1] != "none" else "No API Keys Configured"}""",
1433
+ show_label=False,
1434
+ container=False,
1435
+ elem_classes=["markdown-content"]
1436
+ )
1437
+
1438
+ # Event handlers
1439
+ submit_btn.click(
1440
+ process_query,
1441
+ inputs=[query_input],
1442
+ outputs=[results_output]
1443
+ )
1444
+
1445
+ clear_btn.click(
1446
+ fn=lambda: ("", """**Interface Cleared!** 🧹
1447
+
1448
+ Ready for your next query. Try the example buttons above or ask me anything!
1449
+
1450
+ πŸ’‘ **Quick Tips:**
1451
+ - Try asking about stock prices, weather, news, or travel
1452
+ - Use natural language - no need for specific commands
1453
+ - Complex queries are automatically broken down into tasks"""),
1454
+ outputs=[query_input, results_output]
1455
+ )
1456
+
1457
+ query_input.submit(
1458
+ process_query,
1459
+ inputs=[query_input],
1460
+ outputs=[results_output]
1461
+ )
1462
+
1463
+ # Example button handlers with better queries
1464
+ sentiment_btn.click(
1465
+ fn=lambda: "Analyze sentiment: 'I absolutely love this new AI technology - it's revolutionary and amazing!'",
1466
+ outputs=query_input
1467
+ )
1468
+
1469
+ hiking_btn.click(
1470
+ fn=lambda: "Find moderate hiking trails near Denver",
1471
+ outputs=query_input
1472
+ )
1473
+
1474
+ stock_btn.click(
1475
+ fn=lambda: "What's the current stock price of SPY?",
1476
+ outputs=query_input
1477
+ )
1478
+
1479
+ news_btn.click(
1480
+ fn=lambda: "Latest news about artificial intelligence and technology",
1481
+ outputs=query_input
1482
+ )
1483
+
1484
+ hotel_btn.click(
1485
+ fn=lambda: "Find luxury hotels in New york",
1486
+ outputs=query_input
1487
+ )
1488
+
1489
+ restaurant_btn.click(
1490
+ fn=lambda: "Best Italian restaurants in New York",
1491
+ outputs=query_input
1492
+ )
1493
+
1494
+ return demo
1495
+
1496
+ async def main():
1497
+ """Main entry point for the LLM-Powered MCP Client."""
1498
+ # Set up environment first
1499
+ setup_environment()
1500
+
1501
+ parser = argparse.ArgumentParser(description="LLM-Powered MCP Client with Intelligent Task Decomposition")
1502
+ parser.add_argument(
1503
+ "--server-url",
1504
+ default="https://srikanthnagelli-agents-mcp-hackathon.hf.space/gradio_api/mcp/sse",
1505
+ help="MCP server URL (default: https://srikanthnagelli-agents-mcp-hackathon.hf.space/gradio_api/mcp/sse)"
1506
+ )
1507
+ parser.add_argument(
1508
+ "--local",
1509
+ action="store_true",
1510
+ help="Use local MCP server (http://localhost:7860/gradio_api/mcp/sse) instead of remote server"
1511
+ )
1512
+ parser.add_argument(
1513
+ "--port",
1514
+ type=int,
1515
+ default=7862,
1516
+ help="Port to run the client interface (default: 7862)"
1517
+ )
1518
+ parser.add_argument(
1519
+ "--model",
1520
+ default="anthropic",
1521
+ choices=["anthropic"],
1522
+ help="LLM model for task decomposition (default: anthropic)"
1523
+ )
1524
+
1525
+ args = parser.parse_args()
1526
+
1527
+ # Override server URL if --local flag is used
1528
+ if args.local:
1529
+ server_url = "http://localhost:7860/gradio_api/mcp/sse"
1530
+ print("🏠 Local development mode enabled")
1531
+ else:
1532
+ server_url = args.server_url
1533
+ print("☁️ Using remote MCP server")
1534
+
1535
+ print("πŸš€ VOYAGER AI - LLM-Powered Task Decomposition")
1536
+ print("🧠 Intelligent query analysis and agent coordination")
1537
+ print("πŸ€– Available Models:")
1538
+ if ANTHROPIC_API_KEY:
1539
+ print(" β€’ βœ… Claude Sonnet 4 via Anthropic")
1540
+ else:
1541
+ print(" β€’ ❌ Claude Sonnet 4 (API key missing)")
1542
+
1543
+ print("")
1544
+ print(f"πŸ“‘ MCP Server: {server_url}")
1545
+ print(f"🧠 Default Model: {args.model}")
1546
+ print("═" * 50)
1547
+
1548
+ # Validate selected model
1549
+ if args.model == "anthropic" and not ANTHROPIC_API_KEY:
1550
+ print("⚠️ Warning: Anthropic model selected but API key not configured")
1551
+
1552
+ # Create and launch LLM-powered interface
1553
+ demo = create_mcp_client_interface(server_url, args.model)
1554
+
1555
+ print("🌟 Interface ready! Select your model and ask anything naturally!")
1556
+
1557
+ # Launch the interface - different configs for local vs deployment
1558
+ if args.local:
1559
+ # Local development - with share link
1560
+ demo.launch(
1561
+ server_name="0.0.0.0",
1562
+ server_port=args.port,
1563
+ share=True,
1564
+ show_error=True
1565
+ )
1566
+ else:
1567
+ # Production deployment (e.g., Hugging Face Spaces)
1568
+ demo.launch(
1569
+ server_name="0.0.0.0",
1570
+ server_port=7860,
1571
+ show_error=True
1572
+ )
1573
+
1574
+ if __name__ == "__main__":
1575
+ try:
1576
+ asyncio.run(main())
1577
+ except KeyboardInterrupt:
1578
+ print("πŸ›‘ Client shutdown requested")
1579
+ except Exception as e:
1580
+ print(f"❌ Client error: {e}")
1581
+ sys.exit(1)
requirements.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio[mcp]>=4.44.0
2
+ textblob==0.19.0
3
+ smolagents[mcp]
4
+ smolagents[litellm]
5
+ huggingface-hub==0.32.4
6
+ beautifulsoup4==4.13.4
7
+ lxml==5.4.0
8
+ requests==2.32.3
9
+ aiofiles==24.1.0
10
+ httpx==0.28.1
11
+ typing-extensions
12
+ # LLM dependencies for intelligent task decomposition
13
+ litellm>=1.0.0
14
+ # Anthropic model support
15
+ anthropic>=0.21.0