fast-agent-mcp 0.4.7__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.
- fast_agent/__init__.py +183 -0
- fast_agent/acp/__init__.py +19 -0
- fast_agent/acp/acp_aware_mixin.py +304 -0
- fast_agent/acp/acp_context.py +437 -0
- fast_agent/acp/content_conversion.py +136 -0
- fast_agent/acp/filesystem_runtime.py +427 -0
- fast_agent/acp/permission_store.py +269 -0
- fast_agent/acp/server/__init__.py +5 -0
- fast_agent/acp/server/agent_acp_server.py +1472 -0
- fast_agent/acp/slash_commands.py +1050 -0
- fast_agent/acp/terminal_runtime.py +408 -0
- fast_agent/acp/tool_permission_adapter.py +125 -0
- fast_agent/acp/tool_permissions.py +474 -0
- fast_agent/acp/tool_progress.py +814 -0
- fast_agent/agents/__init__.py +85 -0
- fast_agent/agents/agent_types.py +64 -0
- fast_agent/agents/llm_agent.py +350 -0
- fast_agent/agents/llm_decorator.py +1139 -0
- fast_agent/agents/mcp_agent.py +1337 -0
- fast_agent/agents/tool_agent.py +271 -0
- fast_agent/agents/workflow/agents_as_tools_agent.py +849 -0
- fast_agent/agents/workflow/chain_agent.py +212 -0
- fast_agent/agents/workflow/evaluator_optimizer.py +380 -0
- fast_agent/agents/workflow/iterative_planner.py +652 -0
- fast_agent/agents/workflow/maker_agent.py +379 -0
- fast_agent/agents/workflow/orchestrator_models.py +218 -0
- fast_agent/agents/workflow/orchestrator_prompts.py +248 -0
- fast_agent/agents/workflow/parallel_agent.py +250 -0
- fast_agent/agents/workflow/router_agent.py +353 -0
- fast_agent/cli/__init__.py +0 -0
- fast_agent/cli/__main__.py +73 -0
- fast_agent/cli/commands/acp.py +159 -0
- fast_agent/cli/commands/auth.py +404 -0
- fast_agent/cli/commands/check_config.py +783 -0
- fast_agent/cli/commands/go.py +514 -0
- fast_agent/cli/commands/quickstart.py +557 -0
- fast_agent/cli/commands/serve.py +143 -0
- fast_agent/cli/commands/server_helpers.py +114 -0
- fast_agent/cli/commands/setup.py +174 -0
- fast_agent/cli/commands/url_parser.py +190 -0
- fast_agent/cli/constants.py +40 -0
- fast_agent/cli/main.py +115 -0
- fast_agent/cli/terminal.py +24 -0
- fast_agent/config.py +798 -0
- fast_agent/constants.py +41 -0
- fast_agent/context.py +279 -0
- fast_agent/context_dependent.py +50 -0
- fast_agent/core/__init__.py +92 -0
- fast_agent/core/agent_app.py +448 -0
- fast_agent/core/core_app.py +137 -0
- fast_agent/core/direct_decorators.py +784 -0
- fast_agent/core/direct_factory.py +620 -0
- fast_agent/core/error_handling.py +27 -0
- fast_agent/core/exceptions.py +90 -0
- fast_agent/core/executor/__init__.py +0 -0
- fast_agent/core/executor/executor.py +280 -0
- fast_agent/core/executor/task_registry.py +32 -0
- fast_agent/core/executor/workflow_signal.py +324 -0
- fast_agent/core/fastagent.py +1186 -0
- fast_agent/core/logging/__init__.py +5 -0
- fast_agent/core/logging/events.py +138 -0
- fast_agent/core/logging/json_serializer.py +164 -0
- fast_agent/core/logging/listeners.py +309 -0
- fast_agent/core/logging/logger.py +278 -0
- fast_agent/core/logging/transport.py +481 -0
- fast_agent/core/prompt.py +9 -0
- fast_agent/core/prompt_templates.py +183 -0
- fast_agent/core/validation.py +326 -0
- fast_agent/event_progress.py +62 -0
- fast_agent/history/history_exporter.py +49 -0
- fast_agent/human_input/__init__.py +47 -0
- fast_agent/human_input/elicitation_handler.py +123 -0
- fast_agent/human_input/elicitation_state.py +33 -0
- fast_agent/human_input/form_elements.py +59 -0
- fast_agent/human_input/form_fields.py +256 -0
- fast_agent/human_input/simple_form.py +113 -0
- fast_agent/human_input/types.py +40 -0
- fast_agent/interfaces.py +310 -0
- fast_agent/llm/__init__.py +9 -0
- fast_agent/llm/cancellation.py +22 -0
- fast_agent/llm/fastagent_llm.py +931 -0
- fast_agent/llm/internal/passthrough.py +161 -0
- fast_agent/llm/internal/playback.py +129 -0
- fast_agent/llm/internal/silent.py +41 -0
- fast_agent/llm/internal/slow.py +38 -0
- fast_agent/llm/memory.py +275 -0
- fast_agent/llm/model_database.py +490 -0
- fast_agent/llm/model_factory.py +388 -0
- fast_agent/llm/model_info.py +102 -0
- fast_agent/llm/prompt_utils.py +155 -0
- fast_agent/llm/provider/anthropic/anthropic_utils.py +84 -0
- fast_agent/llm/provider/anthropic/cache_planner.py +56 -0
- fast_agent/llm/provider/anthropic/llm_anthropic.py +796 -0
- fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +462 -0
- fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
- fast_agent/llm/provider/bedrock/llm_bedrock.py +2207 -0
- fast_agent/llm/provider/bedrock/multipart_converter_bedrock.py +84 -0
- fast_agent/llm/provider/google/google_converter.py +466 -0
- fast_agent/llm/provider/google/llm_google_native.py +681 -0
- fast_agent/llm/provider/openai/llm_aliyun.py +31 -0
- fast_agent/llm/provider/openai/llm_azure.py +143 -0
- fast_agent/llm/provider/openai/llm_deepseek.py +76 -0
- fast_agent/llm/provider/openai/llm_generic.py +35 -0
- fast_agent/llm/provider/openai/llm_google_oai.py +32 -0
- fast_agent/llm/provider/openai/llm_groq.py +42 -0
- fast_agent/llm/provider/openai/llm_huggingface.py +85 -0
- fast_agent/llm/provider/openai/llm_openai.py +1195 -0
- fast_agent/llm/provider/openai/llm_openai_compatible.py +138 -0
- fast_agent/llm/provider/openai/llm_openrouter.py +45 -0
- fast_agent/llm/provider/openai/llm_tensorzero_openai.py +128 -0
- fast_agent/llm/provider/openai/llm_xai.py +38 -0
- fast_agent/llm/provider/openai/multipart_converter_openai.py +561 -0
- fast_agent/llm/provider/openai/openai_multipart.py +169 -0
- fast_agent/llm/provider/openai/openai_utils.py +67 -0
- fast_agent/llm/provider/openai/responses.py +133 -0
- fast_agent/llm/provider_key_manager.py +139 -0
- fast_agent/llm/provider_types.py +34 -0
- fast_agent/llm/request_params.py +61 -0
- fast_agent/llm/sampling_converter.py +98 -0
- fast_agent/llm/stream_types.py +9 -0
- fast_agent/llm/usage_tracking.py +445 -0
- fast_agent/mcp/__init__.py +56 -0
- fast_agent/mcp/common.py +26 -0
- fast_agent/mcp/elicitation_factory.py +84 -0
- fast_agent/mcp/elicitation_handlers.py +164 -0
- fast_agent/mcp/gen_client.py +83 -0
- fast_agent/mcp/helpers/__init__.py +36 -0
- fast_agent/mcp/helpers/content_helpers.py +352 -0
- fast_agent/mcp/helpers/server_config_helpers.py +25 -0
- fast_agent/mcp/hf_auth.py +147 -0
- fast_agent/mcp/interfaces.py +92 -0
- fast_agent/mcp/logger_textio.py +108 -0
- fast_agent/mcp/mcp_agent_client_session.py +411 -0
- fast_agent/mcp/mcp_aggregator.py +2175 -0
- fast_agent/mcp/mcp_connection_manager.py +723 -0
- fast_agent/mcp/mcp_content.py +262 -0
- fast_agent/mcp/mime_utils.py +108 -0
- fast_agent/mcp/oauth_client.py +509 -0
- fast_agent/mcp/prompt.py +159 -0
- fast_agent/mcp/prompt_message_extended.py +155 -0
- fast_agent/mcp/prompt_render.py +84 -0
- fast_agent/mcp/prompt_serialization.py +580 -0
- fast_agent/mcp/prompts/__init__.py +0 -0
- fast_agent/mcp/prompts/__main__.py +7 -0
- fast_agent/mcp/prompts/prompt_constants.py +18 -0
- fast_agent/mcp/prompts/prompt_helpers.py +238 -0
- fast_agent/mcp/prompts/prompt_load.py +186 -0
- fast_agent/mcp/prompts/prompt_server.py +552 -0
- fast_agent/mcp/prompts/prompt_template.py +438 -0
- fast_agent/mcp/resource_utils.py +215 -0
- fast_agent/mcp/sampling.py +200 -0
- fast_agent/mcp/server/__init__.py +4 -0
- fast_agent/mcp/server/agent_server.py +613 -0
- fast_agent/mcp/skybridge.py +44 -0
- fast_agent/mcp/sse_tracking.py +287 -0
- fast_agent/mcp/stdio_tracking_simple.py +59 -0
- fast_agent/mcp/streamable_http_tracking.py +309 -0
- fast_agent/mcp/tool_execution_handler.py +137 -0
- fast_agent/mcp/tool_permission_handler.py +88 -0
- fast_agent/mcp/transport_tracking.py +634 -0
- fast_agent/mcp/types.py +24 -0
- fast_agent/mcp/ui_agent.py +48 -0
- fast_agent/mcp/ui_mixin.py +209 -0
- fast_agent/mcp_server_registry.py +89 -0
- fast_agent/py.typed +0 -0
- fast_agent/resources/examples/data-analysis/analysis-campaign.py +189 -0
- fast_agent/resources/examples/data-analysis/analysis.py +68 -0
- fast_agent/resources/examples/data-analysis/fastagent.config.yaml +41 -0
- fast_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +1471 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +297 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
- fast_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
- fast_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
- fast_agent/resources/examples/mcp/elicitations/forms_demo.py +107 -0
- fast_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
- fast_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
- fast_agent/resources/examples/mcp/elicitations/tool_call.py +21 -0
- fast_agent/resources/examples/mcp/state-transfer/agent_one.py +18 -0
- fast_agent/resources/examples/mcp/state-transfer/agent_two.py +18 -0
- fast_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +27 -0
- fast_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +15 -0
- fast_agent/resources/examples/researcher/fastagent.config.yaml +61 -0
- fast_agent/resources/examples/researcher/researcher-eval.py +53 -0
- fast_agent/resources/examples/researcher/researcher-imp.py +189 -0
- fast_agent/resources/examples/researcher/researcher.py +36 -0
- fast_agent/resources/examples/tensorzero/.env.sample +2 -0
- fast_agent/resources/examples/tensorzero/Makefile +31 -0
- fast_agent/resources/examples/tensorzero/README.md +56 -0
- fast_agent/resources/examples/tensorzero/agent.py +35 -0
- fast_agent/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
- fast_agent/resources/examples/tensorzero/demo_images/crab.png +0 -0
- fast_agent/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
- fast_agent/resources/examples/tensorzero/docker-compose.yml +105 -0
- fast_agent/resources/examples/tensorzero/fastagent.config.yaml +19 -0
- fast_agent/resources/examples/tensorzero/image_demo.py +67 -0
- fast_agent/resources/examples/tensorzero/mcp_server/Dockerfile +25 -0
- fast_agent/resources/examples/tensorzero/mcp_server/entrypoint.sh +35 -0
- fast_agent/resources/examples/tensorzero/mcp_server/mcp_server.py +31 -0
- fast_agent/resources/examples/tensorzero/mcp_server/pyproject.toml +11 -0
- fast_agent/resources/examples/tensorzero/simple_agent.py +25 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/system_schema.json +29 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +11 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +35 -0
- fast_agent/resources/examples/workflows/agents_as_tools_extended.py +73 -0
- fast_agent/resources/examples/workflows/agents_as_tools_simple.py +50 -0
- fast_agent/resources/examples/workflows/chaining.py +37 -0
- fast_agent/resources/examples/workflows/evaluator.py +77 -0
- fast_agent/resources/examples/workflows/fastagent.config.yaml +26 -0
- fast_agent/resources/examples/workflows/graded_report.md +89 -0
- fast_agent/resources/examples/workflows/human_input.py +28 -0
- fast_agent/resources/examples/workflows/maker.py +156 -0
- fast_agent/resources/examples/workflows/orchestrator.py +70 -0
- fast_agent/resources/examples/workflows/parallel.py +56 -0
- fast_agent/resources/examples/workflows/router.py +69 -0
- fast_agent/resources/examples/workflows/short_story.md +13 -0
- fast_agent/resources/examples/workflows/short_story.txt +19 -0
- fast_agent/resources/setup/.gitignore +30 -0
- fast_agent/resources/setup/agent.py +28 -0
- fast_agent/resources/setup/fastagent.config.yaml +65 -0
- fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
- fast_agent/resources/setup/pyproject.toml.tmpl +23 -0
- fast_agent/skills/__init__.py +9 -0
- fast_agent/skills/registry.py +235 -0
- fast_agent/tools/elicitation.py +369 -0
- fast_agent/tools/shell_runtime.py +402 -0
- fast_agent/types/__init__.py +59 -0
- fast_agent/types/conversation_summary.py +294 -0
- fast_agent/types/llm_stop_reason.py +78 -0
- fast_agent/types/message_search.py +249 -0
- fast_agent/ui/__init__.py +38 -0
- fast_agent/ui/console.py +59 -0
- fast_agent/ui/console_display.py +1080 -0
- fast_agent/ui/elicitation_form.py +946 -0
- fast_agent/ui/elicitation_style.py +59 -0
- fast_agent/ui/enhanced_prompt.py +1400 -0
- fast_agent/ui/history_display.py +734 -0
- fast_agent/ui/interactive_prompt.py +1199 -0
- fast_agent/ui/markdown_helpers.py +104 -0
- fast_agent/ui/markdown_truncator.py +1004 -0
- fast_agent/ui/mcp_display.py +857 -0
- fast_agent/ui/mcp_ui_utils.py +235 -0
- fast_agent/ui/mermaid_utils.py +169 -0
- fast_agent/ui/message_primitives.py +50 -0
- fast_agent/ui/notification_tracker.py +205 -0
- fast_agent/ui/plain_text_truncator.py +68 -0
- fast_agent/ui/progress_display.py +10 -0
- fast_agent/ui/rich_progress.py +195 -0
- fast_agent/ui/streaming.py +774 -0
- fast_agent/ui/streaming_buffer.py +449 -0
- fast_agent/ui/tool_display.py +422 -0
- fast_agent/ui/usage_display.py +204 -0
- fast_agent/utils/__init__.py +5 -0
- fast_agent/utils/reasoning_stream_parser.py +77 -0
- fast_agent/utils/time.py +22 -0
- fast_agent/workflow_telemetry.py +261 -0
- fast_agent_mcp-0.4.7.dist-info/METADATA +788 -0
- fast_agent_mcp-0.4.7.dist-info/RECORD +261 -0
- fast_agent_mcp-0.4.7.dist-info/WHEEL +4 -0
- fast_agent_mcp-0.4.7.dist-info/entry_points.txt +7 -0
- fast_agent_mcp-0.4.7.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import secrets
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
|
|
5
|
+
# Import necessary types and client from google.genai
|
|
6
|
+
from google import genai
|
|
7
|
+
from google.genai import (
|
|
8
|
+
errors, # For error handling
|
|
9
|
+
types,
|
|
10
|
+
)
|
|
11
|
+
from mcp import Tool as McpTool
|
|
12
|
+
from mcp.types import (
|
|
13
|
+
CallToolRequest,
|
|
14
|
+
CallToolRequestParams,
|
|
15
|
+
ContentBlock,
|
|
16
|
+
TextContent,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from fast_agent.core.exceptions import ProviderKeyError
|
|
20
|
+
from fast_agent.core.prompt import Prompt
|
|
21
|
+
from fast_agent.llm.fastagent_llm import FastAgentLLM
|
|
22
|
+
from fast_agent.llm.model_database import ModelDatabase
|
|
23
|
+
|
|
24
|
+
# Import the new converter class
|
|
25
|
+
from fast_agent.llm.provider.google.google_converter import GoogleConverter
|
|
26
|
+
from fast_agent.llm.provider_types import Provider
|
|
27
|
+
from fast_agent.llm.usage_tracking import TurnUsage
|
|
28
|
+
from fast_agent.types import PromptMessageExtended, RequestParams
|
|
29
|
+
from fast_agent.types.llm_stop_reason import LlmStopReason
|
|
30
|
+
|
|
31
|
+
# Define default model and potentially other Google-specific defaults
|
|
32
|
+
DEFAULT_GOOGLE_MODEL = "gemini25"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Define Google-specific parameter exclusions if necessary
|
|
36
|
+
GOOGLE_EXCLUDE_FIELDS = {
|
|
37
|
+
# Add fields that should not be passed directly from RequestParams to google.genai config
|
|
38
|
+
FastAgentLLM.PARAM_MESSAGES, # Handled by contents
|
|
39
|
+
FastAgentLLM.PARAM_MODEL, # Handled during client/call setup
|
|
40
|
+
FastAgentLLM.PARAM_SYSTEM_PROMPT, # Handled by system_instruction in config
|
|
41
|
+
FastAgentLLM.PARAM_USE_HISTORY, # Handled by FastAgentLLM base / this class's logic
|
|
42
|
+
FastAgentLLM.PARAM_MAX_ITERATIONS, # Handled by this class's loop
|
|
43
|
+
FastAgentLLM.PARAM_MCP_METADATA,
|
|
44
|
+
}.union(FastAgentLLM.BASE_EXCLUDE_FIELDS)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class GoogleNativeLLM(FastAgentLLM[types.Content, types.Content]):
|
|
48
|
+
"""
|
|
49
|
+
Google LLM provider using the native google.genai library.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
53
|
+
super().__init__(*args, provider=Provider.GOOGLE, **kwargs)
|
|
54
|
+
# Initialize the converter
|
|
55
|
+
self._converter = GoogleConverter()
|
|
56
|
+
|
|
57
|
+
def _vertex_cfg(self) -> tuple[bool, str | None, str | None]:
|
|
58
|
+
"""(enabled, project_id, location) for Vertex config; supports dict/mapping or object."""
|
|
59
|
+
google_cfg = getattr(getattr(self.context, "config", None), "google", None)
|
|
60
|
+
vertex = (google_cfg or {}).get("vertex_ai") if isinstance(google_cfg, Mapping) else getattr(google_cfg, "vertex_ai", None)
|
|
61
|
+
if not vertex:
|
|
62
|
+
return (False, None, None)
|
|
63
|
+
if isinstance(vertex, Mapping):
|
|
64
|
+
return (bool(vertex.get("enabled")), vertex.get("project_id"), vertex.get("location"))
|
|
65
|
+
return (bool(getattr(vertex, "enabled", False)), getattr(vertex, "project_id", None), getattr(vertex, "location", None))
|
|
66
|
+
|
|
67
|
+
def _resolve_model_name(self, model: str) -> str:
|
|
68
|
+
"""Resolve model name; for Vertex, apply a generic preview→base fallback.
|
|
69
|
+
|
|
70
|
+
* If the caller passes a full publisher resource name, it is respected as-is.
|
|
71
|
+
* If Vertex is not enabled, the short id is returned unchanged (Developer API path).
|
|
72
|
+
* If Vertex is enabled and the id contains '-preview-', the suffix is stripped so that
|
|
73
|
+
e.g. 'gemini-2.5-flash-preview-09-2025' becomes 'gemini-2.5-flash'.
|
|
74
|
+
"""
|
|
75
|
+
# Fully-qualified publisher / model resource: do not rewrite.
|
|
76
|
+
if model.startswith(("projects/", "publishers/")) or "/publishers/" in model:
|
|
77
|
+
return model
|
|
78
|
+
|
|
79
|
+
enabled, project_id, location = self._vertex_cfg()
|
|
80
|
+
# Developer API path: return the short model id unchanged.
|
|
81
|
+
if not (enabled and project_id and location):
|
|
82
|
+
return model
|
|
83
|
+
|
|
84
|
+
# Vertex path: strip any '-preview-…' suffix to fall back to the base model id.
|
|
85
|
+
base_model = model.split("-preview-", 1)[0] if "-preview-" in model else model
|
|
86
|
+
|
|
87
|
+
return f"projects/{project_id}/locations/{location}/publishers/google/models/{base_model}"
|
|
88
|
+
|
|
89
|
+
def _initialize_google_client(self) -> genai.Client:
|
|
90
|
+
"""
|
|
91
|
+
Initializes the google.genai client.
|
|
92
|
+
|
|
93
|
+
Reads Google API key or Vertex AI configuration from context config.
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
# Prefer Vertex AI (ADC/IAM) if enabled. This path must NOT require an API key.
|
|
97
|
+
vertex_enabled, project_id, location = self._vertex_cfg()
|
|
98
|
+
if vertex_enabled:
|
|
99
|
+
return genai.Client(
|
|
100
|
+
vertexai=True,
|
|
101
|
+
project=project_id,
|
|
102
|
+
location=location,
|
|
103
|
+
# http_options=types.HttpOptions(api_version='v1')
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Otherwise, default to Gemini Developer API (API key required).
|
|
107
|
+
api_key = self._api_key()
|
|
108
|
+
if not api_key:
|
|
109
|
+
raise ProviderKeyError(
|
|
110
|
+
"Google API key not found.",
|
|
111
|
+
"Please configure your Google API key.",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return genai.Client(
|
|
115
|
+
api_key=api_key,
|
|
116
|
+
# http_options=types.HttpOptions(api_version='v1')
|
|
117
|
+
)
|
|
118
|
+
except Exception as e:
|
|
119
|
+
# Catch potential initialization errors and raise ProviderKeyError
|
|
120
|
+
raise ProviderKeyError("Failed to initialize Google GenAI client.", str(e)) from e
|
|
121
|
+
|
|
122
|
+
def _initialize_default_params(self, kwargs: dict) -> RequestParams:
|
|
123
|
+
"""Initialize Google-specific default parameters."""
|
|
124
|
+
chosen_model = kwargs.get("model", DEFAULT_GOOGLE_MODEL)
|
|
125
|
+
# Gemini models have different max output token limits; for example,
|
|
126
|
+
# gemini-2.0-flash only supports up to 8192 output tokens.
|
|
127
|
+
max_tokens = ModelDatabase.get_max_output_tokens(chosen_model) or 65536
|
|
128
|
+
|
|
129
|
+
return RequestParams(
|
|
130
|
+
model=chosen_model,
|
|
131
|
+
systemPrompt=self.instruction, # System instruction will be mapped in _google_completion
|
|
132
|
+
parallel_tool_calls=True, # Assume parallel tool calls are supported by default with native API
|
|
133
|
+
max_iterations=20,
|
|
134
|
+
use_history=True,
|
|
135
|
+
# Pick a safe default per model (e.g. gemini-2.0-flash is limited to 8192).
|
|
136
|
+
maxTokens=max_tokens,
|
|
137
|
+
# Include other relevant default parameters
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
async def _stream_generate_content(
|
|
141
|
+
self,
|
|
142
|
+
*,
|
|
143
|
+
model: str,
|
|
144
|
+
contents: list[types.Content],
|
|
145
|
+
config: types.GenerateContentConfig,
|
|
146
|
+
client: genai.Client,
|
|
147
|
+
) -> types.GenerateContentResponse | None:
|
|
148
|
+
"""Stream Gemini responses and return the final aggregated completion."""
|
|
149
|
+
try:
|
|
150
|
+
response_stream = await client.aio.models.generate_content_stream(
|
|
151
|
+
model=model,
|
|
152
|
+
contents=contents,
|
|
153
|
+
config=config,
|
|
154
|
+
)
|
|
155
|
+
except AttributeError:
|
|
156
|
+
# Older SDKs might not expose streaming; fall back to non-streaming.
|
|
157
|
+
return None
|
|
158
|
+
except errors.APIError:
|
|
159
|
+
raise
|
|
160
|
+
except Exception as exc: # pragma: no cover - defensive fallback
|
|
161
|
+
self.logger.warning(
|
|
162
|
+
"Google streaming failed during setup; falling back to non-streaming",
|
|
163
|
+
exc_info=exc,
|
|
164
|
+
)
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
return await self._consume_google_stream(response_stream, model=model)
|
|
168
|
+
|
|
169
|
+
async def _consume_google_stream(
|
|
170
|
+
self,
|
|
171
|
+
response_stream,
|
|
172
|
+
*,
|
|
173
|
+
model: str,
|
|
174
|
+
) -> types.GenerateContentResponse | None:
|
|
175
|
+
"""Consume the async streaming iterator and aggregate the final response."""
|
|
176
|
+
estimated_tokens = 0
|
|
177
|
+
timeline: list[tuple[str, int | None, str]] = []
|
|
178
|
+
tool_streams: dict[int, dict[str, str]] = {}
|
|
179
|
+
active_tool_index: int | None = None
|
|
180
|
+
tool_counter = 0
|
|
181
|
+
usage_metadata = None
|
|
182
|
+
last_chunk: types.GenerateContentResponse | None = None
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
# Cancellation is handled via asyncio.Task.cancel() which raises CancelledError
|
|
186
|
+
async for chunk in response_stream:
|
|
187
|
+
last_chunk = chunk
|
|
188
|
+
if getattr(chunk, "usage_metadata", None):
|
|
189
|
+
usage_metadata = chunk.usage_metadata
|
|
190
|
+
|
|
191
|
+
if not getattr(chunk, "candidates", None):
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
candidate = chunk.candidates[0]
|
|
195
|
+
content = getattr(candidate, "content", None)
|
|
196
|
+
if content is None or not getattr(content, "parts", None):
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
for part in content.parts:
|
|
200
|
+
if getattr(part, "text", None):
|
|
201
|
+
text = part.text or ""
|
|
202
|
+
if text:
|
|
203
|
+
if timeline and timeline[-1][0] == "text":
|
|
204
|
+
prev_type, prev_index, prev_text = timeline[-1]
|
|
205
|
+
timeline[-1] = (prev_type, prev_index, prev_text + text)
|
|
206
|
+
else:
|
|
207
|
+
timeline.append(("text", None, text))
|
|
208
|
+
estimated_tokens = self._update_streaming_progress(
|
|
209
|
+
text,
|
|
210
|
+
model,
|
|
211
|
+
estimated_tokens,
|
|
212
|
+
)
|
|
213
|
+
self._notify_tool_stream_listeners(
|
|
214
|
+
"text",
|
|
215
|
+
{
|
|
216
|
+
"chunk": text,
|
|
217
|
+
"streams_arguments": False,
|
|
218
|
+
},
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if getattr(part, "function_call", None):
|
|
222
|
+
function_call = part.function_call
|
|
223
|
+
name = getattr(function_call, "name", None) or "tool"
|
|
224
|
+
args = getattr(function_call, "args", None) or {}
|
|
225
|
+
|
|
226
|
+
if active_tool_index is None:
|
|
227
|
+
active_tool_index = tool_counter
|
|
228
|
+
tool_counter += 1
|
|
229
|
+
tool_use_id = f"tool_{self.chat_turn()}_{active_tool_index}"
|
|
230
|
+
tool_streams[active_tool_index] = {
|
|
231
|
+
"name": name,
|
|
232
|
+
"tool_use_id": tool_use_id,
|
|
233
|
+
"buffer": "",
|
|
234
|
+
}
|
|
235
|
+
self._notify_tool_stream_listeners(
|
|
236
|
+
"start",
|
|
237
|
+
{
|
|
238
|
+
"tool_name": name,
|
|
239
|
+
"tool_use_id": tool_use_id,
|
|
240
|
+
"index": active_tool_index,
|
|
241
|
+
"streams_arguments": False,
|
|
242
|
+
},
|
|
243
|
+
)
|
|
244
|
+
timeline.append(("tool_call", active_tool_index, ""))
|
|
245
|
+
|
|
246
|
+
stream_info = tool_streams.get(active_tool_index)
|
|
247
|
+
if not stream_info:
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
serialized_args = json.dumps(args, separators=(",", ":"))
|
|
252
|
+
except Exception:
|
|
253
|
+
serialized_args = str(args)
|
|
254
|
+
|
|
255
|
+
previous = stream_info.get("buffer", "")
|
|
256
|
+
if isinstance(previous, str) and serialized_args.startswith(previous):
|
|
257
|
+
delta = serialized_args[len(previous) :]
|
|
258
|
+
else:
|
|
259
|
+
delta = serialized_args
|
|
260
|
+
stream_info["buffer"] = serialized_args
|
|
261
|
+
|
|
262
|
+
if delta:
|
|
263
|
+
self._notify_tool_stream_listeners(
|
|
264
|
+
"delta",
|
|
265
|
+
{
|
|
266
|
+
"tool_name": stream_info["name"],
|
|
267
|
+
"tool_use_id": stream_info["tool_use_id"],
|
|
268
|
+
"index": active_tool_index,
|
|
269
|
+
"chunk": delta,
|
|
270
|
+
"streams_arguments": False,
|
|
271
|
+
},
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
finish_reason = getattr(candidate, "finish_reason", None)
|
|
275
|
+
if finish_reason:
|
|
276
|
+
finish_value = str(finish_reason).split(".")[-1].upper()
|
|
277
|
+
if finish_value in {"FUNCTION_CALL", "STOP"} and active_tool_index is not None:
|
|
278
|
+
stream_info = tool_streams.get(active_tool_index)
|
|
279
|
+
if stream_info:
|
|
280
|
+
self._notify_tool_stream_listeners(
|
|
281
|
+
"stop",
|
|
282
|
+
{
|
|
283
|
+
"tool_name": stream_info["name"],
|
|
284
|
+
"tool_use_id": stream_info["tool_use_id"],
|
|
285
|
+
"index": active_tool_index,
|
|
286
|
+
"streams_arguments": False,
|
|
287
|
+
},
|
|
288
|
+
)
|
|
289
|
+
active_tool_index = None
|
|
290
|
+
finally:
|
|
291
|
+
stream_close = getattr(response_stream, "aclose", None)
|
|
292
|
+
if callable(stream_close):
|
|
293
|
+
try:
|
|
294
|
+
await stream_close()
|
|
295
|
+
except Exception:
|
|
296
|
+
pass
|
|
297
|
+
|
|
298
|
+
if active_tool_index is not None:
|
|
299
|
+
stream_info = tool_streams.get(active_tool_index)
|
|
300
|
+
if stream_info:
|
|
301
|
+
self._notify_tool_stream_listeners(
|
|
302
|
+
"stop",
|
|
303
|
+
{
|
|
304
|
+
"tool_name": stream_info["name"],
|
|
305
|
+
"tool_use_id": stream_info["tool_use_id"],
|
|
306
|
+
"index": active_tool_index,
|
|
307
|
+
"streams_arguments": False,
|
|
308
|
+
},
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
if not timeline and last_chunk is None:
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
final_parts: list[types.Part] = []
|
|
315
|
+
for entry_type, index, payload in timeline:
|
|
316
|
+
if entry_type == "text":
|
|
317
|
+
final_parts.append(types.Part.from_text(text=payload))
|
|
318
|
+
elif entry_type == "tool_call" and index is not None:
|
|
319
|
+
stream_info = tool_streams.get(index)
|
|
320
|
+
if not stream_info:
|
|
321
|
+
continue
|
|
322
|
+
buffer = stream_info.get("buffer", "")
|
|
323
|
+
try:
|
|
324
|
+
args_obj = json.loads(buffer) if buffer else {}
|
|
325
|
+
except json.JSONDecodeError:
|
|
326
|
+
args_obj = {"__raw": buffer}
|
|
327
|
+
final_parts.append(
|
|
328
|
+
types.Part.from_function_call(
|
|
329
|
+
name=str(stream_info.get("name") or "tool"),
|
|
330
|
+
args=args_obj,
|
|
331
|
+
)
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
final_content = types.Content(role="model", parts=final_parts)
|
|
335
|
+
|
|
336
|
+
if last_chunk is not None:
|
|
337
|
+
final_response = last_chunk.model_copy(deep=True)
|
|
338
|
+
if getattr(final_response, "candidates", None):
|
|
339
|
+
final_candidate = final_response.candidates[0]
|
|
340
|
+
final_candidate.content = final_content
|
|
341
|
+
else:
|
|
342
|
+
final_response.candidates = [types.Candidate(content=final_content)]
|
|
343
|
+
else:
|
|
344
|
+
final_response = types.GenerateContentResponse(
|
|
345
|
+
candidates=[types.Candidate(content=final_content)]
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
if usage_metadata:
|
|
349
|
+
final_response.usage_metadata = usage_metadata
|
|
350
|
+
|
|
351
|
+
return final_response
|
|
352
|
+
|
|
353
|
+
async def _google_completion(
|
|
354
|
+
self,
|
|
355
|
+
message: list[types.Content] | None,
|
|
356
|
+
request_params: RequestParams | None = None,
|
|
357
|
+
tools: list[McpTool] | None = None,
|
|
358
|
+
*,
|
|
359
|
+
response_mime_type: str | None = None,
|
|
360
|
+
response_schema: object | None = None,
|
|
361
|
+
) -> PromptMessageExtended:
|
|
362
|
+
"""
|
|
363
|
+
Process a query using Google's generate_content API and available tools.
|
|
364
|
+
"""
|
|
365
|
+
request_params = self.get_request_params(request_params=request_params)
|
|
366
|
+
responses: list[ContentBlock] = []
|
|
367
|
+
|
|
368
|
+
# Caller supplies the full set of messages to send (history + turn)
|
|
369
|
+
conversation_history: list[types.Content] = list(message or [])
|
|
370
|
+
|
|
371
|
+
self.logger.debug(f"Google completion requested with messages: {conversation_history}")
|
|
372
|
+
self._log_chat_progress(self.chat_turn(), model=request_params.model)
|
|
373
|
+
|
|
374
|
+
available_tools: list[types.Tool] = (
|
|
375
|
+
self._converter.convert_to_google_tools(tools or []) if tools else []
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
# 2. Prepare generate_content arguments
|
|
379
|
+
generate_content_config = self._converter.convert_request_params_to_google_config(
|
|
380
|
+
request_params
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
# Apply structured output config OR tool calling (mutually exclusive)
|
|
384
|
+
if response_schema or response_mime_type:
|
|
385
|
+
# Structured output mode: disable tool use
|
|
386
|
+
if response_mime_type:
|
|
387
|
+
generate_content_config.response_mime_type = response_mime_type
|
|
388
|
+
if response_schema is not None:
|
|
389
|
+
generate_content_config.response_schema = response_schema
|
|
390
|
+
elif available_tools:
|
|
391
|
+
# Tool calling enabled only when not doing structured output
|
|
392
|
+
generate_content_config.tools = available_tools
|
|
393
|
+
generate_content_config.tool_config = types.ToolConfig(
|
|
394
|
+
function_calling_config=types.FunctionCallingConfig(mode="AUTO")
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
# 3. Call the google.genai API
|
|
398
|
+
client = self._initialize_google_client()
|
|
399
|
+
model_name = self._resolve_model_name(request_params.model)
|
|
400
|
+
try:
|
|
401
|
+
# Use the async client
|
|
402
|
+
api_response = None
|
|
403
|
+
streaming_supported = response_schema is None and response_mime_type is None
|
|
404
|
+
if streaming_supported:
|
|
405
|
+
api_response = await self._stream_generate_content(
|
|
406
|
+
model=model_name,
|
|
407
|
+
contents=conversation_history,
|
|
408
|
+
config=generate_content_config,
|
|
409
|
+
client=client,
|
|
410
|
+
)
|
|
411
|
+
if api_response is None:
|
|
412
|
+
api_response = await client.aio.models.generate_content(
|
|
413
|
+
model=model_name,
|
|
414
|
+
contents=conversation_history, # Full conversational context for this turn
|
|
415
|
+
config=generate_content_config,
|
|
416
|
+
)
|
|
417
|
+
self.logger.debug("Google generate_content response:", data=api_response)
|
|
418
|
+
|
|
419
|
+
# Track usage if response is valid and has usage data
|
|
420
|
+
if (
|
|
421
|
+
hasattr(api_response, "usage_metadata")
|
|
422
|
+
and api_response.usage_metadata
|
|
423
|
+
and not isinstance(api_response, BaseException)
|
|
424
|
+
):
|
|
425
|
+
try:
|
|
426
|
+
turn_usage = TurnUsage.from_google(
|
|
427
|
+
api_response.usage_metadata, model_name
|
|
428
|
+
)
|
|
429
|
+
self._finalize_turn_usage(turn_usage)
|
|
430
|
+
|
|
431
|
+
except Exception as e:
|
|
432
|
+
self.logger.warning(f"Failed to track usage: {e}")
|
|
433
|
+
|
|
434
|
+
except errors.APIError as e:
|
|
435
|
+
# Handle specific Google API errors
|
|
436
|
+
self.logger.error(f"Google API Error: {e.code} - {e.message}")
|
|
437
|
+
raise ProviderKeyError(f"Google API Error: {e.code}", e.message or "") from e
|
|
438
|
+
except Exception as e:
|
|
439
|
+
self.logger.error(f"Error during Google generate_content call: {e}")
|
|
440
|
+
# Decide how to handle other exceptions - potentially re-raise or return an error message
|
|
441
|
+
raise e
|
|
442
|
+
finally:
|
|
443
|
+
try:
|
|
444
|
+
await client.aio.aclose()
|
|
445
|
+
except Exception:
|
|
446
|
+
pass
|
|
447
|
+
try:
|
|
448
|
+
client.close()
|
|
449
|
+
except Exception:
|
|
450
|
+
pass
|
|
451
|
+
|
|
452
|
+
# 4. Process the API response
|
|
453
|
+
if not api_response.candidates:
|
|
454
|
+
# No response from the model, we're done
|
|
455
|
+
self.logger.debug("No candidates returned.")
|
|
456
|
+
|
|
457
|
+
candidate = api_response.candidates[0] # Process the first candidate
|
|
458
|
+
|
|
459
|
+
# Convert the model's response content to fast-agent types
|
|
460
|
+
model_response_content_parts = self._converter.convert_from_google_content(
|
|
461
|
+
candidate.content
|
|
462
|
+
)
|
|
463
|
+
stop_reason = LlmStopReason.END_TURN
|
|
464
|
+
tool_calls: dict[str, CallToolRequest] | None = None
|
|
465
|
+
# Add model's response to the working conversation history for this turn
|
|
466
|
+
conversation_history.append(candidate.content)
|
|
467
|
+
|
|
468
|
+
# Extract and process text content and tool calls
|
|
469
|
+
assistant_message_parts = []
|
|
470
|
+
tool_calls_to_execute = []
|
|
471
|
+
|
|
472
|
+
for part in model_response_content_parts:
|
|
473
|
+
if isinstance(part, TextContent):
|
|
474
|
+
responses.append(part) # Add text content to the final responses to be returned
|
|
475
|
+
assistant_message_parts.append(
|
|
476
|
+
part
|
|
477
|
+
) # Collect text for potential assistant message display
|
|
478
|
+
elif isinstance(part, CallToolRequestParams):
|
|
479
|
+
# This is a function call requested by the model
|
|
480
|
+
# If in structured mode, ignore tool calls per either-or rule
|
|
481
|
+
if response_schema or response_mime_type:
|
|
482
|
+
continue
|
|
483
|
+
tool_calls_to_execute.append(part) # Collect tool calls to execute
|
|
484
|
+
|
|
485
|
+
if tool_calls_to_execute:
|
|
486
|
+
stop_reason = LlmStopReason.TOOL_USE
|
|
487
|
+
tool_calls = {}
|
|
488
|
+
for tool_call_params in tool_calls_to_execute:
|
|
489
|
+
# Convert to CallToolRequest and execute
|
|
490
|
+
tool_call_request = CallToolRequest(method="tools/call", params=tool_call_params)
|
|
491
|
+
hex_string = secrets.token_hex(3)[:5]
|
|
492
|
+
tool_calls[hex_string] = tool_call_request
|
|
493
|
+
|
|
494
|
+
self.logger.debug("Tool call results processed.")
|
|
495
|
+
else:
|
|
496
|
+
stop_reason = self._map_finish_reason(getattr(candidate, "finish_reason", None))
|
|
497
|
+
|
|
498
|
+
# Update diagnostic snapshot (never read again)
|
|
499
|
+
# This provides a snapshot of what was sent to the provider for debugging
|
|
500
|
+
self.history.set(conversation_history)
|
|
501
|
+
|
|
502
|
+
self._log_chat_finished(model=model_name) # Use resolved model name
|
|
503
|
+
return Prompt.assistant(*responses, stop_reason=stop_reason, tool_calls=tool_calls)
|
|
504
|
+
|
|
505
|
+
# return responses # Return the accumulated responses (fast-agent content types)
|
|
506
|
+
|
|
507
|
+
async def _apply_prompt_provider_specific(
|
|
508
|
+
self,
|
|
509
|
+
multipart_messages: list[PromptMessageExtended],
|
|
510
|
+
request_params: RequestParams | None = None,
|
|
511
|
+
tools: list[McpTool] | None = None,
|
|
512
|
+
is_template: bool = False,
|
|
513
|
+
) -> PromptMessageExtended:
|
|
514
|
+
"""
|
|
515
|
+
Provider-specific prompt application.
|
|
516
|
+
Templates are handled by the agent; messages already include them.
|
|
517
|
+
"""
|
|
518
|
+
request_params = self.get_request_params(request_params=request_params)
|
|
519
|
+
|
|
520
|
+
# Determine the last message
|
|
521
|
+
last_message = multipart_messages[-1]
|
|
522
|
+
|
|
523
|
+
if last_message.role == "assistant":
|
|
524
|
+
# No generation required; the provided assistant message is the output
|
|
525
|
+
return last_message
|
|
526
|
+
|
|
527
|
+
# Build the provider-native message list for this turn from the last user message
|
|
528
|
+
# This must handle tool results as function responses before any additional user content.
|
|
529
|
+
turn_messages: list[types.Content] = []
|
|
530
|
+
|
|
531
|
+
# 1) Convert tool results (if any) to google function responses
|
|
532
|
+
if last_message.tool_results:
|
|
533
|
+
# Map correlation IDs back to tool names using the last assistant tool_calls
|
|
534
|
+
# found in our high-level message history
|
|
535
|
+
id_to_name: dict[str, str] = {}
|
|
536
|
+
for prev in reversed(multipart_messages):
|
|
537
|
+
if prev.role == "assistant" and prev.tool_calls:
|
|
538
|
+
for call_id, call in prev.tool_calls.items():
|
|
539
|
+
try:
|
|
540
|
+
id_to_name[call_id] = call.params.name
|
|
541
|
+
except Exception:
|
|
542
|
+
pass
|
|
543
|
+
break
|
|
544
|
+
|
|
545
|
+
tool_results_pairs = []
|
|
546
|
+
for call_id, result in last_message.tool_results.items():
|
|
547
|
+
tool_name = id_to_name.get(call_id, "tool")
|
|
548
|
+
tool_results_pairs.append((tool_name, result))
|
|
549
|
+
|
|
550
|
+
if tool_results_pairs:
|
|
551
|
+
turn_messages.extend(
|
|
552
|
+
self._converter.convert_function_results_to_google(tool_results_pairs)
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
# 2) Convert any direct user content in the last message
|
|
556
|
+
if last_message.content:
|
|
557
|
+
user_contents = self._converter.convert_to_google_content([last_message])
|
|
558
|
+
# convert_to_google_content returns a list; preserve order after tool responses
|
|
559
|
+
turn_messages.extend(user_contents)
|
|
560
|
+
|
|
561
|
+
# If we somehow have no provider-native parts, ensure we send an empty user content
|
|
562
|
+
if not turn_messages:
|
|
563
|
+
turn_messages.append(types.Content(role="user", parts=[types.Part.from_text(text="")]))
|
|
564
|
+
|
|
565
|
+
conversation_history: list[types.Content] = []
|
|
566
|
+
if request_params.use_history and len(multipart_messages) > 1:
|
|
567
|
+
conversation_history.extend(self._convert_to_provider_format(multipart_messages[:-1]))
|
|
568
|
+
conversation_history.extend(turn_messages)
|
|
569
|
+
|
|
570
|
+
return await self._google_completion(
|
|
571
|
+
conversation_history,
|
|
572
|
+
request_params=request_params,
|
|
573
|
+
tools=tools,
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
def _convert_extended_messages_to_provider(
|
|
577
|
+
self, messages: list[PromptMessageExtended]
|
|
578
|
+
) -> list[types.Content]:
|
|
579
|
+
"""
|
|
580
|
+
Convert PromptMessageExtended list to Google types.Content format.
|
|
581
|
+
This is called fresh on every API call from _convert_to_provider_format().
|
|
582
|
+
|
|
583
|
+
Args:
|
|
584
|
+
messages: List of PromptMessageExtended objects
|
|
585
|
+
|
|
586
|
+
Returns:
|
|
587
|
+
List of Google types.Content objects
|
|
588
|
+
"""
|
|
589
|
+
return self._converter.convert_to_google_content(messages)
|
|
590
|
+
|
|
591
|
+
def _map_finish_reason(self, finish_reason: object) -> LlmStopReason:
|
|
592
|
+
"""Map Google finish reasons to LlmStopReason robustly."""
|
|
593
|
+
# Normalize to string if it's an enum-like object
|
|
594
|
+
reason = None
|
|
595
|
+
try:
|
|
596
|
+
reason = str(finish_reason) if finish_reason is not None else None
|
|
597
|
+
except Exception:
|
|
598
|
+
reason = None
|
|
599
|
+
|
|
600
|
+
if not reason:
|
|
601
|
+
return LlmStopReason.END_TURN
|
|
602
|
+
|
|
603
|
+
# Extract last token after any dots or enum prefixes
|
|
604
|
+
key = reason.split(".")[-1].upper()
|
|
605
|
+
|
|
606
|
+
if key in {"STOP"}:
|
|
607
|
+
return LlmStopReason.END_TURN
|
|
608
|
+
if key in {"MAX_TOKENS", "LENGTH"}:
|
|
609
|
+
return LlmStopReason.MAX_TOKENS
|
|
610
|
+
if key in {
|
|
611
|
+
"PROHIBITED_CONTENT",
|
|
612
|
+
"SAFETY",
|
|
613
|
+
"RECITATION",
|
|
614
|
+
"BLOCKLIST",
|
|
615
|
+
"SPII",
|
|
616
|
+
"IMAGE_SAFETY",
|
|
617
|
+
}:
|
|
618
|
+
return LlmStopReason.SAFETY
|
|
619
|
+
if key in {"MALFORMED_FUNCTION_CALL", "UNEXPECTED_TOOL_CALL", "TOO_MANY_TOOL_CALLS"}:
|
|
620
|
+
return LlmStopReason.ERROR
|
|
621
|
+
# Some SDKs include OTHER, LANGUAGE, GROUNDING, UNSPECIFIED, etc.
|
|
622
|
+
return LlmStopReason.ERROR
|
|
623
|
+
|
|
624
|
+
async def _apply_prompt_provider_specific_structured(
|
|
625
|
+
self,
|
|
626
|
+
multipart_messages,
|
|
627
|
+
model,
|
|
628
|
+
request_params=None,
|
|
629
|
+
):
|
|
630
|
+
"""
|
|
631
|
+
Provider-specific structured output implementation.
|
|
632
|
+
Note: Message history is managed by base class and converted via
|
|
633
|
+
_convert_to_provider_format() on each call.
|
|
634
|
+
"""
|
|
635
|
+
import json
|
|
636
|
+
|
|
637
|
+
# Determine the last message
|
|
638
|
+
last_message = multipart_messages[-1] if multipart_messages else None
|
|
639
|
+
|
|
640
|
+
# If the last message is an assistant message, attempt to parse its JSON and return
|
|
641
|
+
if last_message and last_message.role == "assistant":
|
|
642
|
+
assistant_text = last_message.last_text()
|
|
643
|
+
if assistant_text:
|
|
644
|
+
try:
|
|
645
|
+
json_data = json.loads(assistant_text)
|
|
646
|
+
validated_model = model.model_validate(json_data)
|
|
647
|
+
return validated_model, last_message
|
|
648
|
+
except (json.JSONDecodeError, Exception) as e:
|
|
649
|
+
self.logger.warning(
|
|
650
|
+
f"Failed to parse assistant message as structured response: {e}"
|
|
651
|
+
)
|
|
652
|
+
return None, last_message
|
|
653
|
+
|
|
654
|
+
# Prepare request params
|
|
655
|
+
request_params = self.get_request_params(request_params)
|
|
656
|
+
|
|
657
|
+
# Build schema for structured output
|
|
658
|
+
schema = None
|
|
659
|
+
try:
|
|
660
|
+
schema = model.model_json_schema()
|
|
661
|
+
except Exception:
|
|
662
|
+
pass
|
|
663
|
+
response_schema = model if schema is None else schema
|
|
664
|
+
|
|
665
|
+
# Convert the last user message to provider-native content for the current turn
|
|
666
|
+
turn_messages: list[types.Content] = []
|
|
667
|
+
if last_message:
|
|
668
|
+
turn_messages = self._converter.convert_to_google_content([last_message])
|
|
669
|
+
|
|
670
|
+
# Delegate to unified completion with structured options enabled (no tools)
|
|
671
|
+
assistant_msg = await self._google_completion(
|
|
672
|
+
turn_messages,
|
|
673
|
+
request_params=request_params,
|
|
674
|
+
tools=None,
|
|
675
|
+
response_mime_type="application/json",
|
|
676
|
+
response_schema=response_schema,
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
# Parse using shared helper for consistency
|
|
680
|
+
parsed, _ = self._structured_from_multipart(assistant_msg, model)
|
|
681
|
+
return parsed, assistant_msg
|