serichard1 commited on
Commit
d097ebb
Β·
1 Parent(s): b0c27d2

support_for_all_clients

Browse files
Files changed (1) hide show
  1. app.py +490 -144
app.py CHANGED
@@ -3,12 +3,16 @@ import os
3
  import json
4
  from typing import List, Dict, Any, Union
5
  from contextlib import AsyncExitStack
 
 
6
 
7
  import gradio as gr
8
  from gradio.components.chatbot import ChatMessage
9
  from mcp import ClientSession, StdioServerParameters
10
  from mcp.client.stdio import stdio_client
11
  from anthropic import Anthropic
 
 
12
  from dotenv import load_dotenv
13
 
14
  load_dotenv()
@@ -20,9 +24,99 @@ class MCPClientWrapper:
20
  def __init__(self):
21
  self.session = None
22
  self.exit_stack = None
23
- self.anthropic = Anthropic()
24
  self.tools = []
25
  self.connected = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  def connect(self) -> str:
28
  return loop.run_until_complete(self._connect())
@@ -62,15 +156,136 @@ class MCPClientWrapper:
62
  self.connected = False
63
  return f"❌ Failed to connect to MCP server: {str(e)}"
64
 
65
- def process_message(self, message: str, history: List[Union[Dict[str, Any], ChatMessage]]) -> tuple:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  if not self.session or not self.connected:
67
  return history + [
68
  {"role": "user", "content": message},
69
  {"role": "assistant", "content": "❌ MCP weather server is not connected. Please check the connection status above."}
70
- ], gr.Textbox(value="")
 
 
 
 
 
71
 
72
- new_messages = loop.run_until_complete(self._process_query(message, history))
73
- return history + [{"role": "user", "content": message}] + new_messages, gr.Textbox(value="")
 
 
 
74
 
75
  async def _process_query(self, message: str, history: List[Union[Dict[str, Any], ChatMessage]]):
76
  claude_messages = []
@@ -85,15 +300,27 @@ class MCPClientWrapper:
85
 
86
  claude_messages.append({"role": "user", "content": message})
87
 
88
- response = self.anthropic.messages.create(
89
- model="claude-3-5-sonnet-20241022",
90
- max_tokens=1500,
91
- messages=claude_messages,
92
- tools=self.tools
93
- )
94
 
95
  result_messages = []
96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  for content in response.content:
98
  if content.type == 'text':
99
  result_messages.append({
@@ -116,154 +343,207 @@ class MCPClientWrapper:
116
  }
117
  })
118
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  result_messages.append({
120
  "role": "assistant",
121
- "content": "```json\n" + json.dumps(tool_args, indent=2, ensure_ascii=True) + "\n```",
122
- "metadata": {
123
- "parent_id": f"tool_call_{tool_name}",
124
- "id": f"params_{tool_name}",
125
- "title": "Tool Parameters"
126
- }
127
  })
128
 
129
  result = await self.session.call_tool(tool_name, tool_args)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
- if result_messages and "metadata" in result_messages[-2]:
132
- result_messages[-2]["metadata"]["status"] = "done"
 
 
133
 
 
134
  result_content = result.content
135
  if isinstance(result_content, list):
136
  result_content = "\n".join(str(item) for item in result_content)
137
 
138
- # Parse and format the weather data response
139
- try:
140
- result_json = json.loads(result_content)
 
 
 
 
 
 
 
 
 
 
 
141
 
142
- if isinstance(result_json, dict):
143
- if result_json.get("type") == "success":
144
- # Format successful weather data response
145
- station_code = result_json.get("station_code", "Unknown")
146
- weather_data = result_json.get("data", {})
147
-
148
- # Create a nicely formatted response
149
- formatted_response = f"## 🌀️ Weather Data for Station: {station_code}\n\n"
150
-
151
- if isinstance(weather_data, dict):
152
- # Show key weather information if available
153
- if "reports" in weather_data:
154
- reports = weather_data["reports"]
155
- if isinstance(reports, list) and len(reports) > 0:
156
- formatted_response += f"**Found {len(reports)} weather reports**\n\n"
157
- # Show first few reports as example
158
- for i, report in enumerate(reports[:3]):
159
- if isinstance(report, dict):
160
- timestamp = report.get("timestamp", "Unknown time")
161
- temperature = report.get("temperature", "N/A")
162
- humidity = report.get("humidity", "N/A")
163
- formatted_response += f"**Report {i+1}** ({timestamp}):\n"
164
- formatted_response += f"- Temperature: {temperature}\n"
165
- formatted_response += f"- Humidity: {humidity}\n\n"
166
-
167
- if len(reports) > 3:
168
- formatted_response += f"... and {len(reports) - 3} more reports\n\n"
169
 
