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.
Files changed (261) hide show
  1. fast_agent/__init__.py +183 -0
  2. fast_agent/acp/__init__.py +19 -0
  3. fast_agent/acp/acp_aware_mixin.py +304 -0
  4. fast_agent/acp/acp_context.py +437 -0
  5. fast_agent/acp/content_conversion.py +136 -0
  6. fast_agent/acp/filesystem_runtime.py +427 -0
  7. fast_agent/acp/permission_store.py +269 -0
  8. fast_agent/acp/server/__init__.py +5 -0
  9. fast_agent/acp/server/agent_acp_server.py +1472 -0
  10. fast_agent/acp/slash_commands.py +1050 -0
  11. fast_agent/acp/terminal_runtime.py +408 -0
  12. fast_agent/acp/tool_permission_adapter.py +125 -0
  13. fast_agent/acp/tool_permissions.py +474 -0
  14. fast_agent/acp/tool_progress.py +814 -0
  15. fast_agent/agents/__init__.py +85 -0
  16. fast_agent/agents/agent_types.py +64 -0
  17. fast_agent/agents/llm_agent.py +350 -0
  18. fast_agent/agents/llm_decorator.py +1139 -0
  19. fast_agent/agents/mcp_agent.py +1337 -0
  20. fast_agent/agents/tool_agent.py +271 -0
  21. fast_agent/agents/workflow/agents_as_tools_agent.py +849 -0
  22. fast_agent/agents/workflow/chain_agent.py +212 -0
  23. fast_agent/agents/workflow/evaluator_optimizer.py +380 -0
  24. fast_agent/agents/workflow/iterative_planner.py +652 -0
  25. fast_agent/agents/workflow/maker_agent.py +379 -0
  26. fast_agent/agents/workflow/orchestrator_models.py +218 -0
  27. fast_agent/agents/workflow/orchestrator_prompts.py +248 -0
  28. fast_agent/agents/workflow/parallel_agent.py +250 -0
  29. fast_agent/agents/workflow/router_agent.py +353 -0
  30. fast_agent/cli/__init__.py +0 -0
  31. fast_agent/cli/__main__.py +73 -0
  32. fast_agent/cli/commands/acp.py +159 -0
  33. fast_agent/cli/commands/auth.py +404 -0
  34. fast_agent/cli/commands/check_config.py +783 -0
  35. fast_agent/cli/commands/go.py +514 -0
  36. fast_agent/cli/commands/quickstart.py +557 -0
  37. fast_agent/cli/commands/serve.py +143 -0
  38. fast_agent/cli/commands/server_helpers.py +114 -0
  39. fast_agent/cli/commands/setup.py +174 -0
  40. fast_agent/cli/commands/url_parser.py +190 -0
  41. fast_agent/cli/constants.py +40 -0
  42. fast_agent/cli/main.py +115 -0
  43. fast_agent/cli/terminal.py +24 -0
  44. fast_agent/config.py +798 -0
  45. fast_agent/constants.py +41 -0
  46. fast_agent/context.py +279 -0
  47. fast_agent/context_dependent.py +50 -0
  48. fast_agent/core/__init__.py +92 -0
  49. fast_agent/core/agent_app.py +448 -0
  50. fast_agent/core/core_app.py +137 -0
  51. fast_agent/core/direct_decorators.py +784 -0
  52. fast_agent/core/direct_factory.py +620 -0
  53. fast_agent/core/error_handling.py +27 -0
  54. fast_agent/core/exceptions.py +90 -0
  55. fast_agent/core/executor/__init__.py +0 -0
  56. fast_agent/core/executor/executor.py +280 -0
  57. fast_agent/core/executor/task_registry.py +32 -0
  58. fast_agent/core/executor/workflow_signal.py +324 -0
  59. fast_agent/core/fastagent.py +1186 -0
  60. fast_agent/core/logging/__init__.py +5 -0
  61. fast_agent/core/logging/events.py +138 -0
  62. fast_agent/core/logging/json_serializer.py +164 -0
  63. fast_agent/core/logging/listeners.py +309 -0
  64. fast_agent/core/logging/logger.py +278 -0
  65. fast_agent/core/logging/transport.py +481 -0
  66. fast_agent/core/prompt.py +9 -0
  67. fast_agent/core/prompt_templates.py +183 -0
  68. fast_agent/core/validation.py +326 -0
  69. fast_agent/event_progress.py +62 -0
  70. fast_agent/history/history_exporter.py +49 -0
  71. fast_agent/human_input/__init__.py +47 -0
  72. fast_agent/human_input/elicitation_handler.py +123 -0
  73. fast_agent/human_input/elicitation_state.py +33 -0
  74. fast_agent/human_input/form_elements.py +59 -0
  75. fast_agent/human_input/form_fields.py +256 -0
  76. fast_agent/human_input/simple_form.py +113 -0
  77. fast_agent/human_input/types.py +40 -0
  78. fast_agent/interfaces.py +310 -0
  79. fast_agent/llm/__init__.py +9 -0
  80. fast_agent/llm/cancellation.py +22 -0
  81. fast_agent/llm/fastagent_llm.py +931 -0
  82. fast_agent/llm/internal/passthrough.py +161 -0
  83. fast_agent/llm/internal/playback.py +129 -0
  84. fast_agent/llm/internal/silent.py +41 -0
  85. fast_agent/llm/internal/slow.py +38 -0
  86. fast_agent/llm/memory.py +275 -0
  87. fast_agent/llm/model_database.py +490 -0
  88. fast_agent/llm/model_factory.py +388 -0
  89. fast_agent/llm/model_info.py +102 -0
  90. fast_agent/llm/prompt_utils.py +155 -0
  91. fast_agent/llm/provider/anthropic/anthropic_utils.py +84 -0
  92. fast_agent/llm/provider/anthropic/cache_planner.py +56 -0
  93. fast_agent/llm/provider/anthropic/llm_anthropic.py +796 -0
  94. fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +462 -0
  95. fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
  96. fast_agent/llm/provider/bedrock/llm_bedrock.py +2207 -0
  97. fast_agent/llm/provider/bedrock/multipart_converter_bedrock.py +84 -0
  98. fast_agent/llm/provider/google/google_converter.py +466 -0
  99. fast_agent/llm/provider/google/llm_google_native.py +681 -0
  100. fast_agent/llm/provider/openai/llm_aliyun.py +31 -0
  101. fast_agent/llm/provider/openai/llm_azure.py +143 -0
  102. fast_agent/llm/provider/openai/llm_deepseek.py +76 -0
  103. fast_agent/llm/provider/openai/llm_generic.py +35 -0
  104. fast_agent/llm/provider/openai/llm_google_oai.py +32 -0
  105. fast_agent/llm/provider/openai/llm_groq.py +42 -0
  106. fast_agent/llm/provider/openai/llm_huggingface.py +85 -0
  107. fast_agent/llm/provider/openai/llm_openai.py +1195 -0
  108. fast_agent/llm/provider/openai/llm_openai_compatible.py +138 -0
  109. fast_agent/llm/provider/openai/llm_openrouter.py +45 -0
  110. fast_agent/llm/provider/openai/llm_tensorzero_openai.py +128 -0
  111. fast_agent/llm/provider/openai/llm_xai.py +38 -0
  112. fast_agent/llm/provider/openai/multipart_converter_openai.py +561 -0
  113. fast_agent/llm/provider/openai/openai_multipart.py +169 -0
  114. fast_agent/llm/provider/openai/openai_utils.py +67 -0
  115. fast_agent/llm/provider/openai/responses.py +133 -0
  116. fast_agent/llm/provider_key_manager.py +139 -0
  117. fast_agent/llm/provider_types.py +34 -0
  118. fast_agent/llm/request_params.py +61 -0
  119. fast_agent/llm/sampling_converter.py +98 -0
  120. fast_agent/llm/stream_types.py +9 -0
  121. fast_agent/llm/usage_tracking.py +445 -0
  122. fast_agent/mcp/__init__.py +56 -0
  123. fast_agent/mcp/common.py +26 -0
  124. fast_agent/mcp/elicitation_factory.py +84 -0
  125. fast_agent/mcp/elicitation_handlers.py +164 -0
  126. fast_agent/mcp/gen_client.py +83 -0
  127. fast_agent/mcp/helpers/__init__.py +36 -0
  128. fast_agent/mcp/helpers/content_helpers.py +352 -0
  129. fast_agent/mcp/helpers/server_config_helpers.py +25 -0
  130. fast_agent/mcp/hf_auth.py +147 -0
  131. fast_agent/mcp/interfaces.py +92 -0
  132. fast_agent/mcp/logger_textio.py +108 -0
  133. fast_agent/mcp/mcp_agent_client_session.py +411 -0
  134. fast_agent/mcp/mcp_aggregator.py +2175 -0
  135. fast_agent/mcp/mcp_connection_manager.py +723 -0
  136. fast_agent/mcp/mcp_content.py +262 -0
  137. fast_agent/mcp/mime_utils.py +108 -0
  138. fast_agent/mcp/oauth_client.py +509 -0
  139. fast_agent/mcp/prompt.py +159 -0
  140. fast_agent/mcp/prompt_message_extended.py +155 -0
  141. fast_agent/mcp/prompt_render.py +84 -0
  142. fast_agent/mcp/prompt_serialization.py +580 -0
  143. fast_agent/mcp/prompts/__init__.py +0 -0
  144. fast_agent/mcp/prompts/__main__.py +7 -0
  145. fast_agent/mcp/prompts/prompt_constants.py +18 -0
  146. fast_agent/mcp/prompts/prompt_helpers.py +238 -0
  147. fast_agent/mcp/prompts/prompt_load.py +186 -0
  148. fast_agent/mcp/prompts/prompt_server.py +552 -0
  149. fast_agent/mcp/prompts/prompt_template.py +438 -0
  150. fast_agent/mcp/resource_utils.py +215 -0
  151. fast_agent/mcp/sampling.py +200 -0
  152. fast_agent/mcp/server/__init__.py +4 -0
  153. fast_agent/mcp/server/agent_server.py +613 -0
  154. fast_agent/mcp/skybridge.py +44 -0
  155. fast_agent/mcp/sse_tracking.py +287 -0
  156. fast_agent/mcp/stdio_tracking_simple.py +59 -0
  157. fast_agent/mcp/streamable_http_tracking.py +309 -0
  158. fast_agent/mcp/tool_execution_handler.py +137 -0
  159. fast_agent/mcp/tool_permission_handler.py +88 -0
  160. fast_agent/mcp/transport_tracking.py +634 -0
  161. fast_agent/mcp/types.py +24 -0
  162. fast_agent/mcp/ui_agent.py +48 -0
  163. fast_agent/mcp/ui_mixin.py +209 -0
  164. fast_agent/mcp_server_registry.py +89 -0
  165. fast_agent/py.typed +0 -0
  166. fast_agent/resources/examples/data-analysis/analysis-campaign.py +189 -0
  167. fast_agent/resources/examples/data-analysis/analysis.py +68 -0
  168. fast_agent/resources/examples/data-analysis/fastagent.config.yaml +41 -0
  169. fast_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +1471 -0
  170. fast_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
  171. fast_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +297 -0
  172. fast_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
  173. fast_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
  174. fast_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
  175. fast_agent/resources/examples/mcp/elicitations/forms_demo.py +107 -0
  176. fast_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
  177. fast_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
  178. fast_agent/resources/examples/mcp/elicitations/tool_call.py +21 -0
  179. fast_agent/resources/examples/mcp/state-transfer/agent_one.py +18 -0
  180. fast_agent/resources/examples/mcp/state-transfer/agent_two.py +18 -0
  181. fast_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +27 -0
  182. fast_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +15 -0
  183. fast_agent/resources/examples/researcher/fastagent.config.yaml +61 -0
  184. fast_agent/resources/examples/researcher/researcher-eval.py +53 -0
  185. fast_agent/resources/examples/researcher/researcher-imp.py +189 -0
  186. fast_agent/resources/examples/researcher/researcher.py +36 -0
  187. fast_agent/resources/examples/tensorzero/.env.sample +2 -0
  188. fast_agent/resources/examples/tensorzero/Makefile +31 -0
  189. fast_agent/resources/examples/tensorzero/README.md +56 -0
  190. fast_agent/resources/examples/tensorzero/agent.py +35 -0
  191. fast_agent/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
  192. fast_agent/resources/examples/tensorzero/demo_images/crab.png +0 -0
  193. fast_agent/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
  194. fast_agent/resources/examples/tensorzero/docker-compose.yml +105 -0
  195. fast_agent/resources/examples/tensorzero/fastagent.config.yaml +19 -0
  196. fast_agent/resources/examples/tensorzero/image_demo.py +67 -0
  197. fast_agent/resources/examples/tensorzero/mcp_server/Dockerfile +25 -0
  198. fast_agent/resources/examples/tensorzero/mcp_server/entrypoint.sh +35 -0
  199. fast_agent/resources/examples/tensorzero/mcp_server/mcp_server.py +31 -0
  200. fast_agent/resources/examples/tensorzero/mcp_server/pyproject.toml +11 -0
  201. fast_agent/resources/examples/tensorzero/simple_agent.py +25 -0
  202. fast_agent/resources/examples/tensorzero/tensorzero_config/system_schema.json +29 -0
  203. fast_agent/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +11 -0
  204. fast_agent/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +35 -0
  205. fast_agent/resources/examples/workflows/agents_as_tools_extended.py +73 -0
  206. fast_agent/resources/examples/workflows/agents_as_tools_simple.py +50 -0
  207. fast_agent/resources/examples/workflows/chaining.py +37 -0
  208. fast_agent/resources/examples/workflows/evaluator.py +77 -0
  209. fast_agent/resources/examples/workflows/fastagent.config.yaml +26 -0
  210. fast_agent/resources/examples/workflows/graded_report.md +89 -0
  211. fast_agent/resources/examples/workflows/human_input.py +28 -0
  212. fast_agent/resources/examples/workflows/maker.py +156 -0
  213. fast_agent/resources/examples/workflows/orchestrator.py +70 -0
  214. fast_agent/resources/examples/workflows/parallel.py +56 -0
  215. fast_agent/resources/examples/workflows/router.py +69 -0
  216. fast_agent/resources/examples/workflows/short_story.md +13 -0
  217. fast_agent/resources/examples/workflows/short_story.txt +19 -0
  218. fast_agent/resources/setup/.gitignore +30 -0
  219. fast_agent/resources/setup/agent.py +28 -0
  220. fast_agent/resources/setup/fastagent.config.yaml +65 -0
  221. fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
  222. fast_agent/resources/setup/pyproject.toml.tmpl +23 -0
  223. fast_agent/skills/__init__.py +9 -0
  224. fast_agent/skills/registry.py +235 -0
  225. fast_agent/tools/elicitation.py +369 -0
  226. fast_agent/tools/shell_runtime.py +402 -0
  227. fast_agent/types/__init__.py +59 -0
  228. fast_agent/types/conversation_summary.py +294 -0
  229. fast_agent/types/llm_stop_reason.py +78 -0
  230. fast_agent/types/message_search.py +249 -0
  231. fast_agent/ui/__init__.py +38 -0
  232. fast_agent/ui/console.py +59 -0
  233. fast_agent/ui/console_display.py +1080 -0
  234. fast_agent/ui/elicitation_form.py +946 -0
  235. fast_agent/ui/elicitation_style.py +59 -0
  236. fast_agent/ui/enhanced_prompt.py +1400 -0
  237. fast_agent/ui/history_display.py +734 -0
  238. fast_agent/ui/interactive_prompt.py +1199 -0
  239. fast_agent/ui/markdown_helpers.py +104 -0
  240. fast_agent/ui/markdown_truncator.py +1004 -0
  241. fast_agent/ui/mcp_display.py +857 -0
  242. fast_agent/ui/mcp_ui_utils.py +235 -0
  243. fast_agent/ui/mermaid_utils.py +169 -0
  244. fast_agent/ui/message_primitives.py +50 -0
  245. fast_agent/ui/notification_tracker.py +205 -0
  246. fast_agent/ui/plain_text_truncator.py +68 -0
  247. fast_agent/ui/progress_display.py +10 -0
  248. fast_agent/ui/rich_progress.py +195 -0
  249. fast_agent/ui/streaming.py +774 -0
  250. fast_agent/ui/streaming_buffer.py +449 -0
  251. fast_agent/ui/tool_display.py +422 -0
  252. fast_agent/ui/usage_display.py +204 -0
  253. fast_agent/utils/__init__.py +5 -0
  254. fast_agent/utils/reasoning_stream_parser.py +77 -0
  255. fast_agent/utils/time.py +22 -0
  256. fast_agent/workflow_telemetry.py +261 -0
  257. fast_agent_mcp-0.4.7.dist-info/METADATA +788 -0
  258. fast_agent_mcp-0.4.7.dist-info/RECORD +261 -0
  259. fast_agent_mcp-0.4.7.dist-info/WHEEL +4 -0
  260. fast_agent_mcp-0.4.7.dist-info/entry_points.txt +7 -0
  261. 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