Nymbo commited on
Commit
cb919f0
·
verified ·
1 Parent(s): 109f11f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +331 -202
app.py CHANGED
@@ -1,224 +1,353 @@
 
 
1
  import os
2
  import json
3
- import requests
4
- import logging
5
- from typing import Dict, List, Optional, Any, Union
6
-
7
- # Setup logging
8
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
9
- logger = logging.getLogger(__name__)
10
-
11
- class MCPClient:
12
- """
13
- Client for interacting with MCP (Model Context Protocol) servers.
14
- Implements a subset of the MCP protocol sufficient for TTS and other basic tools.
15
- """
 
 
 
 
 
 
 
16
 
17
- def __init__(self, server_url: str):
18
- """
19
- Initialize an MCP client for a specific server URL
20
-
21
- Args:
22
- server_url: The URL of the MCP server to connect to
23
- """
24
- self.server_url = server_url
25
- self.session_id = None
26
- logger.info(f"Initialized MCP Client for server: {server_url}")
27
 
28
- def connect(self) -> bool:
29
- """
30
- Establish connection with the MCP server
31
-
32
- Returns:
33
- bool: True if connection was successful, False otherwise
34
- """
 
 
35
  try:
36
- # For a real MCP implementation, this would use the MCP initialization protocol
37
- # This is a simplified version for demonstration purposes
38
- response = requests.post(
39
- f"{self.server_url}/connect",
40
- json={"client": "Serverless-TextGen-Hub", "version": "1.0.0"},
41
- timeout=10
42
- )
43
-
44
- if response.status_code == 200:
45
- result = response.json()
46
- self.session_id = result.get("session_id")
47
- logger.info(f"Connected to MCP server with session ID: {self.session_id}")
48
- return True
49
  else:
50
- logger.error(f"Failed to connect to MCP server: {response.status_code} - {response.text}")
51
- return False
52
  except Exception as e:
53
- logger.error(f"Error connecting to MCP server: {e}")
54
- return False
55
 