170
- formatted_response += "**Raw Data:**\n```json\n" + json.dumps(weather_data, indent=2) + "\n```"
171
- else:
172
- formatted_response += "**Raw Data:**\n```json\n" + json.dumps(weather_data, indent=2) + "\n```"
173
-
174
- result_messages.append({
175
- "role": "assistant",
176
- "content": formatted_response,
177
- "metadata": {
178
- "title": f"Weather Data Retrieved",
179
- "status": "done",
180
- "id": f"success_result_{tool_name}"
181
- }
182
- })
183
-
184
- elif result_json.get("type") == "error":
185
- # Format error response
186
- error_msg = result_json.get("message", "Unknown error occurred")
187
- station_code = result_json.get("station_code", "Unknown")
188
-
189
- error_response = f"## ❌ Error Fetching Weather Data\n\n"
190
- error_response += f"**Station:** {station_code}\n"
191
- error_response += f"**Error:** {error_msg}\n\n"
192
- error_response += "**Suggestions:**\n"
193
- error_response += "- Check if the station code is correct\n"
194
- error_response += "- Ensure the weather API service is running on localhost:8888\n"
195
- error_response += "- Try a different station code\n"
196
-
197
- result_messages.append({
198
- "role": "assistant",
199
- "content": error_response,
200
- "metadata": {
201
- "title": "Weather API Error",
202
- "status": "error",
203
- "id": f"error_result_{tool_name}"
204
- }
205
- })
206
- else:
207
- # Unknown response format
208
- result_messages.append({
209
- "role": "assistant",
210
- "content": "```json\n" + result_content + "\n```",
211
- "metadata": {
212
- "title": "Raw Tool Response",
213
- "status": "done",
214
- "id": f"raw_result_{tool_name}"
215
- }
216
- })
217
- else:
218
- result_messages.append({
219
- "role": "assistant",
220
- "content": "```\n" + result_content + "\n```",
221
- "metadata": {
222
- "title": "Raw Tool Response",
223
- "status": "done",
224
- "id": f"raw_result_{tool_name}"
225
- }
226
- })
227
 
228
- except json.JSONDecodeError:
229
- result_messages.append({
 
230
  "role": "assistant",
231
- "content": "```\n" + result_content + "\n```",
232
  "metadata": {
233
- "title": "Raw Tool Response",
234
  "status": "done",
235
- "id": f"raw_result_{tool_name}"
236
  }
237
- })
238
-
239
- # Let Claude analyze and respond to the weather data
240
- claude_messages.append({"role": "user", "content": f"Tool result for {tool_name}: {result_content}"})
241
- next_response = self.anthropic.messages.create(
242
- model="claude-3-5-sonnet-20241022",
243
- max_tokens=1500,
244
- messages=claude_messages,
245
- )
246
 
247
- if next_response.content and next_response.content[0].type == 'text':
248
- result_messages.append({
 
 
 
 
 
 
 
249
  "role": "assistant",
250
- "content": next_response.content[0].text
251
- })
252
-
253
- return result_messages
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
 
255
  client = MCPClientWrapper()
256
 
257
  def gradio_interface():
258
  with gr.Blocks(title="MCP LEXICON", theme=gr.themes.Soft()) as demo:
259
- gr.Markdown("# 🌀️ LEXICON CHATBOT - ask me anything")
260
  gr.Markdown(
261
- "Ask me about weather data from any weather station! I can fetch hourly reports, "
262
- "help you explore weather patterns, and answer questions about specific stations. "
263
- "Just ask naturally - for example: *'Get weather data for station ABC123'* or *'What stations are available?'*"
264
  )
265
 
266
- # Connection status (auto-updates on load)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  status = gr.Textbox(
268
  label="πŸ”Œ Connection Status",
269
  interactive=False,
@@ -280,14 +560,26 @@ def gradio_interface():
280
  bubble_full_width=False
281
  )
282
 
 
 
 
 
 
 
 
 
 
 
 
283
  # Input row
284
  with gr.Row(equal_height=True):
