massgen 0.0.3__py3-none-any.whl → 0.1.0__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 +142 -8
- massgen/adapters/__init__.py +29 -0
- massgen/adapters/ag2_adapter.py +483 -0
- massgen/adapters/base.py +183 -0
- massgen/adapters/tests/__init__.py +0 -0
- massgen/adapters/tests/test_ag2_adapter.py +439 -0
- massgen/adapters/tests/test_agent_adapter.py +128 -0
- massgen/adapters/utils/__init__.py +2 -0
- massgen/adapters/utils/ag2_utils.py +236 -0
- massgen/adapters/utils/tests/__init__.py +0 -0
- massgen/adapters/utils/tests/test_ag2_utils.py +138 -0
- massgen/agent_config.py +329 -55
- massgen/api_params_handler/__init__.py +10 -0
- massgen/api_params_handler/_api_params_handler_base.py +99 -0
- massgen/api_params_handler/_chat_completions_api_params_handler.py +176 -0
- massgen/api_params_handler/_claude_api_params_handler.py +113 -0
- massgen/api_params_handler/_response_api_params_handler.py +130 -0
- massgen/backend/__init__.py +39 -4
- massgen/backend/azure_openai.py +385 -0
- massgen/backend/base.py +341 -69
- massgen/backend/base_with_mcp.py +1102 -0
- massgen/backend/capabilities.py +386 -0
- massgen/backend/chat_completions.py +577 -130
- massgen/backend/claude.py +1033 -537
- massgen/backend/claude_code.py +1203 -0
- massgen/backend/cli_base.py +209 -0
- massgen/backend/docs/BACKEND_ARCHITECTURE.md +126 -0
- massgen/backend/{CLAUDE_API_RESEARCH.md → docs/CLAUDE_API_RESEARCH.md} +18 -18
- massgen/backend/{GEMINI_API_DOCUMENTATION.md → docs/GEMINI_API_DOCUMENTATION.md} +9 -9
- massgen/backend/docs/Gemini MCP Integration Analysis.md +1050 -0
- massgen/backend/docs/MCP_IMPLEMENTATION_CLAUDE_BACKEND.md +177 -0
- massgen/backend/docs/MCP_INTEGRATION_RESPONSE_BACKEND.md +352 -0
- massgen/backend/docs/OPENAI_GPT5_MODELS.md +211 -0
- massgen/backend/{OPENAI_RESPONSES_API_FORMAT.md → docs/OPENAI_RESPONSE_API_TOOL_CALLS.md} +3 -3
- massgen/backend/docs/OPENAI_response_streaming.md +20654 -0
- massgen/backend/docs/inference_backend.md +257 -0
- massgen/backend/docs/permissions_and_context_files.md +1085 -0
- massgen/backend/external.py +126 -0
- massgen/backend/gemini.py +1850 -241
- massgen/backend/grok.py +40 -156
- massgen/backend/inference.py +156 -0
- massgen/backend/lmstudio.py +171 -0
- massgen/backend/response.py +1095 -322
- massgen/chat_agent.py +131 -113
- massgen/cli.py +1560 -275
- massgen/config_builder.py +2396 -0
- massgen/configs/BACKEND_CONFIGURATION.md +458 -0
- massgen/configs/README.md +559 -216
- massgen/configs/ag2/ag2_case_study.yaml +27 -0
- massgen/configs/ag2/ag2_coder.yaml +34 -0
- massgen/configs/ag2/ag2_coder_case_study.yaml +36 -0
- massgen/configs/ag2/ag2_gemini.yaml +27 -0
- massgen/configs/ag2/ag2_groupchat.yaml +108 -0
- massgen/configs/ag2/ag2_groupchat_gpt.yaml +118 -0
- massgen/configs/ag2/ag2_single_agent.yaml +21 -0
- massgen/configs/basic/multi/fast_timeout_example.yaml +37 -0
- massgen/configs/basic/multi/gemini_4o_claude.yaml +31 -0
- massgen/configs/basic/multi/gemini_gpt5nano_claude.yaml +36 -0
- massgen/configs/{gemini_4o_claude.yaml → basic/multi/geminicode_4o_claude.yaml} +3 -3
- massgen/configs/basic/multi/geminicode_gpt5nano_claude.yaml +36 -0
- massgen/configs/basic/multi/glm_gemini_claude.yaml +25 -0
- massgen/configs/basic/multi/gpt4o_audio_generation.yaml +30 -0
- massgen/configs/basic/multi/gpt4o_image_generation.yaml +31 -0
- massgen/configs/basic/multi/gpt5nano_glm_qwen.yaml +26 -0
- massgen/configs/basic/multi/gpt5nano_image_understanding.yaml +26 -0
- massgen/configs/{three_agents_default.yaml → basic/multi/three_agents_default.yaml} +8 -4
- massgen/configs/basic/multi/three_agents_opensource.yaml +27 -0
- massgen/configs/basic/multi/three_agents_vllm.yaml +20 -0
- massgen/configs/basic/multi/two_agents_gemini.yaml +19 -0
- massgen/configs/{two_agents.yaml → basic/multi/two_agents_gpt5.yaml} +14 -6
- massgen/configs/basic/multi/two_agents_opensource_lmstudio.yaml +31 -0
- massgen/configs/basic/multi/two_qwen_vllm_sglang.yaml +28 -0
- massgen/configs/{single_agent.yaml → basic/single/single_agent.yaml} +1 -1
- massgen/configs/{single_flash2.5.yaml → basic/single/single_flash2.5.yaml} +1 -2
- massgen/configs/basic/single/single_gemini2.5pro.yaml +16 -0
- massgen/configs/basic/single/single_gpt4o_audio_generation.yaml +22 -0
- massgen/configs/basic/single/single_gpt4o_image_generation.yaml +22 -0
- massgen/configs/basic/single/single_gpt4o_video_generation.yaml +24 -0
- massgen/configs/basic/single/single_gpt5nano.yaml +20 -0
- massgen/configs/basic/single/single_gpt5nano_file_search.yaml +18 -0
- massgen/configs/basic/single/single_gpt5nano_image_understanding.yaml +17 -0
- massgen/configs/basic/single/single_gptoss120b.yaml +15 -0
- massgen/configs/basic/single/single_openrouter_audio_understanding.yaml +15 -0
- massgen/configs/basic/single/single_qwen_video_understanding.yaml +15 -0
- massgen/configs/debug/code_execution/command_filtering_blacklist.yaml +29 -0
- massgen/configs/debug/code_execution/command_filtering_whitelist.yaml +28 -0
- massgen/configs/debug/code_execution/docker_verification.yaml +29 -0
- massgen/configs/debug/skip_coordination_test.yaml +27 -0
- massgen/configs/debug/test_sdk_migration.yaml +17 -0
- massgen/configs/docs/DISCORD_MCP_SETUP.md +208 -0
- massgen/configs/docs/TWITTER_MCP_ENESCINAR_SETUP.md +82 -0
- massgen/configs/providers/azure/azure_openai_multi.yaml +21 -0
- massgen/configs/providers/azure/azure_openai_single.yaml +19 -0
- massgen/configs/providers/claude/claude.yaml +14 -0
- massgen/configs/providers/gemini/gemini_gpt5nano.yaml +28 -0
- massgen/configs/providers/local/lmstudio.yaml +11 -0
- massgen/configs/providers/openai/gpt5.yaml +46 -0
- massgen/configs/providers/openai/gpt5_nano.yaml +46 -0
- massgen/configs/providers/others/grok_single_agent.yaml +19 -0
- massgen/configs/providers/others/zai_coding_team.yaml +108 -0
- massgen/configs/providers/others/zai_glm45.yaml +12 -0
- massgen/configs/{creative_team.yaml → teams/creative/creative_team.yaml} +16 -6
- massgen/configs/{travel_planning.yaml → teams/creative/travel_planning.yaml} +16 -6
- massgen/configs/{news_analysis.yaml → teams/research/news_analysis.yaml} +16 -6
- massgen/configs/{research_team.yaml → teams/research/research_team.yaml} +15 -7
- massgen/configs/{technical_analysis.yaml → teams/research/technical_analysis.yaml} +16 -6
- massgen/configs/tools/code-execution/basic_command_execution.yaml +25 -0
- massgen/configs/tools/code-execution/code_execution_use_case_simple.yaml +41 -0
- massgen/configs/tools/code-execution/docker_claude_code.yaml +32 -0
- massgen/configs/tools/code-execution/docker_multi_agent.yaml +32 -0
- massgen/configs/tools/code-execution/docker_simple.yaml +29 -0
- massgen/configs/tools/code-execution/docker_with_resource_limits.yaml +32 -0
- massgen/configs/tools/code-execution/multi_agent_playwright_automation.yaml +57 -0
- massgen/configs/tools/filesystem/cc_gpt5_gemini_filesystem.yaml +34 -0
- massgen/configs/tools/filesystem/claude_code_context_sharing.yaml +68 -0
- massgen/configs/tools/filesystem/claude_code_flash2.5.yaml +43 -0
- massgen/configs/tools/filesystem/claude_code_flash2.5_gptoss.yaml +49 -0
- massgen/configs/tools/filesystem/claude_code_gpt5nano.yaml +31 -0
- massgen/configs/tools/filesystem/claude_code_single.yaml +40 -0
- massgen/configs/tools/filesystem/fs_permissions_test.yaml +87 -0
- massgen/configs/tools/filesystem/gemini_gemini_workspace_cleanup.yaml +54 -0
- massgen/configs/tools/filesystem/gemini_gpt5_filesystem_casestudy.yaml +30 -0
- massgen/configs/tools/filesystem/gemini_gpt5nano_file_context_path.yaml +43 -0
- massgen/configs/tools/filesystem/gemini_gpt5nano_protected_paths.yaml +45 -0
- massgen/configs/tools/filesystem/gpt5mini_cc_fs_context_path.yaml +31 -0
- massgen/configs/tools/filesystem/grok4_gpt5_gemini_filesystem.yaml +32 -0
- massgen/configs/tools/filesystem/multiturn/grok4_gpt5_claude_code_filesystem_multiturn.yaml +58 -0
- massgen/configs/tools/filesystem/multiturn/grok4_gpt5_gemini_filesystem_multiturn.yaml +58 -0
- massgen/configs/tools/filesystem/multiturn/two_claude_code_filesystem_multiturn.yaml +47 -0
- massgen/configs/tools/filesystem/multiturn/two_gemini_flash_filesystem_multiturn.yaml +48 -0
- massgen/configs/tools/mcp/claude_code_discord_mcp_example.yaml +27 -0
- massgen/configs/tools/mcp/claude_code_simple_mcp.yaml +35 -0
- massgen/configs/tools/mcp/claude_code_twitter_mcp_example.yaml +32 -0
- massgen/configs/tools/mcp/claude_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/claude_mcp_test.yaml +27 -0
- massgen/configs/tools/mcp/five_agents_travel_mcp_test.yaml +157 -0
- massgen/configs/tools/mcp/five_agents_weather_mcp_test.yaml +103 -0
- massgen/configs/tools/mcp/gemini_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/gemini_mcp_filesystem_test.yaml +23 -0
- massgen/configs/tools/mcp/gemini_mcp_filesystem_test_sharing.yaml +23 -0
- massgen/configs/tools/mcp/gemini_mcp_filesystem_test_single_agent.yaml +17 -0
- massgen/configs/tools/mcp/gemini_mcp_filesystem_test_with_claude_code.yaml +24 -0
- massgen/configs/tools/mcp/gemini_mcp_test.yaml +27 -0
- massgen/configs/tools/mcp/gemini_notion_mcp.yaml +52 -0
- massgen/configs/tools/mcp/gpt5_nano_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/gpt5_nano_mcp_test.yaml +27 -0
- massgen/configs/tools/mcp/gpt5mini_claude_code_discord_mcp_example.yaml +38 -0
- massgen/configs/tools/mcp/gpt_oss_mcp_example.yaml +25 -0
- massgen/configs/tools/mcp/gpt_oss_mcp_test.yaml +28 -0
- massgen/configs/tools/mcp/grok3_mini_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/grok3_mini_mcp_test.yaml +27 -0
- massgen/configs/tools/mcp/multimcp_gemini.yaml +111 -0
- massgen/configs/tools/mcp/qwen_api_mcp_example.yaml +25 -0
- massgen/configs/tools/mcp/qwen_api_mcp_test.yaml +28 -0
- massgen/configs/tools/mcp/qwen_local_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/qwen_local_mcp_test.yaml +27 -0
- massgen/configs/tools/planning/five_agents_discord_mcp_planning_mode.yaml +140 -0
- massgen/configs/tools/planning/five_agents_filesystem_mcp_planning_mode.yaml +151 -0
- massgen/configs/tools/planning/five_agents_notion_mcp_planning_mode.yaml +151 -0
- massgen/configs/tools/planning/five_agents_twitter_mcp_planning_mode.yaml +155 -0
- massgen/configs/tools/planning/gpt5_mini_case_study_mcp_planning_mode.yaml +73 -0
- massgen/configs/tools/web-search/claude_streamable_http_test.yaml +43 -0
- massgen/configs/tools/web-search/gemini_streamable_http_test.yaml +43 -0
- massgen/configs/tools/web-search/gpt5_mini_streamable_http_test.yaml +43 -0
- massgen/configs/tools/web-search/gpt_oss_streamable_http_test.yaml +44 -0
- massgen/configs/tools/web-search/grok3_mini_streamable_http_test.yaml +43 -0
- massgen/configs/tools/web-search/qwen_api_streamable_http_test.yaml +44 -0
- massgen/configs/tools/web-search/qwen_local_streamable_http_test.yaml +43 -0
- massgen/coordination_tracker.py +708 -0
- massgen/docker/README.md +462 -0
- massgen/filesystem_manager/__init__.py +21 -0
- massgen/filesystem_manager/_base.py +9 -0
- massgen/filesystem_manager/_code_execution_server.py +545 -0
- massgen/filesystem_manager/_docker_manager.py +477 -0
- massgen/filesystem_manager/_file_operation_tracker.py +248 -0
- massgen/filesystem_manager/_filesystem_manager.py +813 -0
- massgen/filesystem_manager/_path_permission_manager.py +1261 -0
- massgen/filesystem_manager/_workspace_tools_server.py +1815 -0
- massgen/formatter/__init__.py +10 -0
- massgen/formatter/_chat_completions_formatter.py +284 -0
- massgen/formatter/_claude_formatter.py +235 -0
- massgen/formatter/_formatter_base.py +156 -0
- massgen/formatter/_response_formatter.py +263 -0
- massgen/frontend/__init__.py +1 -2
- massgen/frontend/coordination_ui.py +471 -286
- massgen/frontend/displays/base_display.py +56 -11
- massgen/frontend/displays/create_coordination_table.py +1956 -0
- massgen/frontend/displays/rich_terminal_display.py +1259 -619
- massgen/frontend/displays/simple_display.py +9 -4
- massgen/frontend/displays/terminal_display.py +27 -68
- massgen/logger_config.py +681 -0
- massgen/mcp_tools/README.md +232 -0
- massgen/mcp_tools/__init__.py +105 -0
- massgen/mcp_tools/backend_utils.py +1035 -0
- massgen/mcp_tools/circuit_breaker.py +195 -0
- massgen/mcp_tools/client.py +894 -0
- massgen/mcp_tools/config_validator.py +138 -0
- massgen/mcp_tools/docs/circuit_breaker.md +646 -0
- massgen/mcp_tools/docs/client.md +950 -0
- massgen/mcp_tools/docs/config_validator.md +478 -0
- massgen/mcp_tools/docs/exceptions.md +1165 -0
- massgen/mcp_tools/docs/security.md +854 -0
- massgen/mcp_tools/exceptions.py +338 -0
- massgen/mcp_tools/hooks.py +212 -0
- massgen/mcp_tools/security.py +780 -0
- massgen/message_templates.py +342 -64
- massgen/orchestrator.py +1515 -241
- massgen/stream_chunk/__init__.py +35 -0
- massgen/stream_chunk/base.py +92 -0
- massgen/stream_chunk/multimodal.py +237 -0
- massgen/stream_chunk/text.py +162 -0
- massgen/tests/mcp_test_server.py +150 -0
- massgen/tests/multi_turn_conversation_design.md +0 -8
- massgen/tests/test_azure_openai_backend.py +156 -0
- massgen/tests/test_backend_capabilities.py +262 -0
- massgen/tests/test_backend_event_loop_all.py +179 -0
- massgen/tests/test_chat_completions_refactor.py +142 -0
- massgen/tests/test_claude_backend.py +15 -28
- massgen/tests/test_claude_code.py +268 -0
- massgen/tests/test_claude_code_context_sharing.py +233 -0
- massgen/tests/test_claude_code_orchestrator.py +175 -0
- massgen/tests/test_cli_backends.py +180 -0
- massgen/tests/test_code_execution.py +679 -0
- massgen/tests/test_external_agent_backend.py +134 -0
- massgen/tests/test_final_presentation_fallback.py +237 -0
- massgen/tests/test_gemini_planning_mode.py +351 -0
- massgen/tests/test_grok_backend.py +7 -10
- massgen/tests/test_http_mcp_server.py +42 -0
- massgen/tests/test_integration_simple.py +198 -0
- massgen/tests/test_mcp_blocking.py +125 -0
- massgen/tests/test_message_context_building.py +29 -47
- massgen/tests/test_orchestrator_final_presentation.py +48 -0
- massgen/tests/test_path_permission_manager.py +2087 -0
- massgen/tests/test_rich_terminal_display.py +14 -13
- massgen/tests/test_timeout.py +133 -0
- massgen/tests/test_v3_3agents.py +11 -12
- massgen/tests/test_v3_simple.py +8 -13
- massgen/tests/test_v3_three_agents.py +11 -18
- massgen/tests/test_v3_two_agents.py +8 -13
- massgen/token_manager/__init__.py +7 -0
- massgen/token_manager/token_manager.py +400 -0
- massgen/utils.py +52 -16
- massgen/v1/agent.py +45 -91
- massgen/v1/agents.py +18 -53
- massgen/v1/backends/gemini.py +50 -153
- massgen/v1/backends/grok.py +21 -54
- massgen/v1/backends/oai.py +39 -111
- massgen/v1/cli.py +36 -93
- massgen/v1/config.py +8 -12
- massgen/v1/logging.py +43 -127
- massgen/v1/main.py +18 -32
- massgen/v1/orchestrator.py +68 -209
- massgen/v1/streaming_display.py +62 -163
- massgen/v1/tools.py +8 -12
- massgen/v1/types.py +9 -23
- massgen/v1/utils.py +5 -23
- massgen-0.1.0.dist-info/METADATA +1245 -0
- massgen-0.1.0.dist-info/RECORD +273 -0
- massgen-0.1.0.dist-info/entry_points.txt +2 -0
- massgen/frontend/logging/__init__.py +0 -9
- massgen/frontend/logging/realtime_logger.py +0 -197
- massgen-0.0.3.dist-info/METADATA +0 -568
- massgen-0.0.3.dist-info/RECORD +0 -76
- massgen-0.0.3.dist-info/entry_points.txt +0 -2
- /massgen/backend/{Function calling openai responses.md → docs/Function calling openai responses.md} +0 -0
- {massgen-0.0.3.dist-info → massgen-0.1.0.dist-info}/WHEEL +0 -0
- {massgen-0.0.3.dist-info → massgen-0.1.0.dist-info}/licenses/LICENSE +0 -0
- {massgen-0.0.3.dist-info → massgen-0.1.0.dist-info}/top_level.txt +0 -0
massgen/backend/gemini.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
1
2
|
"""
|
|
2
3
|
Gemini backend implementation using structured output for voting and answer submission.
|
|
3
4
|
|
|
@@ -18,11 +19,23 @@ TECHNICAL SOLUTION:
|
|
|
18
19
|
- Maintains compatibility with existing MassGen workflow
|
|
19
20
|
"""
|
|
20
21
|
|
|
21
|
-
import
|
|
22
|
-
import json
|
|
22
|
+
import asyncio
|
|
23
23
|
import enum
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
import hashlib
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import re
|
|
28
|
+
import time
|
|
29
|
+
from typing import Any, AsyncGenerator, Awaitable, Callable, Dict, List, Optional
|
|
30
|
+
|
|
31
|
+
from ..logger_config import (
|
|
32
|
+
log_backend_activity,
|
|
33
|
+
log_backend_agent_message,
|
|
34
|
+
log_stream_chunk,
|
|
35
|
+
log_tool_call,
|
|
36
|
+
logger,
|
|
37
|
+
)
|
|
38
|
+
from .base import FilesystemSupport, LLMBackend, StreamChunk
|
|
26
39
|
|
|
27
40
|
try:
|
|
28
41
|
from pydantic import BaseModel, Field
|
|
@@ -30,15 +43,43 @@ except ImportError:
|
|
|
30
43
|
BaseModel = None
|
|
31
44
|
Field = None
|
|
32
45
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
# MCP integration imports
|
|
47
|
+
try:
|
|
48
|
+
from ..mcp_tools import MCPClient, MCPConnectionError, MCPError
|
|
49
|
+
from ..mcp_tools.config_validator import MCPConfigValidator
|
|
50
|
+
from ..mcp_tools.exceptions import (
|
|
51
|
+
MCPConfigurationError,
|
|
52
|
+
MCPServerError,
|
|
53
|
+
MCPTimeoutError,
|
|
54
|
+
MCPValidationError,
|
|
55
|
+
)
|
|
56
|
+
except ImportError: # MCP not installed or import failed within mcp_tools
|
|
57
|
+
MCPClient = None # type: ignore[assignment]
|
|
58
|
+
MCPError = ImportError # type: ignore[assignment]
|
|
59
|
+
MCPConnectionError = ImportError # type: ignore[assignment]
|
|
60
|
+
MCPConfigValidator = None # type: ignore[assignment]
|
|
61
|
+
MCPConfigurationError = ImportError # type: ignore[assignment]
|
|
62
|
+
MCPValidationError = ImportError # type: ignore[assignment]
|
|
63
|
+
MCPTimeoutError = ImportError # type: ignore[assignment]
|
|
64
|
+
MCPServerError = ImportError # type: ignore[assignment]
|
|
65
|
+
|
|
66
|
+
# Import MCP backend utilities
|
|
67
|
+
try:
|
|
68
|
+
from ..mcp_tools.backend_utils import (
|
|
69
|
+
MCPCircuitBreakerManager,
|
|
70
|
+
MCPConfigHelper,
|
|
71
|
+
MCPErrorHandler,
|
|
72
|
+
MCPExecutionManager,
|
|
73
|
+
MCPMessageManager,
|
|
74
|
+
MCPSetupManager,
|
|
75
|
+
)
|
|
76
|
+
except ImportError:
|
|
77
|
+
MCPErrorHandler = None # type: ignore[assignment]
|
|
78
|
+
MCPSetupManager = None # type: ignore[assignment]
|
|
79
|
+
MCPMessageManager = None # type: ignore[assignment]
|
|
80
|
+
MCPCircuitBreakerManager = None # type: ignore[assignment]
|
|
81
|
+
MCPExecutionManager = None # type: ignore[assignment]
|
|
82
|
+
MCPConfigHelper = None # type: ignore[assignment]
|
|
42
83
|
|
|
43
84
|
|
|
44
85
|
class ActionType(enum.Enum):
|
|
@@ -52,9 +93,7 @@ class VoteAction(BaseModel):
|
|
|
52
93
|
"""Structured output for voting action."""
|
|
53
94
|
|
|
54
95
|
action: ActionType = Field(default=ActionType.VOTE, description="Action type")
|
|
55
|
-
agent_id: str = Field(
|
|
56
|
-
description="Anonymous agent ID to vote for (e.g., 'agent1', 'agent2')"
|
|
57
|
-
)
|
|
96
|
+
agent_id: str = Field(description="Anonymous agent ID to vote for (e.g., 'agent1', 'agent2')")
|
|
58
97
|
reason: str = Field(description="Brief reason why this agent has the best answer")
|
|
59
98
|
|
|
60
99
|
|
|
@@ -62,38 +101,804 @@ class NewAnswerAction(BaseModel):
|
|
|
62
101
|
"""Structured output for new answer action."""
|
|
63
102
|
|
|
64
103
|
action: ActionType = Field(default=ActionType.NEW_ANSWER, description="Action type")
|
|
65
|
-
content: str = Field(
|
|
66
|
-
description="Your improved answer. If any builtin tools like search or code execution were used, include how they are used here."
|
|
67
|
-
)
|
|
104
|
+
content: str = Field(description="Your improved answer. If any builtin tools like search or code execution were used, include how they are used here.")
|
|
68
105
|
|
|
69
106
|
|
|
70
107
|
class CoordinationResponse(BaseModel):
|
|
71
108
|
"""Structured response for coordination actions."""
|
|
72
109
|
|
|
73
110
|
action_type: ActionType = Field(description="Type of action to take")
|
|
74
|
-
vote_data: Optional[VoteAction] = Field(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
111
|
+
vote_data: Optional[VoteAction] = Field(default=None, description="Vote data if action is vote")
|
|
112
|
+
answer_data: Optional[NewAnswerAction] = Field(default=None, description="Answer data if action is new_answer")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class MCPResponseTracker:
|
|
116
|
+
"""
|
|
117
|
+
Tracks MCP tool responses across streaming chunks to handle deduplication.
|
|
118
|
+
|
|
119
|
+
Similar to MCPCallTracker but for tracking tool responses to avoid duplicate output.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def __init__(self):
|
|
123
|
+
"""Initialize the tracker with empty storage."""
|
|
124
|
+
self.processed_responses = set() # Store hashes of processed responses
|
|
125
|
+
self.response_history = [] # Store all unique responses with timestamps
|
|
126
|
+
|
|
127
|
+
def get_response_hash(self, tool_name: str, tool_response: Any) -> str:
|
|
128
|
+
"""
|
|
129
|
+
Generate a unique hash for a tool response based on name and response content.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
tool_name: Name of the tool that responded
|
|
133
|
+
tool_response: Response from the tool
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
MD5 hash string identifying this specific response
|
|
137
|
+
"""
|
|
138
|
+
# Create a deterministic string representation
|
|
139
|
+
content = f"{tool_name}:{str(tool_response)}"
|
|
140
|
+
return hashlib.md5(content.encode()).hexdigest()
|
|
141
|
+
|
|
142
|
+
def is_new_response(self, tool_name: str, tool_response: Any) -> bool:
|
|
143
|
+
"""
|
|
144
|
+
Check if this is a new tool response we haven't seen before.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
tool_name: Name of the tool that responded
|
|
148
|
+
tool_response: Response from the tool
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
True if this is a new response, False if already processed
|
|
152
|
+
"""
|
|
153
|
+
response_hash = self.get_response_hash(tool_name, tool_response)
|
|
154
|
+
return response_hash not in self.processed_responses
|
|
155
|
+
|
|
156
|
+
def add_response(self, tool_name: str, tool_response: Any) -> Dict[str, Any]:
|
|
157
|
+
"""
|
|
158
|
+
Add a new response to the tracker.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
tool_name: Name of the tool that responded
|
|
162
|
+
tool_response: Response from the tool
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Dictionary containing response details and timestamp
|
|
166
|
+
"""
|
|
167
|
+
response_hash = self.get_response_hash(tool_name, tool_response)
|
|
168
|
+
self.processed_responses.add(response_hash)
|
|
169
|
+
|
|
170
|
+
record = {
|
|
171
|
+
"tool_name": tool_name,
|
|
172
|
+
"response": tool_response,
|
|
173
|
+
"hash": response_hash,
|
|
174
|
+
"timestamp": time.time(),
|
|
175
|
+
}
|
|
176
|
+
self.response_history.append(record)
|
|
177
|
+
return record
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class MCPCallTracker:
|
|
181
|
+
"""
|
|
182
|
+
Tracks MCP tool calls across streaming chunks to handle deduplication.
|
|
183
|
+
|
|
184
|
+
Uses hashing to identify unique tool calls and timestamps to track when they occurred.
|
|
185
|
+
This ensures we don't double-count the same tool call appearing in multiple chunks.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
def __init__(self):
|
|
189
|
+
"""Initialize the tracker with empty storage."""
|
|
190
|
+
self.processed_calls = set() # Store hashes of processed calls
|
|
191
|
+
self.call_history = [] # Store all unique calls with timestamps
|
|
192
|
+
self.last_chunk_calls = [] # Track calls from the last chunk for deduplication
|
|
193
|
+
self.dedup_window = 0.5 # Time window in seconds for deduplication
|
|
194
|
+
|
|
195
|
+
def get_call_hash(self, tool_name: str, tool_args: Dict[str, Any]) -> str:
|
|
196
|
+
"""
|
|
197
|
+
Generate a unique hash for a tool call based on name and arguments.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
tool_name: Name of the tool being called
|
|
201
|
+
tool_args: Arguments passed to the tool
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
MD5 hash string identifying this specific call
|
|
205
|
+
"""
|
|
206
|
+
# Create a deterministic string representation
|
|
207
|
+
content = f"{tool_name}:{json.dumps(tool_args, sort_keys=True)}"
|
|
208
|
+
return hashlib.md5(content.encode()).hexdigest()
|
|
209
|
+
|
|
210
|
+
def is_new_call(self, tool_name: str, tool_args: Dict[str, Any]) -> bool:
|
|
211
|
+
"""
|
|
212
|
+
Check if this is a new tool call we haven't seen before.
|
|
213
|
+
|
|
214
|
+
Uses a time-window based approach: identical calls within the dedup_window
|
|
215
|
+
are considered duplicates (likely from streaming chunks), while those outside
|
|
216
|
+
the window are considered new calls (likely intentional repeated calls).
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
tool_name: Name of the tool being called
|
|
220
|
+
tool_args: Arguments passed to the tool
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
True if this is a new call, False if we've seen it before
|
|
224
|
+
"""
|
|
225
|
+
call_hash = self.get_call_hash(tool_name, tool_args)
|
|
226
|
+
current_time = time.time()
|
|
227
|
+
|
|
228
|
+
# Check if this call exists in recent history within the dedup window
|
|
229
|
+
for call in self.call_history[-10:]: # Check last 10 calls for efficiency
|
|
230
|
+
if call.get("hash") == call_hash:
|
|
231
|
+
time_diff = current_time - call.get("timestamp", 0)
|
|
232
|
+
if time_diff < self.dedup_window:
|
|
233
|
+
# This is likely a duplicate from streaming chunks
|
|
234
|
+
return False
|
|
235
|
+
# If outside the window, treat as a new intentional call
|
|
236
|
+
|
|
237
|
+
# Mark as processed
|
|
238
|
+
self.processed_calls.add(call_hash)
|
|
239
|
+
return True
|
|
240
|
+
|
|
241
|
+
def add_call(self, tool_name: str, tool_args: Dict[str, Any]) -> Dict[str, Any]:
|
|
242
|
+
"""
|
|
243
|
+
Add a new tool call to the history.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
tool_name: Name of the tool being called
|
|
247
|
+
tool_args: Arguments passed to the tool
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Dictionary containing the call details with timestamp and hash
|
|
251
|
+
"""
|
|
252
|
+
call_record = {
|
|
253
|
+
"name": tool_name,
|
|
254
|
+
"arguments": tool_args,
|
|
255
|
+
"timestamp": time.time(),
|
|
256
|
+
"hash": self.get_call_hash(tool_name, tool_args),
|
|
257
|
+
"sequence": len(self.call_history), # Add sequence number for ordering
|
|
258
|
+
}
|
|
259
|
+
self.call_history.append(call_record)
|
|
260
|
+
|
|
261
|
+
# Clean up old history to prevent memory growth
|
|
262
|
+
if len(self.call_history) > 100:
|
|
263
|
+
self.call_history = self.call_history[-50:]
|
|
264
|
+
|
|
265
|
+
return call_record
|
|
266
|
+
|
|
267
|
+
def get_summary(self) -> str:
|
|
268
|
+
"""
|
|
269
|
+
Get a summary of all tracked tool calls.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Human-readable summary of tool usage
|
|
273
|
+
"""
|
|
274
|
+
if not self.call_history:
|
|
275
|
+
return "No MCP tools called"
|
|
276
|
+
|
|
277
|
+
tool_names = [call["name"] for call in self.call_history]
|
|
278
|
+
unique_tools = list(dict.fromkeys(tool_names)) # Preserve order
|
|
279
|
+
return f"Used {len(self.call_history)} MCP tool calls: {', '.join(unique_tools)}"
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class MCPResponseExtractor:
|
|
283
|
+
"""
|
|
284
|
+
Extracts MCP tool calls and responses from Gemini SDK stream chunks.
|
|
285
|
+
|
|
286
|
+
This class parses the internal SDK chunks to capture:
|
|
287
|
+
- function_call parts (tool invocations)
|
|
288
|
+
- function_response parts (tool results)
|
|
289
|
+
- Paired call-response data for tracking complete tool executions
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
def __init__(self):
|
|
293
|
+
"""Initialize the extractor with empty storage."""
|
|
294
|
+
self.mcp_calls = [] # All tool calls
|
|
295
|
+
self.mcp_responses = [] # All tool responses
|
|
296
|
+
self.call_response_pairs = [] # Matched call-response pairs
|
|
297
|
+
self._pending_call = None # Track current call awaiting response
|
|
298
|
+
|
|
299
|
+
def extract_function_call(self, function_call) -> Optional[Dict[str, Any]]:
|
|
300
|
+
"""
|
|
301
|
+
Extract tool call information from SDK function_call object.
|
|
302
|
+
|
|
303
|
+
Tries multiple methods to extract data from different SDK versions:
|
|
304
|
+
1. Direct attributes (name, args)
|
|
305
|
+
2. Dictionary-like interface (get method)
|
|
306
|
+
3. __dict__ attributes
|
|
307
|
+
4. Protobuf _pb attributes
|
|
308
|
+
"""
|
|
309
|
+
tool_name = None
|
|
310
|
+
tool_args = None
|
|
311
|
+
|
|
312
|
+
# Method 1: Direct attributes
|
|
313
|
+
tool_name = getattr(function_call, "name", None)
|
|
314
|
+
tool_args = getattr(function_call, "args", None)
|
|
315
|
+
|
|
316
|
+
# Method 2: Dictionary-like object
|
|
317
|
+
if tool_name is None:
|
|
318
|
+
try:
|
|
319
|
+
if hasattr(function_call, "get"):
|
|
320
|
+
tool_name = function_call.get("name", None)
|
|
321
|
+
tool_args = function_call.get("args", None)
|
|
322
|
+
except Exception:
|
|
323
|
+
pass
|
|
324
|
+
|
|
325
|
+
# Method 3: __dict__ inspection
|
|
326
|
+
if tool_name is None:
|
|
327
|
+
try:
|
|
328
|
+
if hasattr(function_call, "__dict__"):
|
|
329
|
+
fc_dict = function_call.__dict__
|
|
330
|
+
tool_name = fc_dict.get("name", None)
|
|
331
|
+
tool_args = fc_dict.get("args", None)
|
|
332
|
+
except Exception:
|
|
333
|
+
pass
|
|
334
|
+
|
|
335
|
+
# Method 4: Protobuf _pb attribute
|
|
336
|
+
if tool_name is None:
|
|
337
|
+
try:
|
|
338
|
+
if hasattr(function_call, "_pb"):
|
|
339
|
+
pb = function_call._pb
|
|
340
|
+
if hasattr(pb, "name"):
|
|
341
|
+
tool_name = pb.name
|
|
342
|
+
if hasattr(pb, "args"):
|
|
343
|
+
tool_args = pb.args
|
|
344
|
+
except Exception:
|
|
345
|
+
pass
|
|
346
|
+
|
|
347
|
+
if tool_name:
|
|
348
|
+
call_data = {
|
|
349
|
+
"name": tool_name,
|
|
350
|
+
"arguments": tool_args or {},
|
|
351
|
+
"timestamp": time.time(),
|
|
352
|
+
"raw": str(function_call)[:200], # Truncate for logging
|
|
353
|
+
}
|
|
354
|
+
self.mcp_calls.append(call_data)
|
|
355
|
+
self._pending_call = call_data
|
|
356
|
+
return call_data
|
|
357
|
+
|
|
358
|
+
return None
|
|
359
|
+
|
|
360
|
+
def extract_function_response(self, function_response) -> Optional[Dict[str, Any]]:
|
|
361
|
+
"""
|
|
362
|
+
Extract tool response information from SDK function_response object.
|
|
363
|
+
|
|
364
|
+
Uses same extraction methods as function_call for consistency.
|
|
365
|
+
"""
|
|
366
|
+
tool_name = None
|
|
367
|
+
tool_response = None
|
|
368
|
+
|
|
369
|
+
# Method 1: Direct attributes
|
|
370
|
+
tool_name = getattr(function_response, "name", None)
|
|
371
|
+
tool_response = getattr(function_response, "response", None)
|
|
372
|
+
|
|
373
|
+
# Method 2: Dictionary-like object
|
|
374
|
+
if tool_name is None:
|
|
375
|
+
try:
|
|
376
|
+
if hasattr(function_response, "get"):
|
|
377
|
+
tool_name = function_response.get("name", None)
|
|
378
|
+
tool_response = function_response.get("response", None)
|
|
379
|
+
except Exception:
|
|
380
|
+
pass
|
|
381
|
+
|
|
382
|
+
# Method 3: __dict__ inspection
|
|
383
|
+
if tool_name is None:
|
|
384
|
+
try:
|
|
385
|
+
if hasattr(function_response, "__dict__"):
|
|
386
|
+
fr_dict = function_response.__dict__
|
|
387
|
+
tool_name = fr_dict.get("name", None)
|
|
388
|
+
tool_response = fr_dict.get("response", None)
|
|
389
|
+
except Exception:
|
|
390
|
+
pass
|
|
391
|
+
|
|
392
|
+
# Method 4: Protobuf _pb attribute
|
|
393
|
+
if tool_name is None:
|
|
394
|
+
try:
|
|
395
|
+
if hasattr(function_response, "_pb"):
|
|
396
|
+
pb = function_response._pb
|
|
397
|
+
if hasattr(pb, "name"):
|
|
398
|
+
tool_name = pb.name
|
|
399
|
+
if hasattr(pb, "response"):
|
|
400
|
+
tool_response = pb.response
|
|
401
|
+
except Exception:
|
|
402
|
+
pass
|
|
403
|
+
|
|
404
|
+
if tool_name:
|
|
405
|
+
response_data = {
|
|
406
|
+
"name": tool_name,
|
|
407
|
+
"response": tool_response or {},
|
|
408
|
+
"timestamp": time.time(),
|
|
409
|
+
"raw": str(function_response)[:500], # Truncate for logging
|
|
410
|
+
}
|
|
411
|
+
self.mcp_responses.append(response_data)
|
|
412
|
+
|
|
413
|
+
# Pair with pending call if names match
|
|
414
|
+
if self._pending_call and self._pending_call["name"] == tool_name:
|
|
415
|
+
self.call_response_pairs.append(
|
|
416
|
+
{
|
|
417
|
+
"call": self._pending_call,
|
|
418
|
+
"response": response_data,
|
|
419
|
+
"duration": response_data["timestamp"] - self._pending_call["timestamp"],
|
|
420
|
+
"paired_at": time.time(),
|
|
421
|
+
},
|
|
422
|
+
)
|
|
423
|
+
self._pending_call = None
|
|
424
|
+
|
|
425
|
+
return response_data
|
|
426
|
+
|
|
427
|
+
return None
|
|
428
|
+
|
|
429
|
+
def get_summary(self) -> Dict[str, Any]:
|
|
430
|
+
"""
|
|
431
|
+
Get a summary of all extracted MCP tool interactions.
|
|
432
|
+
"""
|
|
433
|
+
return {
|
|
434
|
+
"total_calls": len(self.mcp_calls),
|
|
435
|
+
"total_responses": len(self.mcp_responses),
|
|
436
|
+
"paired_interactions": len(self.call_response_pairs),
|
|
437
|
+
"pending_call": self._pending_call is not None,
|
|
438
|
+
"tool_names": list(set(call["name"] for call in self.mcp_calls)),
|
|
439
|
+
"average_duration": (sum(pair["duration"] for pair in self.call_response_pairs) / len(self.call_response_pairs) if self.call_response_pairs else 0),
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
def clear(self):
|
|
443
|
+
"""Clear all stored data."""
|
|
444
|
+
self.mcp_calls.clear()
|
|
445
|
+
self.mcp_responses.clear()
|
|
446
|
+
self.call_response_pairs.clear()
|
|
447
|
+
self._pending_call = None
|
|
80
448
|
|
|
81
449
|
|
|
82
450
|
class GeminiBackend(LLMBackend):
|
|
83
|
-
"""Google Gemini backend using structured output for coordination."""
|
|
451
|
+
"""Google Gemini backend using structured output for coordination and MCP tool integration."""
|
|
84
452
|
|
|
85
453
|
def __init__(self, api_key: Optional[str] = None, **kwargs):
|
|
86
454
|
super().__init__(api_key, **kwargs)
|
|
87
|
-
self.api_key = (
|
|
88
|
-
api_key or os.getenv("GOOGLE_API_KEY") or os.getenv("GEMINI_API_KEY")
|
|
89
|
-
)
|
|
455
|
+
self.api_key = api_key or os.getenv("GOOGLE_API_KEY") or os.getenv("GEMINI_API_KEY")
|
|
90
456
|
self.search_count = 0
|
|
91
457
|
self.code_execution_count = 0
|
|
92
458
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
459
|
+
# MCP integration (filesystem MCP server may have been injected by base class)
|
|
460
|
+
self.mcp_servers = self.config.get("mcp_servers", [])
|
|
461
|
+
self.allowed_tools = kwargs.pop("allowed_tools", None)
|
|
462
|
+
self.exclude_tools = kwargs.pop("exclude_tools", None)
|
|
463
|
+
self._mcp_client: Optional[MCPClient] = None
|
|
464
|
+
self._mcp_initialized = False
|
|
465
|
+
|
|
466
|
+
# MCP tool execution monitoring
|
|
467
|
+
self._mcp_tool_calls_count = 0
|
|
468
|
+
self._mcp_tool_failures = 0
|
|
469
|
+
self._mcp_tool_successes = 0
|
|
470
|
+
|
|
471
|
+
# MCP Response Extractor for capturing tool interactions
|
|
472
|
+
self.mcp_extractor = MCPResponseExtractor()
|
|
473
|
+
|
|
474
|
+
# Limit for message history growth within MCP execution loop
|
|
475
|
+
self._max_mcp_message_history = kwargs.pop("max_mcp_message_history", 200)
|
|
476
|
+
self._mcp_connection_retries = 0
|
|
477
|
+
|
|
478
|
+
# Circuit breaker configuration
|
|
479
|
+
self._circuit_breakers_enabled = kwargs.pop("circuit_breaker_enabled", True)
|
|
480
|
+
self._mcp_tools_circuit_breaker = None
|
|
481
|
+
|
|
482
|
+
# Initialize agent_id for use throughout the class
|
|
483
|
+
self.agent_id = kwargs.get("agent_id", None)
|
|
484
|
+
|
|
485
|
+
# Initialize circuit breaker if enabled
|
|
486
|
+
if self._circuit_breakers_enabled:
|
|
487
|
+
# Fail fast if required utilities are missing
|
|
488
|
+
if MCPCircuitBreakerManager is None:
|
|
489
|
+
raise RuntimeError("Circuit breakers enabled but MCPCircuitBreakerManager is not available")
|
|
490
|
+
|
|
491
|
+
try:
|
|
492
|
+
from ..mcp_tools.circuit_breaker import MCPCircuitBreaker
|
|
493
|
+
|
|
494
|
+
# Use shared utility to build circuit breaker configuration
|
|
495
|
+
if MCPConfigHelper is not None:
|
|
496
|
+
mcp_tools_config = MCPConfigHelper.build_circuit_breaker_config("mcp_tools", backend_name="gemini")
|
|
497
|
+
else:
|
|
498
|
+
mcp_tools_config = None
|
|
499
|
+
if mcp_tools_config:
|
|
500
|
+
self._mcp_tools_circuit_breaker = MCPCircuitBreaker(mcp_tools_config, backend_name="gemini", agent_id=self.agent_id)
|
|
501
|
+
log_backend_activity(
|
|
502
|
+
"gemini",
|
|
503
|
+
"Circuit breaker initialized for MCP tools",
|
|
504
|
+
{"enabled": True},
|
|
505
|
+
agent_id=self.agent_id,
|
|
506
|
+
)
|
|
507
|
+
else:
|
|
508
|
+
log_backend_activity(
|
|
509
|
+
"gemini",
|
|
510
|
+
"Circuit breaker config unavailable",
|
|
511
|
+
{"fallback": "disabled"},
|
|
512
|
+
agent_id=self.agent_id,
|
|
513
|
+
)
|
|
514
|
+
self._circuit_breakers_enabled = False
|
|
515
|
+
except ImportError:
|
|
516
|
+
log_backend_activity(
|
|
517
|
+
"gemini",
|
|
518
|
+
"Circuit breaker import failed",
|
|
519
|
+
{"fallback": "disabled"},
|
|
520
|
+
agent_id=self.agent_id,
|
|
521
|
+
)
|
|
522
|
+
self._circuit_breakers_enabled = False
|
|
523
|
+
|
|
524
|
+
def _setup_permission_hooks(self):
|
|
525
|
+
"""Override base class - Gemini uses session-based permissions, not function hooks."""
|
|
526
|
+
logger.debug("[Gemini] Using session-based permissions, skipping function hook setup")
|
|
527
|
+
|
|
528
|
+
async def _setup_mcp_with_status_stream(self, agent_id: Optional[str] = None) -> AsyncGenerator[StreamChunk, None]:
|
|
529
|
+
"""Initialize MCP client with status streaming."""
|
|
530
|
+
status_queue: asyncio.Queue[StreamChunk] = asyncio.Queue()
|
|
531
|
+
|
|
532
|
+
async def status_callback(status: str, details: Dict[str, Any]) -> None:
|
|
533
|
+
"""Callback to queue status updates as StreamChunks."""
|
|
534
|
+
chunk = StreamChunk(
|
|
535
|
+
type="mcp_status",
|
|
536
|
+
status=status,
|
|
537
|
+
content=details.get("message", ""),
|
|
538
|
+
source="mcp_tools",
|
|
539
|
+
)
|
|
540
|
+
await status_queue.put(chunk)
|
|
541
|
+
|
|
542
|
+
# Start the actual setup in background
|
|
543
|
+
setup_task = asyncio.create_task(self._setup_mcp_tools_internal(agent_id, status_callback))
|
|
544
|
+
|
|
545
|
+
# Yield status updates while setup is running
|
|
546
|
+
while not setup_task.done():
|
|
547
|
+
try:
|
|
548
|
+
chunk = await asyncio.wait_for(status_queue.get(), timeout=0.1)
|
|
549
|
+
yield chunk
|
|
550
|
+
except asyncio.TimeoutError:
|
|
551
|
+
continue
|
|
552
|
+
|
|
553
|
+
# Wait for setup to complete and handle any final errors
|
|
554
|
+
try:
|
|
555
|
+
await setup_task
|
|
556
|
+
except Exception as e:
|
|
557
|
+
yield StreamChunk(
|
|
558
|
+
type="mcp_status",
|
|
559
|
+
status="error",
|
|
560
|
+
content=f"MCP setup failed: {e}",
|
|
561
|
+
source="mcp_tools",
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
async def _setup_mcp_tools(self, agent_id: Optional[str] = None) -> None:
|
|
565
|
+
"""Initialize MCP client (sessions only) - backward compatibility."""
|
|
566
|
+
if not self.mcp_servers or self._mcp_initialized:
|
|
567
|
+
return
|
|
568
|
+
# Consume status updates without yielding them
|
|
569
|
+
async for _ in self._setup_mcp_with_status_stream(agent_id):
|
|
570
|
+
pass
|
|
571
|
+
|
|
572
|
+
async def _setup_mcp_tools_internal(
|
|
573
|
+
self,
|
|
574
|
+
agent_id: Optional[str] = None,
|
|
575
|
+
status_callback: Optional[Callable[[str, Dict[str, Any]], Awaitable[None]]] = None,
|
|
576
|
+
) -> None:
|
|
577
|
+
"""Internal MCP setup logic."""
|
|
578
|
+
if not self.mcp_servers or self._mcp_initialized:
|
|
579
|
+
return
|
|
580
|
+
|
|
581
|
+
if MCPClient is None:
|
|
582
|
+
reason = "MCP import failed - MCPClient not available"
|
|
583
|
+
log_backend_activity(
|
|
584
|
+
"gemini",
|
|
585
|
+
"MCP import failed",
|
|
586
|
+
{"reason": reason, "fallback": "workflow_tools"},
|
|
587
|
+
agent_id=agent_id,
|
|
96
588
|
)
|
|
589
|
+
if status_callback:
|
|
590
|
+
await status_callback(
|
|
591
|
+
"error",
|
|
592
|
+
{"message": "MCP import failed - falling back to workflow tools"},
|
|
593
|
+
)
|
|
594
|
+
# Clear MCP servers to prevent further attempts
|
|
595
|
+
self.mcp_servers = []
|
|
596
|
+
return
|
|
597
|
+
|
|
598
|
+
try:
|
|
599
|
+
# Validate MCP configuration before initialization
|
|
600
|
+
validated_config = {
|
|
601
|
+
"mcp_servers": self.mcp_servers,
|
|
602
|
+
"allowed_tools": self.allowed_tools,
|
|
603
|
+
"exclude_tools": self.exclude_tools,
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if MCPConfigValidator is not None:
|
|
607
|
+
try:
|
|
608
|
+
backend_config = {
|
|
609
|
+
"mcp_servers": self.mcp_servers,
|
|
610
|
+
"allowed_tools": self.allowed_tools,
|
|
611
|
+
"exclude_tools": self.exclude_tools,
|
|
612
|
+
}
|
|
613
|
+
# Use the comprehensive validator class for enhanced validation
|
|
614
|
+
validator = MCPConfigValidator()
|
|
615
|
+
validated_config = validator.validate_backend_mcp_config(backend_config)
|
|
616
|
+
self.mcp_servers = validated_config.get("mcp_servers", self.mcp_servers)
|
|
617
|
+
log_backend_activity(
|
|
618
|
+
"gemini",
|
|
619
|
+
"MCP configuration validated",
|
|
620
|
+
{"server_count": len(self.mcp_servers)},
|
|
621
|
+
agent_id=agent_id,
|
|
622
|
+
)
|
|
623
|
+
if status_callback:
|
|
624
|
+
await status_callback(
|
|
625
|
+
"info",
|
|
626
|
+
{"message": f"MCP configuration validated: {len(self.mcp_servers)} servers"},
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
# Log validated server names for debugging
|
|
630
|
+
if True:
|
|
631
|
+
server_names = [server.get("name", "unnamed") for server in self.mcp_servers]
|
|
632
|
+
log_backend_activity(
|
|
633
|
+
"gemini",
|
|
634
|
+
"MCP servers validated",
|
|
635
|
+
{"servers": server_names},
|
|
636
|
+
agent_id=agent_id,
|
|
637
|
+
)
|
|
638
|
+
except MCPConfigurationError as e:
|
|
639
|
+
log_backend_activity(
|
|
640
|
+
"gemini",
|
|
641
|
+
"MCP configuration validation failed",
|
|
642
|
+
{"error": e.original_message},
|
|
643
|
+
agent_id=agent_id,
|
|
644
|
+
)
|
|
645
|
+
if status_callback:
|
|
646
|
+
await status_callback(
|
|
647
|
+
"error",
|
|
648
|
+
{"message": f"Invalid MCP configuration: {e.original_message}"},
|
|
649
|
+
)
|
|
650
|
+
self._mcp_client = None # Clear client state for consistency
|
|
651
|
+
raise RuntimeError(f"Invalid MCP configuration: {e.original_message}") from e
|
|
652
|
+
except MCPValidationError as e:
|
|
653
|
+
log_backend_activity(
|
|
654
|
+
"gemini",
|
|
655
|
+
"MCP validation failed",
|
|
656
|
+
{"error": e.original_message},
|
|
657
|
+
agent_id=agent_id,
|
|
658
|
+
)
|
|
659
|
+
if status_callback:
|
|
660
|
+
await status_callback(
|
|
661
|
+
"error",
|
|
662
|
+
{"message": f"MCP validation error: {e.original_message}"},
|
|
663
|
+
)
|
|
664
|
+
self._mcp_client = None # Clear client state for consistency
|
|
665
|
+
raise RuntimeError(f"MCP validation error: {e.original_message}") from e
|
|
666
|
+
except Exception as e:
|
|
667
|
+
if isinstance(e, (ImportError, AttributeError)):
|
|
668
|
+
log_backend_activity(
|
|
669
|
+
"gemini",
|
|
670
|
+
"MCP validation unavailable",
|
|
671
|
+
{"reason": str(e)},
|
|
672
|
+
agent_id=agent_id,
|
|
673
|
+
)
|
|
674
|
+
# Don't clear client for import errors - validation just unavailable
|
|
675
|
+
else:
|
|
676
|
+
log_backend_activity(
|
|
677
|
+
"gemini",
|
|
678
|
+
"MCP validation error",
|
|
679
|
+
{"error": str(e)},
|
|
680
|
+
agent_id=agent_id,
|
|
681
|
+
)
|
|
682
|
+
self._mcp_client = None # Clear client state for consistency
|
|
683
|
+
raise RuntimeError(f"MCP configuration validation failed: {e}") from e
|
|
684
|
+
else:
|
|
685
|
+
log_backend_activity(
|
|
686
|
+
"gemini",
|
|
687
|
+
"MCP validation skipped",
|
|
688
|
+
{"reason": "validator_unavailable"},
|
|
689
|
+
agent_id=agent_id,
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
# Instead of the current fallback logic
|
|
693
|
+
normalized_servers = MCPSetupManager.normalize_mcp_servers(self.mcp_servers)
|
|
694
|
+
log_backend_activity(
|
|
695
|
+
"gemini",
|
|
696
|
+
"Setting up MCP sessions",
|
|
697
|
+
{"server_count": len(normalized_servers)},
|
|
698
|
+
agent_id=agent_id,
|
|
699
|
+
)
|
|
700
|
+
if status_callback:
|
|
701
|
+
await status_callback(
|
|
702
|
+
"info",
|
|
703
|
+
{"message": f"Setting up MCP sessions for {len(normalized_servers)} servers"},
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
# Apply circuit breaker filtering before connection attempts
|
|
707
|
+
if self._circuit_breakers_enabled and self._mcp_tools_circuit_breaker:
|
|
708
|
+
filtered_servers = MCPCircuitBreakerManager.apply_circuit_breaker_filtering(
|
|
709
|
+
normalized_servers,
|
|
710
|
+
self._mcp_tools_circuit_breaker,
|
|
711
|
+
backend_name="gemini",
|
|
712
|
+
agent_id=agent_id,
|
|
713
|
+
)
|
|
714
|
+
else:
|
|
715
|
+
filtered_servers = normalized_servers
|
|
716
|
+
if not filtered_servers:
|
|
717
|
+
log_backend_activity(
|
|
718
|
+
"gemini",
|
|
719
|
+
"All MCP servers blocked by circuit breaker",
|
|
720
|
+
{},
|
|
721
|
+
agent_id=agent_id,
|
|
722
|
+
)
|
|
723
|
+
if status_callback:
|
|
724
|
+
await status_callback(
|
|
725
|
+
"warning",
|
|
726
|
+
{"message": "All MCP servers blocked by circuit breaker"},
|
|
727
|
+
)
|
|
728
|
+
return
|
|
729
|
+
|
|
730
|
+
if len(filtered_servers) < len(normalized_servers):
|
|
731
|
+
log_backend_activity(
|
|
732
|
+
"gemini",
|
|
733
|
+
"Circuit breaker filtered servers",
|
|
734
|
+
{"filtered_count": len(normalized_servers) - len(filtered_servers)},
|
|
735
|
+
agent_id=agent_id,
|
|
736
|
+
)
|
|
737
|
+
if status_callback:
|
|
738
|
+
await status_callback(
|
|
739
|
+
"warning",
|
|
740
|
+
{"message": f"Circuit breaker filtered {len(normalized_servers) - len(filtered_servers)} servers"},
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
# Extract tool filtering parameters from validated config
|
|
744
|
+
allowed_tools = validated_config.get("allowed_tools")
|
|
745
|
+
exclude_tools = validated_config.get("exclude_tools")
|
|
746
|
+
|
|
747
|
+
# Log tool filtering if configured
|
|
748
|
+
if allowed_tools:
|
|
749
|
+
log_backend_activity(
|
|
750
|
+
"gemini",
|
|
751
|
+
"MCP tool filtering configured",
|
|
752
|
+
{"allowed_tools": allowed_tools},
|
|
753
|
+
agent_id=agent_id,
|
|
754
|
+
)
|
|
755
|
+
if exclude_tools:
|
|
756
|
+
log_backend_activity(
|
|
757
|
+
"gemini",
|
|
758
|
+
"MCP tool filtering configured",
|
|
759
|
+
{"exclude_tools": exclude_tools},
|
|
760
|
+
agent_id=agent_id,
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
# Create client with status callback and hooks
|
|
764
|
+
self._mcp_client = MCPClient(
|
|
765
|
+
filtered_servers,
|
|
766
|
+
timeout_seconds=30,
|
|
767
|
+
allowed_tools=allowed_tools,
|
|
768
|
+
exclude_tools=exclude_tools,
|
|
769
|
+
status_callback=status_callback,
|
|
770
|
+
hooks=self.filesystem_manager.get_pre_tool_hooks() if self.filesystem_manager else {},
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
# Connect the client
|
|
774
|
+
await self._mcp_client.connect()
|
|
775
|
+
|
|
776
|
+
# Determine which servers actually connected
|
|
777
|
+
try:
|
|
778
|
+
connected_server_names = self._mcp_client.get_server_names()
|
|
779
|
+
except Exception:
|
|
780
|
+
connected_server_names = []
|
|
781
|
+
|
|
782
|
+
if not connected_server_names:
|
|
783
|
+
# Treat as connection failure: no active servers
|
|
784
|
+
if self._circuit_breakers_enabled and self._mcp_tools_circuit_breaker:
|
|
785
|
+
await MCPCircuitBreakerManager.record_event(
|
|
786
|
+
filtered_servers,
|
|
787
|
+
self._mcp_tools_circuit_breaker,
|
|
788
|
+
"failure",
|
|
789
|
+
error_message="No servers connected",
|
|
790
|
+
backend_name="gemini",
|
|
791
|
+
agent_id=agent_id,
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
log_backend_activity(
|
|
795
|
+
"gemini",
|
|
796
|
+
"MCP connection failed: no servers connected",
|
|
797
|
+
{},
|
|
798
|
+
agent_id=agent_id,
|
|
799
|
+
)
|
|
800
|
+
if status_callback:
|
|
801
|
+
await status_callback(
|
|
802
|
+
"error",
|
|
803
|
+
{"message": "MCP connection failed: no servers connected"},
|
|
804
|
+
)
|
|
805
|
+
self._mcp_client = None
|
|
806
|
+
return
|
|
807
|
+
|
|
808
|
+
# Record success ONLY for servers that actually connected
|
|
809
|
+
connected_server_configs = [server for server in filtered_servers if server.get("name") in connected_server_names]
|
|
810
|
+
if connected_server_configs:
|
|
811
|
+
if self._circuit_breakers_enabled and self._mcp_tools_circuit_breaker:
|
|
812
|
+
await MCPCircuitBreakerManager.record_event(
|
|
813
|
+
connected_server_configs,
|
|
814
|
+
self._mcp_tools_circuit_breaker,
|
|
815
|
+
"success",
|
|
816
|
+
backend_name="gemini",
|
|
817
|
+
agent_id=agent_id,
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
self._mcp_initialized = True
|
|
821
|
+
log_backend_activity("gemini", "MCP sessions initialized successfully", {}, agent_id=agent_id)
|
|
822
|
+
if status_callback:
|
|
823
|
+
await status_callback(
|
|
824
|
+
"success",
|
|
825
|
+
{"message": f"MCP sessions initialized successfully with {len(connected_server_names)} servers"},
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
except Exception as e:
|
|
829
|
+
# Record failure for circuit breaker
|
|
830
|
+
if self._circuit_breakers_enabled and self._mcp_tools_circuit_breaker:
|
|
831
|
+
servers = MCPSetupManager.normalize_mcp_servers(self.mcp_servers)
|
|
832
|
+
await MCPCircuitBreakerManager.record_event(
|
|
833
|
+
servers,
|
|
834
|
+
self._mcp_tools_circuit_breaker,
|
|
835
|
+
"failure",
|
|
836
|
+
error_message=str(e),
|
|
837
|
+
backend_name="gemini",
|
|
838
|
+
agent_id=agent_id,
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
# Enhanced error handling for different MCP error types
|
|
842
|
+
if isinstance(e, RuntimeError) and "MCP configuration" in str(e):
|
|
843
|
+
raise
|
|
844
|
+
elif isinstance(e, MCPConnectionError):
|
|
845
|
+
log_backend_activity(
|
|
846
|
+
"gemini",
|
|
847
|
+
"MCP connection failed during setup",
|
|
848
|
+
{"error": str(e)},
|
|
849
|
+
agent_id=agent_id,
|
|
850
|
+
)
|
|
851
|
+
if status_callback:
|
|
852
|
+
await status_callback(
|
|
853
|
+
"error",
|
|
854
|
+
{"message": f"Failed to establish MCP connections: {e}"},
|
|
855
|
+
)
|
|
856
|
+
self._mcp_client = None
|
|
857
|
+
raise RuntimeError(f"Failed to establish MCP connections: {e}") from e
|
|
858
|
+
elif isinstance(e, MCPTimeoutError):
|
|
859
|
+
log_backend_activity(
|
|
860
|
+
"gemini",
|
|
861
|
+
"MCP connection timeout during setup",
|
|
862
|
+
{"error": str(e)},
|
|
863
|
+
agent_id=agent_id,
|
|
864
|
+
)
|
|
865
|
+
if status_callback:
|
|
866
|
+
await status_callback("error", {"message": f"MCP connection timeout: {e}"})
|
|
867
|
+
self._mcp_client = None
|
|
868
|
+
raise RuntimeError(f"MCP connection timeout: {e}") from e
|
|
869
|
+
elif isinstance(e, MCPServerError):
|
|
870
|
+
log_backend_activity(
|
|
871
|
+
"gemini",
|
|
872
|
+
"MCP server error during setup",
|
|
873
|
+
{"error": str(e)},
|
|
874
|
+
agent_id=agent_id,
|
|
875
|
+
)
|
|
876
|
+
if status_callback:
|
|
877
|
+
await status_callback("error", {"message": f"MCP server error: {e}"})
|
|
878
|
+
self._mcp_client = None
|
|
879
|
+
raise RuntimeError(f"MCP server error: {e}") from e
|
|
880
|
+
elif isinstance(e, MCPError):
|
|
881
|
+
log_backend_activity(
|
|
882
|
+
"gemini",
|
|
883
|
+
"MCP error during setup",
|
|
884
|
+
{"error": str(e)},
|
|
885
|
+
agent_id=agent_id,
|
|
886
|
+
)
|
|
887
|
+
if status_callback:
|
|
888
|
+
await status_callback("error", {"message": f"MCP error during setup: {e}"})
|
|
889
|
+
self._mcp_client = None
|
|
890
|
+
return
|
|
891
|
+
|
|
892
|
+
else:
|
|
893
|
+
log_backend_activity(
|
|
894
|
+
"gemini",
|
|
895
|
+
"MCP session setup failed",
|
|
896
|
+
{"error": str(e)},
|
|
897
|
+
agent_id=agent_id,
|
|
898
|
+
)
|
|
899
|
+
if status_callback:
|
|
900
|
+
await status_callback("error", {"message": f"MCP session setup failed: {e}"})
|
|
901
|
+
self._mcp_client = None
|
|
97
902
|
|
|
98
903
|
def detect_coordination_tools(self, tools: List[Dict[str, Any]]) -> bool:
|
|
99
904
|
"""Detect if tools contain vote/new_answer coordination tools."""
|
|
@@ -110,9 +915,7 @@ class GeminiBackend(LLMBackend):
|
|
|
110
915
|
|
|
111
916
|
return "vote" in tool_names and "new_answer" in tool_names
|
|
112
917
|
|
|
113
|
-
def build_structured_output_prompt(
|
|
114
|
-
self, base_content: str, valid_agent_ids: Optional[List[str]] = None
|
|
115
|
-
) -> str:
|
|
918
|
+
def build_structured_output_prompt(self, base_content: str, valid_agent_ids: Optional[List[str]] = None) -> str:
|
|
116
919
|
"""Build prompt that encourages structured output for coordination."""
|
|
117
920
|
agent_list = ""
|
|
118
921
|
if valid_agent_ids:
|
|
@@ -127,14 +930,14 @@ If you want to VOTE for an existing agent's answer:
|
|
|
127
930
|
"action_type": "vote",
|
|
128
931
|
"vote_data": {{
|
|
129
932
|
"action": "vote",
|
|
130
|
-
"agent_id": "agent1", // Choose from: {agent_list or
|
|
933
|
+
"agent_id": "agent1", // Choose from: {agent_list or "agent1, agent2, agent3, etc."}
|
|
131
934
|
"reason": "Brief reason for your vote"
|
|
132
935
|
}}
|
|
133
936
|
}}
|
|
134
937
|
|
|
135
938
|
If you want to provide a NEW ANSWER:
|
|
136
939
|
{{
|
|
137
|
-
"action_type": "new_answer",
|
|
940
|
+
"action_type": "new_answer",
|
|
138
941
|
"answer_data": {{
|
|
139
942
|
"action": "new_answer",
|
|
140
943
|
"content": "Your complete improved answer here"
|
|
@@ -143,18 +946,12 @@ If you want to provide a NEW ANSWER:
|
|
|
143
946
|
|
|
144
947
|
Make your decision and include the JSON at the very end of your response."""
|
|
145
948
|
|
|
146
|
-
def extract_structured_response(
|
|
147
|
-
self, response_text: str
|
|
148
|
-
) -> Optional[Dict[str, Any]]:
|
|
949
|
+
def extract_structured_response(self, response_text: str) -> Optional[Dict[str, Any]]:
|
|
149
950
|
"""Extract structured JSON response from model output."""
|
|
150
951
|
try:
|
|
151
|
-
import re
|
|
152
|
-
|
|
153
952
|
# Strategy 0: Look for JSON inside markdown code blocks first
|
|
154
953
|
markdown_json_pattern = r"```json\s*(\{.*?\})\s*```"
|
|
155
|
-
markdown_matches = re.findall(
|
|
156
|
-
markdown_json_pattern, response_text, re.DOTALL
|
|
157
|
-
)
|
|
954
|
+
markdown_matches = re.findall(markdown_json_pattern, response_text, re.DOTALL)
|
|
158
955
|
|
|
159
956
|
for match in reversed(markdown_matches):
|
|
160
957
|
try:
|
|
@@ -231,9 +1028,7 @@ Make your decision and include the JSON at the very end of your response."""
|
|
|
231
1028
|
except Exception:
|
|
232
1029
|
return None
|
|
233
1030
|
|
|
234
|
-
def convert_structured_to_tool_calls(
|
|
235
|
-
self, structured_response: Dict[str, Any]
|
|
236
|
-
) -> List[Dict[str, Any]]:
|
|
1031
|
+
def convert_structured_to_tool_calls(self, structured_response: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
237
1032
|
"""Convert structured response to tool call format."""
|
|
238
1033
|
action_type = structured_response.get("action_type")
|
|
239
1034
|
|
|
@@ -241,7 +1036,7 @@ Make your decision and include the JSON at the very end of your response."""
|
|
|
241
1036
|
vote_data = structured_response.get("vote_data", {})
|
|
242
1037
|
return [
|
|
243
1038
|
{
|
|
244
|
-
"id": f"vote_{hash(str(vote_data)) % 10000}",
|
|
1039
|
+
"id": f"vote_{abs(hash(str(vote_data))) % 10000 + 1}",
|
|
245
1040
|
"type": "function",
|
|
246
1041
|
"function": {
|
|
247
1042
|
"name": "vote",
|
|
@@ -250,39 +1045,198 @@ Make your decision and include the JSON at the very end of your response."""
|
|
|
250
1045
|
"reason": vote_data.get("reason", ""),
|
|
251
1046
|
},
|
|
252
1047
|
},
|
|
253
|
-
}
|
|
1048
|
+
},
|
|
254
1049
|
]
|
|
255
1050
|
|
|
256
1051
|
elif action_type == "new_answer":
|
|
257
1052
|
answer_data = structured_response.get("answer_data", {})
|
|
258
1053
|
return [
|
|
259
1054
|
{
|
|
260
|
-
"id": f"new_answer_{hash(str(answer_data)) % 10000}",
|
|
1055
|
+
"id": f"new_answer_{abs(hash(str(answer_data))) % 10000 + 1}",
|
|
261
1056
|
"type": "function",
|
|
262
1057
|
"function": {
|
|
263
1058
|
"name": "new_answer",
|
|
264
1059
|
"arguments": {"content": answer_data.get("content", "")},
|
|
265
1060
|
},
|
|
266
|
-
}
|
|
1061
|
+
},
|
|
267
1062
|
]
|
|
268
1063
|
|
|
269
1064
|
return []
|
|
270
1065
|
|
|
271
|
-
async def
|
|
272
|
-
|
|
1066
|
+
async def _handle_mcp_retry_error(self, error: Exception, retry_count: int, max_retries: int) -> tuple[bool, AsyncGenerator[StreamChunk, None]]:
|
|
1067
|
+
"""Handle MCP retry errors with specific messaging and fallback logic.
|
|
1068
|
+
|
|
1069
|
+
Returns:
|
|
1070
|
+
tuple: (should_continue_retrying, error_chunks_generator)
|
|
1071
|
+
"""
|
|
1072
|
+
log_type, user_message, _ = MCPErrorHandler.get_error_details(error, None, log=False)
|
|
1073
|
+
|
|
1074
|
+
# Log the retry attempt
|
|
1075
|
+
log_backend_activity(
|
|
1076
|
+
"gemini",
|
|
1077
|
+
f"MCP {log_type} on retry",
|
|
1078
|
+
{"attempt": retry_count, "error": str(error)},
|
|
1079
|
+
agent_id=self.agent_id,
|
|
1080
|
+
)
|
|
1081
|
+
|
|
1082
|
+
# Check if we've exhausted retries
|
|
1083
|
+
if retry_count >= max_retries:
|
|
1084
|
+
|
|
1085
|
+
async def error_chunks():
|
|
1086
|
+
yield StreamChunk(
|
|
1087
|
+
type="content",
|
|
1088
|
+
content=f"\n⚠️ {user_message} after {max_retries} attempts; falling back to workflow tools\n",
|
|
1089
|
+
)
|
|
1090
|
+
|
|
1091
|
+
return False, error_chunks()
|
|
1092
|
+
|
|
1093
|
+
# Continue retrying
|
|
1094
|
+
async def empty_chunks():
|
|
1095
|
+
# Empty generator - just return without yielding anything
|
|
1096
|
+
if False: # Make this a generator without actually yielding
|
|
1097
|
+
yield
|
|
1098
|
+
|
|
1099
|
+
return True, empty_chunks()
|
|
1100
|
+
|
|
1101
|
+
async def _handle_mcp_error_and_fallback(
|
|
1102
|
+
self,
|
|
1103
|
+
error: Exception,
|
|
273
1104
|
) -> AsyncGenerator[StreamChunk, None]:
|
|
274
|
-
"""
|
|
1105
|
+
"""Handle MCP errors with specific messaging"""
|
|
1106
|
+
self._mcp_tool_failures += 1
|
|
1107
|
+
|
|
1108
|
+
log_type, user_message, _ = MCPErrorHandler.get_error_details(error, None, log=False)
|
|
1109
|
+
|
|
1110
|
+
# Log with specific error type
|
|
1111
|
+
log_backend_activity(
|
|
1112
|
+
"gemini",
|
|
1113
|
+
"MCP tool call failed",
|
|
1114
|
+
{
|
|
1115
|
+
"call_number": self._mcp_tool_calls_count,
|
|
1116
|
+
"error_type": log_type,
|
|
1117
|
+
"error": str(error),
|
|
1118
|
+
},
|
|
1119
|
+
agent_id=self.agent_id,
|
|
1120
|
+
)
|
|
1121
|
+
|
|
1122
|
+
# Yield user-friendly error message
|
|
1123
|
+
yield StreamChunk(
|
|
1124
|
+
type="content",
|
|
1125
|
+
content=f"\n⚠️ {user_message} ({error}); continuing without MCP tools\n",
|
|
1126
|
+
)
|
|
1127
|
+
|
|
1128
|
+
async def _execute_mcp_function_with_retry(self, function_name: str, args: Dict[str, Any], agent_id: Optional[str] = None) -> Any:
|
|
1129
|
+
"""Execute MCP function with exponential backoff retry logic."""
|
|
1130
|
+
if MCPExecutionManager is None:
|
|
1131
|
+
raise RuntimeError("MCPExecutionManager is not available - MCP backend utilities are missing")
|
|
1132
|
+
|
|
1133
|
+
# Stats callback for tracking
|
|
1134
|
+
async def stats_callback(action: str) -> int:
|
|
1135
|
+
if action == "increment_calls":
|
|
1136
|
+
self._mcp_tool_calls_count += 1
|
|
1137
|
+
return self._mcp_tool_calls_count
|
|
1138
|
+
elif action == "increment_failures":
|
|
1139
|
+
self._mcp_tool_failures += 1
|
|
1140
|
+
return self._mcp_tool_failures
|
|
1141
|
+
return 0
|
|
1142
|
+
|
|
1143
|
+
# Circuit breaker callback
|
|
1144
|
+
async def circuit_breaker_callback(event: str, error_msg: str) -> None:
|
|
1145
|
+
if event == "failure":
|
|
1146
|
+
if self._circuit_breakers_enabled and self._mcp_tools_circuit_breaker:
|
|
1147
|
+
servers = MCPSetupManager.normalize_mcp_servers(self.mcp_servers)
|
|
1148
|
+
await MCPCircuitBreakerManager.record_event(
|
|
1149
|
+
servers,
|
|
1150
|
+
self._mcp_tools_circuit_breaker,
|
|
1151
|
+
"failure",
|
|
1152
|
+
error_message=error_msg,
|
|
1153
|
+
backend_name="gemini",
|
|
1154
|
+
agent_id=agent_id,
|
|
1155
|
+
)
|
|
1156
|
+
else:
|
|
1157
|
+
# Record success only for currently connected servers
|
|
1158
|
+
connected_names: List[str] = []
|
|
1159
|
+
try:
|
|
1160
|
+
if self._mcp_client:
|
|
1161
|
+
connected_names = self._mcp_client.get_server_names()
|
|
1162
|
+
except Exception:
|
|
1163
|
+
connected_names = []
|
|
1164
|
+
|
|
1165
|
+
if connected_names:
|
|
1166
|
+
servers_to_record = [{"name": name} for name in connected_names]
|
|
1167
|
+
if self._circuit_breakers_enabled and self._mcp_tools_circuit_breaker:
|
|
1168
|
+
await MCPCircuitBreakerManager.record_event(
|
|
1169
|
+
servers_to_record,
|
|
1170
|
+
self._mcp_tools_circuit_breaker,
|
|
1171
|
+
"success",
|
|
1172
|
+
backend_name="gemini",
|
|
1173
|
+
agent_id=agent_id,
|
|
1174
|
+
)
|
|
1175
|
+
|
|
1176
|
+
return await MCPExecutionManager.execute_function_with_retry(
|
|
1177
|
+
function_name=function_name,
|
|
1178
|
+
args=args,
|
|
1179
|
+
functions=self.functions,
|
|
1180
|
+
max_retries=3,
|
|
1181
|
+
stats_callback=stats_callback,
|
|
1182
|
+
circuit_breaker_callback=circuit_breaker_callback,
|
|
1183
|
+
logger_instance=logger,
|
|
1184
|
+
)
|
|
1185
|
+
|
|
1186
|
+
async def stream_with_tools(self, messages: List[Dict[str, Any]], tools: List[Dict[str, Any]], **kwargs) -> AsyncGenerator[StreamChunk, None]:
|
|
1187
|
+
"""Stream response using Gemini API with structured output for coordination and MCP tool support."""
|
|
1188
|
+
# Use instance agent_id (from __init__) or get from kwargs if not set
|
|
1189
|
+
agent_id = self.agent_id or kwargs.get("agent_id", None)
|
|
1190
|
+
client = None
|
|
1191
|
+
stream = None
|
|
1192
|
+
|
|
1193
|
+
log_backend_activity(
|
|
1194
|
+
"gemini",
|
|
1195
|
+
"Starting stream_with_tools",
|
|
1196
|
+
{"num_messages": len(messages), "num_tools": len(tools) if tools else 0},
|
|
1197
|
+
agent_id=agent_id,
|
|
1198
|
+
)
|
|
1199
|
+
|
|
1200
|
+
# Only trim when MCP tools will be used
|
|
1201
|
+
if self.mcp_servers and MCPMessageManager is not None and hasattr(self, "_max_mcp_message_history") and self._max_mcp_message_history > 0:
|
|
1202
|
+
original_count = len(messages)
|
|
1203
|
+
messages = MCPMessageManager.trim_message_history(messages, self._max_mcp_message_history)
|
|
1204
|
+
if len(messages) < original_count:
|
|
1205
|
+
log_backend_activity(
|
|
1206
|
+
"gemini",
|
|
1207
|
+
"Trimmed MCP message history",
|
|
1208
|
+
{
|
|
1209
|
+
"original": original_count,
|
|
1210
|
+
"trimmed": len(messages),
|
|
1211
|
+
"limit": self._max_mcp_message_history,
|
|
1212
|
+
},
|
|
1213
|
+
agent_id=agent_id,
|
|
1214
|
+
)
|
|
1215
|
+
|
|
275
1216
|
try:
|
|
276
1217
|
from google import genai
|
|
277
1218
|
|
|
278
|
-
#
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
1219
|
+
# Setup MCP with status streaming if not already initialized
|
|
1220
|
+
if not self._mcp_initialized and self.mcp_servers:
|
|
1221
|
+
async for chunk in self._setup_mcp_with_status_stream(agent_id):
|
|
1222
|
+
yield chunk
|
|
1223
|
+
elif not self._mcp_initialized:
|
|
1224
|
+
# Setup MCP without streaming for backward compatibility
|
|
1225
|
+
await self._setup_mcp_tools(agent_id)
|
|
1226
|
+
|
|
1227
|
+
# Merge constructor config with stream kwargs (stream kwargs take priority)
|
|
1228
|
+
all_params = {**self.config, **kwargs}
|
|
1229
|
+
|
|
1230
|
+
# Extract framework-specific parameters
|
|
1231
|
+
enable_web_search = all_params.get("enable_web_search", False)
|
|
1232
|
+
enable_code_execution = all_params.get("enable_code_execution", False)
|
|
1233
|
+
|
|
1234
|
+
# Always use SDK MCP sessions when mcp_servers are configured
|
|
1235
|
+
using_sdk_mcp = bool(self.mcp_servers)
|
|
283
1236
|
|
|
284
|
-
#
|
|
1237
|
+
# Analyze tool types
|
|
285
1238
|
is_coordination = self.detect_coordination_tools(tools)
|
|
1239
|
+
|
|
286
1240
|
valid_agent_ids = None
|
|
287
1241
|
|
|
288
1242
|
if is_coordination:
|
|
@@ -291,32 +1245,31 @@ Make your decision and include the JSON at the very end of your response."""
|
|
|
291
1245
|
if tool.get("type") == "function":
|
|
292
1246
|
func_def = tool.get("function", {})
|
|
293
1247
|
if func_def.get("name") == "vote":
|
|
294
|
-
agent_id_param = (
|
|
295
|
-
func_def.get("parameters", {})
|
|
296
|
-
.get("properties", {})
|
|
297
|
-
.get("agent_id", {})
|
|
298
|
-
)
|
|
1248
|
+
agent_id_param = func_def.get("parameters", {}).get("properties", {}).get("agent_id", {})
|
|
299
1249
|
if "enum" in agent_id_param:
|
|
300
1250
|
valid_agent_ids = agent_id_param["enum"]
|
|
301
1251
|
break
|
|
302
1252
|
|
|
303
|
-
# Build content string from messages
|
|
1253
|
+
# Build content string from messages (include tool results for multi-turn tool calling)
|
|
304
1254
|
conversation_content = ""
|
|
305
1255
|
system_message = ""
|
|
306
1256
|
|
|
307
1257
|
for msg in messages:
|
|
308
|
-
|
|
1258
|
+
role = msg.get("role")
|
|
1259
|
+
if role == "system":
|
|
309
1260
|
system_message = msg.get("content", "")
|
|
310
|
-
elif
|
|
1261
|
+
elif role == "user":
|
|
311
1262
|
conversation_content += f"User: {msg.get('content', '')}\n"
|
|
312
|
-
elif
|
|
1263
|
+
elif role == "assistant":
|
|
313
1264
|
conversation_content += f"Assistant: {msg.get('content', '')}\n"
|
|
1265
|
+
elif role == "tool":
|
|
1266
|
+
# Ensure tool outputs are visible to the model on the next turn
|
|
1267
|
+
tool_output = msg.get("content", "")
|
|
1268
|
+
conversation_content += f"Tool Result: {tool_output}\n"
|
|
314
1269
|
|
|
315
1270
|
# For coordination requests, modify the prompt to use structured output
|
|
316
1271
|
if is_coordination:
|
|
317
|
-
conversation_content = self.build_structured_output_prompt(
|
|
318
|
-
conversation_content, valid_agent_ids
|
|
319
|
-
)
|
|
1272
|
+
conversation_content = self.build_structured_output_prompt(conversation_content, valid_agent_ids)
|
|
320
1273
|
|
|
321
1274
|
# Combine system message and conversation
|
|
322
1275
|
full_content = ""
|
|
@@ -327,7 +1280,7 @@ Make your decision and include the JSON at the very end of your response."""
|
|
|
327
1280
|
# Use google-genai package
|
|
328
1281
|
client = genai.Client(api_key=self.api_key)
|
|
329
1282
|
|
|
330
|
-
# Setup builtin tools
|
|
1283
|
+
# Setup builtin tools (only when not using SDK MCP sessions)
|
|
331
1284
|
builtin_tools = []
|
|
332
1285
|
if enable_web_search:
|
|
333
1286
|
try:
|
|
@@ -353,82 +1306,534 @@ Make your decision and include the JSON at the very end of your response."""
|
|
|
353
1306
|
content="\n⚠️ Code execution requires google.genai.types\n",
|
|
354
1307
|
)
|
|
355
1308
|
|
|
356
|
-
config
|
|
357
|
-
|
|
358
|
-
|
|
1309
|
+
# Build config with direct parameter passthrough
|
|
1310
|
+
config = {}
|
|
1311
|
+
|
|
1312
|
+
# Direct passthrough of all parameters except those handled separately
|
|
1313
|
+
excluded_params = self.get_base_excluded_config_params() | {
|
|
1314
|
+
# Gemini specific exclusions
|
|
1315
|
+
"enable_web_search",
|
|
1316
|
+
"enable_code_execution",
|
|
1317
|
+
"use_multi_mcp",
|
|
1318
|
+
"mcp_sdk_auto",
|
|
1319
|
+
"allowed_tools",
|
|
1320
|
+
"exclude_tools",
|
|
359
1321
|
}
|
|
1322
|
+
for key, value in all_params.items():
|
|
1323
|
+
if key not in excluded_params and value is not None:
|
|
1324
|
+
# Handle Gemini-specific parameter mappings
|
|
1325
|
+
if key == "max_tokens":
|
|
1326
|
+
config["max_output_tokens"] = value
|
|
1327
|
+
elif key == "model":
|
|
1328
|
+
model_name = value
|
|
1329
|
+
else:
|
|
1330
|
+
config[key] = value
|
|
1331
|
+
|
|
1332
|
+
# Setup tools configuration (builtins only when not using sessions)
|
|
1333
|
+
all_tools = []
|
|
1334
|
+
|
|
1335
|
+
# Branch 1: SDK auto-calling via MCP sessions (reuse existing MCPClient sessions)
|
|
1336
|
+
if using_sdk_mcp and self.mcp_servers:
|
|
1337
|
+
if not self._mcp_client or not getattr(self._mcp_client, "is_connected", lambda: False)():
|
|
1338
|
+
# Retry MCP connection up to 5 times before falling back
|
|
1339
|
+
max_mcp_retries = 5
|
|
1340
|
+
mcp_connected = False
|
|
1341
|
+
|
|
1342
|
+
for retry_count in range(1, max_mcp_retries + 1):
|
|
1343
|
+
try:
|
|
1344
|
+
# Track retry attempts
|
|
1345
|
+
self._mcp_connection_retries = retry_count
|
|
1346
|
+
|
|
1347
|
+
if retry_count > 1:
|
|
1348
|
+
log_backend_activity(
|
|
1349
|
+
"gemini",
|
|
1350
|
+
"MCP connection retry",
|
|
1351
|
+
{
|
|
1352
|
+
"attempt": retry_count,
|
|
1353
|
+
"max_retries": max_mcp_retries,
|
|
1354
|
+
},
|
|
1355
|
+
agent_id=agent_id,
|
|
1356
|
+
)
|
|
1357
|
+
# Yield retry status
|
|
1358
|
+
yield StreamChunk(
|
|
1359
|
+
type="mcp_status",
|
|
1360
|
+
status="mcp_retry",
|
|
1361
|
+
content=f"Retrying MCP connection (attempt {retry_count}/{max_mcp_retries})",
|
|
1362
|
+
source="mcp_tools",
|
|
1363
|
+
)
|
|
1364
|
+
# Brief delay between retries
|
|
1365
|
+
await asyncio.sleep(0.5 * retry_count) # Progressive backoff
|
|
1366
|
+
|
|
1367
|
+
# Apply circuit breaker filtering before retry attempts
|
|
1368
|
+
if self._circuit_breakers_enabled and self._mcp_tools_circuit_breaker:
|
|
1369
|
+
filtered_retry_servers = MCPCircuitBreakerManager.apply_circuit_breaker_filtering(
|
|
1370
|
+
self.mcp_servers,
|
|
1371
|
+
self._mcp_tools_circuit_breaker,
|
|
1372
|
+
backend_name="gemini",
|
|
1373
|
+
agent_id=agent_id,
|
|
1374
|
+
)
|
|
1375
|
+
else:
|
|
1376
|
+
filtered_retry_servers = self.mcp_servers
|
|
1377
|
+
if not filtered_retry_servers:
|
|
1378
|
+
log_backend_activity(
|
|
1379
|
+
"gemini",
|
|
1380
|
+
"All MCP servers blocked during retry",
|
|
1381
|
+
{},
|
|
1382
|
+
agent_id=agent_id,
|
|
1383
|
+
)
|
|
1384
|
+
# Yield blocked status
|
|
1385
|
+
yield StreamChunk(
|
|
1386
|
+
type="mcp_status",
|
|
1387
|
+
status="mcp_blocked",
|
|
1388
|
+
content="All MCP servers blocked by circuit breaker",
|
|
1389
|
+
source="mcp_tools",
|
|
1390
|
+
)
|
|
1391
|
+
using_sdk_mcp = False
|
|
1392
|
+
break
|
|
1393
|
+
|
|
1394
|
+
# Get validated config for tool filtering parameters
|
|
1395
|
+
backend_config = {"mcp_servers": self.mcp_servers}
|
|
1396
|
+
if MCPConfigValidator is not None:
|
|
1397
|
+
try:
|
|
1398
|
+
validator = MCPConfigValidator()
|
|
1399
|
+
validated_config_retry = validator.validate_backend_mcp_config(backend_config)
|
|
1400
|
+
allowed_tools_retry = validated_config_retry.get("allowed_tools")
|
|
1401
|
+
exclude_tools_retry = validated_config_retry.get("exclude_tools")
|
|
1402
|
+
except Exception:
|
|
1403
|
+
allowed_tools_retry = None
|
|
1404
|
+
exclude_tools_retry = None
|
|
1405
|
+
else:
|
|
1406
|
+
allowed_tools_retry = None
|
|
1407
|
+
exclude_tools_retry = None
|
|
1408
|
+
|
|
1409
|
+
self._mcp_client = await MCPClient.create_and_connect(
|
|
1410
|
+
filtered_retry_servers,
|
|
1411
|
+
timeout_seconds=30,
|
|
1412
|
+
allowed_tools=allowed_tools_retry,
|
|
1413
|
+
exclude_tools=exclude_tools_retry,
|
|
1414
|
+
)
|
|
360
1415
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
1416
|
+
# Record success for circuit breaker
|
|
1417
|
+
if self._circuit_breakers_enabled and self._mcp_tools_circuit_breaker:
|
|
1418
|
+
await MCPCircuitBreakerManager.record_event(
|
|
1419
|
+
filtered_retry_servers,
|
|
1420
|
+
self._mcp_tools_circuit_breaker,
|
|
1421
|
+
"success",
|
|
1422
|
+
backend_name="gemini",
|
|
1423
|
+
agent_id=agent_id,
|
|
1424
|
+
)
|
|
1425
|
+
mcp_connected = True
|
|
1426
|
+
log_backend_activity(
|
|
1427
|
+
"gemini",
|
|
1428
|
+
"MCP connection successful on retry",
|
|
1429
|
+
{"attempt": retry_count},
|
|
1430
|
+
agent_id=agent_id,
|
|
1431
|
+
)
|
|
1432
|
+
# Yield success status
|
|
1433
|
+
yield StreamChunk(
|
|
1434
|
+
type="mcp_status",
|
|
1435
|
+
status="mcp_connected",
|
|
1436
|
+
content=f"MCP connection successful on attempt {retry_count}",
|
|
1437
|
+
source="mcp_tools",
|
|
1438
|
+
)
|
|
1439
|
+
break
|
|
364
1440
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
1441
|
+
except (
|
|
1442
|
+
MCPConnectionError,
|
|
1443
|
+
MCPTimeoutError,
|
|
1444
|
+
MCPServerError,
|
|
1445
|
+
MCPError,
|
|
1446
|
+
Exception,
|
|
1447
|
+
) as e:
|
|
1448
|
+
# Record failure for circuit breaker
|
|
1449
|
+
if self._circuit_breakers_enabled and self._mcp_tools_circuit_breaker:
|
|
1450
|
+
servers = MCPSetupManager.normalize_mcp_servers(self.mcp_servers)
|
|
1451
|
+
await MCPCircuitBreakerManager.record_event(
|
|
1452
|
+
servers,
|
|
1453
|
+
self._mcp_tools_circuit_breaker,
|
|
1454
|
+
"failure",
|
|
1455
|
+
error_message=str(e),
|
|
1456
|
+
backend_name="gemini",
|
|
1457
|
+
agent_id=agent_id,
|
|
1458
|
+
)
|
|
1459
|
+
|
|
1460
|
+
(
|
|
1461
|
+
should_continue,
|
|
1462
|
+
error_chunks,
|
|
1463
|
+
) = await self._handle_mcp_retry_error(e, retry_count, max_mcp_retries)
|
|
1464
|
+
if not should_continue:
|
|
1465
|
+
async for chunk in error_chunks:
|
|
1466
|
+
yield chunk
|
|
1467
|
+
using_sdk_mcp = False
|
|
1468
|
+
|
|
1469
|
+
# If all retries failed, ensure we fall back gracefully
|
|
1470
|
+
if not mcp_connected:
|
|
1471
|
+
using_sdk_mcp = False
|
|
1472
|
+
self._mcp_client = None
|
|
1473
|
+
|
|
1474
|
+
if not using_sdk_mcp:
|
|
1475
|
+
all_tools.extend(builtin_tools)
|
|
1476
|
+
if all_tools:
|
|
1477
|
+
config["tools"] = all_tools
|
|
1478
|
+
|
|
1479
|
+
# For coordination requests, use JSON response format (may conflict with tools/sessions)
|
|
1480
|
+
if is_coordination:
|
|
1481
|
+
# Only request JSON schema when no tools are present
|
|
1482
|
+
if (not using_sdk_mcp) and (not all_tools):
|
|
1483
|
+
config["response_mime_type"] = "application/json"
|
|
1484
|
+
config["response_schema"] = CoordinationResponse.model_json_schema()
|
|
1485
|
+
else:
|
|
1486
|
+
# Tools or sessions are present; fallback to text parsing
|
|
1487
|
+
pass
|
|
1488
|
+
# Log messages being sent after builtin_tools is defined
|
|
1489
|
+
log_backend_agent_message(
|
|
1490
|
+
agent_id or "default",
|
|
1491
|
+
"SEND",
|
|
1492
|
+
{
|
|
1493
|
+
"content": full_content,
|
|
1494
|
+
"builtin_tools": len(builtin_tools) if builtin_tools else 0,
|
|
1495
|
+
},
|
|
1496
|
+
backend_name="gemini",
|
|
1497
|
+
)
|
|
372
1498
|
|
|
373
1499
|
# Use streaming for real-time response
|
|
374
1500
|
full_content_text = ""
|
|
375
1501
|
final_response = None
|
|
1502
|
+
if using_sdk_mcp and self.mcp_servers:
|
|
1503
|
+
# Reuse active sessions from MCPClient
|
|
1504
|
+
try:
|
|
1505
|
+
if not self._mcp_client:
|
|
1506
|
+
raise RuntimeError("MCP client not initialized")
|
|
1507
|
+
mcp_sessions = self._mcp_client.get_active_sessions()
|
|
1508
|
+
if not mcp_sessions:
|
|
1509
|
+
raise RuntimeError("No active MCP sessions available")
|
|
1510
|
+
|
|
1511
|
+
# Convert sessions to permission sessions if filesystem manager is available
|
|
1512
|
+
if self.filesystem_manager:
|
|
1513
|
+
logger.info(f"[Gemini] Converting {len(mcp_sessions)} MCP sessions to permission sessions")
|
|
1514
|
+
try:
|
|
1515
|
+
from ..mcp_tools.hooks import (
|
|
1516
|
+
convert_sessions_to_permission_sessions,
|
|
1517
|
+
)
|
|
376
1518
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
1519
|
+
mcp_sessions = convert_sessions_to_permission_sessions(mcp_sessions, self.filesystem_manager.path_permission_manager)
|
|
1520
|
+
except Exception as e:
|
|
1521
|
+
logger.error(f"[Gemini] Failed to convert sessions to permission sessions: {e}")
|
|
1522
|
+
# Continue with regular sessions on error
|
|
1523
|
+
else:
|
|
1524
|
+
logger.debug("[Gemini] No filesystem manager found, using standard sessions")
|
|
1525
|
+
|
|
1526
|
+
# Apply sessions as tools, do not mix with builtin or function_declarations
|
|
1527
|
+
session_config = dict(config)
|
|
1528
|
+
|
|
1529
|
+
# Get available tools from MCP client for logging
|
|
1530
|
+
available_tools = []
|
|
1531
|
+
if self._mcp_client:
|
|
1532
|
+
available_tools = list(self._mcp_client.tools.keys())
|
|
1533
|
+
|
|
1534
|
+
# Check planning mode - block MCP tools during coordination phase
|
|
1535
|
+
if self.is_planning_mode_enabled():
|
|
1536
|
+
logger.info("[Gemini] Planning mode enabled - blocking MCP tools during coordination")
|
|
1537
|
+
# Don't set tools, which prevents automatic function calling
|
|
1538
|
+
log_backend_activity(
|
|
1539
|
+
"gemini",
|
|
1540
|
+
"MCP tools blocked in planning mode",
|
|
1541
|
+
{
|
|
1542
|
+
"blocked_tools": len(available_tools),
|
|
1543
|
+
"session_count": len(mcp_sessions),
|
|
1544
|
+
},
|
|
1545
|
+
agent_id=agent_id,
|
|
1546
|
+
)
|
|
1547
|
+
else:
|
|
1548
|
+
# Log session types for debugging if needed
|
|
1549
|
+
logger.debug(f"[Gemini] Passing {len(mcp_sessions)} sessions to SDK: {[type(s).__name__ for s in mcp_sessions]}")
|
|
1550
|
+
|
|
1551
|
+
session_config["tools"] = mcp_sessions
|
|
1552
|
+
|
|
1553
|
+
# Track MCP tool usage attempt
|
|
1554
|
+
self._mcp_tool_calls_count += 1
|
|
1555
|
+
|
|
1556
|
+
log_backend_activity(
|
|
1557
|
+
"gemini",
|
|
1558
|
+
"MCP tool call initiated",
|
|
1559
|
+
{
|
|
1560
|
+
"call_number": self._mcp_tool_calls_count,
|
|
1561
|
+
"session_count": len(mcp_sessions),
|
|
1562
|
+
"available_tools": available_tools[:], # Log first 10 tools for brevity
|
|
1563
|
+
"total_tools": len(available_tools),
|
|
1564
|
+
},
|
|
1565
|
+
agent_id=agent_id,
|
|
1566
|
+
)
|
|
1567
|
+
|
|
1568
|
+
# Log MCP tool usage (SDK handles actual tool calling automatically)
|
|
1569
|
+
log_tool_call(
|
|
1570
|
+
agent_id,
|
|
1571
|
+
"mcp_session_tools",
|
|
1572
|
+
{
|
|
1573
|
+
"session_count": len(mcp_sessions),
|
|
1574
|
+
"call_number": self._mcp_tool_calls_count,
|
|
1575
|
+
"available_tools": available_tools,
|
|
1576
|
+
},
|
|
1577
|
+
backend_name="gemini",
|
|
1578
|
+
)
|
|
1579
|
+
|
|
1580
|
+
# Yield detailed MCP status as StreamChunk
|
|
1581
|
+
tools_info = f" ({len(available_tools)} tools available)" if available_tools else ""
|
|
1582
|
+
yield StreamChunk(
|
|
1583
|
+
type="mcp_status",
|
|
1584
|
+
status="mcp_tools_initiated",
|
|
1585
|
+
content=f"MCP tool call initiated (call #{self._mcp_tool_calls_count}){tools_info}: {', '.join(available_tools[:5])}{'...' if len(available_tools) > 5 else ''}",
|
|
1586
|
+
source="mcp_tools",
|
|
1587
|
+
)
|
|
1588
|
+
|
|
1589
|
+
# Use async streaming call with sessions (SDK supports auto-calling MCP here)
|
|
1590
|
+
# The SDK's session feature will still handle tool calling automatically
|
|
1591
|
+
stream = await client.aio.models.generate_content_stream(model=model_name, contents=full_content, config=session_config)
|
|
1592
|
+
|
|
1593
|
+
# Initialize MCPCallTracker and MCPResponseTracker for deduplication across chunks
|
|
1594
|
+
mcp_tracker = MCPCallTracker()
|
|
1595
|
+
mcp_response_tracker = MCPResponseTracker()
|
|
1596
|
+
mcp_tools_used = [] # Keep for backward compatibility
|
|
1597
|
+
|
|
1598
|
+
# Iterate over the asynchronous stream to get chunks as they arrive
|
|
1599
|
+
async for chunk in stream:
|
|
1600
|
+
# ============================================
|
|
1601
|
+
# 1. Process MCP function calls/responses
|
|
1602
|
+
# ============================================
|
|
1603
|
+
if hasattr(chunk, "automatic_function_calling_history") and chunk.automatic_function_calling_history:
|
|
1604
|
+
for history_item in chunk.automatic_function_calling_history:
|
|
1605
|
+
if hasattr(history_item, "parts") and history_item.parts is not None:
|
|
1606
|
+
for part in history_item.parts:
|
|
1607
|
+
# Check for function_call part
|
|
1608
|
+
if hasattr(part, "function_call") and part.function_call:
|
|
1609
|
+
# Use MCPResponseExtractor to extract call data
|
|
1610
|
+
call_data = self.mcp_extractor.extract_function_call(part.function_call)
|
|
1611
|
+
|
|
1612
|
+
if call_data:
|
|
1613
|
+
tool_name = call_data["name"]
|
|
1614
|
+
tool_args = call_data["arguments"]
|
|
1615
|
+
|
|
1616
|
+
# Check if this is a new call using the tracker
|
|
1617
|
+
if mcp_tracker.is_new_call(tool_name, tool_args):
|
|
1618
|
+
# Add to tracker history
|
|
1619
|
+
call_record = mcp_tracker.add_call(tool_name, tool_args)
|
|
1620
|
+
|
|
1621
|
+
# Add to legacy list for compatibility
|
|
1622
|
+
mcp_tools_used.append(
|
|
1623
|
+
{
|
|
1624
|
+
"name": tool_name,
|
|
1625
|
+
"arguments": tool_args,
|
|
1626
|
+
"timestamp": call_record["timestamp"],
|
|
1627
|
+
},
|
|
1628
|
+
)
|
|
1629
|
+
|
|
1630
|
+
# Format timestamp for display
|
|
1631
|
+
timestamp_str = time.strftime(
|
|
1632
|
+
"%H:%M:%S",
|
|
1633
|
+
time.localtime(call_record["timestamp"]),
|
|
1634
|
+
)
|
|
1635
|
+
|
|
1636
|
+
# Yield detailed MCP tool call information
|
|
1637
|
+
yield StreamChunk(
|
|
1638
|
+
type="mcp_status",
|
|
1639
|
+
status="mcp_tool_called",
|
|
1640
|
+
content=f"🔧 MCP Tool Called: {tool_name} at {timestamp_str} with args: {json.dumps(tool_args, indent=2)}",
|
|
1641
|
+
source="mcp_tools",
|
|
1642
|
+
)
|
|
1643
|
+
|
|
1644
|
+
# Log the specific tool call
|
|
1645
|
+
log_tool_call(
|
|
1646
|
+
agent_id,
|
|
1647
|
+
tool_name,
|
|
1648
|
+
tool_args,
|
|
1649
|
+
backend_name="gemini",
|
|
1650
|
+
)
|
|
1651
|
+
|
|
1652
|
+
# Check for function_response part
|
|
1653
|
+
elif hasattr(part, "function_response") and part.function_response:
|
|
1654
|
+
# Use MCPResponseExtractor to extract response data
|
|
1655
|
+
response_data = self.mcp_extractor.extract_function_response(part.function_response)
|
|
1656
|
+
|
|
1657
|
+
if response_data:
|
|
1658
|
+
tool_name = response_data["name"]
|
|
1659
|
+
tool_response = response_data["response"]
|
|
1660
|
+
|
|
1661
|
+
# Check if this is a new response using the tracker
|
|
1662
|
+
if mcp_response_tracker.is_new_response(tool_name, tool_response):
|
|
1663
|
+
# Add to tracker history
|
|
1664
|
+
response_record = mcp_response_tracker.add_response(tool_name, tool_response)
|
|
1665
|
+
|
|
1666
|
+
# Extract text content from CallToolResult
|
|
1667
|
+
response_text = None
|
|
1668
|
+
if isinstance(tool_response, dict) and "result" in tool_response:
|
|
1669
|
+
result = tool_response["result"]
|
|
1670
|
+
# Check if result has content attribute (CallToolResult object)
|
|
1671
|
+
if hasattr(result, "content") and result.content:
|
|
1672
|
+
# Get the first content item (TextContent object)
|
|
1673
|
+
first_content = result.content[0]
|
|
1674
|
+
# Extract the text attribute
|
|
1675
|
+
if hasattr(first_content, "text"):
|
|
1676
|
+
response_text = first_content.text
|
|
1677
|
+
|
|
1678
|
+
# Use extracted text or fallback to string representation
|
|
1679
|
+
if response_text is None:
|
|
1680
|
+
response_text = str(tool_response)
|
|
1681
|
+
|
|
1682
|
+
# Format timestamp for display
|
|
1683
|
+
timestamp_str = time.strftime(
|
|
1684
|
+
"%H:%M:%S",
|
|
1685
|
+
time.localtime(response_record["timestamp"]),
|
|
1686
|
+
)
|
|
1687
|
+
|
|
1688
|
+
# Yield MCP tool response information
|
|
1689
|
+
yield StreamChunk(
|
|
1690
|
+
type="mcp_status",
|
|
1691
|
+
status="mcp_tool_response",
|
|
1692
|
+
content=f"✅ MCP Tool Response from {tool_name} at {timestamp_str}: {response_text}",
|
|
1693
|
+
source="mcp_tools",
|
|
1694
|
+
)
|
|
1695
|
+
|
|
1696
|
+
# Log the tool response
|
|
1697
|
+
log_backend_activity(
|
|
1698
|
+
"gemini",
|
|
1699
|
+
"MCP tool response received",
|
|
1700
|
+
{
|
|
1701
|
+
"tool_name": tool_name,
|
|
1702
|
+
"response_preview": str(tool_response)[:],
|
|
1703
|
+
},
|
|
1704
|
+
agent_id=agent_id,
|
|
1705
|
+
)
|
|
1706
|
+
|
|
1707
|
+
# Track successful MCP tool execution (only on first chunk with MCP history)
|
|
1708
|
+
if not hasattr(self, "_mcp_stream_started"):
|
|
1709
|
+
self._mcp_tool_successes += 1
|
|
1710
|
+
self._mcp_stream_started = True
|
|
1711
|
+
log_backend_activity(
|
|
1712
|
+
"gemini",
|
|
1713
|
+
"MCP tool call succeeded",
|
|
1714
|
+
{"call_number": self._mcp_tool_calls_count},
|
|
1715
|
+
agent_id=agent_id,
|
|
412
1716
|
)
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
1717
|
+
|
|
1718
|
+
# Log MCP tool success as a tool call event
|
|
1719
|
+
log_tool_call(
|
|
1720
|
+
agent_id,
|
|
1721
|
+
"mcp_session_tools",
|
|
1722
|
+
{
|
|
1723
|
+
"session_count": len(mcp_sessions),
|
|
1724
|
+
"call_number": self._mcp_tool_calls_count,
|
|
1725
|
+
},
|
|
1726
|
+
result="success",
|
|
1727
|
+
backend_name="gemini",
|
|
421
1728
|
)
|
|
1729
|
+
|
|
1730
|
+
# Yield MCP success status as StreamChunk
|
|
422
1731
|
yield StreamChunk(
|
|
423
|
-
type="
|
|
424
|
-
|
|
1732
|
+
type="mcp_status",
|
|
1733
|
+
status="mcp_tools_success",
|
|
1734
|
+
content=f"MCP tool call succeeded (call #{self._mcp_tool_calls_count})",
|
|
1735
|
+
source="mcp_tools",
|
|
425
1736
|
)
|
|
426
1737
|
|
|
1738
|
+
# ============================================
|
|
1739
|
+
# 2. Process text content
|
|
1740
|
+
# ============================================
|
|
1741
|
+
if hasattr(chunk, "text") and chunk.text:
|
|
1742
|
+
chunk_text = chunk.text
|
|
1743
|
+
full_content_text += chunk_text
|
|
1744
|
+
log_backend_agent_message(
|
|
1745
|
+
agent_id,
|
|
1746
|
+
"RECV",
|
|
1747
|
+
{"content": chunk_text},
|
|
1748
|
+
backend_name="gemini",
|
|
1749
|
+
)
|
|
1750
|
+
log_stream_chunk("backend.gemini", "content", chunk_text, agent_id)
|
|
1751
|
+
yield StreamChunk(type="content", content=chunk_text)
|
|
1752
|
+
|
|
1753
|
+
# Reset stream tracking
|
|
1754
|
+
if hasattr(self, "_mcp_stream_started"):
|
|
1755
|
+
delattr(self, "_mcp_stream_started")
|
|
1756
|
+
|
|
1757
|
+
# Add MCP usage indicator with detailed summary using tracker
|
|
1758
|
+
tools_summary = mcp_tracker.get_summary()
|
|
1759
|
+
if not tools_summary or tools_summary == "No MCP tools called":
|
|
1760
|
+
tools_summary = "MCP session completed (no tools explicitly called)"
|
|
1761
|
+
else:
|
|
1762
|
+
tools_summary = f"MCP session complete - {tools_summary}"
|
|
1763
|
+
|
|
1764
|
+
log_stream_chunk("backend.gemini", "mcp_indicator", tools_summary, agent_id)
|
|
1765
|
+
yield StreamChunk(
|
|
1766
|
+
type="mcp_status",
|
|
1767
|
+
status="mcp_session_complete",
|
|
1768
|
+
content=f"MCP session complete - {tools_summary}",
|
|
1769
|
+
source="mcp_tools",
|
|
1770
|
+
)
|
|
1771
|
+
except (
|
|
1772
|
+
MCPConnectionError,
|
|
1773
|
+
MCPTimeoutError,
|
|
1774
|
+
MCPServerError,
|
|
1775
|
+
MCPError,
|
|
1776
|
+
Exception,
|
|
1777
|
+
) as e:
|
|
1778
|
+
log_stream_chunk("backend.gemini", "mcp_error", str(e), agent_id)
|
|
1779
|
+
|
|
1780
|
+
# Emit user-friendly error message
|
|
1781
|
+
async for chunk in self._handle_mcp_error_and_fallback(e):
|
|
1782
|
+
yield chunk
|
|
1783
|
+
|
|
1784
|
+
# Fallback to non-MCP streaming with manual configuration
|
|
1785
|
+
manual_config = dict(config)
|
|
1786
|
+
if all_tools:
|
|
1787
|
+
manual_config["tools"] = all_tools
|
|
1788
|
+
|
|
1789
|
+
# Need to create a new stream for fallback since stream is None
|
|
1790
|
+
stream = await client.aio.models.generate_content_stream(model=model_name, contents=full_content, config=manual_config)
|
|
1791
|
+
|
|
1792
|
+
async for chunk in stream:
|
|
1793
|
+
# Process text content
|
|
1794
|
+
if hasattr(chunk, "text") and chunk.text:
|
|
1795
|
+
chunk_text = chunk.text
|
|
1796
|
+
full_content_text += chunk_text
|
|
1797
|
+
# Log fallback content chunks
|
|
1798
|
+
log_stream_chunk(
|
|
1799
|
+
"backend.gemini",
|
|
1800
|
+
"fallback_content",
|
|
1801
|
+
chunk_text,
|
|
1802
|
+
agent_id,
|
|
1803
|
+
)
|
|
1804
|
+
yield StreamChunk(type="content", content=chunk_text)
|
|
1805
|
+
|
|
1806
|
+
else:
|
|
1807
|
+
# Non-MCP path (existing behavior)
|
|
1808
|
+
# Create stream for non-MCP path
|
|
1809
|
+
stream = await client.aio.models.generate_content_stream(model=model_name, contents=full_content, config=config)
|
|
1810
|
+
|
|
1811
|
+
async for chunk in stream:
|
|
1812
|
+
# ============================================
|
|
1813
|
+
# 1. Process text content
|
|
1814
|
+
# ============================================
|
|
1815
|
+
if hasattr(chunk, "text") and chunk.text:
|
|
1816
|
+
chunk_text = chunk.text
|
|
1817
|
+
full_content_text += chunk_text
|
|
1818
|
+
|
|
1819
|
+
# Enhanced logging for non-MCP streaming chunks
|
|
1820
|
+
log_stream_chunk("backend.gemini", "content", chunk_text, agent_id)
|
|
1821
|
+
log_backend_agent_message(
|
|
1822
|
+
agent_id,
|
|
1823
|
+
"RECV",
|
|
1824
|
+
{"content": chunk_text},
|
|
1825
|
+
backend_name="gemini",
|
|
1826
|
+
)
|
|
1827
|
+
|
|
1828
|
+
yield StreamChunk(type="content", content=chunk_text)
|
|
1829
|
+
|
|
427
1830
|
content = full_content_text
|
|
428
1831
|
|
|
429
|
-
# Process
|
|
430
|
-
tool_calls_detected = []
|
|
431
|
-
|
|
1832
|
+
# Process tool calls - only coordination tool calls (MCP manual mode removed)
|
|
1833
|
+
tool_calls_detected: List[Dict[str, Any]] = []
|
|
1834
|
+
|
|
1835
|
+
# Then, process coordination tools if present
|
|
1836
|
+
if is_coordination and content.strip() and not tool_calls_detected:
|
|
432
1837
|
# For structured output mode, the entire content is JSON
|
|
433
1838
|
structured_response = None
|
|
434
1839
|
# Try multiple parsing strategies
|
|
@@ -439,47 +1844,43 @@ Make your decision and include the JSON at the very end of your response."""
|
|
|
439
1844
|
# Strategy 2: Extract JSON from mixed text content (handles markdown-wrapped JSON)
|
|
440
1845
|
structured_response = self.extract_structured_response(content)
|
|
441
1846
|
|
|
442
|
-
if (
|
|
443
|
-
structured_response
|
|
444
|
-
and isinstance(structured_response, dict)
|
|
445
|
-
and "action_type" in structured_response
|
|
446
|
-
):
|
|
1847
|
+
if structured_response and isinstance(structured_response, dict) and "action_type" in structured_response:
|
|
447
1848
|
# Convert to tool calls
|
|
448
|
-
tool_calls = self.convert_structured_to_tool_calls(
|
|
449
|
-
structured_response
|
|
450
|
-
)
|
|
1849
|
+
tool_calls = self.convert_structured_to_tool_calls(structured_response)
|
|
451
1850
|
if tool_calls:
|
|
452
1851
|
tool_calls_detected = tool_calls
|
|
1852
|
+
# Log conversion to tool calls (summary)
|
|
1853
|
+
log_stream_chunk("backend.gemini", "tool_calls", tool_calls, agent_id)
|
|
1854
|
+
|
|
1855
|
+
# Log each coordination tool call for analytics/debugging
|
|
1856
|
+
try:
|
|
1857
|
+
for tool_call in tool_calls:
|
|
1858
|
+
log_tool_call(
|
|
1859
|
+
agent_id,
|
|
1860
|
+
tool_call.get("function", {}).get("name", "unknown_coordination_tool"),
|
|
1861
|
+
tool_call.get("function", {}).get("arguments", {}),
|
|
1862
|
+
result="coordination_tool_called",
|
|
1863
|
+
backend_name="gemini",
|
|
1864
|
+
)
|
|
1865
|
+
except Exception:
|
|
1866
|
+
# Ensure logging does not interrupt flow
|
|
1867
|
+
pass
|
|
453
1868
|
|
|
454
1869
|
# Process builtin tool results if any tools were used
|
|
455
|
-
|
|
456
|
-
if (
|
|
457
|
-
builtin_tools
|
|
458
|
-
and final_response
|
|
459
|
-
and hasattr(final_response, "candidates")
|
|
460
|
-
and final_response.candidates
|
|
461
|
-
):
|
|
1870
|
+
if builtin_tools and final_response and hasattr(final_response, "candidates") and final_response.candidates:
|
|
462
1871
|
# Check for grounding or code execution results
|
|
463
1872
|
candidate = final_response.candidates[0]
|
|
464
1873
|
|
|
465
1874
|
# Check for web search results - only show if actually used
|
|
466
|
-
if (
|
|
467
|
-
hasattr(candidate, "grounding_metadata")
|
|
468
|
-
and candidate.grounding_metadata
|
|
469
|
-
):
|
|
1875
|
+
if hasattr(candidate, "grounding_metadata") and candidate.grounding_metadata:
|
|
470
1876
|
# Check if web search was actually used by looking for queries or chunks
|
|
471
1877
|
search_actually_used = False
|
|
472
1878
|
search_queries = []
|
|
473
1879
|
|
|
474
1880
|
# Look for web search queries
|
|
475
|
-
if (
|
|
476
|
-
hasattr(candidate.grounding_metadata, "web_search_queries")
|
|
477
|
-
and candidate.grounding_metadata.web_search_queries
|
|
478
|
-
):
|
|
1881
|
+
if hasattr(candidate.grounding_metadata, "web_search_queries") and candidate.grounding_metadata.web_search_queries:
|
|
479
1882
|
try:
|
|
480
|
-
for
|
|
481
|
-
query
|
|
482
|
-
) in candidate.grounding_metadata.web_search_queries:
|
|
1883
|
+
for query in candidate.grounding_metadata.web_search_queries:
|
|
483
1884
|
if query and query.strip():
|
|
484
1885
|
search_queries.append(query.strip())
|
|
485
1886
|
search_actually_used = True
|
|
@@ -487,10 +1888,7 @@ Make your decision and include the JSON at the very end of your response."""
|
|
|
487
1888
|
pass
|
|
488
1889
|
|
|
489
1890
|
# Look for grounding chunks (indicates actual search results)
|
|
490
|
-
if (
|
|
491
|
-
hasattr(candidate.grounding_metadata, "grounding_chunks")
|
|
492
|
-
and candidate.grounding_metadata.grounding_chunks
|
|
493
|
-
):
|
|
1891
|
+
if hasattr(candidate.grounding_metadata, "grounding_chunks") and candidate.grounding_metadata.grounding_chunks:
|
|
494
1892
|
try:
|
|
495
1893
|
if len(candidate.grounding_metadata.grounding_chunks) > 0:
|
|
496
1894
|
search_actually_used = True
|
|
@@ -499,6 +1897,23 @@ Make your decision and include the JSON at the very end of your response."""
|
|
|
499
1897
|
|
|
500
1898
|
# Only show indicators if search was actually used
|
|
501
1899
|
if search_actually_used:
|
|
1900
|
+
# Enhanced web search logging
|
|
1901
|
+
log_stream_chunk(
|
|
1902
|
+
"backend.gemini",
|
|
1903
|
+
"web_search_result",
|
|
1904
|
+
{"queries": search_queries, "results_integrated": True},
|
|
1905
|
+
agent_id,
|
|
1906
|
+
)
|
|
1907
|
+
log_tool_call(
|
|
1908
|
+
agent_id,
|
|
1909
|
+
"google_search_retrieval",
|
|
1910
|
+
{
|
|
1911
|
+
"queries": search_queries,
|
|
1912
|
+
"chunks_found": len(candidate.grounding_metadata.grounding_chunks) if hasattr(candidate.grounding_metadata, "grounding_chunks") else 0,
|
|
1913
|
+
},
|
|
1914
|
+
result="search_completed",
|
|
1915
|
+
backend_name="gemini",
|
|
1916
|
+
)
|
|
502
1917
|
yield StreamChunk(
|
|
503
1918
|
type="content",
|
|
504
1919
|
content="🔍 [Builtin Tool: Web Search] Results integrated\n",
|
|
@@ -506,37 +1921,25 @@ Make your decision and include the JSON at the very end of your response."""
|
|
|
506
1921
|
|
|
507
1922
|
# Show search queries
|
|
508
1923
|
for query in search_queries:
|
|
509
|
-
|
|
510
|
-
|
|
1924
|
+
log_stream_chunk(
|
|
1925
|
+
"backend.gemini",
|
|
1926
|
+
"web_search_result",
|
|
1927
|
+
{"queries": search_queries, "results_integrated": True},
|
|
1928
|
+
agent_id,
|
|
511
1929
|
)
|
|
1930
|
+
yield StreamChunk(type="content", content=f"🔍 [Search Query] '{query}'\n")
|
|
512
1931
|
|
|
513
|
-
builtin_result = {
|
|
514
|
-
"id": f"web_search_{hash(str(candidate.grounding_metadata)) % 10000}",
|
|
515
|
-
"tool_type": "google_search_retrieval",
|
|
516
|
-
"status": "completed",
|
|
517
|
-
"metadata": str(candidate.grounding_metadata),
|
|
518
|
-
}
|
|
519
|
-
builtin_tool_results.append(builtin_result)
|
|
520
1932
|
self.search_count += 1
|
|
521
1933
|
|
|
522
1934
|
# Check for code execution in the response parts
|
|
523
|
-
if (
|
|
524
|
-
enable_code_execution
|
|
525
|
-
and hasattr(candidate, "content")
|
|
526
|
-
and hasattr(candidate.content, "parts")
|
|
527
|
-
):
|
|
1935
|
+
if enable_code_execution and hasattr(candidate, "content") and hasattr(candidate.content, "parts"):
|
|
528
1936
|
# Look for executable_code and code_execution_result parts
|
|
529
1937
|
code_parts = []
|
|
530
1938
|
for part in candidate.content.parts:
|
|
531
1939
|
if hasattr(part, "executable_code") and part.executable_code:
|
|
532
|
-
code_content = getattr(
|
|
533
|
-
part.executable_code, "code", str(part.executable_code)
|
|
534
|
-
)
|
|
1940
|
+
code_content = getattr(part.executable_code, "code", str(part.executable_code))
|
|
535
1941
|
code_parts.append(f"Code: {code_content}")
|
|
536
|
-
elif (
|
|
537
|
-
hasattr(part, "code_execution_result")
|
|
538
|
-
and part.code_execution_result
|
|
539
|
-
):
|
|
1942
|
+
elif hasattr(part, "code_execution_result") and part.code_execution_result:
|
|
540
1943
|
result_content = getattr(
|
|
541
1944
|
part.code_execution_result,
|
|
542
1945
|
"output",
|
|
@@ -546,6 +1949,25 @@ Make your decision and include the JSON at the very end of your response."""
|
|
|
546
1949
|
|
|
547
1950
|
if code_parts:
|
|
548
1951
|
# Code execution was actually used
|
|
1952
|
+
log_stream_chunk(
|
|
1953
|
+
"backend.gemini",
|
|
1954
|
+
"code_execution",
|
|
1955
|
+
"Code executed",
|
|
1956
|
+
agent_id,
|
|
1957
|
+
)
|
|
1958
|
+
|
|
1959
|
+
# Log code execution as a tool call event
|
|
1960
|
+
try:
|
|
1961
|
+
log_tool_call(
|
|
1962
|
+
agent_id,
|
|
1963
|
+
"code_execution",
|
|
1964
|
+
{"code_parts_count": len(code_parts)},
|
|
1965
|
+
result="code_executed",
|
|
1966
|
+
backend_name="gemini",
|
|
1967
|
+
)
|
|
1968
|
+
except Exception:
|
|
1969
|
+
pass
|
|
1970
|
+
|
|
549
1971
|
yield StreamChunk(
|
|
550
1972
|
type="content",
|
|
551
1973
|
content="💻 [Builtin Tool: Code Execution] Code executed\n",
|
|
@@ -554,36 +1976,51 @@ Make your decision and include the JSON at the very end of your response."""
|
|
|
554
1976
|
for part in code_parts:
|
|
555
1977
|
if part.startswith("Code: "):
|
|
556
1978
|
code_content = part[6:] # Remove "Code: " prefix
|
|
1979
|
+
log_stream_chunk(
|
|
1980
|
+
"backend.gemini",
|
|
1981
|
+
"code_execution_result",
|
|
1982
|
+
{
|
|
1983
|
+
"code_parts": len(code_parts),
|
|
1984
|
+
"execution_successful": True,
|
|
1985
|
+
"snippet": code_content,
|
|
1986
|
+
},
|
|
1987
|
+
agent_id,
|
|
1988
|
+
)
|
|
557
1989
|
yield StreamChunk(
|
|
558
1990
|
type="content",
|
|
559
1991
|
content=f"💻 [Code Executed]\n```python\n{code_content}\n```\n",
|
|
560
1992
|
)
|
|
561
1993
|
elif part.startswith("Result: "):
|
|
562
1994
|
result_content = part[8:] # Remove "Result: " prefix
|
|
1995
|
+
log_stream_chunk(
|
|
1996
|
+
"backend.gemini",
|
|
1997
|
+
"code_execution_result",
|
|
1998
|
+
{
|
|
1999
|
+
"code_parts": len(code_parts),
|
|
2000
|
+
"execution_successful": True,
|
|
2001
|
+
"result": result_content,
|
|
2002
|
+
},
|
|
2003
|
+
agent_id,
|
|
2004
|
+
)
|
|
563
2005
|
yield StreamChunk(
|
|
564
2006
|
type="content",
|
|
565
2007
|
content=f"📊 [Result] {result_content}\n",
|
|
566
2008
|
)
|
|
567
2009
|
|
|
568
|
-
builtin_result = {
|
|
569
|
-
"id": f"code_execution_{hash(str(code_parts)) % 10000}",
|
|
570
|
-
"tool_type": "code_execution",
|
|
571
|
-
"status": "completed",
|
|
572
|
-
"code_parts": code_parts,
|
|
573
|
-
"output": "; ".join(code_parts),
|
|
574
|
-
}
|
|
575
|
-
builtin_tool_results.append(builtin_result)
|
|
576
2010
|
self.code_execution_count += 1
|
|
577
2011
|
|
|
578
|
-
# Yield builtin tool results
|
|
579
|
-
if builtin_tool_results:
|
|
580
|
-
yield StreamChunk(
|
|
581
|
-
type="builtin_tool_results",
|
|
582
|
-
builtin_tool_results=builtin_tool_results,
|
|
583
|
-
)
|
|
584
|
-
|
|
585
2012
|
# Yield coordination tool calls if detected
|
|
586
2013
|
if tool_calls_detected:
|
|
2014
|
+
# Enhanced tool calls summary logging
|
|
2015
|
+
log_stream_chunk(
|
|
2016
|
+
"backend.gemini",
|
|
2017
|
+
"tool_calls_yielded",
|
|
2018
|
+
{
|
|
2019
|
+
"tool_count": len(tool_calls_detected),
|
|
2020
|
+
"tool_names": [tc.get("function", {}).get("name") for tc in tool_calls_detected],
|
|
2021
|
+
},
|
|
2022
|
+
agent_id,
|
|
2023
|
+
)
|
|
587
2024
|
yield StreamChunk(type="tool_calls", tool_calls=tool_calls_detected)
|
|
588
2025
|
|
|
589
2026
|
# Build complete message
|
|
@@ -591,62 +2028,234 @@ Make your decision and include the JSON at the very end of your response."""
|
|
|
591
2028
|
if tool_calls_detected:
|
|
592
2029
|
complete_message["tool_calls"] = tool_calls_detected
|
|
593
2030
|
|
|
594
|
-
|
|
595
|
-
|
|
2031
|
+
# Enhanced complete message logging with metadata
|
|
2032
|
+
log_stream_chunk(
|
|
2033
|
+
"backend.gemini",
|
|
2034
|
+
"complete_message",
|
|
2035
|
+
{
|
|
2036
|
+
"content_length": len(content.strip()),
|
|
2037
|
+
"has_tool_calls": bool(tool_calls_detected),
|
|
2038
|
+
},
|
|
2039
|
+
agent_id,
|
|
596
2040
|
)
|
|
2041
|
+
yield StreamChunk(type="complete_message", complete_message=complete_message)
|
|
2042
|
+
log_stream_chunk("backend.gemini", "done", None, agent_id)
|
|
597
2043
|
yield StreamChunk(type="done")
|
|
598
2044
|
|
|
599
2045
|
except Exception as e:
|
|
600
|
-
|
|
2046
|
+
error_msg = f"Gemini API error: {e}"
|
|
2047
|
+
# Enhanced error logging with structured details
|
|
2048
|
+
log_stream_chunk(
|
|
2049
|
+
"backend.gemini",
|
|
2050
|
+
"stream_error",
|
|
2051
|
+
{"error_type": type(e).__name__, "error_message": str(e)},
|
|
2052
|
+
agent_id,
|
|
2053
|
+
)
|
|
2054
|
+
yield StreamChunk(type="error", error=error_msg)
|
|
2055
|
+
finally:
|
|
2056
|
+
# Cleanup resources
|
|
2057
|
+
await self._cleanup_resources(stream, client)
|
|
2058
|
+
# Ensure context manager exit for MCP cleanup
|
|
2059
|
+
try:
|
|
2060
|
+
await self.__aexit__(None, None, None)
|
|
2061
|
+
except Exception as e:
|
|
2062
|
+
log_backend_activity(
|
|
2063
|
+
"gemini",
|
|
2064
|
+
"MCP cleanup failed",
|
|
2065
|
+
{"error": str(e)},
|
|
2066
|
+
agent_id=self.agent_id,
|
|
2067
|
+
)
|
|
601
2068
|
|
|
602
2069
|
def get_provider_name(self) -> str:
|
|
603
2070
|
"""Get the provider name."""
|
|
604
2071
|
return "Gemini"
|
|
605
2072
|
|
|
2073
|
+
def get_filesystem_support(self) -> FilesystemSupport:
|
|
2074
|
+
"""Gemini supports filesystem through MCP servers."""
|
|
2075
|
+
return FilesystemSupport.MCP
|
|
2076
|
+
|
|
606
2077
|
def get_supported_builtin_tools(self) -> List[str]:
|
|
607
2078
|
"""Get list of builtin tools supported by Gemini."""
|
|
608
2079
|
return ["google_search_retrieval", "code_execution"]
|
|
609
2080
|
|
|
610
|
-
def
|
|
611
|
-
"""
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
2081
|
+
def get_mcp_results(self) -> Dict[str, Any]:
|
|
2082
|
+
"""
|
|
2083
|
+
Get all captured MCP tool calls and responses.
|
|
2084
|
+
|
|
2085
|
+
Returns:
|
|
2086
|
+
Dict containing:
|
|
2087
|
+
- calls: List of all MCP tool calls
|
|
2088
|
+
- responses: List of all MCP tool responses
|
|
2089
|
+
- pairs: List of matched call-response pairs
|
|
2090
|
+
- summary: Statistical summary of interactions
|
|
2091
|
+
"""
|
|
2092
|
+
return {
|
|
2093
|
+
"calls": self.mcp_extractor.mcp_calls,
|
|
2094
|
+
"responses": self.mcp_extractor.mcp_responses,
|
|
2095
|
+
"pairs": self.mcp_extractor.call_response_pairs,
|
|
2096
|
+
"summary": self.mcp_extractor.get_summary(),
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
def get_mcp_paired_results(self) -> List[Dict[str, Any]]:
|
|
2100
|
+
"""
|
|
2101
|
+
Get only the paired MCP tool calls and responses.
|
|
2102
|
+
|
|
2103
|
+
Returns:
|
|
2104
|
+
List of dictionaries containing matched call-response pairs
|
|
2105
|
+
"""
|
|
2106
|
+
return self.mcp_extractor.call_response_pairs
|
|
2107
|
+
|
|
2108
|
+
def get_mcp_summary(self) -> Dict[str, Any]:
|
|
2109
|
+
"""
|
|
2110
|
+
Get a summary of MCP tool interactions.
|
|
2111
|
+
|
|
2112
|
+
Returns:
|
|
2113
|
+
Dictionary with statistics about MCP tool usage
|
|
2114
|
+
"""
|
|
2115
|
+
return self.mcp_extractor.get_summary()
|
|
2116
|
+
|
|
2117
|
+
def clear_mcp_results(self):
|
|
2118
|
+
"""Clear all stored MCP interaction data."""
|
|
2119
|
+
self.mcp_extractor.clear()
|
|
647
2120
|
|
|
648
2121
|
def reset_tool_usage(self):
|
|
649
2122
|
"""Reset tool usage tracking."""
|
|
650
2123
|
self.search_count = 0
|
|
651
2124
|
self.code_execution_count = 0
|
|
2125
|
+
# Reset MCP monitoring metrics
|
|
2126
|
+
self._mcp_tool_calls_count = 0
|
|
2127
|
+
self._mcp_tool_failures = 0
|
|
2128
|
+
self._mcp_tool_successes = 0
|
|
2129
|
+
self._mcp_connection_retries = 0
|
|
2130
|
+
# Clear MCP extractor data
|
|
2131
|
+
self.mcp_extractor.clear()
|
|
652
2132
|
super().reset_token_usage()
|
|
2133
|
+
|
|
2134
|
+
async def cleanup_mcp(self):
|
|
2135
|
+
"""Cleanup MCP connections."""
|
|
2136
|
+
if self._mcp_client:
|
|
2137
|
+
try:
|
|
2138
|
+
await self._mcp_client.disconnect()
|
|
2139
|
+
log_backend_activity("gemini", "MCP client disconnected", {}, agent_id=self.agent_id)
|
|
2140
|
+
except (
|
|
2141
|
+
MCPConnectionError,
|
|
2142
|
+
MCPTimeoutError,
|
|
2143
|
+
MCPServerError,
|
|
2144
|
+
MCPError,
|
|
2145
|
+
Exception,
|
|
2146
|
+
) as e:
|
|
2147
|
+
MCPErrorHandler.get_error_details(e, "disconnect", log=True)
|
|
2148
|
+
finally:
|
|
2149
|
+
self._mcp_client = None
|
|
2150
|
+
self._mcp_initialized = False
|
|
2151
|
+
|
|
2152
|
+
async def _cleanup_resources(self, stream, client):
|
|
2153
|
+
"""Cleanup google-genai resources to avoid unclosed aiohttp sessions."""
|
|
2154
|
+
# Close stream
|
|
2155
|
+
try:
|
|
2156
|
+
if stream is not None:
|
|
2157
|
+
close_fn = getattr(stream, "aclose", None) or getattr(stream, "close", None)
|
|
2158
|
+
if close_fn is not None:
|
|
2159
|
+
maybe = close_fn()
|
|
2160
|
+
if hasattr(maybe, "__await__"):
|
|
2161
|
+
await maybe
|
|
2162
|
+
except Exception as e:
|
|
2163
|
+
log_backend_activity(
|
|
2164
|
+
"gemini",
|
|
2165
|
+
"Stream cleanup failed",
|
|
2166
|
+
{"error": str(e)},
|
|
2167
|
+
agent_id=self.agent_id,
|
|
2168
|
+
)
|
|
2169
|
+
# Close internal aiohttp session held by google-genai BaseApiClient
|
|
2170
|
+
try:
|
|
2171
|
+
if client is not None:
|
|
2172
|
+
base_client = getattr(client, "_api_client", None)
|
|
2173
|
+
if base_client is not None:
|
|
2174
|
+
session = getattr(base_client, "_aiohttp_session", None)
|
|
2175
|
+
if session is not None and hasattr(session, "close"):
|
|
2176
|
+
if not session.closed:
|
|
2177
|
+
await session.close()
|
|
2178
|
+
log_backend_activity(
|
|
2179
|
+
"gemini",
|
|
2180
|
+
"Closed google-genai aiohttp session",
|
|
2181
|
+
{},
|
|
2182
|
+
agent_id=self.agent_id,
|
|
2183
|
+
)
|
|
2184
|
+
base_client._aiohttp_session = None
|
|
2185
|
+
# Yield control to allow connector cleanup
|
|
2186
|
+
await asyncio.sleep(0)
|
|
2187
|
+
except Exception as e:
|
|
2188
|
+
log_backend_activity(
|
|
2189
|
+
"gemini",
|
|
2190
|
+
"Failed to close google-genai aiohttp session",
|
|
2191
|
+
{"error": str(e)},
|
|
2192
|
+
agent_id=self.agent_id,
|
|
2193
|
+
)
|
|
2194
|
+
# Close internal async transport if exposed
|
|
2195
|
+
try:
|
|
2196
|
+
if client is not None and hasattr(client, "aio") and client.aio is not None:
|
|
2197
|
+
aio_obj = client.aio
|
|
2198
|
+
for method_name in ("close", "stop"):
|
|
2199
|
+
method = getattr(aio_obj, method_name, None)
|
|
2200
|
+
if method:
|
|
2201
|
+
maybe = method()
|
|
2202
|
+
if hasattr(maybe, "__await__"):
|
|
2203
|
+
await maybe
|
|
2204
|
+
break
|
|
2205
|
+
except Exception as e:
|
|
2206
|
+
log_backend_activity(
|
|
2207
|
+
"gemini",
|
|
2208
|
+
"Client AIO cleanup failed",
|
|
2209
|
+
{"error": str(e)},
|
|
2210
|
+
agent_id=self.agent_id,
|
|
2211
|
+
)
|
|
2212
|
+
|
|
2213
|
+
# Close client
|
|
2214
|
+
try:
|
|
2215
|
+
if client is not None:
|
|
2216
|
+
for method_name in ("aclose", "close"):
|
|
2217
|
+
method = getattr(client, method_name, None)
|
|
2218
|
+
if method:
|
|
2219
|
+
maybe = method()
|
|
2220
|
+
if hasattr(maybe, "__await__"):
|
|
2221
|
+
await maybe
|
|
2222
|
+
break
|
|
2223
|
+
except Exception as e:
|
|
2224
|
+
log_backend_activity(
|
|
2225
|
+
"gemini",
|
|
2226
|
+
"Client cleanup failed",
|
|
2227
|
+
{"error": str(e)},
|
|
2228
|
+
agent_id=self.agent_id,
|
|
2229
|
+
)
|
|
2230
|
+
|
|
2231
|
+
async def __aenter__(self) -> "GeminiBackend":
|
|
2232
|
+
"""Async context manager entry."""
|
|
2233
|
+
try:
|
|
2234
|
+
await self._setup_mcp_tools(agent_id=self.agent_id)
|
|
2235
|
+
except Exception as e:
|
|
2236
|
+
log_backend_activity(
|
|
2237
|
+
"gemini",
|
|
2238
|
+
"MCP setup failed during context entry",
|
|
2239
|
+
{"error": str(e)},
|
|
2240
|
+
agent_id=self.agent_id,
|
|
2241
|
+
)
|
|
2242
|
+
return self
|
|
2243
|
+
|
|
2244
|
+
async def __aexit__(
|
|
2245
|
+
self,
|
|
2246
|
+
exc_type: Optional[type],
|
|
2247
|
+
exc_val: Optional[BaseException],
|
|
2248
|
+
exc_tb: Optional[object],
|
|
2249
|
+
) -> None:
|
|
2250
|
+
"""Async context manager exit with automatic resource cleanup."""
|
|
2251
|
+
# Parameters are required by context manager protocol but not used
|
|
2252
|
+
_ = (exc_type, exc_val, exc_tb)
|
|
2253
|
+
try:
|
|
2254
|
+
await self.cleanup_mcp()
|
|
2255
|
+
except Exception as e:
|
|
2256
|
+
log_backend_activity(
|
|
2257
|
+
"gemini",
|
|
2258
|
+
"Backend cleanup error",
|
|
2259
|
+
{"error": str(e)},
|
|
2260
|
+
agent_id=self.agent_id,
|
|
2261
|
+
)
|