56
- def list_tools(self) -> List[Dict]:
57
- """
58
- List available tools from the MCP server
59
-
60
- Returns:
61
- List[Dict]: List of tool definitions from the server
62
- """
63
- if not self.session_id:
64
- if not self.connect():
65
- return []
66
-
67
- try:
68
- # In a real MCP implementation, this would use the tools/list method
69
- response = requests.get(
70
- f"{self.server_url}/tools/list",
71
- headers={"X-MCP-Session": self.session_id},
72
- timeout=10
73
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
- if response.status_code == 200:
76
- result = response.json()
77
- tools = result.get("tools", [])
78
- logger.info(f"Retrieved {len(tools)} tools from MCP server")
79
- return tools
80
- else:
81
- logger.error(f"Failed to list tools: {response.status_code} - {response.text}")
82
- return []
83
- except Exception as e:
84
- logger.error(f"Error listing tools: {e}")
85
- return []
86
 
87
- def call_tool(self, tool_name: str, args: Dict) -> Dict:
88
- """
89
- Call a tool on the MCP server
90
-
91
- Args:
92
- tool_name: Name of the tool to call
93
- args: Arguments to pass to the tool
94
-
95
- Returns:
96
- Dict: Result of the tool call
97
- """
98
- if not self.session_id:
99
- if not self.connect():
100
- return {"error": "Not connected to MCP server"}
101
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  try:
103
- # In a real MCP implementation, this would use the tools/call method
104
- response = requests.post(
105
- f"{self.server_url}/tools/call",
106
- headers={"X-MCP-Session": self.session_id},
107
- json={"name": tool_name, "arguments": args},
108
- timeout=30 # Longer timeout for tool calls
109
  )
110
-
111
- if response.status_code == 200:
112
- result = response.json()
113
- logger.info(f"Successfully called tool {tool_name}")
114
- return result
115
- else:
116
- error_msg = f"Failed to call tool {tool_name}: {response.status_code} - {response.text}"
117
- logger.error(error_msg)
118
- return {"error": error_msg}
119
  except Exception as e:
120
- error_msg = f"Error calling tool {tool_name}: {e}"
121
- logger.error(error_msg)
122
- return {"error": error_msg}
123
-
124
- def close(self):
125
- """Clean up the client connection"""
126
- if self.session_id:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  try:
128
- # For a real MCP implementation, this would use the shutdown method
129
- requests.post(
130
- f"{self.server_url}/disconnect",
131
- headers={"X-MCP-Session": self.session_id},
132
- timeout=5
133
- )
134
- logger.info(f"Disconnected from MCP server")
 
135
  except Exception as e:
136
- logger.error(f"Error disconnecting from MCP server: {e}")
137
- finally:
138
- self.session_id = None
139
 
 
 
 
 
140
 
141
- def get_mcp_servers() -> Dict[str, Dict[str, str]]:
142
- """
143
- Load MCP server configuration from environment variable
144
-
145
- Returns:
146
- Dict[str, Dict[str, str]]: Map of server names to server configurations
147
- """
148
- try:
149
- mcp_config = os.getenv("MCP_CONFIG")
150
- if mcp_config:
151
- servers = json.loads(mcp_config)
152
- logger.info(f"Loaded {len(servers)} MCP servers from configuration")
153
- return servers
154
- else:
155
- logger.warning("No MCP configuration found")
156
- return {}
157
- except Exception as e:
158
- logger.error(f"Error loading MCP configuration: {e}")
159
- return {}
160
 
 
 
 
 
 
161
 
162
- def text_to_speech(text: str, server_name: str = None) -> Optional[str]:
163
- """
164
- Convert text to speech using an MCP TTS server
165
-
166
- Args:
167
- text: The text to convert to speech
168
- server_name: Name of the MCP server to use for TTS
169
-
170
- Returns:
171
- Optional[str]: Data URL containing the audio, or None if conversion failed
172
- """
173
- servers = get_mcp_servers()
174
-
175
- if not server_name or server_name not in servers:
176
- logger.warning(f"TTS server {server_name} not configured")
177
- return None
178
-
179
- server_url = servers[server_name].get("url")
180
- if not server_url:
181
- logger.warning(f"No URL found for TTS server {server_name}")
182
- return None
183
-
184
- client = MCPClient(server_url)
185
-
186
- try:
187
- # List available tools to find the TTS tool
188
- tools = client.list_tools()
189
-
190
- # Find a TTS tool - look for common TTS tool names
191
- tts_tool = next(
192
- (t for t in tools if any(
193
- name in t["name"].lower()
194
- for name in ["text_to_audio", "tts", "text_to_speech", "speech"]
195
- )),
196
- None
197
- )
198
-
199
- if not tts_tool:
200
- logger.warning(f"No TTS tool found on server {server_name}")
201
- return None
202
-
203
- # Call the TTS tool
204
- result = client.call_tool(tts_tool["name"], {"text": text, "speed": 1.0})
205
-
206
- if "error" in result:
207
- logger.error(f"TTS error: {result['error']}")
208
- return None
209
 
210
- # Process the result - usually a base64 encoded WAV
211
- audio_data = result.get("audio") or result.get("content") or result.get("result")
212
 
213
- if isinstance(audio_data, str) and audio_data.startswith("data:audio"):
214
- # Already a data URL
215
- return audio_data
216
- elif isinstance(audio_data, str):
217
- # Assume it's base64 encoded
218
- return f"data:audio/wav;base64,{audio_data}"
219
- else:
220
- logger.error(f"Unexpected TTS result format: {type(audio_data)}")
221
- return None
 
 
 
 
 
 
 
 
 
222
 
223
- finally:
224
- client.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from huggingface_hub import InferenceClient
3
  import os
4
  import json
5
+ import base64
6
+ from PIL import Image
7
+ import io
8
+ import atexit
9
+
10
+ from smolagents import ToolCollection, CodeAgent
11
+ from smolagents.mcp_client import MCPClient as SmolMCPClient
12
+
13
+ ACCESS_TOKEN = os.getenv("HF_TOKEN")
14
+ print("Access token loaded.")
15
+
16
+ mcp_tools_collection = ToolCollection(tools=[])
17
+ mcp_client_instances = []
18
+
19
+ DEFAULT_MCP_SERVERS = [
20
+ {"name": "KokoroTTS (Example)", "type": "sse", "url": "https://fdaudens-kokoro-mcp.hf.space/gradio_api/mcp/sse"}
21
+ ]
22
+
23
+ def load_mcp_tools(server_configs_list):
24
+ global mcp_tools_collection, mcp_client_instances
25
 
26
+ # No explicit close for SmolMCPClient instances as it's not available directly
27
+ # Rely on script termination or GC for now.
28
+ # If you were using ToolCollection per server: tc.close() would be the way.
29
+ print(f"Clearing {len(mcp_client_instances)} previous MCP client instance references.")
30
+ mcp_client_instances = [] # Clear references; old objects will be GC'd if not referenced elsewhere
 
 
 
 
 
31
 
32
+ all_discovered_tools = []
33
+ if not server_configs_list:
34
+ print("No MCP server configurations provided. Clearing MCP tools.")
35
+ mcp_tools_collection = ToolCollection(tools=all_discovered_tools)
36
+ return
37
+
38
+ print(f"Loading MCP tools from {len(server_configs_list)} server configurations...")
39
+ for config in server_configs_list:
40
+ server_name = config.get('name', config.get('url', 'Unknown Server'))
41
  try:
42
+ if config.get("type") == "sse":
43
+ sse_url = config["url"]
44
+ print(f"Attempting to connect to MCP SSE server: {server_name} at {sse_url}")
45
+ smol_mcp_client = SmolMCPClient(server_parameters={"url": sse_url})
46
+ mcp_client_instances.append(smol_mcp_client)
47
+ discovered_tools_from_server = smol_mcp_client.get_tools()
48
+ if discovered_tools_from_server:
49
+ all_discovered_tools.extend(list(discovered_tools_from_server))
50
+ print(f"Discovered {len(discovered_tools_from_server)} tools from {server_name}.")
51
+ else:
52
+ print(f"No tools discovered from {server_name}.")
 
 
53
  else:
54
+ print(f"Unsupported MCP server type '{config.get('type')}' for {server_name}. Skipping.")
 
55
  except Exception as e:
56
+ print(f"Error loading MCP tools from {server_name}: {e}")
 
57
 
58
+ mcp_tools_collection = ToolCollection(tools=all_discovered_tools)
59
+ if mcp_tools_collection and len(mcp_tools_collection.tools) > 0:
60
+ print(f"Successfully loaded a total of {len(mcp_tools_collection.tools)} MCP tools:")
61
+ for tool in mcp_tools_collection.tools:
62
+ print(f" - {tool.name}: {tool.description[:100]}...")
63
+ else:
64
+ print("No MCP tools were loaded, or an error occurred.")
65
+
66
+ def cleanup_mcp_client_instances_on_exit():
67
+ global mcp_client_instances
68
+ print("Attempting to clear MCP client instance references on application exit...")
69
+ # No explicit close called here as per previous fix
70
+ mcp_client_instances = []
71
+ print("MCP client instance reference cleanup finished.")
72
+
73
+ atexit.register(cleanup_mcp_client_instances_on_exit)
74
+
75
+ def encode_image(image_path):
76
+ if not image_path: return None
77
+ try:
78
+ image = Image.open(image_path) if not isinstance(image_path, Image.Image) else image_path
79
+ if image.mode == 'RGBA': image = image.convert('RGB')
80
+ buffered = io.BytesIO()
81
+ image.save(buffered, format="JPEG")
82
+ return base64.b64encode(buffered.getvalue()).decode("utf-8")
83
+ except Exception as e:
84
+ print(f"Error encoding image {image_path}: {e}")
85
+ return None
86
+
87
+ def respond(
88
+ message_input_text,
89
+ image_files_list,
90
+ history: list[tuple[str, str]], # history will be list of (user_str_display, assistant_str_display)
91
+ system_message,
92
+ max_tokens,
93
+ temperature,
94
+ top_p,
95
+ frequency_penalty,
96
+ seed,
97
+ provider,
98
+ custom_api_key,
99
+ custom_model,
100
+ model_search_term,
101
+ selected_model
102
+ ):
103
+ global mcp_tools_collection
104
+ print(f"Respond: Text='{message_input_text}', Images={len(image_files_list) if image_files_list else 0}")
105
+
106
+ token_to_use = custom_api_key if custom_api_key.strip() else ACCESS_TOKEN
107
+ hf_inference_client = InferenceClient(token=token_to_use, provider=provider)
108
+ if seed == -1: seed = None
109
+
110
+ current_user_content_parts = []
111
+ if message_input_text and message_input_text.strip():
112
+ current_user_content_parts.append({"type": "text", "text": message_input_text.strip()})
113
+ if image_files_list:
114
+ for img_path in image_files_list:
115
+ encoded_img = encode_image(img_path)
116
+ if encoded_img:
117
+ current_user_content_parts.append({
118
+ "type": "image_url",
119
+ "image_url": {"url": f"data:image/jpeg;base64,{encoded_img}"}
120
+ })
121
+ if not current_user_content_parts:
122
+ for item in history: yield item # Should not happen if handle_submit filters empty
123
+ return
124
+
125
+ llm_messages = [{"role": "system", "content": system_message}]
126
+ for hist_user_str, hist_assistant in history: # hist_user_str is display string
127
+ # For LLM context, we only care about the text part of history if it was multimodal.
128
+ # Current image handling is only for the *current* turn.
129
+ # If you need to re-process history for multimodal context for LLM, this part needs more logic.
130
+ # For now, assuming hist_user_str is sufficient as text context from past turns.
131
+ if hist_user_str:
132
+ llm_messages.append({"role": "user", "content": hist_user_str})
133
+ if hist_assistant:
134
+ llm_messages.append({"role": "assistant", "content": hist_assistant})
135
 
136
+ llm_messages.append({"role": "user", "content": current_user_content_parts if len(current_user_content_parts) > 1 else (current_user_content_parts[0] if current_user_content_parts else "")})
 
 
 
 
 
 
 
 
 
 
137
 
138
+ # FIX for Issue 1: 'NoneType' object has no attribute 'strip'
139
+ model_to_use = (custom_model.strip() if custom_model else "") or selected_model
140
+ print(f"Model selected for inference: {model_to_use}")
141
+
142
+ active_mcp_tools = list(mcp_tools_collection.tools) if mcp_tools_collection else []
143
+
144
+ if active_mcp_tools:
145
+ print(f"MCP tools are active ({len(active_mcp_tools)} tools). Using CodeAgent.")
146
+ class HFClientWrapperForAgent:
147
+ def __init__(self, hf_client, model_id, outer_scope_params):
148
+ self.client = hf_client
149
+ self.model_id = model_id
150
+ self.params = outer_scope_params
151
+ def generate(self, agent_llm_messages, tools=None, tool_choice=None, **kwargs):
152
+ api_params = {
153
+ "model": self.model_id, "messages": agent_llm_messages, "stream": False,
154
+ "max_tokens": self.params['max_tokens'], "temperature": self.params['temperature'],
155
+ "top_p": self.params['top_p'], "frequency_penalty": self.params['frequency_penalty'],
156
+ }
157
+ if self.params['seed'] is not None: api_params["seed"] = self.params['seed']
158
+ if tools: api_params["tools"] = tools
159
+ if tool_choice: api_params["tool_choice"] = tool_choice
160
+
161
+ print(f"Agent's HFClientWrapper calling LLM: {self.model_id} with params: {api_params}")
162
+ completion = self.client.chat_completion(**api_params)
163
+
164
+ # FIX for Issue 2 (Potential): Ensure content is not None for text responses
165
+ if completion.choices and completion.choices[0].message and \
166
+ completion.choices[0].message.content is None and \
167
+ (not completion.choices[0].message.tool_calls or not completion.choices[0].message.tool_calls):
168
+ print("Warning (HFClientWrapperForAgent): Model returned None content. Setting to empty string.")
169
+ completion.choices[0].message.content = ""
170
+ return completion
171
+
172
+ outer_scope_llm_params = {
173
+ "max_tokens": max_tokens, "temperature": temperature, "top_p": top_p,
174
+ "frequency_penalty": frequency_penalty, "seed": seed
175
+ }
176
+ agent_model_adapter = HFClientWrapperForAgent(hf_inference_client, model_to_use, outer_scope_llm_params)
177
+ agent = CodeAgent(tools=active_mcp_tools, model=agent_model_adapter, messages_constructor=lambda: llm_messages[:-1].copy()) # Prime with history
178
+
179
+ current_query_for_agent = message_input_text.strip() if message_input_text else "User provided image(s)."
180
+ if not current_query_for_agent and image_files_list:
181
+ current_query_for_agent = "Process the provided image(s) or follow related instructions."
182
+ elif not current_query_for_agent and not image_files_list:
183
+ current_query_for_agent = "..." # Should be caught by earlier check
184
+
185
+ print(f"Query for CodeAgent.run: '{current_query_for_agent}' with {len(llm_messages)-1} history messages for priming.")
186
+ try:
187
+ agent_final_text_response = agent.run(current_query_for_agent)
188
+ yield agent_final_text_response
189
+ print("Completed response generation via CodeAgent.")
190
+ except Exception as e:
191
+ print(f"Error during CodeAgent execution: {e}") # This will now print the actual underlying error
192
+ yield f"Error using tools: {str(e)}" # The str(e) might be the user-facing error
193
+ return
194
+ else:
195
+ print("No MCP tools active. Proceeding with direct LLM call (streaming).")
196
+ response_stream_content = ""
197
  try:
198
+ stream = hf_inference_client.chat_completion(
199
+ model=model_to_use, messages=llm_messages, stream=True,
200
+ max_tokens=max_tokens, temperature=temperature, top_p=top_p,
201
+ frequency_penalty=frequency_penalty, seed=seed
 
 
202
  )
203
+ for chunk in stream:
204
+ if hasattr(chunk, 'choices') and len(chunk.choices) > 0:
205
+ delta = chunk.choices[0].delta
206
+ if hasattr(delta, 'content') and delta.content:
207
+ token_text = delta.content
208
+ response_stream_content += token_text
209
+ yield response_stream_content
210
+ print("\nCompleted streaming response generation.")
 
211
  except Exception as e:
212
+ print(f"Error during direct LLM inference: {e}")
213
+ yield response_stream_content + f"\nError: {str(e)}"
214
+
215
+ def validate_provider(api_key, provider):
216
+ if not api_key.strip() and provider != "hf-inference":
217
+ return gr.update(value="hf-inference")
218
+ return gr.update(value=provider)
219
+
220
+ with gr.Blocks(theme="Nymbo/Nymbo_Theme") as demo:
221
+ # UserWarning for type='tuples' is known. Consider changing to type='messages' later for robustness.
222
+ chatbot = gr.Chatbot(
223
+ label="Serverless TextGen Hub", height=600, show_copy_button=True,
224
+ placeholder="Select a model, (optionally) load MCP Tools, and begin chatting.",
225
+ layout="panel", bubble_full_width=False
226
+ )
227
+ msg_input_box = gr.MultimodalTextbox(
228
+ placeholder="Type a message or upload images...", show_label=False,
229
+ container=False, scale=12, file_types=["image"],
230
+ file_count="multiple", sources=["upload"]
231
+ )
232
+ with gr.Accordion("Settings", open=False):
233
+ system_message_box = gr.Textbox(value="You are a helpful AI assistant.", label="System Prompt")
234
+ with gr.Row():
235
+ max_tokens_slider = gr.Slider(1, 4096, value=512, step=1, label="Max tokens")
236
+ temperature_slider = gr.Slider(0.1, 4.0, value=0.7, step=0.1, label="Temperature")
237
+ top_p_slider = gr.Slider(0.1, 1.0, value=0.95, step=0.05, label="Top-P")
238
+ with gr.Row():
239
+ frequency_penalty_slider = gr.Slider(-2.0, 2.0, value=0.0, step=0.1, label="Frequency Penalty")
240
+ seed_slider = gr.Slider(-1, 65535, value=-1, step=1, label="Seed (-1 for random)")
241
+ providers_list = ["hf-inference", "cerebras", "together", "sambanova", "novita", "cohere", "fireworks-ai", "hyperbolic", "nebius"]
242
+ provider_radio = gr.Radio(choices=providers_list, value="hf-inference", label="Inference Provider")
243
+ byok_textbox = gr.Textbox(label="BYOK (Hugging Face API Key)", type="password", placeholder="Enter token if not using 'hf-inference'")
244
+ custom_model_box = gr.Textbox(label="Custom Model ID", placeholder="org/model-name (overrides selection below)")
245
+ model_search_box = gr.Textbox(label="Filter Featured Models", placeholder="Search...")
246
+ models_list = [
247
+ "meta-llama/Llama-3.2-11B-Vision-Instruct", "meta-llama/Llama-3.3-70B-Instruct",
248
+ "meta-llama/Llama-3.1-70B-Instruct", "meta-llama/Llama-3.0-70B-Instruct",
249
+ "meta-llama/Llama-3.2-3B-Instruct", "meta-llama/Llama-3.2-1B-Instruct",
250
+ "meta-llama/Llama-3.1-8B-Instruct", "NousResearch/Hermes-3-Llama-3.1-8B",
251
+ "NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO", "mistralai/Mistral-Nemo-Instruct-2407",
252
+ "mistralai/Mixtral-8x7B-Instruct-v0.1", "mistralai/Mistral-7B-Instruct-v0.3",
253
+ "mistralai/Mistral-7B-Instruct-v0.2", "Qwen/Qwen3-235B-A22B", "Qwen/Qwen3-32B",
254
+ "Qwen/Qwen2.5-72B-Instruct", "Qwen/Qwen2.5-3B-Instruct", "Qwen/Qwen2.5-0.5B-Instruct",
255
+ "Qwen/QwQ-32B", "Qwen/Qwen2.5-Coder-32B-Instruct", "microsoft/Phi-3.5-mini-instruct",
256
+ "microsoft/Phi-3-mini-128k-instruct", "microsoft/Phi-3-mini-4k-instruct",
257
+ ]
258
+ featured_model_radio = gr.Radio(label="Select a Featured Model", choices=models_list, value="meta-llama/Llama-3.2-11B-Vision-Instruct", interactive=True)
259
+ gr.Markdown("[All Text models](https://huggingface.co/models?pipeline_tag=text-generation) | [All Multimodal models](https://huggingface.co/models?pipeline_tag=image-text-to-text)")
260
+
261
+ with gr.Accordion("MCP Client Settings (Connect to External Tools)", open=False):
262
+ gr.Markdown("Configure connections to MCP Servers to allow the LLM to use external tools. The LLM will decide when to use these tools based on your prompts.")
263
+ mcp_server_config_input = gr.Textbox(
264
+ label="MCP Server Configurations (JSON Array)",
265
+ info='Example: [{"name": "MyToolServer", "type": "sse", "url": "http://server_url/gradio_api/mcp/sse"}]',
266
+ lines=3, placeholder='Enter a JSON list of server configurations here.',
267
+ value=json.dumps(DEFAULT_MCP_SERVERS, indent=2)
268
+ )
269
+ mcp_load_status_display = gr.Textbox(label="MCP Load Status", interactive=False)
270
+ load_mcp_tools_btn = gr.Button("Load/Reload MCP Tools")
271
+
272
+ def handle_load_mcp_tools_click(config_str_from_ui):
273
+ if not config_str_from_ui:
274
+ load_mcp_tools([])
275
+ return "MCP tool loading attempted with empty config. Tools cleared."
276
  try:
277
+ parsed_configs = json.loads(config_str_from_ui)
278
+ if not isinstance(parsed_configs, list): return "Error: MCP configuration must be a valid JSON list."
279
+ load_mcp_tools(parsed_configs)
280
+ if mcp_tools_collection and len(mcp_tools_collection.tools) > 0:
281
+ loaded_tool_names = [t.name for t in mcp_tools_collection.tools]
282
+ return f"Successfully loaded {len(loaded_tool_names)} MCP tools: {', '.join(loaded_tool_names)}"
283
+ else: return "No MCP tools loaded, or an error occurred. Check console for details."
284
+ except json.JSONDecodeError: return "Error: Invalid JSON format in MCP server configurations."
285
  except Exception as e:
286
+ print(f"Unhandled error in handle_load_mcp_tools_click: {e}")
287
+ return f"Error loading MCP tools: {str(e)}. Check console."
288
+ load_mcp_tools_btn.click(handle_load_mcp_tools_click, inputs=[mcp_server_config_input], outputs=mcp_load_status_display)
289
 
290
+ def filter_models(search_term):
291
+ return gr.update(choices=[m for m in models_list if search_term.lower() in m.lower()])
292
+ def set_custom_model_from_radio(selected):
293
+ return selected
294
 
295
+ def handle_submit(msg_content_dict, current_chat_history):
296
+ text = msg_content_dict.get("text", "").strip()
297
+ files = msg_content_dict.get("files", []) # list of file paths
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
 
299
+ if not text and not files: # Skip if both are empty
300
+ print("Skipping empty submission from multimodal textbox.")
301
+ # Yield current history to prevent Gradio from complaining about no output
302
+ yield current_chat_history, {"text": "", "files": []} # Clear input
303
+ return
304
 
305
+ # FIX for Issue 4: Pydantic FileMessage error by ensuring user part of history is a string
306
+ user_display_parts = []
307
+ if text:
308
+ user_display_parts.append(text)
309
+ if files:
310
+ for f_path in files:
311
+ base_name = os.path.basename(f_path) if f_path else "file"
312
+ f_path_str = f_path if f_path else ""
313
+ user_display_parts.append(f"\n![{base_name}]({f_path_str})")
314
+ user_display_message_for_chatbot = " ".join(user_display_parts).strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
 
316
+ current_chat_history.append([user_display_message_for_chatbot, None])
 
317
 
318
+ # Prepare history for respond function (ensure user part is string)
319
+ history_for_respond = []
320
+ for user_h, assistant_h in current_chat_history[:-1]: # History before current turn
321
+ history_for_respond.append((str(user_h) if user_h is not None else "", assistant_h))
322
+
323
+
324
+ assistant_response_accumulator = ""
325
+ for streamed_chunk in respond(
326
+ text, files,
327
+ history_for_respond,
328
+ system_message_box.value, max_tokens_slider.value, temperature_slider.value,
329
+ top_p_slider.value, frequency_penalty_slider.value, seed_slider.value,
330
+ provider_radio.value, byok_textbox.value, custom_model_box.value,
331
+ model_search_box.value, featured_model_radio.value
332
+ ):
333
+ assistant_response_accumulator = streamed_chunk
334
+ current_chat_history[-1][1] = assistant_response_accumulator
335
+ yield current_chat_history, {"text": "", "files": []}
336
 
337
+ msg_input_box.submit(
338
+ handle_submit,
339
+ [msg_input_box, chatbot],
340
+ [chatbot, msg_input_box]
341
+ )
342
+ model_search_box.change(filter_models, model_search_box, featured_model_radio)
343
+ featured_model_radio.change(set_custom_model_from_radio, featured_model_radio, custom_model_box)
344
+ byok_textbox.change(validate_provider, [byok_textbox, provider_radio], provider_radio)
345
+ provider_radio.change(validate_provider, [byok_textbox, provider_radio], provider_radio)
346
+
347
+ load_mcp_tools(DEFAULT_MCP_SERVERS) # Load defaults on startup
348
+ print(f"Initial MCP tools loaded: {len(mcp_tools_collection.tools) if mcp_tools_collection else 0} tools.")
349
+ print("Gradio interface initialized.")
350
+
351
+ if __name__ == "__main__":
352
+ print("Launching the Serverless TextGen Hub demo application.")
353
+ demo.launch(show_api=False)