285
  msg = gr.Textbox(
286
  label="πŸ’¬ Ask about weather data",
287
- placeholder="e.g., 'Get weather data for station NYC001' or 'Show me available weather stations' or 'What's the latest data from station LAX123?'",
288
  scale=4
289
  )
290
  with gr.Column(scale=1):
 
291
  clear_btn = gr.Button("πŸ—‘οΈ Clear Chat", size="lg")
292
  reconnect_btn = gr.Button("πŸ”„ Reconnect", size="lg")
293
 
@@ -298,35 +590,89 @@ def gradio_interface():
298
  "What weather stations are available?",
299
  "Get weather data for station ABC123",
300
  "Show me the latest hourly reports for station NYC001",
301
- "Get weather data for station LAX789 from page 2",
302
- "Fetch weather data for station CHI456 between 2024-01-01 and 2024-01-31"
303
  ],
304
  inputs=msg,
305
  label="πŸ’‘ Example Queries"
306
  )
307
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
  # Auto-connect when the interface loads
309
  def auto_connect():
310
  return client.connect()
311
 
 
 
 
312
  # Event handlers
313
  demo.load(auto_connect, outputs=status)
314
- msg.submit(client.process_message, [msg, chatbot], [chatbot, msg])
315
- clear_btn.click(lambda: [], None, chatbot)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  reconnect_btn.click(auto_connect, outputs=status)
317
 
318
  return demo
319
 
320
  if __name__ == "__main__":
321
- if not os.getenv("ANTHROPIC_API_KEY"):
322
- print("⚠️ Warning: ANTHROPIC_API_KEY not found in environment.")
323
- print("Please set it in your .env file or environment variables.")
324
- print("Example .env file content:")
325
- print("ANTHROPIC_API_KEY=your_api_key_here")
 
 
 
 
 
 
 
 
 
 
 
 
326
 
327
- print("πŸš€ Starting MCP Weather Client...")
328
- print("πŸ“‘ Will auto-connect to gradio_mcp_server.py")
329
- print("🌐 Weather API endpoint: http://localhost:8888/weather/stations")
 
 
330
 
331
  interface = gradio_interface()
332
  interface.launch(debug=True, share=True)
 
3
  import json
4
  from typing import List, Dict, Any, Union
5
  from contextlib import AsyncExitStack
6
+ import mimetypes
7
+ import tempfile
8
 
9
  import gradio as gr
10
  from gradio.components.chatbot import ChatMessage
11
  from mcp import ClientSession, StdioServerParameters
12
  from mcp.client.stdio import stdio_client
13
  from anthropic import Anthropic
14
+ from openai import OpenAI
15
+ from mistralai.client import MistralClient
16
  from dotenv import load_dotenv
17
 
18
  load_dotenv()
 
24
  def __init__(self):
25
  self.session = None
26
  self.exit_stack = None
 
27
  self.tools = []
28
  self.connected = False
