Spaces:
Running
on
Zero
Running
on
Zero
Y Phung Nguyen
commited on
Commit
·
6ab08df
1
Parent(s):
2f3ac98
Add timeout mcp
Browse files
agent.py
CHANGED
|
@@ -169,10 +169,12 @@ async def list_tools() -> list[Tool]:
|
|
| 169 |
@app.call_tool()
|
| 170 |
async def call_tool(name: str, arguments: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
|
| 171 |
"""Handle tool calls"""
|
|
|
|
| 172 |
if name == "generate_content":
|
| 173 |
try:
|
| 174 |
user_prompt = arguments.get("user_prompt")
|
| 175 |
if not user_prompt:
|
|
|
|
| 176 |
return [TextContent(type="text", text="Error: user_prompt is required")]
|
| 177 |
|
| 178 |
system_prompt = arguments.get("system_prompt")
|
|
@@ -221,6 +223,10 @@ async def call_tool(name: str, arguments: dict) -> Sequence[TextContent | ImageC
|
|
| 221 |
"max_output_tokens": GEMINI_MAX_OUTPUT_TOKENS
|
| 222 |
}
|
| 223 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
# Use asyncio.to_thread to make the blocking call async
|
| 225 |
# The API accepts contents as a list and config as a separate parameter
|
| 226 |
def generate_sync():
|
|
@@ -230,7 +236,11 @@ async def call_tool(name: str, arguments: dict) -> Sequence[TextContent | ImageC
|
|
| 230 |
config=generation_config,
|
| 231 |
)
|
| 232 |
|
| 233 |
-
response = await asyncio.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
|
| 235 |
# Extract text from response
|
| 236 |
if response and hasattr(response, 'text') and response.text:
|
|
@@ -253,8 +263,15 @@ async def call_tool(name: str, arguments: dict) -> Sequence[TextContent | ImageC
|
|
| 253 |
logger.warning("Gemini returned empty response")
|
| 254 |
return [TextContent(type="text", text="Error: No response from Gemini")]
|
| 255 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
except Exception as e:
|
| 257 |
-
logger.error(f"Error generating content: {e}")
|
|
|
|
|
|
|
| 258 |
return [TextContent(type="text", text=f"Error: {str(e)}")]
|
| 259 |
|
| 260 |
except Exception as e:
|
|
|
|
| 169 |
@app.call_tool()
|
| 170 |
async def call_tool(name: str, arguments: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
|
| 171 |
"""Handle tool calls"""
|
| 172 |
+
logger.info(f"🔵 MCP tool call received: {name}")
|
| 173 |
if name == "generate_content":
|
| 174 |
try:
|
| 175 |
user_prompt = arguments.get("user_prompt")
|
| 176 |
if not user_prompt:
|
| 177 |
+
logger.error("❌ user_prompt is required but missing")
|
| 178 |
return [TextContent(type="text", text="Error: user_prompt is required")]
|
| 179 |
|
| 180 |
system_prompt = arguments.get("system_prompt")
|
|
|
|
| 223 |
"max_output_tokens": GEMINI_MAX_OUTPUT_TOKENS
|
| 224 |
}
|
| 225 |
|
| 226 |
+
# Convert timeout from milliseconds to seconds, cap at 100s to stay under 120s function limit
|
| 227 |
+
timeout_seconds = min(GEMINI_TIMEOUT / 1000.0, 100.0)
|
| 228 |
+
logger.info(f"🔵 Calling Gemini API with model={model}, timeout={timeout_seconds}s...")
|
| 229 |
+
|
| 230 |
# Use asyncio.to_thread to make the blocking call async
|
| 231 |
# The API accepts contents as a list and config as a separate parameter
|
| 232 |
def generate_sync():
|
|
|
|
| 236 |
config=generation_config,
|
| 237 |
)
|
| 238 |
|
| 239 |
+
response = await asyncio.wait_for(
|
| 240 |
+
asyncio.to_thread(generate_sync),
|
| 241 |
+
timeout=timeout_seconds
|
| 242 |
+
)
|
| 243 |
+
logger.info(f"✅ Gemini API call completed successfully")
|
| 244 |
|
| 245 |
# Extract text from response
|
| 246 |
if response and hasattr(response, 'text') and response.text:
|
|
|
|
| 263 |
logger.warning("Gemini returned empty response")
|
| 264 |
return [TextContent(type="text", text="Error: No response from Gemini")]
|
| 265 |
|
| 266 |
+
except asyncio.TimeoutError:
|
| 267 |
+
timeout_seconds = min(GEMINI_TIMEOUT / 1000.0, 100.0)
|
| 268 |
+
error_msg = f"Gemini API call timed out after {timeout_seconds}s"
|
| 269 |
+
logger.error(f"❌ {error_msg}")
|
| 270 |
+
return [TextContent(type="text", text=f"Error: {error_msg}")]
|
| 271 |
except Exception as e:
|
| 272 |
+
logger.error(f"❌ Error generating content: {type(e).__name__}: {e}")
|
| 273 |
+
import traceback
|
| 274 |
+
logger.debug(f"Full traceback: {traceback.format_exc()}")
|
| 275 |
return [TextContent(type="text", text=f"Error: {str(e)}")]
|
| 276 |
|
| 277 |
except Exception as e:
|
client.py
CHANGED
|
@@ -44,6 +44,7 @@ async def get_mcp_session():
|
|
| 44 |
|
| 45 |
# Reuse existing session if available
|
| 46 |
if config.global_mcp_session is not None:
|
|
|
|
| 47 |
return config.global_mcp_session
|
| 48 |
|
| 49 |
try:
|
|
@@ -198,23 +199,27 @@ async def call_agent(user_prompt: str, system_prompt: str = None, files: list =
|
|
| 198 |
try:
|
| 199 |
session = await get_mcp_session()
|
| 200 |
if session is None:
|
| 201 |
-
logger.
|
| 202 |
# Invalidate session to force retry on next call
|
| 203 |
config.global_mcp_session = None
|
| 204 |
config.global_mcp_stdio_ctx = None
|
| 205 |
return ""
|
| 206 |
|
|
|
|
|
|
|
| 207 |
tools = await get_cached_mcp_tools()
|
| 208 |
if not tools:
|
| 209 |
logger.info("MCP tools cache empty, refreshing...")
|
| 210 |
tools = await get_cached_mcp_tools(force_refresh=True)
|
| 211 |
if not tools:
|
| 212 |
-
logger.error("Unable to obtain MCP tool catalog for Gemini calls")
|
| 213 |
# Invalidate session to force retry on next call
|
| 214 |
config.global_mcp_session = None
|
| 215 |
config.global_mcp_stdio_ctx = None
|
| 216 |
return ""
|
| 217 |
|
|
|
|
|
|
|
| 218 |
generate_tool = None
|
| 219 |
for tool in tools:
|
| 220 |
if tool.name == "generate_content" or "generate_content" in tool.name.lower():
|
|
@@ -239,24 +244,51 @@ async def call_agent(user_prompt: str, system_prompt: str = None, files: list =
|
|
| 239 |
if temperature is not None:
|
| 240 |
arguments["temperature"] = temperature
|
| 241 |
|
| 242 |
-
logger.
|
| 243 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
|
| 245 |
if hasattr(result, 'content') and result.content:
|
| 246 |
for item in result.content:
|
| 247 |
if hasattr(item, 'text'):
|
| 248 |
response_text = item.text.strip()
|
| 249 |
if response_text:
|
| 250 |
-
logger.
|
| 251 |
return response_text
|
| 252 |
logger.warning("⚠️ Gemini MCP returned empty or invalid result")
|
| 253 |
return ""
|
| 254 |
except Exception as e:
|
| 255 |
error_type = type(e).__name__
|
| 256 |
error_msg = str(e)
|
| 257 |
-
logger.error(f"Gemini MCP call error: {error_type}: {error_msg}")
|
| 258 |
import traceback
|
| 259 |
-
logger.
|
| 260 |
# Invalidate session on error to force retry
|
| 261 |
config.global_mcp_session = None
|
| 262 |
config.global_mcp_stdio_ctx = None
|
|
|
|
| 44 |
|
| 45 |
# Reuse existing session if available
|
| 46 |
if config.global_mcp_session is not None:
|
| 47 |
+
logger.debug("Reusing existing MCP session")
|
| 48 |
return config.global_mcp_session
|
| 49 |
|
| 50 |
try:
|
|
|
|
| 199 |
try:
|
| 200 |
session = await get_mcp_session()
|
| 201 |
if session is None:
|
| 202 |
+
logger.error("❌ Failed to get MCP session for Gemini call - check GEMINI_API_KEY and agent.py")
|
| 203 |
# Invalidate session to force retry on next call
|
| 204 |
config.global_mcp_session = None
|
| 205 |
config.global_mcp_stdio_ctx = None
|
| 206 |
return ""
|
| 207 |
|
| 208 |
+
logger.debug(f"MCP session obtained: {type(session).__name__}")
|
| 209 |
+
|
| 210 |
tools = await get_cached_mcp_tools()
|
| 211 |
if not tools:
|
| 212 |
logger.info("MCP tools cache empty, refreshing...")
|
| 213 |
tools = await get_cached_mcp_tools(force_refresh=True)
|
| 214 |
if not tools:
|
| 215 |
+
logger.error("❌ Unable to obtain MCP tool catalog for Gemini calls")
|
| 216 |
# Invalidate session to force retry on next call
|
| 217 |
config.global_mcp_session = None
|
| 218 |
config.global_mcp_stdio_ctx = None
|
| 219 |
return ""
|
| 220 |
|
| 221 |
+
logger.debug(f"Found {len(tools)} MCP tools available")
|
| 222 |
+
|
| 223 |
generate_tool = None
|
| 224 |
for tool in tools:
|
| 225 |
if tool.name == "generate_content" or "generate_content" in tool.name.lower():
|
|
|
|
| 244 |
if temperature is not None:
|
| 245 |
arguments["temperature"] = temperature
|
| 246 |
|
| 247 |
+
logger.info(f"🔵 Calling MCP tool '{generate_tool.name}' with model={model or 'default'}, temperature={temperature}")
|
| 248 |
+
logger.debug(f"MCP tool arguments keys: {list(arguments.keys())}")
|
| 249 |
+
logger.debug(f"User prompt length: {len(user_prompt)} chars")
|
| 250 |
+
|
| 251 |
+
# Add timeout to prevent hanging (max 20s to stay under 120s function limit)
|
| 252 |
+
# Also add a shorter timeout for the initial connection check
|
| 253 |
+
try:
|
| 254 |
+
logger.debug("Starting MCP tool call with 20s timeout...")
|
| 255 |
+
result = await asyncio.wait_for(
|
| 256 |
+
session.call_tool(generate_tool.name, arguments=arguments),
|
| 257 |
+
timeout=20.0
|
| 258 |
+
)
|
| 259 |
+
logger.info(f"✅ MCP tool call completed successfully")
|
| 260 |
+
except asyncio.TimeoutError:
|
| 261 |
+
logger.error(f"❌ MCP tool call timed out after 20s - this exceeds the function time limit")
|
| 262 |
+
logger.error(f" This suggests the MCP server (agent.py) is not responding or the Gemini API call is hanging")
|
| 263 |
+
logger.error(f" Check if agent.py process is still running and responsive")
|
| 264 |
+
# Invalidate session on timeout to force retry
|
| 265 |
+
config.global_mcp_session = None
|
| 266 |
+
config.global_mcp_stdio_ctx = None
|
| 267 |
+
return ""
|
| 268 |
+
except Exception as call_error:
|
| 269 |
+
logger.error(f"❌ MCP tool call failed with exception: {type(call_error).__name__}: {call_error}")
|
| 270 |
+
import traceback
|
| 271 |
+
logger.error(f" Traceback: {traceback.format_exc()}")
|
| 272 |
+
# Invalidate session on error to force retry
|
| 273 |
+
config.global_mcp_session = None
|
| 274 |
+
config.global_mcp_stdio_ctx = None
|
| 275 |
+
raise # Re-raise to be caught by outer exception handler
|
| 276 |
|
| 277 |
if hasattr(result, 'content') and result.content:
|
| 278 |
for item in result.content:
|
| 279 |
if hasattr(item, 'text'):
|
| 280 |
response_text = item.text.strip()
|
| 281 |
if response_text:
|
| 282 |
+
logger.info(f"✅ Gemini MCP returned {len(response_text)} chars")
|
| 283 |
return response_text
|
| 284 |
logger.warning("⚠️ Gemini MCP returned empty or invalid result")
|
| 285 |
return ""
|
| 286 |
except Exception as e:
|
| 287 |
error_type = type(e).__name__
|
| 288 |
error_msg = str(e)
|
| 289 |
+
logger.error(f"❌ Gemini MCP call error: {error_type}: {error_msg}")
|
| 290 |
import traceback
|
| 291 |
+
logger.error(f"Full traceback: {traceback.format_exc()}")
|
| 292 |
# Invalidate session on error to force retry
|
| 293 |
config.global_mcp_session = None
|
| 294 |
config.global_mcp_stdio_ctx = None
|