massgen 0.1.4__py3-none-any.whl → 0.1.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of massgen might be problematic. Click here for more details.
- massgen/__init__.py +1 -1
- massgen/backend/base_with_custom_tool_and_mcp.py +453 -23
- massgen/backend/capabilities.py +39 -0
- massgen/backend/chat_completions.py +111 -197
- massgen/backend/claude.py +210 -181
- massgen/backend/gemini.py +1015 -1559
- massgen/backend/grok.py +3 -2
- massgen/backend/response.py +160 -220
- massgen/chat_agent.py +340 -20
- massgen/cli.py +399 -25
- massgen/config_builder.py +20 -54
- massgen/config_validator.py +931 -0
- massgen/configs/README.md +95 -10
- massgen/configs/memory/gpt5mini_gemini_baseline_research_to_implementation.yaml +94 -0
- massgen/configs/memory/gpt5mini_gemini_context_window_management.yaml +187 -0
- massgen/configs/memory/gpt5mini_gemini_research_to_implementation.yaml +127 -0
- massgen/configs/memory/gpt5mini_high_reasoning_gemini.yaml +107 -0
- massgen/configs/memory/single_agent_compression_test.yaml +64 -0
- massgen/configs/tools/custom_tools/claude_code_custom_tool_with_mcp_example.yaml +1 -0
- massgen/configs/tools/custom_tools/claude_custom_tool_example_no_path.yaml +1 -1
- massgen/configs/tools/custom_tools/claude_custom_tool_with_mcp_example.yaml +1 -0
- massgen/configs/tools/custom_tools/computer_use_browser_example.yaml +1 -1
- massgen/configs/tools/custom_tools/computer_use_docker_example.yaml +1 -1
- massgen/configs/tools/custom_tools/gemini_custom_tool_with_mcp_example.yaml +1 -0
- massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_mcp_example.yaml +1 -0
- massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_mcp_example.yaml +1 -0
- massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_mcp_example.yaml +1 -0
- massgen/configs/tools/custom_tools/interop/ag2_and_langgraph_lesson_planner.yaml +65 -0
- massgen/configs/tools/custom_tools/interop/ag2_and_openai_assistant_lesson_planner.yaml +65 -0
- massgen/configs/tools/custom_tools/interop/ag2_lesson_planner_example.yaml +48 -0
- massgen/configs/tools/custom_tools/interop/agentscope_lesson_planner_example.yaml +48 -0
- massgen/configs/tools/custom_tools/interop/langgraph_lesson_planner_example.yaml +49 -0
- massgen/configs/tools/custom_tools/interop/openai_assistant_lesson_planner_example.yaml +50 -0
- massgen/configs/tools/custom_tools/interop/smolagent_lesson_planner_example.yaml +49 -0
- massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_mcp_example.yaml +1 -0
- massgen/configs/tools/custom_tools/two_models_with_tools_example.yaml +44 -0
- massgen/formatter/_gemini_formatter.py +61 -15
- massgen/memory/README.md +277 -0
- massgen/memory/__init__.py +26 -0
- massgen/memory/_base.py +193 -0
- massgen/memory/_compression.py +237 -0
- massgen/memory/_context_monitor.py +211 -0
- massgen/memory/_conversation.py +255 -0
- massgen/memory/_fact_extraction_prompts.py +333 -0
- massgen/memory/_mem0_adapters.py +257 -0
- massgen/memory/_persistent.py +687 -0
- massgen/memory/docker-compose.qdrant.yml +36 -0
- massgen/memory/docs/DESIGN.md +388 -0
- massgen/memory/docs/QUICKSTART.md +409 -0
- massgen/memory/docs/SUMMARY.md +319 -0
- massgen/memory/docs/agent_use_memory.md +408 -0
- massgen/memory/docs/orchestrator_use_memory.md +586 -0
- massgen/memory/examples.py +237 -0
- massgen/orchestrator.py +207 -7
- massgen/tests/memory/test_agent_compression.py +174 -0
- massgen/tests/memory/test_context_window_management.py +286 -0
- massgen/tests/memory/test_force_compression.py +154 -0
- massgen/tests/memory/test_simple_compression.py +147 -0
- massgen/tests/test_ag2_lesson_planner.py +223 -0
- massgen/tests/test_agent_memory.py +534 -0
- massgen/tests/test_config_validator.py +1156 -0
- massgen/tests/test_conversation_memory.py +382 -0
- massgen/tests/test_langgraph_lesson_planner.py +223 -0
- massgen/tests/test_orchestrator_memory.py +620 -0
- massgen/tests/test_persistent_memory.py +435 -0
- massgen/token_manager/token_manager.py +6 -0
- massgen/tool/__init__.py +2 -9
- massgen/tool/_decorators.py +52 -0
- massgen/tool/_extraframework_agents/ag2_lesson_planner_tool.py +251 -0
- massgen/tool/_extraframework_agents/agentscope_lesson_planner_tool.py +303 -0
- massgen/tool/_extraframework_agents/langgraph_lesson_planner_tool.py +275 -0
- massgen/tool/_extraframework_agents/openai_assistant_lesson_planner_tool.py +247 -0
- massgen/tool/_extraframework_agents/smolagent_lesson_planner_tool.py +180 -0
- massgen/tool/_manager.py +102 -16
- massgen/tool/_registered_tool.py +3 -0
- massgen/tool/_result.py +3 -0
- {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/METADATA +138 -77
- {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/RECORD +82 -37
- massgen/backend/gemini_mcp_manager.py +0 -545
- massgen/backend/gemini_trackers.py +0 -344
- {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/WHEEL +0 -0
- {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/entry_points.txt +0 -0
- {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/licenses/LICENSE +0 -0
- {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/top_level.txt +0 -0
massgen/backend/gemini.py
CHANGED
|
@@ -19,10 +19,10 @@ TECHNICAL SOLUTION:
|
|
|
19
19
|
- Maintains compatibility with existing MassGen workflow
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
|
+
import asyncio
|
|
22
23
|
import json
|
|
23
24
|
import logging
|
|
24
25
|
import os
|
|
25
|
-
import time
|
|
26
26
|
from typing import Any, AsyncGenerator, Dict, List, Optional
|
|
27
27
|
|
|
28
28
|
from ..api_params_handler._gemini_api_params_handler import GeminiAPIParamsHandler
|
|
@@ -35,10 +35,12 @@ from ..logger_config import (
|
|
|
35
35
|
logger,
|
|
36
36
|
)
|
|
37
37
|
from .base import FilesystemSupport, StreamChunk
|
|
38
|
-
from .base_with_custom_tool_and_mcp import
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
from .base_with_custom_tool_and_mcp import (
|
|
39
|
+
CustomToolAndMCPBackend,
|
|
40
|
+
CustomToolChunk,
|
|
41
|
+
ToolExecutionConfig,
|
|
42
|
+
)
|
|
43
|
+
from .gemini_utils import CoordinationResponse, PostEvaluationResponse
|
|
42
44
|
|
|
43
45
|
|
|
44
46
|
# Suppress Gemini SDK logger warning about non-text parts in response
|
|
@@ -53,51 +55,31 @@ class NoFunctionCallWarning(logging.Filter):
|
|
|
53
55
|
|
|
54
56
|
logging.getLogger("google_genai.types").addFilter(NoFunctionCallWarning())
|
|
55
57
|
|
|
56
|
-
try:
|
|
57
|
-
from pydantic import BaseModel, Field
|
|
58
|
-
except ImportError:
|
|
59
|
-
BaseModel = None
|
|
60
|
-
Field = None
|
|
61
|
-
|
|
62
58
|
# MCP integration imports
|
|
63
59
|
try:
|
|
64
60
|
from ..mcp_tools import (
|
|
65
|
-
MCPClient,
|
|
66
|
-
MCPConfigurationError,
|
|
67
|
-
MCPConfigValidator,
|
|
68
61
|
MCPConnectionError,
|
|
69
62
|
MCPError,
|
|
70
63
|
MCPServerError,
|
|
71
64
|
MCPTimeoutError,
|
|
72
|
-
MCPValidationError,
|
|
73
65
|
)
|
|
74
66
|
except ImportError: # MCP not installed or import failed within mcp_tools
|
|
75
|
-
MCPClient = None # type: ignore[assignment]
|
|
76
67
|
MCPError = ImportError # type: ignore[assignment]
|
|
77
68
|
MCPConnectionError = ImportError # type: ignore[assignment]
|
|
78
|
-
MCPConfigValidator = None # type: ignore[assignment]
|
|
79
|
-
MCPConfigurationError = ImportError # type: ignore[assignment]
|
|
80
|
-
MCPValidationError = ImportError # type: ignore[assignment]
|
|
81
69
|
MCPTimeoutError = ImportError # type: ignore[assignment]
|
|
82
70
|
MCPServerError = ImportError # type: ignore[assignment]
|
|
83
71
|
|
|
84
72
|
# Import MCP backend utilities
|
|
85
73
|
try:
|
|
86
74
|
from ..mcp_tools.backend_utils import (
|
|
87
|
-
MCPCircuitBreakerManager,
|
|
88
|
-
MCPConfigHelper,
|
|
89
75
|
MCPErrorHandler,
|
|
90
|
-
MCPExecutionManager,
|
|
91
76
|
MCPMessageManager,
|
|
92
|
-
|
|
77
|
+
MCPResourceManager,
|
|
93
78
|
)
|
|
94
79
|
except ImportError:
|
|
95
80
|
MCPErrorHandler = None # type: ignore[assignment]
|
|
96
|
-
MCPSetupManager = None # type: ignore[assignment]
|
|
97
81
|
MCPMessageManager = None # type: ignore[assignment]
|
|
98
|
-
|
|
99
|
-
MCPExecutionManager = None # type: ignore[assignment]
|
|
100
|
-
MCPConfigHelper = None # type: ignore[assignment]
|
|
82
|
+
MCPResourceManager = None # type: ignore[assignment]
|
|
101
83
|
|
|
102
84
|
|
|
103
85
|
def format_tool_response_as_json(response_text: str) -> str:
|
|
@@ -145,11 +127,8 @@ class GeminiBackend(CustomToolAndMCPBackend):
|
|
|
145
127
|
self._mcp_tool_successes = 0
|
|
146
128
|
self._mcp_connection_retries = 0
|
|
147
129
|
|
|
148
|
-
#
|
|
149
|
-
self.
|
|
150
|
-
|
|
151
|
-
# Initialize Gemini MCP manager after all attributes are ready
|
|
152
|
-
self.mcp_manager = GeminiMCPManager(self)
|
|
130
|
+
# Active tool result capture during manual tool execution
|
|
131
|
+
self._active_tool_result_store: Optional[Dict[str, str]] = None
|
|
153
132
|
|
|
154
133
|
def _setup_permission_hooks(self):
|
|
155
134
|
"""Override base class - Gemini uses session-based permissions, not function hooks."""
|
|
@@ -166,10 +145,10 @@ class GeminiBackend(CustomToolAndMCPBackend):
|
|
|
166
145
|
|
|
167
146
|
async def _setup_mcp_tools(self) -> None:
|
|
168
147
|
"""
|
|
169
|
-
Override parent class -
|
|
148
|
+
Override parent class - Use base class MCP setup for manual execution pattern.
|
|
170
149
|
This method is called by the parent class's __aenter__() context manager.
|
|
171
150
|
"""
|
|
172
|
-
await
|
|
151
|
+
await super()._setup_mcp_tools()
|
|
173
152
|
|
|
174
153
|
def supports_upload_files(self) -> bool:
|
|
175
154
|
"""
|
|
@@ -178,13 +157,46 @@ class GeminiBackend(CustomToolAndMCPBackend):
|
|
|
178
157
|
"""
|
|
179
158
|
return False
|
|
180
159
|
|
|
160
|
+
def _create_client(self, **kwargs):
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
async def _stream_with_custom_and_mcp_tools(
|
|
164
|
+
self,
|
|
165
|
+
current_messages: List[Dict[str, Any]],
|
|
166
|
+
tools: List[Dict[str, Any]],
|
|
167
|
+
client,
|
|
168
|
+
**kwargs,
|
|
169
|
+
) -> AsyncGenerator[StreamChunk, None]:
|
|
170
|
+
yield StreamChunk(type="error", error="Not implemented")
|
|
171
|
+
|
|
181
172
|
async def stream_with_tools(self, messages: List[Dict[str, Any]], tools: List[Dict[str, Any]], **kwargs) -> AsyncGenerator[StreamChunk, None]:
|
|
182
|
-
"""Stream response using Gemini API with
|
|
173
|
+
"""Stream response using Gemini API with manual MCP execution pattern.
|
|
174
|
+
|
|
175
|
+
Tool Execution Behavior:
|
|
176
|
+
- Custom tools: Always executed (not blocked by planning mode or circuit breaker)
|
|
177
|
+
- MCP tools: Blocked by planning mode during coordination, blocked by circuit breaker when servers fail
|
|
178
|
+
- Provider tools (vote/new_answer): Emitted as StreamChunks but not executed (handled by orchestrator)
|
|
179
|
+
"""
|
|
183
180
|
# Use instance agent_id (from __init__) or get from kwargs if not set
|
|
184
181
|
agent_id = self.agent_id or kwargs.get("agent_id", None)
|
|
185
182
|
client = None
|
|
186
183
|
stream = None
|
|
187
184
|
|
|
185
|
+
# Build execution context for tools (generic, not tool-specific)
|
|
186
|
+
# This is required for custom tool execution
|
|
187
|
+
from .base_with_custom_tool_and_mcp import ExecutionContext
|
|
188
|
+
|
|
189
|
+
self._execution_context = ExecutionContext(
|
|
190
|
+
messages=messages,
|
|
191
|
+
agent_system_message=kwargs.get("system_message", None),
|
|
192
|
+
agent_id=self.agent_id,
|
|
193
|
+
backend_name="gemini",
|
|
194
|
+
current_stage=self.coordination_stage,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Track whether MCP tools were actually used in this turn
|
|
198
|
+
mcp_used = False
|
|
199
|
+
|
|
188
200
|
log_backend_activity(
|
|
189
201
|
"gemini",
|
|
190
202
|
"Starting stream_with_tools",
|
|
@@ -192,7 +204,7 @@ class GeminiBackend(CustomToolAndMCPBackend):
|
|
|
192
204
|
agent_id=agent_id,
|
|
193
205
|
)
|
|
194
206
|
|
|
195
|
-
#
|
|
207
|
+
# Trim message history for MCP if needed
|
|
196
208
|
if self.mcp_servers and MCPMessageManager is not None and hasattr(self, "_max_mcp_message_history") and self._max_mcp_message_history > 0:
|
|
197
209
|
original_count = len(messages)
|
|
198
210
|
messages = MCPMessageManager.trim_message_history(messages, self._max_mcp_message_history)
|
|
@@ -210,29 +222,26 @@ class GeminiBackend(CustomToolAndMCPBackend):
|
|
|
210
222
|
|
|
211
223
|
try:
|
|
212
224
|
from google import genai
|
|
225
|
+
from google.genai import types
|
|
213
226
|
|
|
214
|
-
# Setup MCP
|
|
227
|
+
# Setup MCP using base class if not already initialized
|
|
215
228
|
if not self._mcp_initialized and self.mcp_servers:
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
229
|
+
await self._setup_mcp_tools()
|
|
230
|
+
if self._mcp_initialized:
|
|
231
|
+
yield StreamChunk(
|
|
232
|
+
type="mcp_status",
|
|
233
|
+
status="mcp_initialized",
|
|
234
|
+
content="✅ [MCP] Tools initialized",
|
|
235
|
+
source="mcp_tools",
|
|
236
|
+
)
|
|
221
237
|
|
|
222
|
-
# Merge constructor config with stream kwargs
|
|
238
|
+
# Merge constructor config with stream kwargs
|
|
223
239
|
all_params = {**self.config, **kwargs}
|
|
224
240
|
|
|
225
|
-
#
|
|
226
|
-
all_params.get("enable_web_search", False)
|
|
227
|
-
enable_code_execution = all_params.get("enable_code_execution", False)
|
|
228
|
-
|
|
229
|
-
# Always use SDK MCP sessions when mcp_servers are configured
|
|
230
|
-
using_sdk_mcp = bool(self.mcp_servers)
|
|
231
|
-
|
|
232
|
-
# Custom tool handling - add custom tools if any
|
|
241
|
+
# Detect custom tools
|
|
233
242
|
using_custom_tools = bool(self.custom_tool_manager and len(self._custom_tool_names) > 0)
|
|
234
243
|
|
|
235
|
-
#
|
|
244
|
+
# Detect coordination mode
|
|
236
245
|
is_coordination = self.formatter.has_coordination_tools(tools)
|
|
237
246
|
is_post_evaluation = self.formatter.has_post_evaluation_tools(tools)
|
|
238
247
|
|
|
@@ -258,1355 +267,502 @@ class GeminiBackend(CustomToolAndMCPBackend):
|
|
|
258
267
|
# For post-evaluation, modify prompt to use structured output
|
|
259
268
|
full_content = self.formatter.build_post_evaluation_prompt(full_content)
|
|
260
269
|
|
|
261
|
-
#
|
|
270
|
+
# Create Gemini client
|
|
262
271
|
client = genai.Client(api_key=self.api_key)
|
|
263
272
|
|
|
264
|
-
# Setup builtin tools via API params handler
|
|
273
|
+
# Setup builtin tools via API params handler
|
|
265
274
|
builtin_tools = self.api_params_handler.get_provider_tools(all_params)
|
|
266
|
-
|
|
275
|
+
|
|
276
|
+
# Build config via API params handler
|
|
267
277
|
config = await self.api_params_handler.build_api_params(messages, tools, all_params)
|
|
268
|
-
|
|
278
|
+
|
|
279
|
+
# Extract model name
|
|
269
280
|
model_name = all_params.get("model")
|
|
270
281
|
|
|
271
|
-
#
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if using_sdk_mcp and self.mcp_servers:
|
|
276
|
-
if not self._mcp_client or not getattr(self._mcp_client, "is_connected", lambda: False)():
|
|
277
|
-
mcp_connected, status_chunks = await self.mcp_manager.setup_mcp_sessions_with_retry(agent_id, max_retries=5)
|
|
278
|
-
async for chunk in status_chunks:
|
|
279
|
-
yield chunk
|
|
280
|
-
if not mcp_connected:
|
|
281
|
-
using_sdk_mcp = False
|
|
282
|
-
self._mcp_client = None
|
|
283
|
-
|
|
284
|
-
if not using_sdk_mcp and not using_custom_tools:
|
|
285
|
-
all_tools.extend(builtin_tools)
|
|
286
|
-
if all_tools:
|
|
287
|
-
config["tools"] = all_tools
|
|
288
|
-
|
|
289
|
-
# For coordination requests, use JSON response format (may conflict with tools/sessions)
|
|
290
|
-
if is_coordination:
|
|
291
|
-
# Only request JSON schema when no tools are present
|
|
292
|
-
if (not using_sdk_mcp) and (not using_custom_tools) and (not all_tools):
|
|
293
|
-
config["response_mime_type"] = "application/json"
|
|
294
|
-
config["response_schema"] = CoordinationResponse.model_json_schema()
|
|
295
|
-
else:
|
|
296
|
-
# Tools or sessions are present; fallback to text parsing
|
|
297
|
-
pass
|
|
298
|
-
elif is_post_evaluation:
|
|
299
|
-
# For post-evaluation, use JSON response format for structured decisions
|
|
300
|
-
from .gemini_utils import PostEvaluationResponse
|
|
282
|
+
# ====================================================================
|
|
283
|
+
# Tool Registration Phase: Convert and register tools for manual execution
|
|
284
|
+
# ====================================================================
|
|
285
|
+
tools_to_apply = []
|
|
301
286
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
config["response_schema"] = PostEvaluationResponse.model_json_schema()
|
|
305
|
-
else:
|
|
306
|
-
# Tools or sessions are present; fallback to text parsing
|
|
307
|
-
pass
|
|
308
|
-
# Log messages being sent after builtin_tools is defined
|
|
309
|
-
log_backend_agent_message(
|
|
310
|
-
agent_id or "default",
|
|
311
|
-
"SEND",
|
|
312
|
-
{
|
|
313
|
-
"content": full_content,
|
|
314
|
-
"builtin_tools": len(builtin_tools) if builtin_tools else 0,
|
|
315
|
-
},
|
|
316
|
-
backend_name="gemini",
|
|
317
|
-
)
|
|
318
|
-
|
|
319
|
-
# Use streaming for real-time response
|
|
320
|
-
full_content_text = ""
|
|
321
|
-
final_response = None
|
|
322
|
-
# Buffer the last response chunk that contains candidate metadata so we can
|
|
323
|
-
# inspect builtin tool usage (grounding/code execution) after streaming
|
|
324
|
-
last_response_with_candidates = None
|
|
325
|
-
if (using_sdk_mcp and self.mcp_servers) or using_custom_tools:
|
|
326
|
-
# Process MCP and/or custom tools
|
|
287
|
+
# Add custom tools if available
|
|
288
|
+
if using_custom_tools:
|
|
327
289
|
try:
|
|
328
|
-
#
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
# Try to initialize MCP sessions
|
|
337
|
-
if using_sdk_mcp and self.mcp_servers:
|
|
338
|
-
try:
|
|
339
|
-
if not self._mcp_client:
|
|
340
|
-
raise RuntimeError("MCP client not initialized")
|
|
341
|
-
mcp_sessions = self.mcp_manager.get_active_mcp_sessions(
|
|
342
|
-
convert_to_permission_sessions=bool(self.filesystem_manager),
|
|
343
|
-
)
|
|
344
|
-
if not mcp_sessions:
|
|
345
|
-
# If no MCP sessions, record error but don't interrupt (may still have custom tools)
|
|
346
|
-
mcp_error = RuntimeError("No active MCP sessions available")
|
|
347
|
-
logger.warning(f"[Gemini] MCP sessions unavailable: {mcp_error}")
|
|
348
|
-
except Exception as e:
|
|
349
|
-
mcp_error = e
|
|
350
|
-
logger.warning(f"[Gemini] Failed to initialize MCP sessions: {e}")
|
|
351
|
-
|
|
352
|
-
# Try to initialize custom tools
|
|
353
|
-
if using_custom_tools:
|
|
354
|
-
try:
|
|
355
|
-
# Get custom tools schemas (in OpenAI format)
|
|
356
|
-
custom_tools_schemas = self._get_custom_tools_schemas()
|
|
357
|
-
if custom_tools_schemas:
|
|
358
|
-
# Convert to Gemini SDK format using formatter
|
|
359
|
-
# formatter handles: OpenAI format -> Gemini dict -> FunctionDeclaration objects
|
|
360
|
-
custom_tools_functions = self.formatter.format_custom_tools(
|
|
361
|
-
custom_tools_schemas,
|
|
362
|
-
return_sdk_objects=True,
|
|
363
|
-
)
|
|
364
|
-
|
|
365
|
-
if custom_tools_functions:
|
|
366
|
-
logger.debug(
|
|
367
|
-
f"[Gemini] Loaded {len(custom_tools_functions)} custom tools " f"as FunctionDeclarations",
|
|
368
|
-
)
|
|
369
|
-
else:
|
|
370
|
-
custom_tools_error = RuntimeError("Custom tools conversion failed")
|
|
371
|
-
logger.warning(f"[Gemini] Custom tools unavailable: {custom_tools_error}")
|
|
372
|
-
else:
|
|
373
|
-
custom_tools_error = RuntimeError("No custom tools available")
|
|
374
|
-
logger.warning(f"[Gemini] Custom tools unavailable: {custom_tools_error}")
|
|
375
|
-
except Exception as e:
|
|
376
|
-
custom_tools_error = e
|
|
377
|
-
logger.warning(f"[Gemini] Failed to initialize custom tools: {e}")
|
|
378
|
-
|
|
379
|
-
# Check if at least one tool system is available
|
|
380
|
-
has_mcp = bool(mcp_sessions and not mcp_error)
|
|
381
|
-
has_custom_tools = bool(custom_tools_functions and not custom_tools_error)
|
|
382
|
-
|
|
383
|
-
if not has_mcp and not has_custom_tools:
|
|
384
|
-
# Both failed, raise error to enter fallback
|
|
385
|
-
raise RuntimeError(
|
|
386
|
-
f"Both MCP and custom tools unavailable. " f"MCP error: {mcp_error}. Custom tools error: {custom_tools_error}",
|
|
290
|
+
# Get custom tools schemas (in OpenAI format)
|
|
291
|
+
custom_tools_schemas = self._get_custom_tools_schemas()
|
|
292
|
+
if custom_tools_schemas:
|
|
293
|
+
# Convert to Gemini SDK format using formatter
|
|
294
|
+
custom_tools_functions = self.formatter.format_custom_tools(
|
|
295
|
+
custom_tools_schemas,
|
|
296
|
+
return_sdk_objects=True,
|
|
387
297
|
)
|
|
388
298
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
# ====================================================================
|
|
392
|
-
session_config = dict(config)
|
|
393
|
-
|
|
394
|
-
# Collect all available tool information
|
|
395
|
-
available_mcp_tools = []
|
|
396
|
-
if has_mcp and self._mcp_client:
|
|
397
|
-
available_mcp_tools = list(self._mcp_client.tools.keys())
|
|
398
|
-
|
|
399
|
-
available_custom_tool_names = list(self._custom_tool_names) if has_custom_tools else []
|
|
400
|
-
|
|
401
|
-
# Apply tools to config
|
|
402
|
-
tools_to_apply = []
|
|
403
|
-
sessions_applied = False
|
|
404
|
-
custom_tools_applied = False
|
|
405
|
-
|
|
406
|
-
# Add MCP sessions (if available and not blocked by planning mode)
|
|
407
|
-
if has_mcp:
|
|
408
|
-
if not self.mcp_manager.should_block_mcp_tools_in_planning_mode(
|
|
409
|
-
self.is_planning_mode_enabled(),
|
|
410
|
-
available_mcp_tools,
|
|
411
|
-
):
|
|
412
|
-
logger.debug(
|
|
413
|
-
f"[Gemini] Passing {len(mcp_sessions)} MCP sessions to SDK: " f"{[type(s).__name__ for s in mcp_sessions]}",
|
|
414
|
-
)
|
|
415
|
-
tools_to_apply.extend(mcp_sessions)
|
|
416
|
-
sessions_applied = True
|
|
417
|
-
|
|
418
|
-
if self.is_planning_mode_enabled():
|
|
419
|
-
blocked_tools = self.get_planning_mode_blocked_tools()
|
|
420
|
-
|
|
421
|
-
if not blocked_tools:
|
|
422
|
-
# Empty set means block ALL MCP tools (backward compatible)
|
|
423
|
-
logger.info("[Gemini] Planning mode enabled - blocking ALL MCP tools during coordination")
|
|
424
|
-
# Don't set tools at all - this prevents any MCP tool execution
|
|
425
|
-
log_backend_activity(
|
|
426
|
-
"gemini",
|
|
427
|
-
"All MCP tools blocked in planning mode",
|
|
428
|
-
{
|
|
429
|
-
"blocked_tools": len(available_mcp_tools),
|
|
430
|
-
"session_count": len(mcp_sessions),
|
|
431
|
-
},
|
|
432
|
-
agent_id=agent_id,
|
|
433
|
-
)
|
|
434
|
-
else:
|
|
435
|
-
# Selective blocking - allow non-blocked tools to be called
|
|
436
|
-
# The execution layer (_execute_mcp_function_with_retry) will enforce blocking
|
|
437
|
-
# but we still register all tools so non-blocked ones can be used
|
|
438
|
-
logger.info(f"[Gemini] Planning mode enabled - allowing non-blocked MCP tools, blocking {len(blocked_tools)} specific tools")
|
|
439
|
-
|
|
440
|
-
# Pass all sessions - the backend's is_mcp_tool_blocked() will handle selective blocking
|
|
441
|
-
session_config["tools"] = mcp_sessions
|
|
442
|
-
|
|
443
|
-
log_backend_activity(
|
|
444
|
-
"gemini",
|
|
445
|
-
"Selective MCP tools blocked in planning mode",
|
|
446
|
-
{
|
|
447
|
-
"total_tools": len(available_mcp_tools),
|
|
448
|
-
"blocked_tools": len(blocked_tools),
|
|
449
|
-
"allowed_tools": len(available_mcp_tools) - len(blocked_tools),
|
|
450
|
-
},
|
|
451
|
-
agent_id=agent_id,
|
|
452
|
-
)
|
|
453
|
-
|
|
454
|
-
# Add custom tools (if available)
|
|
455
|
-
if has_custom_tools:
|
|
456
|
-
# Wrap FunctionDeclarations in a Tool object for Gemini SDK
|
|
457
|
-
try:
|
|
458
|
-
from google.genai import types
|
|
459
|
-
|
|
460
|
-
# Create a Tool object containing all custom function declarations
|
|
299
|
+
if custom_tools_functions:
|
|
300
|
+
# Wrap FunctionDeclarations in a Tool object for Gemini SDK
|
|
461
301
|
custom_tool = types.Tool(function_declarations=custom_tools_functions)
|
|
462
|
-
|
|
463
|
-
logger.debug(
|
|
464
|
-
f"[Gemini] Wrapped {len(custom_tools_functions)} custom tools " f"in Tool object for SDK",
|
|
465
|
-
)
|
|
466
302
|
tools_to_apply.append(custom_tool)
|
|
467
|
-
custom_tools_applied = True
|
|
468
|
-
except Exception as e:
|
|
469
|
-
logger.error(f"[Gemini] Failed to wrap custom tools in Tool object: {e}")
|
|
470
|
-
custom_tools_error = e
|
|
471
|
-
|
|
472
|
-
# Apply tool configuration
|
|
473
|
-
if tools_to_apply:
|
|
474
|
-
session_config["tools"] = tools_to_apply
|
|
475
303
|
|
|
476
|
-
|
|
477
|
-
# MassGen uses declarative mode: SDK should return function call requests
|
|
478
|
-
# instead of automatically executing them
|
|
479
|
-
if has_custom_tools:
|
|
480
|
-
from google.genai import types
|
|
304
|
+
logger.debug(f"[Gemini] Registered {len(custom_tools_functions)} custom tools for manual execution")
|
|
481
305
|
|
|
482
|
-
|
|
483
|
-
|
|
306
|
+
yield StreamChunk(
|
|
307
|
+
type="custom_tool_status",
|
|
308
|
+
status="custom_tools_registered",
|
|
309
|
+
content=f"🔧 [Custom Tools] Registered {len(custom_tools_functions)} tools",
|
|
310
|
+
source="custom_tools",
|
|
484
311
|
)
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
# ====================================================================
|
|
488
|
-
# Logging and status output
|
|
489
|
-
# ====================================================================
|
|
490
|
-
if sessions_applied:
|
|
491
|
-
# Track MCP tool usage attempt
|
|
492
|
-
self._mcp_tool_calls_count += 1
|
|
493
|
-
|
|
494
|
-
log_backend_activity(
|
|
495
|
-
"gemini",
|
|
496
|
-
"MCP tool call initiated",
|
|
497
|
-
{
|
|
498
|
-
"call_number": self._mcp_tool_calls_count,
|
|
499
|
-
"session_count": len(mcp_sessions),
|
|
500
|
-
"available_tools": available_mcp_tools[:],
|
|
501
|
-
"total_tools": len(available_mcp_tools),
|
|
502
|
-
},
|
|
503
|
-
agent_id=agent_id,
|
|
504
|
-
)
|
|
312
|
+
except Exception as e:
|
|
313
|
+
logger.warning(f"[Gemini] Failed to register custom tools: {e}")
|
|
505
314
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
"call_number": self._mcp_tool_calls_count,
|
|
512
|
-
"available_tools": available_mcp_tools,
|
|
513
|
-
},
|
|
514
|
-
backend_name="gemini",
|
|
515
|
-
)
|
|
315
|
+
# Add MCP tools if available (unless blocked by planning mode)
|
|
316
|
+
if self._mcp_initialized and self._mcp_functions:
|
|
317
|
+
# Check planning mode
|
|
318
|
+
if self.is_planning_mode_enabled():
|
|
319
|
+
blocked_tools = self.get_planning_mode_blocked_tools()
|
|
516
320
|
|
|
517
|
-
|
|
321
|
+
if not blocked_tools:
|
|
322
|
+
# Empty set means block ALL MCP tools (backward compatible)
|
|
323
|
+
logger.info("[Gemini] Planning mode enabled - blocking ALL MCP tools during coordination")
|
|
518
324
|
yield StreamChunk(
|
|
519
325
|
type="mcp_status",
|
|
520
|
-
status="
|
|
521
|
-
content=
|
|
522
|
-
source="
|
|
326
|
+
status="planning_mode_blocked",
|
|
327
|
+
content="🚫 [MCP] Planning mode active - all MCP tools blocked during coordination",
|
|
328
|
+
source="planning_mode",
|
|
523
329
|
)
|
|
524
330
|
|
|
525
|
-
|
|
526
|
-
#
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
"tool_count": len(custom_tools_functions),
|
|
532
|
-
"available_tools": available_custom_tool_names,
|
|
533
|
-
},
|
|
534
|
-
agent_id=agent_id,
|
|
535
|
-
)
|
|
331
|
+
else:
|
|
332
|
+
# Selective blocking - register all MCP tools, execution layer will block specific ones
|
|
333
|
+
logger.info(f"[Gemini] Planning mode enabled - registering all MCP tools, will block {len(blocked_tools)} at execution")
|
|
334
|
+
try:
|
|
335
|
+
# Convert MCP tools using formatter
|
|
336
|
+
mcp_tools_functions = self.formatter.format_mcp_tools(self._mcp_functions, return_sdk_objects=True)
|
|
536
337
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
status="custom_tools_initiated",
|
|
542
|
-
content=f"Custom tools initiated ({len(custom_tools_functions)} tools available): {tools_preview}{tools_suffix}",
|
|
543
|
-
source="custom_tools",
|
|
544
|
-
)
|
|
338
|
+
if mcp_tools_functions:
|
|
339
|
+
# Wrap in Tool object
|
|
340
|
+
mcp_tool = types.Tool(function_declarations=mcp_tools_functions)
|
|
341
|
+
tools_to_apply.append(mcp_tool)
|
|
545
342
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
# ====================================================================
|
|
549
|
-
# Use async streaming call with sessions/tools
|
|
550
|
-
stream = await client.aio.models.generate_content_stream(
|
|
551
|
-
model=model_name,
|
|
552
|
-
contents=full_content,
|
|
553
|
-
config=session_config,
|
|
554
|
-
)
|
|
343
|
+
# Mark MCP as used since tools are registered (even with selective blocking)
|
|
344
|
+
mcp_used = True
|
|
555
345
|
|
|
556
|
-
|
|
557
|
-
mcp_tracker = MCPCallTracker()
|
|
558
|
-
mcp_response_tracker = MCPResponseTracker()
|
|
559
|
-
custom_tracker = MCPCallTracker() # Reuse MCPCallTracker for custom tools
|
|
560
|
-
custom_response_tracker = MCPResponseTracker() # Reuse for custom tools
|
|
346
|
+
logger.debug(f"[Gemini] Registered {len(mcp_tools_functions)} MCP tools for selective blocking")
|
|
561
347
|
|
|
562
|
-
|
|
563
|
-
|
|
348
|
+
yield StreamChunk(
|
|
349
|
+
type="mcp_status",
|
|
350
|
+
status="mcp_tools_registered",
|
|
351
|
+
content=f"🔧 [MCP] Registered {len(mcp_tools_functions)} tools (selective blocking enabled)",
|
|
352
|
+
source="mcp_tools",
|
|
353
|
+
)
|
|
354
|
+
except Exception as e:
|
|
355
|
+
logger.warning(f"[Gemini] Failed to register MCP tools: {e}")
|
|
356
|
+
else:
|
|
357
|
+
# No planning mode - register all MCP tools
|
|
358
|
+
try:
|
|
359
|
+
# Convert MCP tools using formatter
|
|
360
|
+
mcp_tools_functions = self.formatter.format_mcp_tools(self._mcp_functions, return_sdk_objects=True)
|
|
361
|
+
|
|
362
|
+
if mcp_tools_functions:
|
|
363
|
+
# Wrap in Tool object
|
|
364
|
+
mcp_tool = types.Tool(function_declarations=mcp_tools_functions)
|
|
365
|
+
tools_to_apply.append(mcp_tool)
|
|
366
|
+
|
|
367
|
+
# Mark MCP as used since tools are registered
|
|
368
|
+
mcp_used = True
|
|
369
|
+
|
|
370
|
+
logger.debug(f"[Gemini] Registered {len(mcp_tools_functions)} MCP tools for manual execution")
|
|
371
|
+
|
|
372
|
+
yield StreamChunk(
|
|
373
|
+
type="mcp_status",
|
|
374
|
+
status="mcp_tools_registered",
|
|
375
|
+
content=f"🔧 [MCP] Registered {len(mcp_tools_functions)} tools",
|
|
376
|
+
source="mcp_tools",
|
|
377
|
+
)
|
|
378
|
+
except Exception as e:
|
|
379
|
+
logger.warning(f"[Gemini] Failed to register MCP tools: {e}")
|
|
380
|
+
|
|
381
|
+
# Apply tools to config
|
|
382
|
+
if tools_to_apply:
|
|
383
|
+
config["tools"] = tools_to_apply
|
|
384
|
+
# Disable automatic function calling for manual execution
|
|
385
|
+
config["automatic_function_calling"] = types.AutomaticFunctionCallingConfig(disable=True)
|
|
386
|
+
logger.debug("[Gemini] Disabled automatic function calling for manual execution")
|
|
387
|
+
else:
|
|
388
|
+
# No custom/MCP tools, add builtin tools if any
|
|
389
|
+
if builtin_tools:
|
|
390
|
+
config["tools"] = builtin_tools
|
|
564
391
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
392
|
+
# For coordination/post-evaluation requests, use JSON response format when no tools present
|
|
393
|
+
if not tools_to_apply and not builtin_tools:
|
|
394
|
+
if is_coordination:
|
|
395
|
+
config["response_mime_type"] = "application/json"
|
|
396
|
+
config["response_schema"] = CoordinationResponse.model_json_schema()
|
|
397
|
+
elif is_post_evaluation:
|
|
398
|
+
config["response_mime_type"] = "application/json"
|
|
399
|
+
config["response_schema"] = PostEvaluationResponse.model_json_schema()
|
|
570
400
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
call_data = self.mcp_extractor.extract_function_call(part.function_call)
|
|
582
|
-
|
|
583
|
-
if call_data:
|
|
584
|
-
tool_name = call_data["name"]
|
|
585
|
-
tool_args = call_data["arguments"]
|
|
586
|
-
|
|
587
|
-
# DEBUG: Log tool matching
|
|
588
|
-
logger.info(f"🔍 [DEBUG] Function call detected: tool_name='{tool_name}'")
|
|
589
|
-
logger.info(f"🔍 [DEBUG] Available MCP tools: {available_mcp_tools}")
|
|
590
|
-
logger.info(f"🔍 [DEBUG] Available custom tools: {list(self._custom_tool_names) if has_custom_tools else []}")
|
|
591
|
-
|
|
592
|
-
# Determine if it's MCP tool or custom tool
|
|
593
|
-
# MCP tools may come from SDK without prefix, so we need to check both:
|
|
594
|
-
# 1. Direct match (tool_name in list)
|
|
595
|
-
# 2. Prefixed match (mcp__server__tool_name in list)
|
|
596
|
-
is_mcp_tool = False
|
|
597
|
-
if has_mcp:
|
|
598
|
-
# Direct match
|
|
599
|
-
if tool_name in available_mcp_tools:
|
|
600
|
-
is_mcp_tool = True
|
|
601
|
-
else:
|
|
602
|
-
# Try matching with MCP prefix format: mcp__<server>__<tool>
|
|
603
|
-
# Check if any available MCP tool ends with the current tool_name
|
|
604
|
-
for mcp_tool in available_mcp_tools:
|
|
605
|
-
# Format: mcp__server__toolname
|
|
606
|
-
if mcp_tool.startswith("mcp__") and mcp_tool.endswith(f"__{tool_name}"):
|
|
607
|
-
is_mcp_tool = True
|
|
608
|
-
logger.info(f"🔍 [DEBUG] Matched MCP tool: {tool_name} -> {mcp_tool}")
|
|
609
|
-
break
|
|
610
|
-
|
|
611
|
-
is_custom_tool = has_custom_tools and tool_name in self._custom_tool_names
|
|
612
|
-
|
|
613
|
-
logger.info(f"🔍 [DEBUG] Tool matching result: is_mcp_tool={is_mcp_tool}, is_custom_tool={is_custom_tool}")
|
|
614
|
-
|
|
615
|
-
if is_custom_tool:
|
|
616
|
-
# Process custom tool call
|
|
617
|
-
if custom_tracker.is_new_call(tool_name, tool_args):
|
|
618
|
-
call_record = custom_tracker.add_call(tool_name, tool_args)
|
|
619
|
-
|
|
620
|
-
custom_tools_used.append(
|
|
621
|
-
{
|
|
622
|
-
"name": tool_name,
|
|
623
|
-
"arguments": tool_args,
|
|
624
|
-
"timestamp": call_record["timestamp"],
|
|
625
|
-
},
|
|
626
|
-
)
|
|
627
|
-
|
|
628
|
-
timestamp_str = time.strftime(
|
|
629
|
-
"%H:%M:%S",
|
|
630
|
-
time.localtime(call_record["timestamp"]),
|
|
631
|
-
)
|
|
632
|
-
|
|
633
|
-
yield StreamChunk(
|
|
634
|
-
type="custom_tool_status",
|
|
635
|
-
status="custom_tool_called",
|
|
636
|
-
content=f"🔧 Custom Tool Called: {tool_name} at {timestamp_str} with args: {json.dumps(tool_args, indent=2)}",
|
|
637
|
-
source="custom_tools",
|
|
638
|
-
)
|
|
639
|
-
|
|
640
|
-
log_tool_call(
|
|
641
|
-
agent_id,
|
|
642
|
-
tool_name,
|
|
643
|
-
tool_args,
|
|
644
|
-
backend_name="gemini",
|
|
645
|
-
)
|
|
646
|
-
elif is_mcp_tool:
|
|
647
|
-
# Process MCP tool call
|
|
648
|
-
if mcp_tracker.is_new_call(tool_name, tool_args):
|
|
649
|
-
call_record = mcp_tracker.add_call(tool_name, tool_args)
|
|
650
|
-
|
|
651
|
-
mcp_tools_used.append(
|
|
652
|
-
{
|
|
653
|
-
"name": tool_name,
|
|
654
|
-
"arguments": tool_args,
|
|
655
|
-
"timestamp": call_record["timestamp"],
|
|
656
|
-
},
|
|
657
|
-
)
|
|
658
|
-
|
|
659
|
-
timestamp_str = time.strftime(
|
|
660
|
-
"%H:%M:%S",
|
|
661
|
-
time.localtime(call_record["timestamp"]),
|
|
662
|
-
)
|
|
663
|
-
|
|
664
|
-
yield StreamChunk(
|
|
665
|
-
type="mcp_status",
|
|
666
|
-
status="mcp_tool_called",
|
|
667
|
-
content=f"🔧 MCP Tool Called: {tool_name} at {timestamp_str} with args: {json.dumps(tool_args, indent=2)}",
|
|
668
|
-
source="mcp_tools",
|
|
669
|
-
)
|
|
670
|
-
|
|
671
|
-
log_tool_call(
|
|
672
|
-
agent_id,
|
|
673
|
-
tool_name,
|
|
674
|
-
tool_args,
|
|
675
|
-
backend_name="gemini",
|
|
676
|
-
)
|
|
677
|
-
|
|
678
|
-
# Then check automatic_function_calling_history (for MCP tools that were auto-executed)
|
|
679
|
-
if hasattr(chunk, "automatic_function_calling_history") and chunk.automatic_function_calling_history:
|
|
680
|
-
for history_item in chunk.automatic_function_calling_history:
|
|
681
|
-
if hasattr(history_item, "parts") and history_item.parts is not None:
|
|
682
|
-
for part in history_item.parts:
|
|
683
|
-
# Check for function_call part
|
|
684
|
-
if hasattr(part, "function_call") and part.function_call:
|
|
685
|
-
# Use MCPResponseExtractor to extract call data
|
|
686
|
-
call_data = self.mcp_extractor.extract_function_call(part.function_call)
|
|
687
|
-
|
|
688
|
-
if call_data:
|
|
689
|
-
tool_name = call_data["name"]
|
|
690
|
-
tool_args = call_data["arguments"]
|
|
691
|
-
|
|
692
|
-
# DEBUG: Log tool matching (from automatic_function_calling_history)
|
|
693
|
-
logger.info(f"🔍 [DEBUG-AUTO] Function call in history: tool_name='{tool_name}'")
|
|
694
|
-
logger.info(f"🔍 [DEBUG-AUTO] Available MCP tools: {available_mcp_tools}")
|
|
695
|
-
logger.info(f"🔍 [DEBUG-AUTO] Available custom tools: {list(self._custom_tool_names) if has_custom_tools else []}")
|
|
696
|
-
|
|
697
|
-
# Determine if it's MCP tool or custom tool
|
|
698
|
-
# MCP tools may come from SDK without prefix, so we need to check both
|
|
699
|
-
is_mcp_tool = False
|
|
700
|
-
if has_mcp:
|
|
701
|
-
if tool_name in available_mcp_tools:
|
|
702
|
-
is_mcp_tool = True
|
|
703
|
-
else:
|
|
704
|
-
# Try matching with MCP prefix format
|
|
705
|
-
for mcp_tool in available_mcp_tools:
|
|
706
|
-
if mcp_tool.startswith("mcp__") and mcp_tool.endswith(f"__{tool_name}"):
|
|
707
|
-
is_mcp_tool = True
|
|
708
|
-
logger.info(f"🔍 [DEBUG-AUTO] Matched MCP tool: {tool_name} -> {mcp_tool}")
|
|
709
|
-
break
|
|
710
|
-
|
|
711
|
-
is_custom_tool = has_custom_tools and tool_name in self._custom_tool_names
|
|
712
|
-
|
|
713
|
-
logger.info(f"🔍 [DEBUG-AUTO] Tool matching result: is_mcp_tool={is_mcp_tool}, is_custom_tool={is_custom_tool}")
|
|
714
|
-
|
|
715
|
-
if is_mcp_tool:
|
|
716
|
-
# Process MCP tool call
|
|
717
|
-
if mcp_tracker.is_new_call(tool_name, tool_args):
|
|
718
|
-
call_record = mcp_tracker.add_call(tool_name, tool_args)
|
|
719
|
-
|
|
720
|
-
mcp_tools_used.append(
|
|
721
|
-
{
|
|
722
|
-
"name": tool_name,
|
|
723
|
-
"arguments": tool_args,
|
|
724
|
-
"timestamp": call_record["timestamp"],
|
|
725
|
-
},
|
|
726
|
-
)
|
|
727
|
-
|
|
728
|
-
timestamp_str = time.strftime(
|
|
729
|
-
"%H:%M:%S",
|
|
730
|
-
time.localtime(call_record["timestamp"]),
|
|
731
|
-
)
|
|
732
|
-
|
|
733
|
-
yield StreamChunk(
|
|
734
|
-
type="mcp_status",
|
|
735
|
-
status="mcp_tool_called",
|
|
736
|
-
content=f"🔧 MCP Tool Called: {tool_name} at {timestamp_str} with args: {json.dumps(tool_args, indent=2)}",
|
|
737
|
-
source="mcp_tools",
|
|
738
|
-
)
|
|
739
|
-
|
|
740
|
-
log_tool_call(
|
|
741
|
-
agent_id,
|
|
742
|
-
tool_name,
|
|
743
|
-
tool_args,
|
|
744
|
-
backend_name="gemini",
|
|
745
|
-
)
|
|
746
|
-
|
|
747
|
-
elif is_custom_tool:
|
|
748
|
-
# Process custom tool call
|
|
749
|
-
if custom_tracker.is_new_call(tool_name, tool_args):
|
|
750
|
-
call_record = custom_tracker.add_call(tool_name, tool_args)
|
|
751
|
-
|
|
752
|
-
custom_tools_used.append(
|
|
753
|
-
{
|
|
754
|
-
"name": tool_name,
|
|
755
|
-
"arguments": tool_args,
|
|
756
|
-
"timestamp": call_record["timestamp"],
|
|
757
|
-
},
|
|
758
|
-
)
|
|
759
|
-
|
|
760
|
-
timestamp_str = time.strftime(
|
|
761
|
-
"%H:%M:%S",
|
|
762
|
-
time.localtime(call_record["timestamp"]),
|
|
763
|
-
)
|
|
764
|
-
|
|
765
|
-
yield StreamChunk(
|
|
766
|
-
type="custom_tool_status",
|
|
767
|
-
status="custom_tool_called",
|
|
768
|
-
content=f"🔧 Custom Tool Called: {tool_name} at {timestamp_str} with args: {json.dumps(tool_args, indent=2)}",
|
|
769
|
-
source="custom_tools",
|
|
770
|
-
)
|
|
771
|
-
|
|
772
|
-
log_tool_call(
|
|
773
|
-
agent_id,
|
|
774
|
-
tool_name,
|
|
775
|
-
tool_args,
|
|
776
|
-
backend_name="gemini",
|
|
777
|
-
)
|
|
778
|
-
|
|
779
|
-
# Check for function_response part
|
|
780
|
-
elif hasattr(part, "function_response") and part.function_response:
|
|
781
|
-
response_data = self.mcp_extractor.extract_function_response(part.function_response)
|
|
782
|
-
|
|
783
|
-
if response_data:
|
|
784
|
-
tool_name = response_data["name"]
|
|
785
|
-
tool_response = response_data["response"]
|
|
786
|
-
|
|
787
|
-
# Determine if it's MCP tool or custom tool
|
|
788
|
-
# MCP tools may come from SDK without prefix
|
|
789
|
-
is_mcp_tool = False
|
|
790
|
-
if has_mcp:
|
|
791
|
-
if tool_name in available_mcp_tools:
|
|
792
|
-
is_mcp_tool = True
|
|
793
|
-
else:
|
|
794
|
-
# Try matching with MCP prefix format
|
|
795
|
-
for mcp_tool in available_mcp_tools:
|
|
796
|
-
if mcp_tool.startswith("mcp__") and mcp_tool.endswith(f"__{tool_name}"):
|
|
797
|
-
is_mcp_tool = True
|
|
798
|
-
break
|
|
799
|
-
|
|
800
|
-
is_custom_tool = has_custom_tools and tool_name in self._custom_tool_names
|
|
801
|
-
|
|
802
|
-
if is_mcp_tool:
|
|
803
|
-
# Process MCP tool response
|
|
804
|
-
if mcp_response_tracker.is_new_response(tool_name, tool_response):
|
|
805
|
-
response_record = mcp_response_tracker.add_response(tool_name, tool_response)
|
|
806
|
-
|
|
807
|
-
# Extract text content from CallToolResult
|
|
808
|
-
response_text = None
|
|
809
|
-
if isinstance(tool_response, dict) and "result" in tool_response:
|
|
810
|
-
result = tool_response["result"]
|
|
811
|
-
if hasattr(result, "content") and result.content:
|
|
812
|
-
first_content = result.content[0]
|
|
813
|
-
if hasattr(first_content, "text"):
|
|
814
|
-
response_text = first_content.text
|
|
815
|
-
|
|
816
|
-
if response_text is None:
|
|
817
|
-
response_text = str(tool_response)
|
|
818
|
-
|
|
819
|
-
timestamp_str = time.strftime(
|
|
820
|
-
"%H:%M:%S",
|
|
821
|
-
time.localtime(response_record["timestamp"]),
|
|
822
|
-
)
|
|
823
|
-
|
|
824
|
-
# Format response as JSON if possible
|
|
825
|
-
formatted_response = format_tool_response_as_json(response_text)
|
|
826
|
-
|
|
827
|
-
yield StreamChunk(
|
|
828
|
-
type="mcp_status",
|
|
829
|
-
status="mcp_tool_response",
|
|
830
|
-
content=f"✅ MCP Tool Response from {tool_name} at {timestamp_str}: {formatted_response}",
|
|
831
|
-
source="mcp_tools",
|
|
832
|
-
)
|
|
833
|
-
|
|
834
|
-
log_backend_activity(
|
|
835
|
-
"gemini",
|
|
836
|
-
"MCP tool response received",
|
|
837
|
-
{
|
|
838
|
-
"tool_name": tool_name,
|
|
839
|
-
"response_preview": str(tool_response)[:],
|
|
840
|
-
},
|
|
841
|
-
agent_id=agent_id,
|
|
842
|
-
)
|
|
843
|
-
|
|
844
|
-
elif is_custom_tool:
|
|
845
|
-
# Process custom tool response
|
|
846
|
-
if custom_response_tracker.is_new_response(tool_name, tool_response):
|
|
847
|
-
response_record = custom_response_tracker.add_response(tool_name, tool_response)
|
|
848
|
-
|
|
849
|
-
# Extract text from response
|
|
850
|
-
response_text = str(tool_response)
|
|
851
|
-
|
|
852
|
-
timestamp_str = time.strftime(
|
|
853
|
-
"%H:%M:%S",
|
|
854
|
-
time.localtime(response_record["timestamp"]),
|
|
855
|
-
)
|
|
856
|
-
|
|
857
|
-
# Format response as JSON if possible
|
|
858
|
-
formatted_response = format_tool_response_as_json(response_text)
|
|
859
|
-
|
|
860
|
-
yield StreamChunk(
|
|
861
|
-
type="custom_tool_status",
|
|
862
|
-
status="custom_tool_response",
|
|
863
|
-
content=f"✅ Custom Tool Response from {tool_name} at {timestamp_str}: {formatted_response}",
|
|
864
|
-
source="custom_tools",
|
|
865
|
-
)
|
|
866
|
-
|
|
867
|
-
log_backend_activity(
|
|
868
|
-
"gemini",
|
|
869
|
-
"Custom tool response received",
|
|
870
|
-
{
|
|
871
|
-
"tool_name": tool_name,
|
|
872
|
-
"response_preview": str(tool_response),
|
|
873
|
-
},
|
|
874
|
-
agent_id=agent_id,
|
|
875
|
-
)
|
|
876
|
-
|
|
877
|
-
# ============================================
|
|
878
|
-
# 2. Process text content
|
|
879
|
-
# ============================================
|
|
880
|
-
if hasattr(chunk, "text") and chunk.text:
|
|
881
|
-
chunk_text = chunk.text
|
|
882
|
-
full_content_text += chunk_text
|
|
883
|
-
log_backend_agent_message(
|
|
884
|
-
agent_id,
|
|
885
|
-
"RECV",
|
|
886
|
-
{"content": chunk_text},
|
|
887
|
-
backend_name="gemini",
|
|
888
|
-
)
|
|
889
|
-
log_stream_chunk("backend.gemini", "content", chunk_text, agent_id)
|
|
890
|
-
yield StreamChunk(type="content", content=chunk_text)
|
|
401
|
+
# Log messages being sent
|
|
402
|
+
log_backend_agent_message(
|
|
403
|
+
agent_id or "default",
|
|
404
|
+
"SEND",
|
|
405
|
+
{
|
|
406
|
+
"content": full_content,
|
|
407
|
+
"custom_tools": len(tools_to_apply) if tools_to_apply else 0,
|
|
408
|
+
},
|
|
409
|
+
backend_name="gemini",
|
|
410
|
+
)
|
|
891
411
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
delattr(self, "_mcp_stream_started")
|
|
901
|
-
|
|
902
|
-
# ====================================================================
|
|
903
|
-
# Tool execution loop: Execute tools until model stops calling them
|
|
904
|
-
# ====================================================================
|
|
905
|
-
# Note: When automatic_function_calling is disabled, BOTH custom and MCP tools
|
|
906
|
-
# need to be manually executed. The model may make multiple rounds of tool calls
|
|
907
|
-
# (e.g., call custom tool first, then MCP tool after seeing the result).
|
|
908
|
-
|
|
909
|
-
executed_tool_calls = set() # Track which tools we've already executed
|
|
910
|
-
max_tool_rounds = 10 # Prevent infinite loops
|
|
911
|
-
tool_round = 0
|
|
912
|
-
|
|
913
|
-
while tool_round < max_tool_rounds:
|
|
914
|
-
# Find new tool calls that haven't been executed yet
|
|
915
|
-
new_custom_tools = []
|
|
916
|
-
new_mcp_tools = []
|
|
917
|
-
|
|
918
|
-
for tool_call in custom_tools_used:
|
|
919
|
-
call_signature = f"custom_{tool_call['name']}_{json.dumps(tool_call['arguments'], sort_keys=True)}"
|
|
920
|
-
if call_signature not in executed_tool_calls:
|
|
921
|
-
new_custom_tools.append(tool_call)
|
|
922
|
-
executed_tool_calls.add(call_signature)
|
|
923
|
-
|
|
924
|
-
for tool_call in mcp_tools_used:
|
|
925
|
-
call_signature = f"mcp_{tool_call['name']}_{json.dumps(tool_call['arguments'], sort_keys=True)}"
|
|
926
|
-
if call_signature not in executed_tool_calls:
|
|
927
|
-
new_mcp_tools.append(tool_call)
|
|
928
|
-
executed_tool_calls.add(call_signature)
|
|
929
|
-
|
|
930
|
-
# If no new tools to execute, break the loop
|
|
931
|
-
if not new_custom_tools and not new_mcp_tools:
|
|
932
|
-
break
|
|
412
|
+
# ====================================================================
|
|
413
|
+
# Streaming Phase: Stream with simple function call detection
|
|
414
|
+
# ====================================================================
|
|
415
|
+
stream = await client.aio.models.generate_content_stream(
|
|
416
|
+
model=model_name,
|
|
417
|
+
contents=full_content,
|
|
418
|
+
config=config,
|
|
419
|
+
)
|
|
933
420
|
|
|
934
|
-
|
|
935
|
-
|
|
421
|
+
# Simple list accumulation for function calls (no trackers)
|
|
422
|
+
captured_function_calls = []
|
|
423
|
+
full_content_text = ""
|
|
424
|
+
last_response_with_candidates = None
|
|
936
425
|
|
|
937
|
-
|
|
938
|
-
|
|
426
|
+
# Stream chunks and capture function calls
|
|
427
|
+
async for chunk in stream:
|
|
428
|
+
# Detect function calls in candidates
|
|
429
|
+
if hasattr(chunk, "candidates") and chunk.candidates:
|
|
430
|
+
for candidate in chunk.candidates:
|
|
431
|
+
if hasattr(candidate, "content") and candidate.content:
|
|
432
|
+
if hasattr(candidate.content, "parts") and candidate.content.parts:
|
|
433
|
+
for part in candidate.content.parts:
|
|
434
|
+
# Check for function_call part
|
|
435
|
+
if hasattr(part, "function_call") and part.function_call:
|
|
436
|
+
# Extract call data
|
|
437
|
+
tool_name = part.function_call.name
|
|
438
|
+
tool_args = dict(part.function_call.args) if part.function_call.args else {}
|
|
439
|
+
|
|
440
|
+
# Create call record
|
|
441
|
+
call_id = f"call_{len(captured_function_calls)}"
|
|
442
|
+
captured_function_calls.append(
|
|
443
|
+
{
|
|
444
|
+
"call_id": call_id,
|
|
445
|
+
"name": tool_name,
|
|
446
|
+
"arguments": json.dumps(tool_args),
|
|
447
|
+
},
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
logger.info(f"[Gemini] Function call detected: {tool_name}")
|
|
451
|
+
|
|
452
|
+
# Process text content
|
|
453
|
+
if hasattr(chunk, "text") and chunk.text:
|
|
454
|
+
chunk_text = chunk.text
|
|
455
|
+
full_content_text += chunk_text
|
|
456
|
+
log_backend_agent_message(
|
|
457
|
+
agent_id,
|
|
458
|
+
"RECV",
|
|
459
|
+
{"content": chunk_text},
|
|
460
|
+
backend_name="gemini",
|
|
461
|
+
)
|
|
462
|
+
log_stream_chunk("backend.gemini", "content", chunk_text, agent_id)
|
|
463
|
+
yield StreamChunk(type="content", content=chunk_text)
|
|
939
464
|
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
tool_args = tool_call["arguments"]
|
|
465
|
+
# Buffer last chunk with candidates
|
|
466
|
+
if hasattr(chunk, "candidates") and chunk.candidates:
|
|
467
|
+
last_response_with_candidates = chunk
|
|
944
468
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
)
|
|
469
|
+
# ====================================================================
|
|
470
|
+
# Structured Coordination Output Parsing
|
|
471
|
+
# ====================================================================
|
|
472
|
+
# Check for structured coordination output when no function calls captured
|
|
473
|
+
if is_coordination and not captured_function_calls and full_content_text:
|
|
474
|
+
# Try to parse structured response from text content
|
|
475
|
+
parsed = self.formatter.extract_structured_response(full_content_text)
|
|
953
476
|
|
|
954
|
-
|
|
955
|
-
|
|
477
|
+
if parsed and isinstance(parsed, dict):
|
|
478
|
+
# Convert structured response to tool calls
|
|
479
|
+
tool_calls = self.formatter.convert_structured_to_tool_calls(parsed)
|
|
956
480
|
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
481
|
+
if tool_calls:
|
|
482
|
+
# Categorize the tool calls
|
|
483
|
+
mcp_calls, custom_calls, provider_calls = self._categorize_tool_calls(tool_calls)
|
|
484
|
+
|
|
485
|
+
# Handle provider (workflow) calls - these are coordination actions
|
|
486
|
+
# We yield StreamChunk entries but do NOT execute them
|
|
487
|
+
if provider_calls:
|
|
488
|
+
# Convert provider calls to tool_calls format for orchestrator
|
|
489
|
+
workflow_tool_calls = []
|
|
490
|
+
for call in provider_calls:
|
|
491
|
+
tool_name = call.get("name", "")
|
|
492
|
+
tool_args_str = call.get("arguments", "{}")
|
|
493
|
+
|
|
494
|
+
# Parse arguments if they're a string
|
|
495
|
+
if isinstance(tool_args_str, str):
|
|
496
|
+
try:
|
|
497
|
+
tool_args = json.loads(tool_args_str)
|
|
498
|
+
except json.JSONDecodeError:
|
|
499
|
+
tool_args = {}
|
|
500
|
+
else:
|
|
501
|
+
tool_args = tool_args_str
|
|
964
502
|
|
|
965
|
-
#
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
503
|
+
# Log the coordination action
|
|
504
|
+
logger.info(f"[Gemini] Structured coordination action: {tool_name}")
|
|
505
|
+
log_tool_call(
|
|
506
|
+
agent_id,
|
|
507
|
+
tool_name,
|
|
508
|
+
tool_args,
|
|
509
|
+
None,
|
|
510
|
+
backend_name="gemini",
|
|
971
511
|
)
|
|
972
512
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
logger.error(error_msg)
|
|
976
|
-
yield StreamChunk(
|
|
977
|
-
type="custom_tool_status",
|
|
978
|
-
status="custom_tool_error",
|
|
979
|
-
content=f"❌ {error_msg}",
|
|
980
|
-
source="custom_tools",
|
|
981
|
-
)
|
|
982
|
-
# Add error response
|
|
983
|
-
tool_responses.append(
|
|
513
|
+
# Build tool call in standard format
|
|
514
|
+
workflow_tool_calls.append(
|
|
984
515
|
{
|
|
985
|
-
"
|
|
986
|
-
"
|
|
516
|
+
"id": call.get("call_id", f"call_{len(workflow_tool_calls)}"),
|
|
517
|
+
"type": "function",
|
|
518
|
+
"function": {
|
|
519
|
+
"name": tool_name,
|
|
520
|
+
"arguments": tool_args,
|
|
521
|
+
},
|
|
987
522
|
},
|
|
988
523
|
)
|
|
989
524
|
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
tool_args = tool_call["arguments"]
|
|
994
|
-
|
|
995
|
-
try:
|
|
996
|
-
# Execute the MCP tool via MCP client
|
|
997
|
-
if not self._mcp_client:
|
|
998
|
-
raise RuntimeError("MCP client not initialized")
|
|
999
|
-
|
|
1000
|
-
# Convert tool name to prefixed format if needed
|
|
1001
|
-
# MCP client expects: mcp__server__toolname
|
|
1002
|
-
# Gemini SDK returns: toolname (without prefix)
|
|
1003
|
-
prefixed_tool_name = tool_name
|
|
1004
|
-
if not tool_name.startswith("mcp__"):
|
|
1005
|
-
# Find the matching prefixed tool name
|
|
1006
|
-
for mcp_tool in available_mcp_tools:
|
|
1007
|
-
if mcp_tool.startswith("mcp__") and mcp_tool.endswith(f"__{tool_name}"):
|
|
1008
|
-
prefixed_tool_name = mcp_tool
|
|
1009
|
-
logger.info(f"🔧 [DEBUG] Converting tool name for execution: {tool_name} -> {prefixed_tool_name}")
|
|
1010
|
-
break
|
|
1011
|
-
|
|
1012
|
-
mcp_result = await self._mcp_client.call_tool(prefixed_tool_name, tool_args)
|
|
1013
|
-
|
|
1014
|
-
# Extract text from CallToolResult object
|
|
1015
|
-
result_str = None
|
|
1016
|
-
if mcp_result:
|
|
1017
|
-
if hasattr(mcp_result, "content") and mcp_result.content:
|
|
1018
|
-
first_content = mcp_result.content[0]
|
|
1019
|
-
if hasattr(first_content, "text"):
|
|
1020
|
-
result_str = first_content.text
|
|
1021
|
-
|
|
1022
|
-
if result_str is None:
|
|
1023
|
-
result_str = str(mcp_result) if mcp_result else "None"
|
|
1024
|
-
|
|
1025
|
-
# Format result as JSON if possible
|
|
1026
|
-
formatted_result = format_tool_response_as_json(result_str)
|
|
1027
|
-
result_preview = formatted_result
|
|
1028
|
-
|
|
1029
|
-
# Yield execution status
|
|
525
|
+
# Emit tool_calls chunk for orchestrator to process
|
|
526
|
+
if workflow_tool_calls:
|
|
527
|
+
log_stream_chunk("backend.gemini", "tool_calls", workflow_tool_calls, agent_id)
|
|
1030
528
|
yield StreamChunk(
|
|
1031
|
-
type="
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
source="mcp_tools",
|
|
529
|
+
type="tool_calls",
|
|
530
|
+
tool_calls=workflow_tool_calls,
|
|
531
|
+
source="gemini",
|
|
1035
532
|
)
|
|
1036
533
|
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
},
|
|
1043
|
-
)
|
|
1044
|
-
|
|
1045
|
-
except Exception as e:
|
|
1046
|
-
error_msg = f"Error executing MCP tool {tool_name}: {str(e)}"
|
|
1047
|
-
logger.error(error_msg)
|
|
534
|
+
# Do not execute workflow tools - just return after yielding
|
|
535
|
+
# The orchestrator will handle these coordination actions
|
|
536
|
+
if provider_calls:
|
|
537
|
+
# Emit completion status if MCP was actually used
|
|
538
|
+
if mcp_used:
|
|
1048
539
|
yield StreamChunk(
|
|
1049
540
|
type="mcp_status",
|
|
1050
|
-
status="
|
|
1051
|
-
content=
|
|
541
|
+
status="mcp_session_complete",
|
|
542
|
+
content="✅ [MCP] Session completed",
|
|
1052
543
|
source="mcp_tools",
|
|
1053
544
|
)
|
|
1054
|
-
# Add error response
|
|
1055
|
-
tool_responses.append(
|
|
1056
|
-
{
|
|
1057
|
-
"name": tool_name,
|
|
1058
|
-
"response": {"error": str(e)},
|
|
1059
|
-
},
|
|
1060
|
-
)
|
|
1061
545
|
|
|
1062
|
-
|
|
1063
|
-
|
|
546
|
+
yield StreamChunk(type="done")
|
|
547
|
+
return
|
|
548
|
+
|
|
549
|
+
# ====================================================================
|
|
550
|
+
# Tool Execution Phase: Execute captured function calls using base class
|
|
551
|
+
# ====================================================================
|
|
552
|
+
if captured_function_calls:
|
|
553
|
+
# Categorize function calls using base class helper
|
|
554
|
+
mcp_calls, custom_calls, provider_calls = self._categorize_tool_calls(captured_function_calls)
|
|
555
|
+
|
|
556
|
+
# ====================================================================
|
|
557
|
+
# Handle provider (workflow) calls - emit as StreamChunks but do NOT execute
|
|
558
|
+
# ====================================================================
|
|
559
|
+
if provider_calls:
|
|
560
|
+
# Convert provider calls to tool_calls format for orchestrator
|
|
561
|
+
workflow_tool_calls = []
|
|
562
|
+
for call in provider_calls:
|
|
563
|
+
tool_name = call.get("name", "")
|
|
564
|
+
tool_args_str = call.get("arguments", "{}")
|
|
565
|
+
|
|
566
|
+
# Parse arguments if they're a string
|
|
567
|
+
if isinstance(tool_args_str, str):
|
|
1064
568
|
try:
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
# Build conversation history
|
|
1072
|
-
conversation_history = []
|
|
1073
|
-
|
|
1074
|
-
# Add original user content
|
|
1075
|
-
conversation_history.append(
|
|
1076
|
-
types.Content(
|
|
1077
|
-
parts=[types.Part(text=full_content)],
|
|
1078
|
-
role="user",
|
|
1079
|
-
),
|
|
1080
|
-
)
|
|
1081
|
-
|
|
1082
|
-
# Add model's function call response (tools from THIS round)
|
|
1083
|
-
model_parts = []
|
|
1084
|
-
for tool_call in round_function_calls:
|
|
1085
|
-
model_parts.append(
|
|
1086
|
-
types.Part.from_function_call(
|
|
1087
|
-
name=tool_call["name"],
|
|
1088
|
-
args=tool_call["arguments"],
|
|
1089
|
-
),
|
|
1090
|
-
)
|
|
1091
|
-
|
|
1092
|
-
conversation_history.append(
|
|
1093
|
-
types.Content(
|
|
1094
|
-
parts=model_parts,
|
|
1095
|
-
role="model",
|
|
1096
|
-
),
|
|
1097
|
-
)
|
|
1098
|
-
|
|
1099
|
-
# Add function response (as user message with function_response parts)
|
|
1100
|
-
response_parts = []
|
|
1101
|
-
for resp in tool_responses:
|
|
1102
|
-
response_parts.append(
|
|
1103
|
-
types.Part.from_function_response(
|
|
1104
|
-
name=resp["name"],
|
|
1105
|
-
response=resp["response"],
|
|
1106
|
-
),
|
|
1107
|
-
)
|
|
1108
|
-
|
|
1109
|
-
conversation_history.append(
|
|
1110
|
-
types.Content(
|
|
1111
|
-
parts=response_parts,
|
|
1112
|
-
role="user",
|
|
1113
|
-
),
|
|
1114
|
-
)
|
|
1115
|
-
|
|
1116
|
-
# Make continuation call
|
|
1117
|
-
yield StreamChunk(
|
|
1118
|
-
type="custom_tool_status",
|
|
1119
|
-
status="continuation_call",
|
|
1120
|
-
content=f"🔄 Making continuation call with {len(tool_responses)} tool results...",
|
|
1121
|
-
source="custom_tools",
|
|
1122
|
-
)
|
|
1123
|
-
|
|
1124
|
-
# Use same session_config as before
|
|
1125
|
-
continuation_stream = await client.aio.models.generate_content_stream(
|
|
1126
|
-
model=model_name,
|
|
1127
|
-
contents=conversation_history,
|
|
1128
|
-
config=session_config,
|
|
1129
|
-
)
|
|
569
|
+
tool_args = json.loads(tool_args_str)
|
|
570
|
+
except json.JSONDecodeError:
|
|
571
|
+
tool_args = {}
|
|
572
|
+
else:
|
|
573
|
+
tool_args = tool_args_str
|
|
1130
574
|
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
if hasattr(candidate.content, "parts") and candidate.content.parts:
|
|
1141
|
-
for part in candidate.content.parts:
|
|
1142
|
-
# Check for function_call part
|
|
1143
|
-
if hasattr(part, "function_call") and part.function_call:
|
|
1144
|
-
call_data = self.mcp_extractor.extract_function_call(part.function_call)
|
|
1145
|
-
|
|
1146
|
-
if call_data:
|
|
1147
|
-
tool_name = call_data["name"]
|
|
1148
|
-
tool_args = call_data["arguments"]
|
|
1149
|
-
|
|
1150
|
-
# Determine if it's MCP tool or custom tool
|
|
1151
|
-
# MCP tools may come from SDK without prefix
|
|
1152
|
-
is_mcp_tool = False
|
|
1153
|
-
if has_mcp:
|
|
1154
|
-
if tool_name in available_mcp_tools:
|
|
1155
|
-
is_mcp_tool = True
|
|
1156
|
-
else:
|
|
1157
|
-
# Try matching with MCP prefix format
|
|
1158
|
-
for mcp_tool in available_mcp_tools:
|
|
1159
|
-
if mcp_tool.startswith("mcp__") and mcp_tool.endswith(f"__{tool_name}"):
|
|
1160
|
-
is_mcp_tool = True
|
|
1161
|
-
break
|
|
1162
|
-
|
|
1163
|
-
is_custom_tool = has_custom_tools and tool_name in self._custom_tool_names
|
|
1164
|
-
|
|
1165
|
-
if is_custom_tool:
|
|
1166
|
-
# Process custom tool call
|
|
1167
|
-
if custom_tracker.is_new_call(tool_name, tool_args):
|
|
1168
|
-
call_record = custom_tracker.add_call(tool_name, tool_args)
|
|
1169
|
-
|
|
1170
|
-
custom_tools_used.append(
|
|
1171
|
-
{
|
|
1172
|
-
"name": tool_name,
|
|
1173
|
-
"arguments": tool_args,
|
|
1174
|
-
"timestamp": call_record["timestamp"],
|
|
1175
|
-
},
|
|
1176
|
-
)
|
|
1177
|
-
|
|
1178
|
-
timestamp_str = time.strftime(
|
|
1179
|
-
"%H:%M:%S",
|
|
1180
|
-
time.localtime(call_record["timestamp"]),
|
|
1181
|
-
)
|
|
1182
|
-
|
|
1183
|
-
yield StreamChunk(
|
|
1184
|
-
type="custom_tool_status",
|
|
1185
|
-
status="custom_tool_called",
|
|
1186
|
-
content=f"🔧 Custom Tool Called: {tool_name} at {timestamp_str} with args: {json.dumps(tool_args, indent=2)}",
|
|
1187
|
-
source="custom_tools",
|
|
1188
|
-
)
|
|
1189
|
-
|
|
1190
|
-
log_tool_call(
|
|
1191
|
-
agent_id,
|
|
1192
|
-
tool_name,
|
|
1193
|
-
tool_args,
|
|
1194
|
-
backend_name="gemini",
|
|
1195
|
-
)
|
|
1196
|
-
elif is_mcp_tool:
|
|
1197
|
-
# Process MCP tool call
|
|
1198
|
-
if mcp_tracker.is_new_call(tool_name, tool_args):
|
|
1199
|
-
call_record = mcp_tracker.add_call(tool_name, tool_args)
|
|
1200
|
-
|
|
1201
|
-
mcp_tools_used.append(
|
|
1202
|
-
{
|
|
1203
|
-
"name": tool_name,
|
|
1204
|
-
"arguments": tool_args,
|
|
1205
|
-
"timestamp": call_record["timestamp"],
|
|
1206
|
-
},
|
|
1207
|
-
)
|
|
1208
|
-
|
|
1209
|
-
timestamp_str = time.strftime(
|
|
1210
|
-
"%H:%M:%S",
|
|
1211
|
-
time.localtime(call_record["timestamp"]),
|
|
1212
|
-
)
|
|
1213
|
-
|
|
1214
|
-
yield StreamChunk(
|
|
1215
|
-
type="mcp_status",
|
|
1216
|
-
status="mcp_tool_called",
|
|
1217
|
-
content=f"🔧 MCP Tool Called: {tool_name} at {timestamp_str} with args: {json.dumps(tool_args, indent=2)}",
|
|
1218
|
-
source="mcp_tools",
|
|
1219
|
-
)
|
|
1220
|
-
|
|
1221
|
-
log_tool_call(
|
|
1222
|
-
agent_id,
|
|
1223
|
-
tool_name,
|
|
1224
|
-
tool_args,
|
|
1225
|
-
backend_name="gemini",
|
|
1226
|
-
)
|
|
1227
|
-
|
|
1228
|
-
# Check automatic_function_calling_history (for auto-executed MCP tools)
|
|
1229
|
-
if hasattr(chunk, "automatic_function_calling_history") and chunk.automatic_function_calling_history:
|
|
1230
|
-
for history_item in chunk.automatic_function_calling_history:
|
|
1231
|
-
if hasattr(history_item, "parts") and history_item.parts is not None:
|
|
1232
|
-
for part in history_item.parts:
|
|
1233
|
-
# Check for function_call part
|
|
1234
|
-
if hasattr(part, "function_call") and part.function_call:
|
|
1235
|
-
call_data = self.mcp_extractor.extract_function_call(part.function_call)
|
|
1236
|
-
|
|
1237
|
-
if call_data:
|
|
1238
|
-
tool_name = call_data["name"]
|
|
1239
|
-
tool_args = call_data["arguments"]
|
|
1240
|
-
|
|
1241
|
-
# Determine if it's MCP tool or custom tool
|
|
1242
|
-
# MCP tools may come from SDK without prefix
|
|
1243
|
-
is_mcp_tool = False
|
|
1244
|
-
if has_mcp:
|
|
1245
|
-
if tool_name in available_mcp_tools:
|
|
1246
|
-
is_mcp_tool = True
|
|
1247
|
-
else:
|
|
1248
|
-
# Try matching with MCP prefix format
|
|
1249
|
-
for mcp_tool in available_mcp_tools:
|
|
1250
|
-
if mcp_tool.startswith("mcp__") and mcp_tool.endswith(f"__{tool_name}"):
|
|
1251
|
-
is_mcp_tool = True
|
|
1252
|
-
break
|
|
1253
|
-
|
|
1254
|
-
is_custom_tool = has_custom_tools and tool_name in self._custom_tool_names
|
|
1255
|
-
|
|
1256
|
-
if is_mcp_tool:
|
|
1257
|
-
# Process MCP tool call
|
|
1258
|
-
if mcp_tracker.is_new_call(tool_name, tool_args):
|
|
1259
|
-
call_record = mcp_tracker.add_call(tool_name, tool_args)
|
|
1260
|
-
|
|
1261
|
-
mcp_tools_used.append(
|
|
1262
|
-
{
|
|
1263
|
-
"name": tool_name,
|
|
1264
|
-
"arguments": tool_args,
|
|
1265
|
-
"timestamp": call_record["timestamp"],
|
|
1266
|
-
},
|
|
1267
|
-
)
|
|
1268
|
-
|
|
1269
|
-
timestamp_str = time.strftime(
|
|
1270
|
-
"%H:%M:%S",
|
|
1271
|
-
time.localtime(call_record["timestamp"]),
|
|
1272
|
-
)
|
|
1273
|
-
|
|
1274
|
-
yield StreamChunk(
|
|
1275
|
-
type="mcp_status",
|
|
1276
|
-
status="mcp_tool_called",
|
|
1277
|
-
content=f"🔧 MCP Tool Called: {tool_name} at {timestamp_str} with args: {json.dumps(tool_args, indent=2)}",
|
|
1278
|
-
source="mcp_tools",
|
|
1279
|
-
)
|
|
1280
|
-
|
|
1281
|
-
log_tool_call(
|
|
1282
|
-
agent_id,
|
|
1283
|
-
tool_name,
|
|
1284
|
-
tool_args,
|
|
1285
|
-
backend_name="gemini",
|
|
1286
|
-
)
|
|
1287
|
-
|
|
1288
|
-
elif is_custom_tool:
|
|
1289
|
-
# Process custom tool call
|
|
1290
|
-
if custom_tracker.is_new_call(tool_name, tool_args):
|
|
1291
|
-
call_record = custom_tracker.add_call(tool_name, tool_args)
|
|
1292
|
-
|
|
1293
|
-
custom_tools_used.append(
|
|
1294
|
-
{
|
|
1295
|
-
"name": tool_name,
|
|
1296
|
-
"arguments": tool_args,
|
|
1297
|
-
"timestamp": call_record["timestamp"],
|
|
1298
|
-
},
|
|
1299
|
-
)
|
|
1300
|
-
|
|
1301
|
-
timestamp_str = time.strftime(
|
|
1302
|
-
"%H:%M:%S",
|
|
1303
|
-
time.localtime(call_record["timestamp"]),
|
|
1304
|
-
)
|
|
1305
|
-
|
|
1306
|
-
yield StreamChunk(
|
|
1307
|
-
type="custom_tool_status",
|
|
1308
|
-
status="custom_tool_called",
|
|
1309
|
-
content=f"🔧 Custom Tool Called: {tool_name} at {timestamp_str} with args: {json.dumps(tool_args, indent=2)}",
|
|
1310
|
-
source="custom_tools",
|
|
1311
|
-
)
|
|
1312
|
-
|
|
1313
|
-
log_tool_call(
|
|
1314
|
-
agent_id,
|
|
1315
|
-
tool_name,
|
|
1316
|
-
tool_args,
|
|
1317
|
-
backend_name="gemini",
|
|
1318
|
-
)
|
|
1319
|
-
|
|
1320
|
-
# Check for function_response part
|
|
1321
|
-
elif hasattr(part, "function_response") and part.function_response:
|
|
1322
|
-
response_data = self.mcp_extractor.extract_function_response(part.function_response)
|
|
1323
|
-
|
|
1324
|
-
if response_data:
|
|
1325
|
-
tool_name = response_data["name"]
|
|
1326
|
-
tool_response = response_data["response"]
|
|
1327
|
-
|
|
1328
|
-
# Determine if it's MCP tool or custom tool
|
|
1329
|
-
# MCP tools may come from SDK without prefix
|
|
1330
|
-
is_mcp_tool = False
|
|
1331
|
-
if has_mcp:
|
|
1332
|
-
if tool_name in available_mcp_tools:
|
|
1333
|
-
is_mcp_tool = True
|
|
1334
|
-
else:
|
|
1335
|
-
# Try matching with MCP prefix format
|
|
1336
|
-
for mcp_tool in available_mcp_tools:
|
|
1337
|
-
if mcp_tool.startswith("mcp__") and mcp_tool.endswith(f"__{tool_name}"):
|
|
1338
|
-
is_mcp_tool = True
|
|
1339
|
-
break
|
|
1340
|
-
|
|
1341
|
-
is_custom_tool = has_custom_tools and tool_name in self._custom_tool_names
|
|
1342
|
-
|
|
1343
|
-
if is_mcp_tool:
|
|
1344
|
-
# Process MCP tool response
|
|
1345
|
-
if mcp_response_tracker.is_new_response(tool_name, tool_response):
|
|
1346
|
-
response_record = mcp_response_tracker.add_response(tool_name, tool_response)
|
|
1347
|
-
|
|
1348
|
-
# Extract text content from CallToolResult
|
|
1349
|
-
response_text = None
|
|
1350
|
-
if isinstance(tool_response, dict) and "result" in tool_response:
|
|
1351
|
-
result = tool_response["result"]
|
|
1352
|
-
if hasattr(result, "content") and result.content:
|
|
1353
|
-
first_content = result.content[0]
|
|
1354
|
-
if hasattr(first_content, "text"):
|
|
1355
|
-
response_text = first_content.text
|
|
1356
|
-
|
|
1357
|
-
if response_text is None:
|
|
1358
|
-
response_text = str(tool_response)
|
|
1359
|
-
|
|
1360
|
-
timestamp_str = time.strftime(
|
|
1361
|
-
"%H:%M:%S",
|
|
1362
|
-
time.localtime(response_record["timestamp"]),
|
|
1363
|
-
)
|
|
1364
|
-
|
|
1365
|
-
# Format response as JSON if possible
|
|
1366
|
-
formatted_response = format_tool_response_as_json(response_text)
|
|
1367
|
-
|
|
1368
|
-
yield StreamChunk(
|
|
1369
|
-
type="mcp_status",
|
|
1370
|
-
status="mcp_tool_response",
|
|
1371
|
-
content=f"✅ MCP Tool Response from {tool_name} at {timestamp_str}: {formatted_response}",
|
|
1372
|
-
source="mcp_tools",
|
|
1373
|
-
)
|
|
1374
|
-
|
|
1375
|
-
log_backend_activity(
|
|
1376
|
-
"gemini",
|
|
1377
|
-
"MCP tool response received",
|
|
1378
|
-
{
|
|
1379
|
-
"tool_name": tool_name,
|
|
1380
|
-
"response_preview": str(tool_response)[:],
|
|
1381
|
-
},
|
|
1382
|
-
agent_id=agent_id,
|
|
1383
|
-
)
|
|
1384
|
-
|
|
1385
|
-
elif is_custom_tool:
|
|
1386
|
-
# Process custom tool response
|
|
1387
|
-
if custom_response_tracker.is_new_response(tool_name, tool_response):
|
|
1388
|
-
response_record = custom_response_tracker.add_response(tool_name, tool_response)
|
|
1389
|
-
|
|
1390
|
-
# Extract text from response
|
|
1391
|
-
response_text = str(tool_response)
|
|
1392
|
-
|
|
1393
|
-
timestamp_str = time.strftime(
|
|
1394
|
-
"%H:%M:%S",
|
|
1395
|
-
time.localtime(response_record["timestamp"]),
|
|
1396
|
-
)
|
|
1397
|
-
|
|
1398
|
-
# Format response as JSON if possible
|
|
1399
|
-
formatted_response = format_tool_response_as_json(response_text)
|
|
1400
|
-
|
|
1401
|
-
yield StreamChunk(
|
|
1402
|
-
type="custom_tool_status",
|
|
1403
|
-
status="custom_tool_response",
|
|
1404
|
-
content=f"✅ Custom Tool Response from {tool_name} at {timestamp_str}: {formatted_response}",
|
|
1405
|
-
source="custom_tools",
|
|
1406
|
-
)
|
|
1407
|
-
|
|
1408
|
-
log_backend_activity(
|
|
1409
|
-
"gemini",
|
|
1410
|
-
"Custom tool response received",
|
|
1411
|
-
{
|
|
1412
|
-
"tool_name": tool_name,
|
|
1413
|
-
"response_preview": str(tool_response),
|
|
1414
|
-
},
|
|
1415
|
-
agent_id=agent_id,
|
|
1416
|
-
)
|
|
1417
|
-
|
|
1418
|
-
# ============================================
|
|
1419
|
-
# Process text content
|
|
1420
|
-
# ============================================
|
|
1421
|
-
if hasattr(chunk, "text") and chunk.text:
|
|
1422
|
-
chunk_text = chunk.text
|
|
1423
|
-
full_content_text += chunk_text
|
|
1424
|
-
log_stream_chunk("backend.gemini", "continuation_content", chunk_text, agent_id)
|
|
1425
|
-
yield StreamChunk(type="content", content=chunk_text)
|
|
1426
|
-
|
|
1427
|
-
# ============================================
|
|
1428
|
-
# Buffer last chunk
|
|
1429
|
-
# ============================================
|
|
1430
|
-
if hasattr(chunk, "candidates") and chunk.candidates:
|
|
1431
|
-
last_response_with_candidates = chunk
|
|
1432
|
-
|
|
1433
|
-
except Exception as e:
|
|
1434
|
-
error_msg = f"Error in continuation call: {str(e)}"
|
|
1435
|
-
logger.error(error_msg)
|
|
1436
|
-
yield StreamChunk(
|
|
1437
|
-
type="custom_tool_status",
|
|
1438
|
-
status="continuation_error",
|
|
1439
|
-
content=f"❌ {error_msg}",
|
|
1440
|
-
source="custom_tools",
|
|
1441
|
-
)
|
|
575
|
+
# Log the coordination action
|
|
576
|
+
logger.info(f"[Gemini] Function call coordination action: {tool_name}")
|
|
577
|
+
log_tool_call(
|
|
578
|
+
agent_id,
|
|
579
|
+
tool_name,
|
|
580
|
+
tool_args,
|
|
581
|
+
None,
|
|
582
|
+
backend_name="gemini",
|
|
583
|
+
)
|
|
1442
584
|
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
585
|
+
# Build tool call in standard format
|
|
586
|
+
workflow_tool_calls.append(
|
|
587
|
+
{
|
|
588
|
+
"id": call.get("call_id", f"call_{len(workflow_tool_calls)}"),
|
|
589
|
+
"type": "function",
|
|
590
|
+
"function": {
|
|
591
|
+
"name": tool_name,
|
|
592
|
+
"arguments": tool_args,
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
)
|
|
1446
596
|
|
|
1447
|
-
#
|
|
1448
|
-
if
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
597
|
+
# Emit tool_calls chunk for orchestrator to process
|
|
598
|
+
if workflow_tool_calls:
|
|
599
|
+
log_stream_chunk("backend.gemini", "tool_calls", workflow_tool_calls, agent_id)
|
|
600
|
+
yield StreamChunk(
|
|
601
|
+
type="tool_calls",
|
|
602
|
+
tool_calls=workflow_tool_calls,
|
|
603
|
+
source="gemini",
|
|
604
|
+
)
|
|
1454
605
|
|
|
1455
|
-
|
|
606
|
+
if mcp_used:
|
|
1456
607
|
yield StreamChunk(
|
|
1457
608
|
type="mcp_status",
|
|
1458
609
|
status="mcp_session_complete",
|
|
1459
|
-
content=
|
|
610
|
+
content="✅ [MCP] Session completed",
|
|
1460
611
|
source="mcp_tools",
|
|
1461
612
|
)
|
|
1462
613
|
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
614
|
+
yield StreamChunk(type="done")
|
|
615
|
+
return
|
|
616
|
+
|
|
617
|
+
# Initialize for execution
|
|
618
|
+
updated_messages = messages.copy()
|
|
619
|
+
processed_call_ids = set()
|
|
620
|
+
|
|
621
|
+
# Configuration for custom tool execution
|
|
622
|
+
CUSTOM_TOOL_CONFIG = ToolExecutionConfig(
|
|
623
|
+
tool_type="custom",
|
|
624
|
+
chunk_type="custom_tool_status",
|
|
625
|
+
emoji_prefix="🔧 [Custom Tool]",
|
|
626
|
+
success_emoji="✅ [Custom Tool]",
|
|
627
|
+
error_emoji="❌ [Custom Tool Error]",
|
|
628
|
+
source_prefix="custom_",
|
|
629
|
+
status_called="custom_tool_called",
|
|
630
|
+
status_response="custom_tool_response",
|
|
631
|
+
status_error="custom_tool_error",
|
|
632
|
+
execution_callback=self._execute_custom_tool,
|
|
633
|
+
)
|
|
1472
634
|
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
635
|
+
# Configuration for MCP tool execution
|
|
636
|
+
MCP_TOOL_CONFIG = ToolExecutionConfig(
|
|
637
|
+
tool_type="mcp",
|
|
638
|
+
chunk_type="mcp_status",
|
|
639
|
+
emoji_prefix="🔧 [MCP Tool]",
|
|
640
|
+
success_emoji="✅ [MCP Tool]",
|
|
641
|
+
error_emoji="❌ [MCP Tool Error]",
|
|
642
|
+
source_prefix="mcp_",
|
|
643
|
+
status_called="mcp_tool_called",
|
|
644
|
+
status_response="mcp_tool_response",
|
|
645
|
+
status_error="mcp_tool_error",
|
|
646
|
+
execution_callback=self._execute_mcp_function_with_retry,
|
|
647
|
+
)
|
|
1480
648
|
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
# Determine error type
|
|
1495
|
-
is_mcp_error = isinstance(e, (MCPConnectionError, MCPTimeoutError, MCPServerError, MCPError))
|
|
1496
|
-
is_custom_tool_error = not is_mcp_error and using_custom_tools
|
|
1497
|
-
|
|
1498
|
-
# Emit user-friendly error message
|
|
1499
|
-
if is_mcp_error:
|
|
1500
|
-
async for chunk in self.mcp_manager.handle_mcp_error_and_fallback(e):
|
|
649
|
+
# Capture tool execution results for continuation loop
|
|
650
|
+
tool_results: Dict[str, str] = {}
|
|
651
|
+
self._active_tool_result_store = tool_results
|
|
652
|
+
|
|
653
|
+
try:
|
|
654
|
+
# Execute custom tools
|
|
655
|
+
for call in custom_calls:
|
|
656
|
+
async for chunk in self._execute_tool_with_logging(
|
|
657
|
+
call,
|
|
658
|
+
CUSTOM_TOOL_CONFIG,
|
|
659
|
+
updated_messages,
|
|
660
|
+
processed_call_ids,
|
|
661
|
+
):
|
|
1501
662
|
yield chunk
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
content=f"⚠️ [Custom Tools] Error: {str(e)}; falling back to non-custom-tool mode",
|
|
1507
|
-
source="custom_tools",
|
|
1508
|
-
)
|
|
1509
|
-
else:
|
|
663
|
+
|
|
664
|
+
# Check circuit breaker before MCP tool execution
|
|
665
|
+
if mcp_calls and not await self._check_circuit_breaker_before_execution():
|
|
666
|
+
logger.warning("[Gemini] All MCP servers blocked by circuit breaker")
|
|
1510
667
|
yield StreamChunk(
|
|
1511
668
|
type="mcp_status",
|
|
1512
|
-
status="
|
|
1513
|
-
content=
|
|
1514
|
-
source="
|
|
669
|
+
status="mcp_blocked",
|
|
670
|
+
content="⚠️ [MCP] All servers blocked by circuit breaker",
|
|
671
|
+
source="circuit_breaker",
|
|
1515
672
|
)
|
|
673
|
+
# Clear mcp_calls to skip execution
|
|
674
|
+
mcp_calls = []
|
|
675
|
+
|
|
676
|
+
# Execute MCP tools
|
|
677
|
+
for call in mcp_calls:
|
|
678
|
+
# Mark MCP as used when at least one MCP call is about to be executed
|
|
679
|
+
mcp_used = True
|
|
680
|
+
|
|
681
|
+
async for chunk in self._execute_tool_with_logging(
|
|
682
|
+
call,
|
|
683
|
+
MCP_TOOL_CONFIG,
|
|
684
|
+
updated_messages,
|
|
685
|
+
processed_call_ids,
|
|
686
|
+
):
|
|
687
|
+
yield chunk
|
|
688
|
+
finally:
|
|
689
|
+
self._active_tool_result_store = None
|
|
1516
690
|
|
|
1517
|
-
|
|
1518
|
-
manual_config = dict(config)
|
|
691
|
+
executed_calls = custom_calls + mcp_calls
|
|
1519
692
|
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
custom_tools_schemas = self._get_custom_tools_schemas()
|
|
1525
|
-
if custom_tools_schemas:
|
|
1526
|
-
# Convert to Gemini format using formatter
|
|
1527
|
-
custom_tools_functions = self.formatter.format_custom_tools(
|
|
1528
|
-
custom_tools_schemas,
|
|
1529
|
-
return_sdk_objects=True,
|
|
1530
|
-
)
|
|
1531
|
-
# Wrap FunctionDeclarations in a Tool object for Gemini SDK
|
|
1532
|
-
from google.genai import types
|
|
693
|
+
# Build initial conversation history using SDK Content objects
|
|
694
|
+
conversation_history: List[types.Content] = [
|
|
695
|
+
types.Content(parts=[types.Part(text=full_content)], role="user"),
|
|
696
|
+
]
|
|
1533
697
|
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
698
|
+
if executed_calls:
|
|
699
|
+
model_parts = []
|
|
700
|
+
for call in executed_calls:
|
|
701
|
+
args_payload: Any = call.get("arguments", {})
|
|
702
|
+
if isinstance(args_payload, str):
|
|
703
|
+
try:
|
|
704
|
+
args_payload = json.loads(args_payload)
|
|
705
|
+
except json.JSONDecodeError:
|
|
706
|
+
args_payload = {}
|
|
707
|
+
if not isinstance(args_payload, dict):
|
|
708
|
+
args_payload = {}
|
|
709
|
+
model_parts.append(
|
|
710
|
+
types.Part.from_function_call(
|
|
711
|
+
name=call.get("name", ""),
|
|
712
|
+
args=args_payload,
|
|
713
|
+
),
|
|
714
|
+
)
|
|
715
|
+
if model_parts:
|
|
716
|
+
conversation_history.append(types.Content(parts=model_parts, role="model"))
|
|
717
|
+
|
|
718
|
+
response_parts = []
|
|
719
|
+
for call in executed_calls:
|
|
720
|
+
call_id = call.get("call_id")
|
|
721
|
+
result_text = tool_results.get(call_id or "", "No result")
|
|
722
|
+
response_parts.append(
|
|
723
|
+
types.Part.from_function_response(
|
|
724
|
+
name=call.get("name", ""),
|
|
725
|
+
response={"result": result_text},
|
|
726
|
+
),
|
|
727
|
+
)
|
|
728
|
+
if response_parts:
|
|
729
|
+
conversation_history.append(types.Content(parts=response_parts, role="user"))
|
|
1565
730
|
|
|
1566
|
-
|
|
1567
|
-
# Both failed or cannot determine: use builtin tools
|
|
1568
|
-
if all_tools:
|
|
1569
|
-
manual_config["tools"] = all_tools
|
|
1570
|
-
logger.info("[Gemini] Fallback: using builtin tools only (all advanced tools failed)")
|
|
731
|
+
last_continuation_chunk = None
|
|
1571
732
|
|
|
1572
|
-
|
|
1573
|
-
|
|
733
|
+
while True:
|
|
734
|
+
continuation_stream = await client.aio.models.generate_content_stream(
|
|
1574
735
|
model=model_name,
|
|
1575
|
-
contents=
|
|
1576
|
-
config=
|
|
736
|
+
contents=conversation_history,
|
|
737
|
+
config=config,
|
|
1577
738
|
)
|
|
739
|
+
stream = continuation_stream
|
|
1578
740
|
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
full_content_text += chunk_text
|
|
1584
|
-
log_stream_chunk(
|
|
1585
|
-
"backend.gemini",
|
|
1586
|
-
"fallback_content",
|
|
1587
|
-
chunk_text,
|
|
1588
|
-
agent_id,
|
|
1589
|
-
)
|
|
1590
|
-
yield StreamChunk(type="content", content=chunk_text)
|
|
1591
|
-
# Buffer last chunk with candidates for fallback path
|
|
741
|
+
new_function_calls = []
|
|
742
|
+
continuation_text = ""
|
|
743
|
+
|
|
744
|
+
async for chunk in continuation_stream:
|
|
1592
745
|
if hasattr(chunk, "candidates") and chunk.candidates:
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
746
|
+
last_continuation_chunk = chunk
|
|
747
|
+
for candidate in chunk.candidates:
|
|
748
|
+
if hasattr(candidate, "content") and candidate.content:
|
|
749
|
+
if hasattr(candidate.content, "parts") and candidate.content.parts:
|
|
750
|
+
for part in candidate.content.parts:
|
|
751
|
+
if hasattr(part, "function_call") and part.function_call:
|
|
752
|
+
tool_name = part.function_call.name
|
|
753
|
+
tool_args = dict(part.function_call.args) if part.function_call.args else {}
|
|
754
|
+
call_id = f"call_{len(new_function_calls)}"
|
|
755
|
+
new_function_calls.append(
|
|
756
|
+
{
|
|
757
|
+
"call_id": call_id,
|
|
758
|
+
"name": tool_name,
|
|
759
|
+
"arguments": json.dumps(tool_args),
|
|
760
|
+
},
|
|
761
|
+
)
|
|
1603
762
|
|
|
1604
|
-
# Process streaming chunks
|
|
1605
|
-
async for chunk in stream:
|
|
1606
|
-
# Process text content
|
|
1607
763
|
if hasattr(chunk, "text") and chunk.text:
|
|
1608
764
|
chunk_text = chunk.text
|
|
1609
|
-
|
|
765
|
+
continuation_text += chunk_text
|
|
1610
766
|
log_backend_agent_message(
|
|
1611
767
|
agent_id,
|
|
1612
768
|
"RECV",
|
|
@@ -1615,86 +771,316 @@ class GeminiBackend(CustomToolAndMCPBackend):
|
|
|
1615
771
|
)
|
|
1616
772
|
log_stream_chunk("backend.gemini", "content", chunk_text, agent_id)
|
|
1617
773
|
yield StreamChunk(type="content", content=chunk_text)
|
|
1618
|
-
# Buffer last chunk with candidates for non-MCP path
|
|
1619
|
-
if hasattr(chunk, "candidates") and chunk.candidates:
|
|
1620
|
-
last_response_with_candidates = chunk
|
|
1621
774
|
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
775
|
+
if continuation_text:
|
|
776
|
+
conversation_history.append(
|
|
777
|
+
types.Content(parts=[types.Part(text=continuation_text)], role="model"),
|
|
778
|
+
)
|
|
779
|
+
full_content_text += continuation_text
|
|
780
|
+
|
|
781
|
+
if last_continuation_chunk:
|
|
782
|
+
last_response_with_candidates = last_continuation_chunk
|
|
783
|
+
|
|
784
|
+
if not new_function_calls:
|
|
785
|
+
# ====================================================================
|
|
786
|
+
# Continuation Structured Coordination Output Parsing
|
|
787
|
+
# ====================================================================
|
|
788
|
+
# Check for structured coordination output when no function calls in continuation
|
|
789
|
+
if is_coordination and full_content_text:
|
|
790
|
+
# Try to parse structured response from accumulated text content
|
|
791
|
+
parsed = self.formatter.extract_structured_response(full_content_text)
|
|
792
|
+
|
|
793
|
+
if parsed and isinstance(parsed, dict):
|
|
794
|
+
# Convert structured response to tool calls
|
|
795
|
+
tool_calls = self.formatter.convert_structured_to_tool_calls(parsed)
|
|
796
|
+
|
|
797
|
+
if tool_calls:
|
|
798
|
+
# Categorize the tool calls
|
|
799
|
+
mcp_calls, custom_calls, provider_calls = self._categorize_tool_calls(tool_calls)
|
|
800
|
+
|
|
801
|
+
if provider_calls:
|
|
802
|
+
# Convert provider calls to tool_calls format for orchestrator
|
|
803
|
+
workflow_tool_calls = []
|
|
804
|
+
for call in provider_calls:
|
|
805
|
+
tool_name = call.get("name", "")
|
|
806
|
+
tool_args_str = call.get("arguments", "{}")
|
|
807
|
+
|
|
808
|
+
# Parse arguments if they're a string
|
|
809
|
+
if isinstance(tool_args_str, str):
|
|
810
|
+
try:
|
|
811
|
+
tool_args = json.loads(tool_args_str)
|
|
812
|
+
except json.JSONDecodeError:
|
|
813
|
+
tool_args = {}
|
|
814
|
+
else:
|
|
815
|
+
tool_args = tool_args_str
|
|
816
|
+
|
|
817
|
+
# Log the coordination action
|
|
818
|
+
logger.info(f"[Gemini] Continuation structured coordination action: {tool_name}")
|
|
819
|
+
log_tool_call(
|
|
820
|
+
agent_id,
|
|
821
|
+
tool_name,
|
|
822
|
+
tool_args,
|
|
823
|
+
None,
|
|
824
|
+
backend_name="gemini",
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
# Build tool call in standard format
|
|
828
|
+
workflow_tool_calls.append(
|
|
829
|
+
{
|
|
830
|
+
"id": call.get("call_id", f"call_{len(workflow_tool_calls)}"),
|
|
831
|
+
"type": "function",
|
|
832
|
+
"function": {
|
|
833
|
+
"name": tool_name,
|
|
834
|
+
"arguments": tool_args,
|
|
835
|
+
},
|
|
836
|
+
},
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
# Emit tool_calls chunk for orchestrator to process
|
|
840
|
+
if workflow_tool_calls:
|
|
841
|
+
log_stream_chunk("backend.gemini", "tool_calls", workflow_tool_calls, agent_id)
|
|
842
|
+
yield StreamChunk(
|
|
843
|
+
type="tool_calls",
|
|
844
|
+
tool_calls=workflow_tool_calls,
|
|
845
|
+
source="gemini",
|
|
846
|
+
)
|
|
847
|
+
|
|
848
|
+
if mcp_used:
|
|
849
|
+
yield StreamChunk(
|
|
850
|
+
type="mcp_status",
|
|
851
|
+
status="mcp_session_complete",
|
|
852
|
+
content="✅ [MCP] Session completed",
|
|
853
|
+
source="mcp_tools",
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
yield StreamChunk(type="done")
|
|
857
|
+
return
|
|
858
|
+
|
|
859
|
+
# No structured output found, break continuation loop
|
|
860
|
+
break
|
|
861
|
+
|
|
862
|
+
next_mcp_calls, next_custom_calls, provider_calls = self._categorize_tool_calls(new_function_calls)
|
|
863
|
+
|
|
864
|
+
# Handle provider calls emitted during continuation
|
|
865
|
+
if provider_calls:
|
|
866
|
+
workflow_tool_calls = []
|
|
867
|
+
for call in provider_calls:
|
|
868
|
+
tool_name = call.get("name", "")
|
|
869
|
+
tool_args_str = call.get("arguments", "{}")
|
|
870
|
+
|
|
871
|
+
if isinstance(tool_args_str, str):
|
|
872
|
+
try:
|
|
873
|
+
tool_args = json.loads(tool_args_str)
|
|
874
|
+
except json.JSONDecodeError:
|
|
875
|
+
tool_args = {}
|
|
876
|
+
else:
|
|
877
|
+
tool_args = tool_args_str
|
|
878
|
+
|
|
879
|
+
logger.info(f"[Gemini] Continuation coordination action: {tool_name}")
|
|
880
|
+
log_tool_call(
|
|
881
|
+
agent_id,
|
|
882
|
+
tool_name,
|
|
883
|
+
tool_args,
|
|
884
|
+
None,
|
|
885
|
+
backend_name="gemini",
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
workflow_tool_calls.append(
|
|
889
|
+
{
|
|
890
|
+
"id": call.get("call_id", f"call_{len(workflow_tool_calls)}"),
|
|
891
|
+
"type": "function",
|
|
892
|
+
"function": {
|
|
893
|
+
"name": tool_name,
|
|
894
|
+
"arguments": tool_args,
|
|
895
|
+
},
|
|
896
|
+
},
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
if workflow_tool_calls:
|
|
900
|
+
log_stream_chunk("backend.gemini", "tool_calls", workflow_tool_calls, agent_id)
|
|
901
|
+
yield StreamChunk(
|
|
902
|
+
type="tool_calls",
|
|
903
|
+
tool_calls=workflow_tool_calls,
|
|
904
|
+
source="gemini",
|
|
905
|
+
)
|
|
906
|
+
|
|
907
|
+
if mcp_used:
|
|
908
|
+
yield StreamChunk(
|
|
909
|
+
type="mcp_status",
|
|
910
|
+
status="mcp_session_complete",
|
|
911
|
+
content="✅ [MCP] Session completed",
|
|
912
|
+
source="mcp_tools",
|
|
913
|
+
)
|
|
914
|
+
|
|
915
|
+
yield StreamChunk(type="done")
|
|
916
|
+
return
|
|
917
|
+
|
|
918
|
+
new_tool_results: Dict[str, str] = {}
|
|
919
|
+
self._active_tool_result_store = new_tool_results
|
|
920
|
+
|
|
921
|
+
try:
|
|
922
|
+
for call in next_custom_calls:
|
|
923
|
+
async for chunk in self._execute_tool_with_logging(
|
|
924
|
+
call,
|
|
925
|
+
CUSTOM_TOOL_CONFIG,
|
|
926
|
+
updated_messages,
|
|
927
|
+
processed_call_ids,
|
|
928
|
+
):
|
|
929
|
+
yield chunk
|
|
930
|
+
|
|
931
|
+
if next_mcp_calls and not await self._check_circuit_breaker_before_execution():
|
|
932
|
+
logger.warning("[Gemini] All MCP servers blocked by circuit breaker during continuation")
|
|
933
|
+
yield StreamChunk(
|
|
934
|
+
type="mcp_status",
|
|
935
|
+
status="mcp_blocked",
|
|
936
|
+
content="⚠️ [MCP] All servers blocked by circuit breaker",
|
|
937
|
+
source="circuit_breaker",
|
|
938
|
+
)
|
|
939
|
+
next_mcp_calls = []
|
|
940
|
+
|
|
941
|
+
for call in next_mcp_calls:
|
|
942
|
+
mcp_used = True
|
|
943
|
+
|
|
944
|
+
async for chunk in self._execute_tool_with_logging(
|
|
945
|
+
call,
|
|
946
|
+
MCP_TOOL_CONFIG,
|
|
947
|
+
updated_messages,
|
|
948
|
+
processed_call_ids,
|
|
949
|
+
):
|
|
950
|
+
yield chunk
|
|
951
|
+
finally:
|
|
952
|
+
self._active_tool_result_store = None
|
|
953
|
+
|
|
954
|
+
if new_tool_results:
|
|
955
|
+
tool_results.update(new_tool_results)
|
|
956
|
+
|
|
957
|
+
executed_calls = next_custom_calls + next_mcp_calls
|
|
958
|
+
|
|
959
|
+
if executed_calls:
|
|
960
|
+
model_parts = []
|
|
961
|
+
for call in executed_calls:
|
|
962
|
+
args_payload: Any = call.get("arguments", {})
|
|
963
|
+
if isinstance(args_payload, str):
|
|
964
|
+
try:
|
|
965
|
+
args_payload = json.loads(args_payload)
|
|
966
|
+
except json.JSONDecodeError:
|
|
967
|
+
args_payload = {}
|
|
968
|
+
if not isinstance(args_payload, dict):
|
|
969
|
+
args_payload = {}
|
|
970
|
+
model_parts.append(
|
|
971
|
+
types.Part.from_function_call(
|
|
972
|
+
name=call.get("name", ""),
|
|
973
|
+
args=args_payload,
|
|
974
|
+
),
|
|
975
|
+
)
|
|
976
|
+
if model_parts:
|
|
977
|
+
conversation_history.append(types.Content(parts=model_parts, role="model"))
|
|
978
|
+
|
|
979
|
+
response_parts = []
|
|
980
|
+
for call in executed_calls:
|
|
981
|
+
call_id = call.get("call_id")
|
|
982
|
+
result_text = new_tool_results.get(call_id or "", "No result")
|
|
983
|
+
response_parts.append(
|
|
984
|
+
types.Part.from_function_response(
|
|
985
|
+
name=call.get("name", ""),
|
|
986
|
+
response={"result": result_text},
|
|
987
|
+
),
|
|
988
|
+
)
|
|
989
|
+
if response_parts:
|
|
990
|
+
conversation_history.append(types.Content(parts=response_parts, role="user"))
|
|
1631
991
|
|
|
1632
|
-
|
|
992
|
+
# ====================================================================
|
|
993
|
+
# Completion Phase: Process structured tool calls and builtin indicators
|
|
994
|
+
# ====================================================================
|
|
995
|
+
final_response = last_response_with_candidates
|
|
1633
996
|
|
|
1634
|
-
# Process tool calls - coordination and post-evaluation tool calls (MCP manual mode removed)
|
|
1635
997
|
tool_calls_detected: List[Dict[str, Any]] = []
|
|
1636
998
|
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
# For structured output mode, the entire content is JSON
|
|
999
|
+
if (is_coordination or is_post_evaluation) and full_content_text.strip():
|
|
1000
|
+
content = full_content_text
|
|
1640
1001
|
structured_response = None
|
|
1641
|
-
|
|
1002
|
+
|
|
1642
1003
|
try:
|
|
1643
|
-
# Strategy 1: Parse entire content as JSON (works for both modes)
|
|
1644
1004
|
structured_response = json.loads(content.strip())
|
|
1645
1005
|
except json.JSONDecodeError:
|
|
1646
|
-
# Strategy 2: Extract JSON from mixed text content (handles markdown-wrapped JSON)
|
|
1647
1006
|
structured_response = self.formatter.extract_structured_response(content)
|
|
1648
1007
|
|
|
1649
|
-
if structured_response and isinstance(structured_response, dict) and "action_type"
|
|
1650
|
-
|
|
1651
|
-
tool_calls = self.formatter.convert_structured_to_tool_calls(structured_response)
|
|
1652
|
-
if tool_calls:
|
|
1653
|
-
tool_calls_detected = tool_calls
|
|
1654
|
-
# Log conversion to tool calls (summary)
|
|
1655
|
-
log_stream_chunk("backend.gemini", "tool_calls", tool_calls, agent_id)
|
|
1008
|
+
if structured_response and isinstance(structured_response, dict) and structured_response.get("action_type"):
|
|
1009
|
+
raw_tool_calls = self.formatter.convert_structured_to_tool_calls(structured_response)
|
|
1656
1010
|
|
|
1657
|
-
|
|
1011
|
+
if raw_tool_calls:
|
|
1658
1012
|
tool_type = "post_evaluation" if is_post_evaluation else "coordination"
|
|
1659
|
-
|
|
1660
|
-
|
|
1013
|
+
workflow_tool_calls: List[Dict[str, Any]] = []
|
|
1014
|
+
|
|
1015
|
+
for call in raw_tool_calls:
|
|
1016
|
+
tool_name = call.get("name", "")
|
|
1017
|
+
tool_args_str = call.get("arguments", "{}")
|
|
1018
|
+
|
|
1019
|
+
if isinstance(tool_args_str, str):
|
|
1020
|
+
try:
|
|
1021
|
+
tool_args = json.loads(tool_args_str)
|
|
1022
|
+
except json.JSONDecodeError:
|
|
1023
|
+
tool_args = {}
|
|
1024
|
+
else:
|
|
1025
|
+
tool_args = tool_args_str
|
|
1026
|
+
|
|
1027
|
+
try:
|
|
1661
1028
|
log_tool_call(
|
|
1662
1029
|
agent_id,
|
|
1663
|
-
|
|
1664
|
-
|
|
1030
|
+
tool_name or f"unknown_{tool_type}_tool",
|
|
1031
|
+
tool_args,
|
|
1665
1032
|
result=f"{tool_type}_tool_called",
|
|
1666
1033
|
backend_name="gemini",
|
|
1667
1034
|
)
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1035
|
+
except Exception:
|
|
1036
|
+
pass
|
|
1037
|
+
|
|
1038
|
+
workflow_tool_calls.append(
|
|
1039
|
+
{
|
|
1040
|
+
"id": call.get("call_id", f"call_{len(workflow_tool_calls)}"),
|
|
1041
|
+
"type": "function",
|
|
1042
|
+
"function": {
|
|
1043
|
+
"name": tool_name,
|
|
1044
|
+
"arguments": tool_args,
|
|
1045
|
+
},
|
|
1046
|
+
},
|
|
1047
|
+
)
|
|
1671
1048
|
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1049
|
+
if workflow_tool_calls:
|
|
1050
|
+
tool_calls_detected = workflow_tool_calls
|
|
1051
|
+
log_stream_chunk("backend.gemini", "tool_calls", workflow_tool_calls, agent_id)
|
|
1052
|
+
|
|
1053
|
+
if tool_calls_detected:
|
|
1054
|
+
yield StreamChunk(type="tool_calls", tool_calls=tool_calls_detected, source="gemini")
|
|
1055
|
+
|
|
1056
|
+
if mcp_used:
|
|
1057
|
+
yield StreamChunk(
|
|
1058
|
+
type="mcp_status",
|
|
1059
|
+
status="mcp_session_complete",
|
|
1060
|
+
content="✅ [MCP] Session completed",
|
|
1061
|
+
source="mcp_tools",
|
|
1062
|
+
)
|
|
1063
|
+
|
|
1064
|
+
yield StreamChunk(type="done")
|
|
1065
|
+
return
|
|
1675
1066
|
|
|
1676
|
-
# Process builtin tool results if any tools were used
|
|
1677
1067
|
if builtin_tools and final_response and hasattr(final_response, "candidates") and final_response.candidates:
|
|
1678
|
-
# Check for grounding or code execution results
|
|
1679
1068
|
candidate = final_response.candidates[0]
|
|
1680
1069
|
|
|
1681
|
-
# Check for web search results - only show if actually used
|
|
1682
1070
|
if hasattr(candidate, "grounding_metadata") and candidate.grounding_metadata:
|
|
1683
|
-
# Check if web search was actually used by looking for queries or chunks
|
|
1684
1071
|
search_actually_used = False
|
|
1685
|
-
search_queries = []
|
|
1072
|
+
search_queries: List[str] = []
|
|
1686
1073
|
|
|
1687
|
-
# Look for web search queries
|
|
1688
1074
|
if hasattr(candidate.grounding_metadata, "web_search_queries") and candidate.grounding_metadata.web_search_queries:
|
|
1689
1075
|
try:
|
|
1690
1076
|
for query in candidate.grounding_metadata.web_search_queries:
|
|
1691
|
-
if query and query.strip():
|
|
1692
|
-
|
|
1077
|
+
if query and isinstance(query, str) and query.strip():
|
|
1078
|
+
trimmed_query = query.strip()
|
|
1079
|
+
search_queries.append(trimmed_query)
|
|
1693
1080
|
search_actually_used = True
|
|
1694
1081
|
except (TypeError, AttributeError):
|
|
1695
1082
|
pass
|
|
1696
1083
|
|
|
1697
|
-
# Look for grounding chunks (indicates actual search results)
|
|
1698
1084
|
if hasattr(candidate.grounding_metadata, "grounding_chunks") and candidate.grounding_metadata.grounding_chunks:
|
|
1699
1085
|
try:
|
|
1700
1086
|
if len(candidate.grounding_metadata.grounding_chunks) > 0:
|
|
@@ -1702,9 +1088,7 @@ class GeminiBackend(CustomToolAndMCPBackend):
|
|
|
1702
1088
|
except (TypeError, AttributeError):
|
|
1703
1089
|
pass
|
|
1704
1090
|
|
|
1705
|
-
# Only show indicators if search was actually used
|
|
1706
1091
|
if search_actually_used:
|
|
1707
|
-
# Enhanced web search logging
|
|
1708
1092
|
log_stream_chunk(
|
|
1709
1093
|
"backend.gemini",
|
|
1710
1094
|
"web_search_result",
|
|
@@ -1716,17 +1100,17 @@ class GeminiBackend(CustomToolAndMCPBackend):
|
|
|
1716
1100
|
"google_search_retrieval",
|
|
1717
1101
|
{
|
|
1718
1102
|
"queries": search_queries,
|
|
1719
|
-
"chunks_found": len(
|
|
1103
|
+
"chunks_found": len(getattr(candidate.grounding_metadata, "grounding_chunks", []) or []),
|
|
1720
1104
|
},
|
|
1721
1105
|
result="search_completed",
|
|
1722
1106
|
backend_name="gemini",
|
|
1723
1107
|
)
|
|
1108
|
+
|
|
1724
1109
|
yield StreamChunk(
|
|
1725
1110
|
type="content",
|
|
1726
1111
|
content="🔍 [Builtin Tool: Web Search] Results integrated\n",
|
|
1727
1112
|
)
|
|
1728
1113
|
|
|
1729
|
-
# Show search queries
|
|
1730
1114
|
for query in search_queries:
|
|
1731
1115
|
log_stream_chunk(
|
|
1732
1116
|
"backend.gemini",
|
|
@@ -1738,240 +1122,308 @@ class GeminiBackend(CustomToolAndMCPBackend):
|
|
|
1738
1122
|
|
|
1739
1123
|
self.search_count += 1
|
|
1740
1124
|
|
|
1741
|
-
|
|
1125
|
+
enable_code_execution = bool(
|
|
1126
|
+
all_params.get("enable_code_execution") or all_params.get("code_execution"),
|
|
1127
|
+
)
|
|
1128
|
+
|
|
1742
1129
|
if enable_code_execution and hasattr(candidate, "content") and hasattr(candidate.content, "parts"):
|
|
1743
|
-
|
|
1744
|
-
|
|
1130
|
+
code_parts: List[str] = []
|
|
1131
|
+
|
|
1745
1132
|
for part in candidate.content.parts:
|
|
1746
1133
|
if hasattr(part, "executable_code") and part.executable_code:
|
|
1747
1134
|
code_content = getattr(part.executable_code, "code", str(part.executable_code))
|
|
1748
1135
|
code_parts.append(f"Code: {code_content}")
|
|
1749
1136
|
elif hasattr(part, "code_execution_result") and part.code_execution_result:
|
|
1750
|
-
result_content = getattr(
|
|
1751
|
-
part.code_execution_result,
|
|
1752
|
-
"output",
|
|
1753
|
-
str(part.code_execution_result),
|
|
1754
|
-
)
|
|
1137
|
+
result_content = getattr(part.code_execution_result, "output", str(part.code_execution_result))
|
|
1755
1138
|
code_parts.append(f"Result: {result_content}")
|
|
1756
1139
|
|
|
1757
1140
|
if code_parts:
|
|
1758
|
-
# Code execution was actually used
|
|
1759
1141
|
log_stream_chunk(
|
|
1760
1142
|
"backend.gemini",
|
|
1761
1143
|
"code_execution",
|
|
1762
1144
|
"Code executed",
|
|
1763
1145
|
agent_id,
|
|
1764
1146
|
)
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
result="code_executed",
|
|
1773
|
-
backend_name="gemini",
|
|
1774
|
-
)
|
|
1775
|
-
except Exception:
|
|
1776
|
-
pass
|
|
1147
|
+
log_tool_call(
|
|
1148
|
+
agent_id,
|
|
1149
|
+
"code_execution",
|
|
1150
|
+
{"details": code_parts},
|
|
1151
|
+
result="code_execution_completed",
|
|
1152
|
+
backend_name="gemini",
|
|
1153
|
+
)
|
|
1777
1154
|
|
|
1778
1155
|
yield StreamChunk(
|
|
1779
1156
|
type="content",
|
|
1780
|
-
content="
|
|
1157
|
+
content="🧮 [Builtin Tool: Code Execution] Results integrated\n",
|
|
1781
1158
|
)
|
|
1782
|
-
|
|
1783
|
-
for
|
|
1784
|
-
|
|
1785
|
-
code_content = part[6:] # Remove "Code: " prefix
|
|
1786
|
-
log_stream_chunk(
|
|
1787
|
-
"backend.gemini",
|
|
1788
|
-
"code_execution_result",
|
|
1789
|
-
{
|
|
1790
|
-
"code_parts": len(code_parts),
|
|
1791
|
-
"execution_successful": True,
|
|
1792
|
-
"snippet": code_content,
|
|
1793
|
-
},
|
|
1794
|
-
agent_id,
|
|
1795
|
-
)
|
|
1796
|
-
yield StreamChunk(
|
|
1797
|
-
type="content",
|
|
1798
|
-
content=f"💻 [Code Executed]\n```python\n{code_content}\n```\n",
|
|
1799
|
-
)
|
|
1800
|
-
elif part.startswith("Result: "):
|
|
1801
|
-
result_content = part[8:] # Remove "Result: " prefix
|
|
1802
|
-
log_stream_chunk(
|
|
1803
|
-
"backend.gemini",
|
|
1804
|
-
"code_execution_result",
|
|
1805
|
-
{
|
|
1806
|
-
"code_parts": len(code_parts),
|
|
1807
|
-
"execution_successful": True,
|
|
1808
|
-
"result": result_content,
|
|
1809
|
-
},
|
|
1810
|
-
agent_id,
|
|
1811
|
-
)
|
|
1812
|
-
yield StreamChunk(
|
|
1813
|
-
type="content",
|
|
1814
|
-
content=f"📊 [Result] {result_content}\n",
|
|
1815
|
-
)
|
|
1159
|
+
|
|
1160
|
+
for entry in code_parts:
|
|
1161
|
+
yield StreamChunk(type="content", content=f"🧮 {entry}\n")
|
|
1816
1162
|
|
|
1817
1163
|
self.code_execution_count += 1
|
|
1818
1164
|
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
"
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1165
|
+
elif final_response and hasattr(final_response, "candidates"):
|
|
1166
|
+
for candidate in final_response.candidates:
|
|
1167
|
+
if hasattr(candidate, "grounding_metadata"):
|
|
1168
|
+
self.search_count += 1
|
|
1169
|
+
logger.debug(f"[Gemini] Grounding (web search) used, count: {self.search_count}")
|
|
1170
|
+
|
|
1171
|
+
if hasattr(candidate, "content") and candidate.content:
|
|
1172
|
+
if hasattr(candidate.content, "parts"):
|
|
1173
|
+
for part in candidate.content.parts:
|
|
1174
|
+
if hasattr(part, "executable_code") or hasattr(part, "code_execution_result"):
|
|
1175
|
+
self.code_execution_count += 1
|
|
1176
|
+
logger.debug(f"[Gemini] Code execution used, count: {self.code_execution_count}")
|
|
1177
|
+
break
|
|
1178
|
+
|
|
1179
|
+
# Emit completion status
|
|
1180
|
+
if mcp_used:
|
|
1181
|
+
yield StreamChunk(
|
|
1182
|
+
type="mcp_status",
|
|
1183
|
+
status="mcp_session_complete",
|
|
1184
|
+
content="✅ [MCP] Session completed",
|
|
1185
|
+
source="mcp_tools",
|
|
1830
1186
|
)
|
|
1831
|
-
yield StreamChunk(type="tool_calls", tool_calls=tool_calls_detected)
|
|
1832
1187
|
|
|
1833
|
-
# Build complete message
|
|
1834
|
-
complete_message = {"role": "assistant", "content": content.strip()}
|
|
1835
|
-
if tool_calls_detected:
|
|
1836
|
-
complete_message["tool_calls"] = tool_calls_detected
|
|
1837
|
-
|
|
1838
|
-
# Enhanced complete message logging with metadata
|
|
1839
|
-
log_stream_chunk(
|
|
1840
|
-
"backend.gemini",
|
|
1841
|
-
"complete_message",
|
|
1842
|
-
{
|
|
1843
|
-
"content_length": len(content.strip()),
|
|
1844
|
-
"has_tool_calls": bool(tool_calls_detected),
|
|
1845
|
-
},
|
|
1846
|
-
agent_id,
|
|
1847
|
-
)
|
|
1848
|
-
yield StreamChunk(type="complete_message", complete_message=complete_message)
|
|
1849
|
-
log_stream_chunk("backend.gemini", "done", None, agent_id)
|
|
1850
1188
|
yield StreamChunk(type="done")
|
|
1851
1189
|
|
|
1852
1190
|
except Exception as e:
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
"backend.gemini",
|
|
1857
|
-
"stream_error",
|
|
1858
|
-
{"error_type": type(e).__name__, "error_message": str(e)},
|
|
1859
|
-
agent_id,
|
|
1860
|
-
)
|
|
1861
|
-
yield StreamChunk(type="error", error=error_msg)
|
|
1191
|
+
logger.error(f"[Gemini] Error in stream_with_tools: {e}")
|
|
1192
|
+
raise
|
|
1193
|
+
|
|
1862
1194
|
finally:
|
|
1863
|
-
|
|
1864
|
-
await self.mcp_manager.cleanup_genai_resources(stream, client)
|
|
1865
|
-
# Ensure context manager exit for MCP cleanup
|
|
1866
|
-
try:
|
|
1867
|
-
await self.__aexit__(None, None, None)
|
|
1868
|
-
except Exception as e:
|
|
1869
|
-
log_backend_activity(
|
|
1870
|
-
"gemini",
|
|
1871
|
-
"MCP cleanup failed",
|
|
1872
|
-
{"error": str(e)},
|
|
1873
|
-
agent_id=self.agent_id,
|
|
1874
|
-
)
|
|
1195
|
+
await self._cleanup_genai_resources(stream, client)
|
|
1875
1196
|
|
|
1876
|
-
def
|
|
1877
|
-
|
|
1878
|
-
|
|
1197
|
+
async def _try_close_resource(
|
|
1198
|
+
self,
|
|
1199
|
+
resource: Any,
|
|
1200
|
+
method_names: tuple,
|
|
1201
|
+
resource_label: str,
|
|
1202
|
+
) -> bool:
|
|
1203
|
+
"""Try to close a resource using one of the provided method names.
|
|
1879
1204
|
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1205
|
+
Args:
|
|
1206
|
+
resource: Object to close
|
|
1207
|
+
method_names: Method names to try (e.g., ("aclose", "close"))
|
|
1208
|
+
resource_label: Label for error logging
|
|
1883
1209
|
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1210
|
+
Returns:
|
|
1211
|
+
True if closed successfully, False otherwise
|
|
1212
|
+
"""
|
|
1213
|
+
if resource is None:
|
|
1214
|
+
return False
|
|
1887
1215
|
|
|
1888
|
-
|
|
1216
|
+
for method_name in method_names:
|
|
1217
|
+
close_method = getattr(resource, method_name, None)
|
|
1218
|
+
if close_method is not None:
|
|
1219
|
+
try:
|
|
1220
|
+
result = close_method()
|
|
1221
|
+
if hasattr(result, "__await__"):
|
|
1222
|
+
await result
|
|
1223
|
+
return True
|
|
1224
|
+
except Exception as e:
|
|
1225
|
+
log_backend_activity(
|
|
1226
|
+
"gemini",
|
|
1227
|
+
f"{resource_label} cleanup failed",
|
|
1228
|
+
{"error": str(e), "method": method_name},
|
|
1229
|
+
agent_id=self.agent_id,
|
|
1230
|
+
)
|
|
1231
|
+
return False
|
|
1232
|
+
return False
|
|
1233
|
+
|
|
1234
|
+
async def _cleanup_genai_resources(self, stream, client) -> None:
|
|
1235
|
+
"""Cleanup google-genai resources to avoid unclosed aiohttp sessions.
|
|
1236
|
+
|
|
1237
|
+
Cleanup order is critical: stream → session → transport → client.
|
|
1238
|
+
Each resource is cleaned independently with error isolation.
|
|
1889
1239
|
"""
|
|
1890
|
-
|
|
1240
|
+
# 1. Close stream
|
|
1241
|
+
await self._try_close_resource(stream, ("aclose", "close"), "Stream")
|
|
1242
|
+
|
|
1243
|
+
# 2. Close internal aiohttp session (requires special handling)
|
|
1244
|
+
if client is not None:
|
|
1245
|
+
base_client = getattr(client, "_api_client", None)
|
|
1246
|
+
if base_client is not None:
|
|
1247
|
+
session = getattr(base_client, "_aiohttp_session", None)
|
|
1248
|
+
if session is not None and not getattr(session, "closed", True):
|
|
1249
|
+
try:
|
|
1250
|
+
await session.close()
|
|
1251
|
+
log_backend_activity(
|
|
1252
|
+
"gemini",
|
|
1253
|
+
"Closed google-genai aiohttp session",
|
|
1254
|
+
{},
|
|
1255
|
+
agent_id=self.agent_id,
|
|
1256
|
+
)
|
|
1257
|
+
base_client._aiohttp_session = None
|
|
1258
|
+
# Yield control to allow connector cleanup
|
|
1259
|
+
await asyncio.sleep(0)
|
|
1260
|
+
except Exception as e:
|
|
1261
|
+
log_backend_activity(
|
|
1262
|
+
"gemini",
|
|
1263
|
+
"Failed to close google-genai aiohttp session",
|
|
1264
|
+
{"error": str(e)},
|
|
1265
|
+
agent_id=self.agent_id,
|
|
1266
|
+
)
|
|
1891
1267
|
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1268
|
+
# 3. Close internal async transport
|
|
1269
|
+
if client is not None:
|
|
1270
|
+
aio_obj = getattr(client, "aio", None)
|
|
1271
|
+
await self._try_close_resource(aio_obj, ("close", "stop"), "Client AIO")
|
|
1272
|
+
|
|
1273
|
+
# 4. Close client
|
|
1274
|
+
await self._try_close_resource(client, ("aclose", "close"), "Client")
|
|
1275
|
+
|
|
1276
|
+
def _append_tool_result_message(
|
|
1277
|
+
self,
|
|
1278
|
+
updated_messages: List[Dict[str, Any]],
|
|
1279
|
+
call: Dict[str, Any],
|
|
1280
|
+
result: Any,
|
|
1281
|
+
tool_type: str,
|
|
1282
|
+
) -> None:
|
|
1283
|
+
"""Append tool result to messages in Gemini conversation format.
|
|
1284
|
+
|
|
1285
|
+
Gemini uses a different message format than OpenAI/Response API.
|
|
1286
|
+
We need to append messages in a format that Gemini SDK can understand
|
|
1287
|
+
when making recursive calls.
|
|
1288
|
+
|
|
1289
|
+
Args:
|
|
1290
|
+
updated_messages: Message list to append to
|
|
1291
|
+
call: Tool call dictionary with call_id, name, arguments
|
|
1292
|
+
result: Tool execution result
|
|
1293
|
+
tool_type: "custom" or "mcp"
|
|
1898
1294
|
"""
|
|
1899
|
-
|
|
1900
|
-
"
|
|
1901
|
-
"
|
|
1902
|
-
"
|
|
1903
|
-
"summary": self.mcp_extractor.get_summary(),
|
|
1295
|
+
tool_result_msg = {
|
|
1296
|
+
"role": "tool",
|
|
1297
|
+
"name": call.get("name", ""),
|
|
1298
|
+
"content": str(result),
|
|
1904
1299
|
}
|
|
1300
|
+
updated_messages.append(tool_result_msg)
|
|
1905
1301
|
|
|
1906
|
-
|
|
1907
|
-
""
|
|
1908
|
-
|
|
1302
|
+
tool_results_store = getattr(self, "_active_tool_result_store", None)
|
|
1303
|
+
call_id = call.get("call_id")
|
|
1304
|
+
if isinstance(tool_results_store, dict) and call_id:
|
|
1305
|
+
tool_results_store[call_id] = str(result)
|
|
1909
1306
|
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1307
|
+
def _append_tool_error_message(
|
|
1308
|
+
self,
|
|
1309
|
+
updated_messages: List[Dict[str, Any]],
|
|
1310
|
+
call: Dict[str, Any],
|
|
1311
|
+
error_msg: str,
|
|
1312
|
+
tool_type: str,
|
|
1313
|
+
) -> None:
|
|
1314
|
+
"""Append tool error to messages in Gemini conversation format.
|
|
1914
1315
|
|
|
1915
|
-
|
|
1316
|
+
Args:
|
|
1317
|
+
updated_messages: Message list to append to
|
|
1318
|
+
call: Tool call dictionary with call_id, name, arguments
|
|
1319
|
+
error_msg: Error message string
|
|
1320
|
+
tool_type: "custom" or "mcp"
|
|
1916
1321
|
"""
|
|
1917
|
-
|
|
1322
|
+
# Append error as function result
|
|
1323
|
+
error_result_msg = {
|
|
1324
|
+
"role": "tool",
|
|
1325
|
+
"name": call.get("name", ""),
|
|
1326
|
+
"content": f"Error: {error_msg}",
|
|
1327
|
+
}
|
|
1328
|
+
updated_messages.append(error_result_msg)
|
|
1918
1329
|
|
|
1919
|
-
|
|
1920
|
-
|
|
1330
|
+
tool_results_store = getattr(self, "_active_tool_result_store", None)
|
|
1331
|
+
call_id = call.get("call_id")
|
|
1332
|
+
if isinstance(tool_results_store, dict) and call_id:
|
|
1333
|
+
tool_results_store[call_id] = f"Error: {error_msg}"
|
|
1334
|
+
|
|
1335
|
+
async def _execute_custom_tool(self, call: Dict[str, Any]) -> AsyncGenerator[CustomToolChunk, None]:
|
|
1336
|
+
"""Execute custom tool with streaming support - async generator for base class.
|
|
1337
|
+
|
|
1338
|
+
This method is called by _execute_tool_with_logging and yields CustomToolChunk
|
|
1339
|
+
objects for intermediate streaming output. The base class detects the async
|
|
1340
|
+
generator and streams intermediate results to users in real-time.
|
|
1341
|
+
|
|
1342
|
+
Args:
|
|
1343
|
+
call: Tool call dictionary with name and arguments
|
|
1344
|
+
|
|
1345
|
+
Yields:
|
|
1346
|
+
CustomToolChunk objects with streaming data
|
|
1347
|
+
|
|
1348
|
+
Note:
|
|
1349
|
+
- Intermediate chunks (completed=False) are streamed to users in real-time
|
|
1350
|
+
- Final chunk (completed=True) contains the accumulated result for message history
|
|
1351
|
+
- The base class automatically handles extracting and displaying intermediate chunks
|
|
1921
1352
|
"""
|
|
1922
|
-
|
|
1353
|
+
async for chunk in self.stream_custom_tool_execution(call):
|
|
1354
|
+
yield chunk
|
|
1355
|
+
|
|
1356
|
+
def get_provider_name(self) -> str:
|
|
1357
|
+
"""Get the provider name."""
|
|
1358
|
+
return "Gemini"
|
|
1359
|
+
|
|
1360
|
+
def get_filesystem_support(self) -> FilesystemSupport:
|
|
1361
|
+
"""Gemini supports filesystem through MCP servers."""
|
|
1362
|
+
return FilesystemSupport.MCP
|
|
1923
1363
|
|
|
1924
|
-
def
|
|
1925
|
-
"""
|
|
1926
|
-
|
|
1364
|
+
def get_supported_builtin_tools(self) -> List[str]:
|
|
1365
|
+
"""Get list of builtin tools supported by Gemini."""
|
|
1366
|
+
return ["google_search_retrieval", "code_execution"]
|
|
1927
1367
|
|
|
1928
1368
|
def reset_tool_usage(self):
|
|
1929
1369
|
"""Reset tool usage tracking."""
|
|
1930
1370
|
self.search_count = 0
|
|
1931
1371
|
self.code_execution_count = 0
|
|
1932
|
-
# Reset MCP monitoring metrics
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1372
|
+
# Reset MCP monitoring metrics when available
|
|
1373
|
+
for attr in (
|
|
1374
|
+
"_mcp_tool_calls_count",
|
|
1375
|
+
"_mcp_tool_failures",
|
|
1376
|
+
"_mcp_tool_successes",
|
|
1377
|
+
"_mcp_connection_retries",
|
|
1378
|
+
):
|
|
1379
|
+
if hasattr(self, attr):
|
|
1380
|
+
setattr(self, attr, 0)
|
|
1939
1381
|
super().reset_token_usage()
|
|
1940
1382
|
|
|
1941
1383
|
async def cleanup_mcp(self):
|
|
1942
1384
|
"""Cleanup MCP connections - override parent class to use Gemini-specific cleanup."""
|
|
1943
|
-
if
|
|
1385
|
+
if MCPResourceManager:
|
|
1944
1386
|
try:
|
|
1945
|
-
await
|
|
1946
|
-
|
|
1947
|
-
except
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1387
|
+
await super().cleanup_mcp()
|
|
1388
|
+
return
|
|
1389
|
+
except Exception as error:
|
|
1390
|
+
log_backend_activity(
|
|
1391
|
+
"gemini",
|
|
1392
|
+
"MCP cleanup via resource manager failed",
|
|
1393
|
+
{"error": str(error)},
|
|
1394
|
+
agent_id=self.agent_id,
|
|
1395
|
+
)
|
|
1396
|
+
# Fall back to manual cleanup below
|
|
1397
|
+
|
|
1398
|
+
if not self._mcp_client:
|
|
1399
|
+
return
|
|
1400
|
+
|
|
1401
|
+
try:
|
|
1402
|
+
await self._mcp_client.disconnect()
|
|
1403
|
+
log_backend_activity("gemini", "MCP client disconnected", {}, agent_id=self.agent_id)
|
|
1404
|
+
except (
|
|
1405
|
+
MCPConnectionError,
|
|
1406
|
+
MCPTimeoutError,
|
|
1407
|
+
MCPServerError,
|
|
1408
|
+
MCPError,
|
|
1409
|
+
Exception,
|
|
1410
|
+
) as e:
|
|
1411
|
+
if MCPErrorHandler:
|
|
1954
1412
|
MCPErrorHandler.get_error_details(e, "disconnect", log=True)
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1413
|
+
else:
|
|
1414
|
+
logger.exception("[Gemini] MCP disconnect error during cleanup")
|
|
1415
|
+
finally:
|
|
1416
|
+
self._mcp_client = None
|
|
1417
|
+
self._mcp_initialized = False
|
|
1418
|
+
if hasattr(self, "_mcp_functions"):
|
|
1419
|
+
self._mcp_functions.clear()
|
|
1420
|
+
if hasattr(self, "_mcp_function_names"):
|
|
1421
|
+
self._mcp_function_names.clear()
|
|
1963
1422
|
|
|
1964
1423
|
async def __aenter__(self) -> "GeminiBackend":
|
|
1965
1424
|
"""Async context manager entry."""
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
except Exception as e:
|
|
1969
|
-
log_backend_activity(
|
|
1970
|
-
"gemini",
|
|
1971
|
-
"MCP setup failed during context entry",
|
|
1972
|
-
{"error": str(e)},
|
|
1973
|
-
agent_id=self.agent_id,
|
|
1974
|
-
)
|
|
1425
|
+
# Call parent class __aenter__ which handles MCP setup
|
|
1426
|
+
await super().__aenter__()
|
|
1975
1427
|
return self
|
|
1976
1428
|
|
|
1977
1429
|
async def __aexit__(
|
|
@@ -1984,11 +1436,15 @@ class GeminiBackend(CustomToolAndMCPBackend):
|
|
|
1984
1436
|
# Parameters are required by context manager protocol but not used
|
|
1985
1437
|
_ = (exc_type, exc_val, exc_tb)
|
|
1986
1438
|
try:
|
|
1987
|
-
await
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1439
|
+
await super().__aexit__(exc_type, exc_val, exc_tb)
|
|
1440
|
+
finally:
|
|
1441
|
+
if not MCPResourceManager:
|
|
1442
|
+
try:
|
|
1443
|
+
await self.cleanup_mcp()
|
|
1444
|
+
except Exception as e:
|
|
1445
|
+
log_backend_activity(
|
|
1446
|
+
"gemini",
|
|
1447
|
+
"Backend cleanup error",
|
|
1448
|
+
{"error": str(e)},
|
|
1449
|
+
agent_id=self.agent_id,
|
|
1450
|
+
)
|