29
+
30
+ # Initialize all LLM clients
31
+ self.anthropic_client = None
32
+ self.openai_client = None
33
+ self.mistral_client = None
34
+ self.llama_client = None
35
+
36
+ # Current selected provider and model
37
+ self.current_provider = "claude"
38
+ self.current_model = "claude-3-5-sonnet-20241022"
39
+
40
+ self._initialize_clients()
41
+
42
+ def _initialize_clients(self):
43
+ """Initialize available LLM clients based on environment variables."""
44
+ try:
45
+ if os.getenv("ANTHROPIC_API_KEY"):
46
+ self.anthropic_client = Anthropic()
47
+ except Exception as e:
48
+ print(f"⚠️ Failed to initialize Anthropic client: {e}")
49
+
50
+ try:
51
+ if os.getenv("OPENAI_API_KEY"):
52
+ self.openai_client = OpenAI()
53
+ except Exception as e:
54
+ print(f"⚠️ Failed to initialize OpenAI client: {e}")
55
+
56
+ try:
57
+ if os.getenv("MISTRAL_API_KEY"):
58
+ self.mistral_client = MistralClient(api_key=os.getenv("MISTRAL_API_KEY"))
59
+ except Exception as e:
60
+ print(f"⚠️ Failed to initialize Mistral client: {e}")
61
+
62
+ try:
63
+ if os.getenv("LLAMAINDEX_API_KEY"):
64
+ # Using OpenAI-compatible endpoint for Llama
65
+ self.llama_client = OpenAI(
66
+ api_key=os.getenv("LLAMAINDEX_API_KEY"),
67
+ base_url="https://api.llamaindex.ai/v1" # Adjust based on your provider
68
+ )
69
+ except Exception as e:
70
+ print(f"⚠️ Failed to initialize Llama client: {e}")
71
+
72
+ def get_available_providers(self):
73
+ """Get list of available LLM providers."""
74
+ providers = {}
75
+ if self.anthropic_client:
76
+ providers["claude"] = {
77
+ "name": "Claude (Anthropic)",
78
+ "models": [
79
+ "claude-3-5-sonnet-20241022",
80
+ "claude-3-5-haiku-20241022",
81
+ "claude-3-opus-20240229"
82
+ ]
83
+ }
84
+ if self.openai_client:
85
+ providers["openai"] = {
86
+ "name": "OpenAI",
87
+ "models": [
88
+ "gpt-4o",
89
+ "gpt-4o-mini",
90
+ "gpt-4-turbo",
91
+ "gpt-3.5-turbo"
92
+ ]
93
+ }
94
+ if self.mistral_client:
95
+ providers["mistral"] = {
96
+ "name": "Mistral AI",
97
+ "models": [
98
+ "mistral-large-latest",
99
+ "mistral-medium-latest",
100
+ "mistral-small-latest",
101
+ "open-mixtral-8x7b"
102
+ ]
103
+ }
104
+ if self.llama_client:
105
+ providers["llama"] = {
106
+ "name": "Llama",
107
+ "models": [
108
+ "llama-3.1-70b-instruct",
109
+ "llama-3.1-8b-instruct",
110
+ "llama-2-70b-chat"
111
+ ]
112
+ }
113
+ return providers
114
+
115
+ def update_provider(self, provider: str, model: str):
116
+ """Update the current provider and model."""
117
+ self.current_provider = provider
118
+ self.current_model = model
119
+ return f"βœ… Switched to {provider}: {model}"
120
 
121
  def connect(self) -> str:
122
  return loop.run_until_complete(self._connect())
 
156
  self.connected = False
157
  return f"❌ Failed to connect to MCP server: {str(e)}"
158
 
