fast-agent-mcp 0.3.16__py3-none-any.whl → 0.3.17__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 fast-agent-mcp might be problematic. Click here for more details.
- fast_agent/agents/mcp_agent.py +1 -1
- fast_agent/cli/constants.py +2 -0
- fast_agent/cli/main.py +1 -1
- fast_agent/interfaces.py +4 -0
- fast_agent/llm/model_database.py +4 -1
- fast_agent/llm/model_factory.py +4 -2
- fast_agent/llm/model_info.py +19 -43
- fast_agent/llm/provider/google/llm_google_native.py +238 -7
- fast_agent/llm/provider/openai/llm_openai.py +229 -32
- fast_agent/skills/registry.py +17 -9
- fast_agent/tools/shell_runtime.py +4 -4
- fast_agent/ui/console_display.py +43 -1259
- fast_agent/ui/enhanced_prompt.py +26 -12
- fast_agent/ui/markdown_helpers.py +104 -0
- fast_agent/ui/markdown_truncator.py +103 -45
- fast_agent/ui/message_primitives.py +50 -0
- fast_agent/ui/streaming.py +638 -0
- fast_agent/ui/tool_display.py +417 -0
- {fast_agent_mcp-0.3.16.dist-info → fast_agent_mcp-0.3.17.dist-info}/METADATA +1 -1
- {fast_agent_mcp-0.3.16.dist-info → fast_agent_mcp-0.3.17.dist-info}/RECORD +23 -19
- {fast_agent_mcp-0.3.16.dist-info → fast_agent_mcp-0.3.17.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.3.16.dist-info → fast_agent_mcp-0.3.17.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.3.16.dist-info → fast_agent_mcp-0.3.17.dist-info}/licenses/LICENSE +0 -0
fast_agent/agents/mcp_agent.py
CHANGED
|
@@ -157,7 +157,7 @@ class McpAgent(ABC, ToolAgent):
|
|
|
157
157
|
if skills_configured:
|
|
158
158
|
modes.append("skills")
|
|
159
159
|
if shell_flag_requested:
|
|
160
|
-
modes.append("
|
|
160
|
+
modes.append("switch")
|
|
161
161
|
self._shell_access_modes = tuple(modes)
|
|
162
162
|
self._bash_tool = self._shell_runtime.tool
|
|
163
163
|
if self._shell_runtime_enabled:
|
fast_agent/cli/constants.py
CHANGED
fast_agent/cli/main.py
CHANGED
|
@@ -8,7 +8,7 @@ from fast_agent.cli.terminal import Application
|
|
|
8
8
|
from fast_agent.ui.console import console as shared_console
|
|
9
9
|
|
|
10
10
|
app = typer.Typer(
|
|
11
|
-
help="fast-agent
|
|
11
|
+
help="Use `fast-agent go --help` for interactive shell arguments and options.",
|
|
12
12
|
add_completion=False, # We'll add this later when we have more commands
|
|
13
13
|
)
|
|
14
14
|
|
fast_agent/interfaces.py
CHANGED
|
@@ -18,6 +18,7 @@ from typing import (
|
|
|
18
18
|
Type,
|
|
19
19
|
TypeVar,
|
|
20
20
|
Union,
|
|
21
|
+
runtime_checkable,
|
|
21
22
|
)
|
|
22
23
|
|
|
23
24
|
from a2a.types import AgentCard
|
|
@@ -59,6 +60,7 @@ class ModelFactoryFunctionProtocol(Protocol):
|
|
|
59
60
|
def __call__(self, model: str | None = None) -> LLMFactoryProtocol: ...
|
|
60
61
|
|
|
61
62
|
|
|
63
|
+
@runtime_checkable
|
|
62
64
|
class FastAgentLLMProtocol(Protocol):
|
|
63
65
|
"""Protocol defining the interface for LLMs"""
|
|
64
66
|
|
|
@@ -111,6 +113,7 @@ class FastAgentLLMProtocol(Protocol):
|
|
|
111
113
|
def clear(self, *, clear_prompts: bool = False) -> None: ...
|
|
112
114
|
|
|
113
115
|
|
|
116
|
+
@runtime_checkable
|
|
114
117
|
class LlmAgentProtocol(Protocol):
|
|
115
118
|
"""Protocol defining the minimal interface for LLM agents."""
|
|
116
119
|
|
|
@@ -132,6 +135,7 @@ class LlmAgentProtocol(Protocol):
|
|
|
132
135
|
def pop_last_message(self) -> PromptMessageExtended | None: ...
|
|
133
136
|
|
|
134
137
|
|
|
138
|
+
@runtime_checkable
|
|
135
139
|
class AgentProtocol(LlmAgentProtocol, Protocol):
|
|
136
140
|
"""Standard agent interface with flexible input types."""
|
|
137
141
|
|
fast_agent/llm/model_database.py
CHANGED
|
@@ -87,7 +87,7 @@ class ModelDatabase:
|
|
|
87
87
|
)
|
|
88
88
|
|
|
89
89
|
GEMINI_PRO = ModelParameters(
|
|
90
|
-
context_window=
|
|
90
|
+
context_window=1_048_576, max_output_tokens=65_536, tokenizes=GOOGLE_MULTIMODAL
|
|
91
91
|
)
|
|
92
92
|
|
|
93
93
|
QWEN_STANDARD = ModelParameters(
|
|
@@ -245,6 +245,9 @@ class ModelDatabase:
|
|
|
245
245
|
"gemini-2.5-pro-preview": GEMINI_2_5_PRO,
|
|
246
246
|
"gemini-2.5-flash-preview-05-20": GEMINI_FLASH,
|
|
247
247
|
"gemini-2.5-pro-preview-05-06": GEMINI_PRO,
|
|
248
|
+
"gemini-2.5-pro": GEMINI_PRO,
|
|
249
|
+
"gemini-2.5-flash-preview-09-2025": GEMINI_FLASH,
|
|
250
|
+
"gemini-2.5-flash": GEMINI_FLASH,
|
|
248
251
|
# xAI Grok Models
|
|
249
252
|
"grok-4-fast-reasoning": GROK_4_VLM,
|
|
250
253
|
"grok-4-fast-non-reasoning": GROK_4_VLM,
|
fast_agent/llm/model_factory.py
CHANGED
|
@@ -90,7 +90,9 @@ class ModelFactory:
|
|
|
90
90
|
"deepseek-chat": Provider.DEEPSEEK,
|
|
91
91
|
"gemini-2.0-flash": Provider.GOOGLE,
|
|
92
92
|
"gemini-2.5-flash-preview-05-20": Provider.GOOGLE,
|
|
93
|
+
"gemini-2.5-flash-preview-09-2025": Provider.GOOGLE,
|
|
93
94
|
"gemini-2.5-pro-preview-05-06": Provider.GOOGLE,
|
|
95
|
+
"gemini-2.5-pro": Provider.GOOGLE,
|
|
94
96
|
"grok-4": Provider.XAI,
|
|
95
97
|
"grok-4-0709": Provider.XAI,
|
|
96
98
|
"grok-3": Provider.XAI,
|
|
@@ -120,8 +122,8 @@ class ModelFactory:
|
|
|
120
122
|
"deepseekv3": "deepseek-chat",
|
|
121
123
|
"deepseek": "deepseek-chat",
|
|
122
124
|
"gemini2": "gemini-2.0-flash",
|
|
123
|
-
"gemini25": "gemini-2.5-flash-preview-
|
|
124
|
-
"gemini25pro": "gemini-2.5-pro
|
|
125
|
+
"gemini25": "gemini-2.5-flash-preview-09-2025",
|
|
126
|
+
"gemini25pro": "gemini-2.5-pro",
|
|
125
127
|
"kimi": "groq.moonshotai/kimi-k2-instruct-0905",
|
|
126
128
|
"gpt-oss": "groq.openai/gpt-oss-120b",
|
|
127
129
|
"gpt-oss-20b": "groq.openai/gpt-oss-20b",
|
fast_agent/llm/model_info.py
CHANGED
|
@@ -8,14 +8,15 @@ capabilities (Text/Document/Vision), backed by the model database.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
from dataclasses import dataclass
|
|
11
|
-
from typing import TYPE_CHECKING, List, Optional
|
|
11
|
+
from typing import TYPE_CHECKING, List, Optional
|
|
12
12
|
|
|
13
13
|
from fast_agent.llm.model_database import ModelDatabase
|
|
14
|
+
from fast_agent.llm.model_factory import ModelFactory
|
|
14
15
|
from fast_agent.llm.provider_types import Provider
|
|
15
16
|
|
|
16
17
|
if TYPE_CHECKING:
|
|
17
18
|
# Import behind TYPE_CHECKING to avoid import cycles at runtime
|
|
18
|
-
from fast_agent.interfaces import
|
|
19
|
+
from fast_agent.interfaces import FastAgentLLMProtocol
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
@dataclass(frozen=True)
|
|
@@ -32,16 +33,24 @@ class ModelInfo:
|
|
|
32
33
|
|
|
33
34
|
@property
|
|
34
35
|
def supports_text(self) -> bool:
|
|
36
|
+
if "text/plain" in (self.tokenizes or []):
|
|
37
|
+
return True
|
|
35
38
|
return ModelDatabase.supports_mime(self.name, "text/plain")
|
|
36
39
|
|
|
37
40
|
@property
|
|
38
41
|
def supports_document(self) -> bool:
|
|
39
42
|
# Document support currently keyed off PDF support
|
|
43
|
+
if "application/pdf" in (self.tokenizes or []):
|
|
44
|
+
return True
|
|
40
45
|
return ModelDatabase.supports_mime(self.name, "pdf")
|
|
41
46
|
|
|
42
47
|
@property
|
|
43
48
|
def supports_vision(self) -> bool:
|
|
44
49
|
# Any common image format indicates vision support
|
|
50
|
+
tokenizes = self.tokenizes or []
|
|
51
|
+
if any(mt in tokenizes for mt in ("image/jpeg", "image/png", "image/webp")):
|
|
52
|
+
return True
|
|
53
|
+
|
|
45
54
|
return any(
|
|
46
55
|
ModelDatabase.supports_mime(self.name, mt)
|
|
47
56
|
for mt in ("image/jpeg", "image/png", "image/webp")
|
|
@@ -62,14 +71,15 @@ class ModelInfo:
|
|
|
62
71
|
|
|
63
72
|
@classmethod
|
|
64
73
|
def from_name(cls, name: str, provider: Provider | None = None) -> Optional["ModelInfo"]:
|
|
65
|
-
|
|
74
|
+
canonical_name = ModelFactory.MODEL_ALIASES.get(name, name)
|
|
75
|
+
params = ModelDatabase.get_model_params(canonical_name)
|
|
66
76
|
if not params:
|
|
67
77
|
# Unknown model: return a conservative default that supports text only.
|
|
68
78
|
# This matches the desired behavior for TDV display fallbacks.
|
|
69
79
|
if provider is None:
|
|
70
80
|
provider = Provider.GENERIC
|
|
71
81
|
return ModelInfo(
|
|
72
|
-
name=
|
|
82
|
+
name=canonical_name,
|
|
73
83
|
provider=provider,
|
|
74
84
|
context_window=None,
|
|
75
85
|
max_output_tokens=None,
|
|
@@ -78,49 +88,15 @@ class ModelInfo:
|
|
|
78
88
|
reasoning=None,
|
|
79
89
|
)
|
|
80
90
|
|
|
91
|
+
if provider is None:
|
|
92
|
+
provider = ModelFactory.DEFAULT_PROVIDERS.get(canonical_name, Provider.GENERIC)
|
|
93
|
+
|
|
81
94
|
return ModelInfo(
|
|
82
|
-
name=
|
|
83
|
-
provider=provider
|
|
95
|
+
name=canonical_name,
|
|
96
|
+
provider=provider,
|
|
84
97
|
context_window=params.context_window,
|
|
85
98
|
max_output_tokens=params.max_output_tokens,
|
|
86
99
|
tokenizes=params.tokenizes,
|
|
87
100
|
json_mode=params.json_mode,
|
|
88
101
|
reasoning=params.reasoning,
|
|
89
102
|
)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def get_model_info(
|
|
93
|
-
subject: Union["AgentProtocol", "FastAgentLLMProtocol", str, None],
|
|
94
|
-
provider: Provider | None = None,
|
|
95
|
-
) -> Optional[ModelInfo]:
|
|
96
|
-
"""Resolve a ModelInfo from an Agent, LLM, or model name.
|
|
97
|
-
|
|
98
|
-
Keeps the public API small while enabling type-safe access to model
|
|
99
|
-
capabilities across the codebase.
|
|
100
|
-
"""
|
|
101
|
-
if subject is None:
|
|
102
|
-
return None
|
|
103
|
-
|
|
104
|
-
# Agent → LLM
|
|
105
|
-
try:
|
|
106
|
-
from fast_agent.interfaces import AgentProtocol as _AgentProtocol
|
|
107
|
-
except Exception:
|
|
108
|
-
_AgentProtocol = None # type: ignore
|
|
109
|
-
|
|
110
|
-
if _AgentProtocol and isinstance(subject, _AgentProtocol): # type: ignore[arg-type]
|
|
111
|
-
return ModelInfo.from_llm(subject.llm)
|
|
112
|
-
|
|
113
|
-
# LLM → ModelInfo
|
|
114
|
-
try:
|
|
115
|
-
from fast_agent.interfaces import FastAgentLLMProtocol as _LLMProtocol
|
|
116
|
-
except Exception:
|
|
117
|
-
_LLMProtocol = None # type: ignore
|
|
118
|
-
|
|
119
|
-
if _LLMProtocol and isinstance(subject, _LLMProtocol): # type: ignore[arg-type]
|
|
120
|
-
return ModelInfo.from_llm(subject)
|
|
121
|
-
|
|
122
|
-
# String model name
|
|
123
|
-
if isinstance(subject, str):
|
|
124
|
-
return ModelInfo.from_name(subject, provider)
|
|
125
|
-
|
|
126
|
-
return None
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import secrets
|
|
2
3
|
from typing import Dict, List
|
|
3
4
|
|
|
@@ -49,8 +50,6 @@ class GoogleNativeLLM(FastAgentLLM[types.Content, types.Content]):
|
|
|
49
50
|
|
|
50
51
|
def __init__(self, *args, **kwargs) -> None:
|
|
51
52
|
super().__init__(*args, provider=Provider.GOOGLE, **kwargs)
|
|
52
|
-
# Initialize the google.genai client
|
|
53
|
-
self._google_client = self._initialize_google_client()
|
|
54
53
|
# Initialize the converter
|
|
55
54
|
self._converter = GoogleConverter()
|
|
56
55
|
|
|
@@ -109,6 +108,218 @@ class GoogleNativeLLM(FastAgentLLM[types.Content, types.Content]):
|
|
|
109
108
|
# Include other relevant default parameters
|
|
110
109
|
)
|
|
111
110
|
|
|
111
|
+
async def _stream_generate_content(
|
|
112
|
+
self,
|
|
113
|
+
*,
|
|
114
|
+
model: str,
|
|
115
|
+
contents: List[types.Content],
|
|
116
|
+
config: types.GenerateContentConfig,
|
|
117
|
+
client: genai.Client,
|
|
118
|
+
) -> types.GenerateContentResponse | None:
|
|
119
|
+
"""Stream Gemini responses and return the final aggregated completion."""
|
|
120
|
+
try:
|
|
121
|
+
response_stream = await client.aio.models.generate_content_stream(
|
|
122
|
+
model=model,
|
|
123
|
+
contents=contents,
|
|
124
|
+
config=config,
|
|
125
|
+
)
|
|
126
|
+
except AttributeError:
|
|
127
|
+
# Older SDKs might not expose streaming; fall back to non-streaming.
|
|
128
|
+
return None
|
|
129
|
+
except errors.APIError:
|
|
130
|
+
raise
|
|
131
|
+
except Exception as exc: # pragma: no cover - defensive fallback
|
|
132
|
+
self.logger.warning(
|
|
133
|
+
"Google streaming failed during setup; falling back to non-streaming",
|
|
134
|
+
exc_info=exc,
|
|
135
|
+
)
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
return await self._consume_google_stream(response_stream, model=model)
|
|
139
|
+
|
|
140
|
+
async def _consume_google_stream(
|
|
141
|
+
self,
|
|
142
|
+
response_stream,
|
|
143
|
+
*,
|
|
144
|
+
model: str,
|
|
145
|
+
) -> types.GenerateContentResponse | None:
|
|
146
|
+
"""Consume the async streaming iterator and aggregate the final response."""
|
|
147
|
+
estimated_tokens = 0
|
|
148
|
+
timeline: List[tuple[str, int | None, str]] = []
|
|
149
|
+
tool_streams: Dict[int, Dict[str, str]] = {}
|
|
150
|
+
active_tool_index: int | None = None
|
|
151
|
+
tool_counter = 0
|
|
152
|
+
usage_metadata = None
|
|
153
|
+
last_chunk: types.GenerateContentResponse | None = None
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
async for chunk in response_stream:
|
|
157
|
+
last_chunk = chunk
|
|
158
|
+
if getattr(chunk, "usage_metadata", None):
|
|
159
|
+
usage_metadata = chunk.usage_metadata
|
|
160
|
+
|
|
161
|
+
if not getattr(chunk, "candidates", None):
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
candidate = chunk.candidates[0]
|
|
165
|
+
content = getattr(candidate, "content", None)
|
|
166
|
+
if content is None or not getattr(content, "parts", None):
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
for part in content.parts:
|
|
170
|
+
if getattr(part, "text", None):
|
|
171
|
+
text = part.text or ""
|
|
172
|
+
if text:
|
|
173
|
+
if timeline and timeline[-1][0] == "text":
|
|
174
|
+
prev_type, prev_index, prev_text = timeline[-1]
|
|
175
|
+
timeline[-1] = (prev_type, prev_index, prev_text + text)
|
|
176
|
+
else:
|
|
177
|
+
timeline.append(("text", None, text))
|
|
178
|
+
estimated_tokens = self._update_streaming_progress(
|
|
179
|
+
text,
|
|
180
|
+
model,
|
|
181
|
+
estimated_tokens,
|
|
182
|
+
)
|
|
183
|
+
self._notify_tool_stream_listeners(
|
|
184
|
+
"text",
|
|
185
|
+
{
|
|
186
|
+
"chunk": text,
|
|
187
|
+
"streams_arguments": False,
|
|
188
|
+
},
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if getattr(part, "function_call", None):
|
|
192
|
+
function_call = part.function_call
|
|
193
|
+
name = getattr(function_call, "name", None) or "tool"
|
|
194
|
+
args = getattr(function_call, "args", None) or {}
|
|
195
|
+
|
|
196
|
+
if active_tool_index is None:
|
|
197
|
+
active_tool_index = tool_counter
|
|
198
|
+
tool_counter += 1
|
|
199
|
+
tool_use_id = f"tool_{self.chat_turn()}_{active_tool_index}"
|
|
200
|
+
tool_streams[active_tool_index] = {
|
|
201
|
+
"name": name,
|
|
202
|
+
"tool_use_id": tool_use_id,
|
|
203
|
+
"buffer": "",
|
|
204
|
+
}
|
|
205
|
+
self._notify_tool_stream_listeners(
|
|
206
|
+
"start",
|
|
207
|
+
{
|
|
208
|
+
"tool_name": name,
|
|
209
|
+
"tool_use_id": tool_use_id,
|
|
210
|
+
"index": active_tool_index,
|
|
211
|
+
"streams_arguments": False,
|
|
212
|
+
},
|
|
213
|
+
)
|
|
214
|
+
timeline.append(("tool_call", active_tool_index, ""))
|
|
215
|
+
|
|
216
|
+
stream_info = tool_streams.get(active_tool_index)
|
|
217
|
+
if not stream_info:
|
|
218
|
+
continue
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
serialized_args = json.dumps(args, separators=(",", ":"))
|
|
222
|
+
except Exception:
|
|
223
|
+
serialized_args = str(args)
|
|
224
|
+
|
|
225
|
+
previous = stream_info.get("buffer", "")
|
|
226
|
+
if isinstance(previous, str) and serialized_args.startswith(previous):
|
|
227
|
+
delta = serialized_args[len(previous) :]
|
|
228
|
+
else:
|
|
229
|
+
delta = serialized_args
|
|
230
|
+
stream_info["buffer"] = serialized_args
|
|
231
|
+
|
|
232
|
+
if delta:
|
|
233
|
+
self._notify_tool_stream_listeners(
|
|
234
|
+
"delta",
|
|
235
|
+
{
|
|
236
|
+
"tool_name": stream_info["name"],
|
|
237
|
+
"tool_use_id": stream_info["tool_use_id"],
|
|
238
|
+
"index": active_tool_index,
|
|
239
|
+
"chunk": delta,
|
|
240
|
+
"streams_arguments": False,
|
|
241
|
+
},
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
finish_reason = getattr(candidate, "finish_reason", None)
|
|
245
|
+
if finish_reason:
|
|
246
|
+
finish_value = str(finish_reason).split(".")[-1].upper()
|
|
247
|
+
if finish_value in {"FUNCTION_CALL", "STOP"} and active_tool_index is not None:
|
|
248
|
+
stream_info = tool_streams.get(active_tool_index)
|
|
249
|
+
if stream_info:
|
|
250
|
+
self._notify_tool_stream_listeners(
|
|
251
|
+
"stop",
|
|
252
|
+
{
|
|
253
|
+
"tool_name": stream_info["name"],
|
|
254
|
+
"tool_use_id": stream_info["tool_use_id"],
|
|
255
|
+
"index": active_tool_index,
|
|
256
|
+
"streams_arguments": False,
|
|
257
|
+
},
|
|
258
|
+
)
|
|
259
|
+
active_tool_index = None
|
|
260
|
+
finally:
|
|
261
|
+
stream_close = getattr(response_stream, "aclose", None)
|
|
262
|
+
if callable(stream_close):
|
|
263
|
+
try:
|
|
264
|
+
await stream_close()
|
|
265
|
+
except Exception:
|
|
266
|
+
pass
|
|
267
|
+
|
|
268
|
+
if active_tool_index is not None:
|
|
269
|
+
stream_info = tool_streams.get(active_tool_index)
|
|
270
|
+
if stream_info:
|
|
271
|
+
self._notify_tool_stream_listeners(
|
|
272
|
+
"stop",
|
|
273
|
+
{
|
|
274
|
+
"tool_name": stream_info["name"],
|
|
275
|
+
"tool_use_id": stream_info["tool_use_id"],
|
|
276
|
+
"index": active_tool_index,
|
|
277
|
+
"streams_arguments": False,
|
|
278
|
+
},
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
if not timeline and last_chunk is None:
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
final_parts: List[types.Part] = []
|
|
285
|
+
for entry_type, index, payload in timeline:
|
|
286
|
+
if entry_type == "text":
|
|
287
|
+
final_parts.append(types.Part.from_text(text=payload))
|
|
288
|
+
elif entry_type == "tool_call" and index is not None:
|
|
289
|
+
stream_info = tool_streams.get(index)
|
|
290
|
+
if not stream_info:
|
|
291
|
+
continue
|
|
292
|
+
buffer = stream_info.get("buffer", "")
|
|
293
|
+
try:
|
|
294
|
+
args_obj = json.loads(buffer) if buffer else {}
|
|
295
|
+
except json.JSONDecodeError:
|
|
296
|
+
args_obj = {"__raw": buffer}
|
|
297
|
+
final_parts.append(
|
|
298
|
+
types.Part.from_function_call(
|
|
299
|
+
name=str(stream_info.get("name") or "tool"),
|
|
300
|
+
args=args_obj,
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
final_content = types.Content(role="model", parts=final_parts)
|
|
305
|
+
|
|
306
|
+
if last_chunk is not None:
|
|
307
|
+
final_response = last_chunk.model_copy(deep=True)
|
|
308
|
+
if getattr(final_response, "candidates", None):
|
|
309
|
+
final_candidate = final_response.candidates[0]
|
|
310
|
+
final_candidate.content = final_content
|
|
311
|
+
else:
|
|
312
|
+
final_response.candidates = [types.Candidate(content=final_content)]
|
|
313
|
+
else:
|
|
314
|
+
final_response = types.GenerateContentResponse(
|
|
315
|
+
candidates=[types.Candidate(content=final_content)]
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
if usage_metadata:
|
|
319
|
+
final_response.usage_metadata = usage_metadata
|
|
320
|
+
|
|
321
|
+
return final_response
|
|
322
|
+
|
|
112
323
|
async def _google_completion(
|
|
113
324
|
self,
|
|
114
325
|
message: List[types.Content] | None,
|
|
@@ -163,13 +374,24 @@ class GoogleNativeLLM(FastAgentLLM[types.Content, types.Content]):
|
|
|
163
374
|
)
|
|
164
375
|
|
|
165
376
|
# 3. Call the google.genai API
|
|
377
|
+
client = self._initialize_google_client()
|
|
166
378
|
try:
|
|
167
379
|
# Use the async client
|
|
168
|
-
api_response =
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
380
|
+
api_response = None
|
|
381
|
+
streaming_supported = response_schema is None and response_mime_type is None
|
|
382
|
+
if streaming_supported:
|
|
383
|
+
api_response = await self._stream_generate_content(
|
|
384
|
+
model=request_params.model,
|
|
385
|
+
contents=conversation_history,
|
|
386
|
+
config=generate_content_config,
|
|
387
|
+
client=client,
|
|
388
|
+
)
|
|
389
|
+
if api_response is None:
|
|
390
|
+
api_response = await client.aio.models.generate_content(
|
|
391
|
+
model=request_params.model,
|
|
392
|
+
contents=conversation_history, # Full conversational context for this turn
|
|
393
|
+
config=generate_content_config,
|
|
394
|
+
)
|
|
173
395
|
self.logger.debug("Google generate_content response:", data=api_response)
|
|
174
396
|
|
|
175
397
|
# Track usage if response is valid and has usage data
|
|
@@ -195,6 +417,15 @@ class GoogleNativeLLM(FastAgentLLM[types.Content, types.Content]):
|
|
|
195
417
|
self.logger.error(f"Error during Google generate_content call: {e}")
|
|
196
418
|
# Decide how to handle other exceptions - potentially re-raise or return an error message
|
|
197
419
|
raise e
|
|
420
|
+
finally:
|
|
421
|
+
try:
|
|
422
|
+
await client.aio.aclose()
|
|
423
|
+
except Exception:
|
|
424
|
+
pass
|
|
425
|
+
try:
|
|
426
|
+
client.close()
|
|
427
|
+
except Exception:
|
|
428
|
+
pass
|
|
198
429
|
|
|
199
430
|
# 4. Process the API response
|
|
200
431
|
if not api_response.candidates:
|