159
+ def read_uploaded_file(self, file_path: str) -> str:
160
+ """Read and process uploaded file content."""
161
+ if not file_path or not os.path.exists(file_path):
162
+ return ""
163
+
164
+ try:
165
+ # Get file info
166
+ file_size = os.path.getsize(file_path)
167
+ file_name = os.path.basename(file_path)
168
+ mime_type, _ = mimetypes.guess_type(file_path)
169
+
170
+ # Check file size (limit to 10MB)
171
+ if file_size > 10 * 1024 * 1024:
172
+ return f"\n\nπŸ” **File Upload Error**: {file_name} is too large (>10MB). Please upload a smaller file."
173
+
174
+ # Try to read as text
175
+ encodings_to_try = ['utf-8', 'utf-16', 'latin-1', 'cp1252']
176
+
177
+ for encoding in encodings_to_try:
178
+ try:
179
+ with open(file_path, 'r', encoding=encoding) as f:
180
+ content = f.read()
181
+
182
+ # If content is too long, truncate it
183
+ max_chars = 50000 # Roughly 50k characters
184
+ if len(content) > max_chars:
185
+ content = content[:max_chars] + f"\n\n[Content truncated - showing first {max_chars} characters of {len(content)} total]"
186
+
187
+ file_info = f"\n\nπŸ” **Uploaded File**: {file_name}"
188
+ if mime_type:
189
+ file_info += f" ({mime_type})"
190
+ file_info += f" - {file_size:,} bytes\n\n```\n{content}\n```"
191
+
192
+ return file_info
193
+
194
+ except UnicodeDecodeError:
195
+ continue
196
+
197
+ # If all text encodings fail, it's likely a binary file
198
+ return f"\n\nπŸ” **File Upload**: {file_name} appears to be a binary file and cannot be displayed as text."
199
+
200
+ except Exception as e:
201
+ return f"\n\nπŸ” **File Upload Error**: Could not read {file_name}: {str(e)}"
202
+
203
+ def _convert_tools_for_provider(self, provider: str):
204
+ """Convert MCP tools format to provider-specific format."""
205
+ if provider == "claude":
206
+ return self.tools
207
+ elif provider in ["openai", "llama"]:
208
+ # Convert to OpenAI tools format
209
+ openai_tools = []
210
+ for tool in self.tools:
211
+ openai_tools.append({
212
+ "type": "function",
213
+ "function": {
214
+ "name": tool["name"],
215
+ "description": tool["description"],
216
+ "parameters": tool["input_schema"]
217
+ }
218
+ })
219
+ return openai_tools
220
+ elif provider == "mistral":
221
+ # Convert to Mistral tools format
222
+ mistral_tools = []
223
+ for tool in self.tools:
224
+ mistral_tools.append({
225
+ "type": "function",
226
+ "function": {
227
+ "name": tool["name"],
228
+ "description": tool["description"],
229
+ "parameters": tool["input_schema"]
230
+ }
231
+ })
232
+ return mistral_tools
233
+ else:
234
+ return []
235
+
236
+ async def _call_llm(self, messages: List[Dict], provider: str, model: str):
237
+ """Call the appropriate LLM based on provider."""
238
+ try:
239
+ if provider == "claude" and self.anthropic_client:
240
+ return self.anthropic_client.messages.create(
241
+ model=model,
242
+ max_tokens=1500,
243
+ messages=messages,
244
+ tools=self._convert_tools_for_provider(provider)
245
+ )
246
+ elif provider == "openai" and self.openai_client:
247
+ return self.openai_client.chat.completions.create(
248
+ model=model,
249
+ max_tokens=1500,
250
+ messages=messages,
251
+ tools=self._convert_tools_for_provider(provider)
252
+ )
253
+ elif provider == "llama" and self.llama_client:
254
+ return self.llama_client.chat.completions.create(
255
+ model=model,
256
+ max_tokens=1500,
257
+ messages=messages,
258
+ tools=self._convert_tools_for_provider(provider)
259
+ )
260
+ elif provider == "mistral" and self.mistral_client:
261
+ return self.mistral_client.chat(
262
+ model=model,
263
+ max_tokens=1500,
264
+ messages=messages,
265
+ tools=self._convert_tools_for_provider(provider)
266
+ )
267
+ else:
268
+ raise Exception(f"Provider {provider} not available or not initialized")
269
+ except Exception as e:
270
+ raise Exception(f"Error calling {provider}: {str(e)}")
271
+
272
+ def process_message(self, message: str, history: List[Union[Dict[str, Any], ChatMessage]], uploaded_file) -> tuple:
273
  if not self.session or not self.connected:
274
  return history + [
275
  {"role": "user", "content": message},
276
  {"role": "assistant", "content": "❌ MCP weather server is not connected. Please check the connection status above."}
277
+ ], gr.Textbox(value=""), gr.File(value=None)
278
+
279
+ # Process uploaded file if present
280
+ file_content = ""
281
+ if uploaded_file:
282
+ file_content = self.read_uploaded_file(uploaded_file.name if hasattr(uploaded_file, 'name') else uploaded_file)
283
 
284
+ # Combine message with file content
285
+ full_message = message + file_content
286
+
287
+ new_messages = loop.run_until_complete(self._process_query(full_message, history))
288
+ return history + [{"role": "user", "content": full_message}] + new_messages, gr.Textbox(value=""), gr.File(value=None)
289
 
290
  async def _process_query(self, message: str, history: List[Union[Dict[str, Any], ChatMessage]]):
291
  claude_messages = []
 
300
 
301
  claude_messages.append({"role": "user", "content": message})
302
 
303
+ try:
304
+ response = await self._call_llm(claude_messages, self.current_provider, self.current_model)
305
+ except Exception as e:
306
+ return [{"role": "assistant", "content": f"❌ Error with {self.current_provider}: {str(e)}"}]
 
 
307
 
308
  result_messages = []
309
 
310
+ # Handle different response formats based on provider
311
+ if self.current_provider == "claude":
312
+ return await self._process_claude_response(response, claude_messages)
313
+ elif self.current_provider in ["openai", "llama"]:
314
+ return await self._process_openai_response(response, claude_messages)
315
+ elif self.current_provider == "mistral":
316
+ return await self._process_mistral_response(response, claude_messages)
317
+
318
+ return result_messages
319
+
320
+ async def _process_claude_response(self, response, claude_messages):
321
+ """Process Claude API response."""
322
+ result_messages = []
323
+
324
  for content in response.content:
325
  if content.type == 'text':
326
  result_messages.append({
 
343
  }
344
  })
345
 
346
+ result = await self.session.call_tool(tool_name, tool_args)
347
+ result_content = result.content
348
+ if isinstance(result_content, list):
349
+ result_content = "\n".join(str(item) for item in result_content)
350
+
351
+ # Format the response
352
+ formatted_response = self._format_weather_response(result_content, tool_name)
353
+ result_messages.append(formatted_response)
354
+
355
+ # Let the LLM analyze and respond
356
+ claude_messages.append({"role": "user", "content": f"Tool result for {tool_name}: {result_content}"})
357
+ next_response = await self._call_llm(claude_messages, self.current_provider, self.current_model)
358
+
359
+ if hasattr(next_response, 'content') and next_response.content and next_response.content[0].type == 'text':
360
+ result_messages.append({
361
+ "role": "assistant",
362
+ "content": next_response.content[0].text
363
+ })
364
+
365
+ return result_messages
366
+
367
+ async def _process_openai_response(self, response, claude_messages):
368
+ """Process OpenAI/Llama API response."""
369
+ result_messages = []
370
+
371
+ message = response.choices[0].message
372
+
373
+ if message.content:
374
+ result_messages.append({
375
+ "role": "assistant",
376
+ "content": message.content
377
+ })
378
+
379
+ if message.tool_calls:
380
+ for tool_call in message.tool_calls:
381
+ tool_name = tool_call.function.name
382
+ tool_args = json.loads(tool_call.function.arguments)
383
+
384
  result_messages.append({
385
  "role": "assistant",
386
+ "content": f"πŸ”§ I'll use the **{tool_name}** tool to fetch the weather data you requested."
 
 
 
 
 
387
  })
388
 
389
  result = await self.session.call_tool(tool_name, tool_args)
390
+ result_content = result.content
391
+ if isinstance(result_content, list):
392
+ result_content = "\n".join(str(item) for item in result_content)
393
+
394
+ formatted_response = self._format_weather_response(result_content, tool_name)
395
+ result_messages.append(formatted_response)
396
+
397
+ return result_messages
398
+
399
+ async def _process_mistral_response(self, response, claude_messages):
400
+ """Process Mistral API response."""
401
+ result_messages = []
402
+
403
+ message = response.choices[0].message
404
+
405
+ if message.content:
406
+ result_messages.append({
407
+ "role": "assistant",
408
+ "content": message.content
409
+ })
410
+
411
+ if hasattr(message, 'tool_calls') and message.tool_calls:
412
+ for tool_call in message.tool_calls:
413
+ tool_name = tool_call.function.name
414
+ tool_args = json.loads(tool_call.function.arguments)
415
 
416
+ result_messages.append({
417
+ "role": "assistant",
418
+ "content": f"πŸ”§ I'll use the **{tool_name}** tool to fetch the weather data you requested."
419
+ })
420
 
421
+ result = await self.session.call_tool(tool_name, tool_args)
422
  result_content = result.content
423
  if isinstance(result_content, list):
424
  result_content = "\n".join(str(item) for item in result_content)
425
 
426
+ formatted_response = self._format_weather_response(result_content, tool_name)
427
+ result_messages.append(formatted_response)
428
+
429
+ return result_messages
430
+
431
+ def _format_weather_response(self, result_content: str, tool_name: str):
432
+ """Format weather data response."""
433
+ try:
434
+ result_json = json.loads(result_content)
435
+
436
+ if isinstance(result_json, dict):
437
+ if result_json.get("type") == "success":
438
+ station_code = result_json.get("station_code", "Unknown")
439
+ weather_data = result_json.get("data", {})
440
 
441
+ formatted_response = f"## 🌀️ Weather Data for Station: {station_code}\n\n"
442
+
443
+ if isinstance(weather_data, dict):
444
+ if "reports" in weather_data:
445
+ reports = weather_data["reports"]
446
+ if isinstance(reports, list) and len(reports) > 0:
447
+ formatted_response += f"**Found {len(reports)} weather reports**\n\n"
448
+ for i, report in enumerate(reports[:3]):
449
+ if isinstance(report, dict):
450
+ timestamp = report.get("timestamp", "Unknown time")
451
+ temperature = report.get("temperature", "N/A")
452
+ humidity = report.get("humidity", "N/A")
453
+ formatted_response += f"**Report {i+1}** ({timestamp}):\n"
454
+ formatted_response += f"- Temperature: {temperature}\n"
455
+ formatted_response += f"- Humidity: {humidity}\n\n"
 
 
 
 
 
 
 
 
 
 
 
 
456
 
457
+ if len(reports) > 3:
458
+ formatted_response += f"... and {len(reports) - 3} more reports\n\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
459
 
460
+ formatted_response += "**Raw Data:**\n```json\n" + json.dumps(weather_data, indent=2) + "\n```"
461
+
462
+ return {
463
  "role": "assistant",
464
+ "content": formatted_response,
465
  "metadata": {
466
+ "title": f"Weather Data Retrieved",
467
  "status": "done",
468
+ "id": f"success_result_{tool_name}"
469
  }
470
+ }
 
 
 
 
 
 
 
 
471
 
472
+ elif result_json.get("type") == "error":
473
+ error_msg = result_json.get("message", "Unknown error occurred")
474
+ station_code = result_json.get("station_code", "Unknown")
475
+
476
+ error_response = f"## ❌ Error Fetching Weather Data\n\n"
477
+ error_response += f"**Station:** {station_code}\n"
478
+ error_response += f"**Error:** {error_msg}\n\n"
479
+
480
+ return {
481
  "role": "assistant",
482
+ "content": error_response,
483
+ "metadata": {
484
+ "title": "Weather API Error",
485
+ "status": "error",
486
+ "id": f"error_result_{tool_name}"
487
+ }
488
+ }
489
+
490
+ except json.JSONDecodeError:
491
+ pass
492
+
493
+ return {
494
+ "role": "assistant",
495
+ "content": "```\n" + result_content + "\n```",
496
+ "metadata": {
497
+ "title": "Raw Tool Response",
498
+ "status": "done",
499
+ "id": f"raw_result_{tool_name}"
500
+ }
501
+ }
502
 
503
  client = MCPClientWrapper()
504
 
505
  def gradio_interface():
506
  with gr.Blocks(title="MCP LEXICON", theme=gr.themes.Soft()) as demo:
507
+ gr.Markdown("# 🌀️ LEXICON CHATBOT - Multi-LLM Weather Assistant")
508
  gr.Markdown(
509
+ "Ask me about weather data from any weather station! I support multiple AI providers "
510
+ "and can process uploaded files for additional context. Choose your preferred AI model below."
 
511
  )
512
 
513
+ # LLM Provider Selection
514
+ with gr.Row():
515
+ with gr.Column(scale=2):
516
+ available_providers = client.get_available_providers()
517
+ if not available_providers:
518
+ gr.Markdown("⚠️ **No LLM providers available**. Please check your API keys in environment variables.")
519
+ provider_dropdown = gr.Dropdown(choices=[], value=None, label="πŸ€– AI Provider", interactive=False)
520
+ model_dropdown = gr.Dropdown(choices=[], value=None, label="🎯 Model", interactive=False)
521
+ else:
522
+ provider_choices = [(info["name"], key) for key, info in available_providers.items()]
523
+ default_provider = list(available_providers.keys())[0]
524
+
525
+ provider_dropdown = gr.Dropdown(
526
+ choices=provider_choices,
527
+ value=default_provider,
528
+ label="πŸ€– AI Provider",
529
+ interactive=True
530
+ )
531
+
532
+ model_dropdown = gr.Dropdown(
533
+ choices=available_providers[default_provider]["models"],
534
+ value=available_providers[default_provider]["models"][0],
535
+ label="🎯 Model",
536
+ interactive=True
537
+ )
538
+
539
+ with gr.Column(scale=1):
540
+ current_model_display = gr.Textbox(
541
+ label="πŸ”„ Current Selection",
542
+ value=f"{client.current_provider}: {client.current_model}",
543
+ interactive=False
544
+ )
545
+
546
+ # Connection status
547
  status = gr.Textbox(
548
  label="πŸ”Œ Connection Status",
549
  interactive=False,
 
560
  bubble_full_width=False
561
  )
562
 
563
+ # File upload component (already exists in your code!)
564
+ file_upload = gr.File(
565
+ label="πŸ“Ž Upload File (optional)",
566
+ file_count="single",
567
+ file_types=[
568
+ ".txt", ".md", ".py", ".js", ".html", ".css", ".json", ".csv",
569
+ ".xml", ".yml", ".yaml", ".ini", ".cfg", ".log", ".sql"
570
+ ],
571
+ height=100
572
+ )
573
+
574
  # Input row
575
  with gr.Row(equal_height=True):
576
  msg = gr.Textbox(
577
  label="πŸ’¬ Ask about weather data",
578
+ placeholder="e.g., 'Get weather data for station NYC001' or upload a file with additional context",
579
  scale=4
580
  )
581
  with gr.Column(scale=1):
582
+ send_btn = gr.Button("πŸ“€ Send", size="lg", variant="primary")
583
  clear_btn = gr.Button("πŸ—‘οΈ Clear Chat", size="lg")
584
  reconnect_btn = gr.Button("πŸ”„ Reconnect", size="lg")
585
 
 
590
  "What weather stations are available?",
591
  "Get weather data for station ABC123",
592
  "Show me the latest hourly reports for station NYC001",
593
+ "Analyze the uploaded data and compare it with weather patterns",
594
+ "Explain the weather trends from the uploaded CSV file"
595
  ],
596
  inputs=msg,
597
  label="πŸ’‘ Example Queries"
598
  )
599
 
600
+ # Provider/Model update functions
601
+ def update_models(provider):
602
+ available_providers = client.get_available_providers()
603
+ if provider in available_providers:
604
+ models = available_providers[provider]["models"]
605
+ return gr.Dropdown(choices=models, value=models[0])
606
+ return gr.Dropdown(choices=[], value=None)
607
+
608
+ def update_current_selection(provider, model):
609
+ if provider and model:
610
+ status_msg = client.update_provider(provider, model)
611
+ return f"{provider}: {model}", status_msg
612
+ return current_model_display.value, "❌ Please select both provider and model"
613
+
614
  # Auto-connect when the interface loads
615
  def auto_connect():
616
  return client.connect()
617
 
618
+ def clear_all():
619
+ return [], gr.File(value=None)
620
+
621
  # Event handlers
622
  demo.load(auto_connect, outputs=status)
623
+
624
+ # Provider/Model selection
625
+ provider_dropdown.change(update_models, provider_dropdown, model_dropdown)
626
+ model_dropdown.change(
627
+ update_current_selection,
628
+ [provider_dropdown, model_dropdown],
629
+ [current_model_display, status]
630
+ )
631
+
632
+ # Send message on button click or enter key
633
+ send_btn.click(
634
+ client.process_message,
635
+ [msg, chatbot, file_upload],
636
+ [chatbot, msg, file_upload]
637
+ )
638
+ msg.submit(
639
+ client.process_message,
640
+ [msg, chatbot, file_upload],
641
+ [chatbot, msg, file_upload]
642
+ )
643
+
644
+ # Clear chat and file
645
+ clear_btn.click(clear_all, None, [chatbot, file_upload])
646
+
647
+ # Reconnect
648
  reconnect_btn.click(auto_connect, outputs=status)
649
 
650
  return demo
651
 
652
  if __name__ == "__main__":
653
+ # Check for API keys
654
+ api_keys = {
655
+ "ANTHROPIC_API_KEY": os.getenv("ANTHROPIC_API_KEY"),
656
+ "OPENAI_API_KEY": os.getenv("OPENAI_API_KEY"),
657
+ "MISTRAL_API_KEY": os.getenv("MISTRAL_API_KEY"),
658
+ "LLAMAINDEX_API_KEY": os.getenv("LLAMAINDEX_API_KEY")
659
+ }
660
+
661
+ available_keys = [key for key, value in api_keys.items() if value]
662
+
663
+ if not available_keys:
664
+ print("⚠️ Warning: No API keys found in environment.")
665
+ print("Please set at least one of the following in your .env file:")
666
+ for key in api_keys.keys():
667
+ print(f" {key}=your_api_key_here")
668
+ else:
669
+ print("πŸ”‘ Available API keys:", ", ".join(available_keys))
670
 
671
+ print("πŸš€ Starting Multi-LLM MCP Weather Client...")
672
+ print("πŸ”‘ Will auto-connect to gradio_mcp_server.py")
673
+ print("🌐 Weather API endpoint: https://lexicon.osfarm.org/weather/stations")
674
+ print("πŸ“Ž File upload enabled - supports text, code, and data files")
675
+ print("πŸ€– Multi-LLM support: Claude, OpenAI, Mistral, Llama")
676
 
677
  interface = gradio_interface()
678
  interface.launch(debug=True, share=True)