fast-agent-mcp 0.3.15__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/__init__.py +2 -0
- fast_agent/agents/agent_types.py +5 -0
- fast_agent/agents/llm_agent.py +7 -0
- fast_agent/agents/llm_decorator.py +6 -0
- fast_agent/agents/mcp_agent.py +134 -10
- fast_agent/cli/__main__.py +35 -0
- fast_agent/cli/commands/check_config.py +85 -0
- fast_agent/cli/commands/go.py +100 -36
- fast_agent/cli/constants.py +15 -1
- fast_agent/cli/main.py +2 -1
- fast_agent/config.py +39 -10
- fast_agent/constants.py +8 -0
- fast_agent/context.py +24 -15
- fast_agent/core/direct_decorators.py +9 -0
- fast_agent/core/fastagent.py +101 -1
- fast_agent/core/logging/listeners.py +8 -0
- fast_agent/interfaces.py +12 -0
- fast_agent/llm/fastagent_llm.py +45 -0
- fast_agent/llm/memory.py +26 -1
- 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/anthropic/llm_anthropic.py +112 -0
- fast_agent/llm/provider/google/llm_google_native.py +238 -7
- fast_agent/llm/provider/openai/llm_openai.py +382 -19
- fast_agent/llm/provider/openai/responses.py +133 -0
- fast_agent/resources/setup/agent.py +2 -0
- fast_agent/resources/setup/fastagent.config.yaml +6 -0
- fast_agent/skills/__init__.py +9 -0
- fast_agent/skills/registry.py +208 -0
- fast_agent/tools/shell_runtime.py +404 -0
- fast_agent/ui/console_display.py +47 -996
- fast_agent/ui/elicitation_form.py +76 -24
- fast_agent/ui/elicitation_style.py +2 -2
- fast_agent/ui/enhanced_prompt.py +107 -37
- fast_agent/ui/history_display.py +20 -5
- fast_agent/ui/interactive_prompt.py +108 -3
- 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.15.dist-info → fast_agent_mcp-0.3.17.dist-info}/METADATA +8 -7
- {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.17.dist-info}/RECORD +47 -39
- {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.17.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.17.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.17.dist-info}/licenses/LICENSE +0 -0
fast_agent/core/fastagent.py
CHANGED
|
@@ -6,6 +6,7 @@ directly creates Agent instances without proxies.
|
|
|
6
6
|
|
|
7
7
|
import argparse
|
|
8
8
|
import asyncio
|
|
9
|
+
import pathlib
|
|
9
10
|
import sys
|
|
10
11
|
from contextlib import asynccontextmanager
|
|
11
12
|
from importlib.metadata import version as get_version
|
|
@@ -76,12 +77,14 @@ from fast_agent.core.validation import (
|
|
|
76
77
|
validate_workflow_references,
|
|
77
78
|
)
|
|
78
79
|
from fast_agent.mcp.prompts.prompt_load import load_prompt
|
|
80
|
+
from fast_agent.skills import SkillManifest, SkillRegistry
|
|
79
81
|
from fast_agent.ui.usage_display import display_usage_report
|
|
80
82
|
|
|
81
83
|
if TYPE_CHECKING:
|
|
82
84
|
from mcp.client.session import ElicitationFnT
|
|
83
85
|
from pydantic import AnyUrl
|
|
84
86
|
|
|
87
|
+
from fast_agent.constants import DEFAULT_AGENT_INSTRUCTION
|
|
85
88
|
from fast_agent.interfaces import AgentProtocol
|
|
86
89
|
from fast_agent.types import PromptMessageExtended
|
|
87
90
|
|
|
@@ -102,6 +105,7 @@ class FastAgent:
|
|
|
102
105
|
ignore_unknown_args: bool = False,
|
|
103
106
|
parse_cli_args: bool = True,
|
|
104
107
|
quiet: bool = False, # Add quiet parameter
|
|
108
|
+
skills_directory: str | pathlib.Path | None = None,
|
|
105
109
|
**kwargs,
|
|
106
110
|
) -> None:
|
|
107
111
|
"""
|
|
@@ -119,6 +123,10 @@ class FastAgent:
|
|
|
119
123
|
"""
|
|
120
124
|
self.args = argparse.Namespace() # Initialize args always
|
|
121
125
|
self._programmatic_quiet = quiet # Store the programmatic quiet setting
|
|
126
|
+
self._skills_directory_override = (
|
|
127
|
+
Path(skills_directory).expanduser() if skills_directory else None
|
|
128
|
+
)
|
|
129
|
+
self._default_skill_manifests: List[SkillManifest] = []
|
|
122
130
|
|
|
123
131
|
# --- Wrap argument parsing logic ---
|
|
124
132
|
if parse_cli_args:
|
|
@@ -173,6 +181,10 @@ class FastAgent:
|
|
|
173
181
|
default="0.0.0.0",
|
|
174
182
|
help="Host address to bind to when running as a server with SSE transport",
|
|
175
183
|
)
|
|
184
|
+
parser.add_argument(
|
|
185
|
+
"--skills",
|
|
186
|
+
help="Path to skills directory to use instead of default .claude/skills",
|
|
187
|
+
)
|
|
176
188
|
|
|
177
189
|
if ignore_unknown_args:
|
|
178
190
|
known_args, _ = parser.parse_known_args()
|
|
@@ -200,6 +212,14 @@ class FastAgent:
|
|
|
200
212
|
if self._programmatic_quiet:
|
|
201
213
|
self.args.quiet = True
|
|
202
214
|
|
|
215
|
+
# Apply CLI skills directory if not already set programmatically
|
|
216
|
+
if (
|
|
217
|
+
self._skills_directory_override is None
|
|
218
|
+
and hasattr(self.args, "skills")
|
|
219
|
+
and self.args.skills
|
|
220
|
+
):
|
|
221
|
+
self._skills_directory_override = Path(self.args.skills).expanduser()
|
|
222
|
+
|
|
203
223
|
self.name = name
|
|
204
224
|
self.config_path = config_path
|
|
205
225
|
|
|
@@ -271,6 +291,7 @@ class FastAgent:
|
|
|
271
291
|
from collections.abc import Coroutine
|
|
272
292
|
from pathlib import Path
|
|
273
293
|
|
|
294
|
+
from fast_agent.skills import SkillManifest, SkillRegistry
|
|
274
295
|
from fast_agent.types import RequestParams
|
|
275
296
|
|
|
276
297
|
P = ParamSpec("P")
|
|
@@ -281,11 +302,12 @@ class FastAgent:
|
|
|
281
302
|
name: str = "default",
|
|
282
303
|
instruction_or_kwarg: Optional[str | Path | AnyUrl] = None,
|
|
283
304
|
*,
|
|
284
|
-
instruction: str | Path | AnyUrl =
|
|
305
|
+
instruction: str | Path | AnyUrl = DEFAULT_AGENT_INSTRUCTION,
|
|
285
306
|
servers: List[str] = [],
|
|
286
307
|
tools: Optional[Dict[str, List[str]]] = None,
|
|
287
308
|
resources: Optional[Dict[str, List[str]]] = None,
|
|
288
309
|
prompts: Optional[Dict[str, List[str]]] = None,
|
|
310
|
+
skills: Optional[List[SkillManifest | SkillRegistry | Path | str | None]] = None,
|
|
289
311
|
model: Optional[str] = None,
|
|
290
312
|
use_history: bool = True,
|
|
291
313
|
request_params: RequestParams | None = None,
|
|
@@ -430,6 +452,21 @@ class FastAgent:
|
|
|
430
452
|
with tracer.start_as_current_span(self.name):
|
|
431
453
|
try:
|
|
432
454
|
async with self.app.run():
|
|
455
|
+
registry = getattr(self.context, "skill_registry", None)
|
|
456
|
+
if self._skills_directory_override is not None:
|
|
457
|
+
override_registry = SkillRegistry(
|
|
458
|
+
base_dir=Path.cwd(),
|
|
459
|
+
override_directory=self._skills_directory_override,
|
|
460
|
+
)
|
|
461
|
+
self.context.skill_registry = override_registry
|
|
462
|
+
registry = override_registry
|
|
463
|
+
|
|
464
|
+
default_skills: List[SkillManifest] = []
|
|
465
|
+
if registry:
|
|
466
|
+
default_skills = registry.load_manifests()
|
|
467
|
+
|
|
468
|
+
self._apply_skills_to_agent_configs(default_skills)
|
|
469
|
+
|
|
433
470
|
# Apply quiet mode if requested
|
|
434
471
|
if quiet_mode:
|
|
435
472
|
cfg = self.app.context.config
|
|
@@ -621,6 +658,69 @@ class FastAgent:
|
|
|
621
658
|
except Exception:
|
|
622
659
|
pass
|
|
623
660
|
|
|
661
|
+
def _apply_skills_to_agent_configs(self, default_skills: List[SkillManifest]) -> None:
|
|
662
|
+
self._default_skill_manifests = list(default_skills)
|
|
663
|
+
|
|
664
|
+
for agent_data in self.agents.values():
|
|
665
|
+
config_obj = agent_data.get("config")
|
|
666
|
+
if not config_obj:
|
|
667
|
+
continue
|
|
668
|
+
|
|
669
|
+
resolved = self._resolve_skills(config_obj.skills)
|
|
670
|
+
if not resolved:
|
|
671
|
+
resolved = list(default_skills)
|
|
672
|
+
else:
|
|
673
|
+
resolved = self._deduplicate_skills(resolved)
|
|
674
|
+
|
|
675
|
+
config_obj.skill_manifests = resolved
|
|
676
|
+
|
|
677
|
+
def _resolve_skills(
|
|
678
|
+
self,
|
|
679
|
+
entry: SkillManifest
|
|
680
|
+
| SkillRegistry
|
|
681
|
+
| Path
|
|
682
|
+
| str
|
|
683
|
+
| List[SkillManifest | SkillRegistry | Path | str | None]
|
|
684
|
+
| None,
|
|
685
|
+
) -> List[SkillManifest]:
|
|
686
|
+
if entry is None:
|
|
687
|
+
return []
|
|
688
|
+
if isinstance(entry, list):
|
|
689
|
+
manifests: List[SkillManifest] = []
|
|
690
|
+
for item in entry:
|
|
691
|
+
manifests.extend(self._resolve_skills(item))
|
|
692
|
+
return manifests
|
|
693
|
+
if isinstance(entry, SkillManifest):
|
|
694
|
+
return [entry]
|
|
695
|
+
if isinstance(entry, SkillRegistry):
|
|
696
|
+
try:
|
|
697
|
+
return entry.load_manifests()
|
|
698
|
+
except Exception:
|
|
699
|
+
logger.debug(
|
|
700
|
+
"Failed to load skills from registry",
|
|
701
|
+
data={"registry": type(entry).__name__},
|
|
702
|
+
)
|
|
703
|
+
return []
|
|
704
|
+
if isinstance(entry, Path):
|
|
705
|
+
return SkillRegistry.load_directory(entry.expanduser().resolve())
|
|
706
|
+
if isinstance(entry, str):
|
|
707
|
+
return SkillRegistry.load_directory(Path(entry).expanduser().resolve())
|
|
708
|
+
|
|
709
|
+
logger.debug(
|
|
710
|
+
"Unsupported skill entry type",
|
|
711
|
+
data={"type": type(entry).__name__},
|
|
712
|
+
)
|
|
713
|
+
return []
|
|
714
|
+
|
|
715
|
+
@staticmethod
|
|
716
|
+
def _deduplicate_skills(manifests: List[SkillManifest]) -> List[SkillManifest]:
|
|
717
|
+
unique: Dict[str, SkillManifest] = {}
|
|
718
|
+
for manifest in manifests:
|
|
719
|
+
key = manifest.name.lower()
|
|
720
|
+
if key not in unique:
|
|
721
|
+
unique[key] = manifest
|
|
722
|
+
return list(unique.values())
|
|
723
|
+
|
|
624
724
|
def _handle_error(self, e: Exception, error_type: Optional[str] = None) -> None:
|
|
625
725
|
"""
|
|
626
726
|
Handle errors with consistent formatting and messaging.
|
|
@@ -64,6 +64,14 @@ def convert_log_event(event: Event) -> "ProgressEvent | None":
|
|
|
64
64
|
chat_turn = event_data.get("chat_turn")
|
|
65
65
|
if chat_turn is not None:
|
|
66
66
|
details = f"{model} turn {chat_turn}"
|
|
67
|
+
|
|
68
|
+
tool_name = event_data.get("tool_name")
|
|
69
|
+
tool_event = event_data.get("tool_event")
|
|
70
|
+
if tool_name:
|
|
71
|
+
tool_suffix = tool_name
|
|
72
|
+
if tool_event:
|
|
73
|
+
tool_suffix = f"{tool_suffix} ({tool_event})"
|
|
74
|
+
details = f"{details} • {tool_suffix}".strip()
|
|
67
75
|
else:
|
|
68
76
|
if not target:
|
|
69
77
|
target = event_data.get("target", "unknown")
|
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
|
|
|
@@ -87,9 +89,15 @@ class FastAgentLLMProtocol(Protocol):
|
|
|
87
89
|
|
|
88
90
|
def add_stream_listener(self, listener: Callable[[str], None]) -> Callable[[], None]: ...
|
|
89
91
|
|
|
92
|
+
def add_tool_stream_listener(
|
|
93
|
+
self, listener: Callable[[str, Dict[str, Any] | None], None]
|
|
94
|
+
) -> Callable[[], None]: ...
|
|
95
|
+
|
|
90
96
|
@property
|
|
91
97
|
def message_history(self) -> List[PromptMessageExtended]: ...
|
|
92
98
|
|
|
99
|
+
def pop_last_message(self) -> PromptMessageExtended | None: ...
|
|
100
|
+
|
|
93
101
|
@property
|
|
94
102
|
def usage_accumulator(self) -> UsageAccumulator | None: ...
|
|
95
103
|
|
|
@@ -105,6 +113,7 @@ class FastAgentLLMProtocol(Protocol):
|
|
|
105
113
|
def clear(self, *, clear_prompts: bool = False) -> None: ...
|
|
106
114
|
|
|
107
115
|
|
|
116
|
+
@runtime_checkable
|
|
108
117
|
class LlmAgentProtocol(Protocol):
|
|
109
118
|
"""Protocol defining the minimal interface for LLM agents."""
|
|
110
119
|
|
|
@@ -123,7 +132,10 @@ class LlmAgentProtocol(Protocol):
|
|
|
123
132
|
|
|
124
133
|
def clear(self, *, clear_prompts: bool = False) -> None: ...
|
|
125
134
|
|
|
135
|
+
def pop_last_message(self) -> PromptMessageExtended | None: ...
|
|
136
|
+
|
|
126
137
|
|
|
138
|
+
@runtime_checkable
|
|
127
139
|
class AgentProtocol(LlmAgentProtocol, Protocol):
|
|
128
140
|
"""Standard agent interface with flexible input types."""
|
|
129
141
|
|
fast_agent/llm/fastagent_llm.py
CHANGED
|
@@ -159,6 +159,7 @@ class FastAgentLLM(ContextDependent, FastAgentLLMProtocol, Generic[MessageParamT
|
|
|
159
159
|
# Initialize usage tracking
|
|
160
160
|
self._usage_accumulator = UsageAccumulator()
|
|
161
161
|
self._stream_listeners: set[Callable[[str], None]] = set()
|
|
162
|
+
self._tool_stream_listeners: set[Callable[[str, Dict[str, Any] | None], None]] = set()
|
|
162
163
|
|
|
163
164
|
def _initialize_default_params(self, kwargs: dict) -> RequestParams:
|
|
164
165
|
"""Initialize default parameters for the LLM.
|
|
@@ -534,6 +535,37 @@ class FastAgentLLM(ContextDependent, FastAgentLLMProtocol, Generic[MessageParamT
|
|
|
534
535
|
except Exception:
|
|
535
536
|
self.logger.exception("Stream listener raised an exception")
|
|
536
537
|
|
|
538
|
+
def add_tool_stream_listener(
|
|
539
|
+
self, listener: Callable[[str, Dict[str, Any] | None], None]
|
|
540
|
+
) -> Callable[[], None]:
|
|
541
|
+
"""Register a callback invoked with tool streaming events.
|
|
542
|
+
|
|
543
|
+
Args:
|
|
544
|
+
listener: Callable receiving event_type (str) and optional info dict.
|
|
545
|
+
|
|
546
|
+
Returns:
|
|
547
|
+
A function that removes the listener when called.
|
|
548
|
+
"""
|
|
549
|
+
|
|
550
|
+
self._tool_stream_listeners.add(listener)
|
|
551
|
+
|
|
552
|
+
def remove() -> None:
|
|
553
|
+
self._tool_stream_listeners.discard(listener)
|
|
554
|
+
|
|
555
|
+
return remove
|
|
556
|
+
|
|
557
|
+
def _notify_tool_stream_listeners(
|
|
558
|
+
self, event_type: str, payload: Dict[str, Any] | None = None
|
|
559
|
+
) -> None:
|
|
560
|
+
"""Notify listeners about tool streaming lifecycle events."""
|
|
561
|
+
|
|
562
|
+
data = payload or {}
|
|
563
|
+
for listener in list(self._tool_stream_listeners):
|
|
564
|
+
try:
|
|
565
|
+
listener(event_type, data)
|
|
566
|
+
except Exception:
|
|
567
|
+
self.logger.exception("Tool stream listener raised an exception")
|
|
568
|
+
|
|
537
569
|
def _log_chat_finished(self, model: Optional[str] = None) -> None:
|
|
538
570
|
"""Log a chat finished event"""
|
|
539
571
|
data = {
|
|
@@ -643,6 +675,19 @@ class FastAgentLLM(ContextDependent, FastAgentLLMProtocol, Generic[MessageParamT
|
|
|
643
675
|
"""
|
|
644
676
|
return self._message_history
|
|
645
677
|
|
|
678
|
+
def pop_last_message(self) -> PromptMessageExtended | None:
|
|
679
|
+
"""Remove and return the most recent message from the conversation history."""
|
|
680
|
+
if not self._message_history:
|
|
681
|
+
return None
|
|
682
|
+
|
|
683
|
+
removed = self._message_history.pop()
|
|
684
|
+
try:
|
|
685
|
+
self.history.pop()
|
|
686
|
+
except Exception:
|
|
687
|
+
# If provider-specific memory isn't available, ignore to avoid crashing UX
|
|
688
|
+
pass
|
|
689
|
+
return removed
|
|
690
|
+
|
|
646
691
|
def clear(self, *, clear_prompts: bool = False) -> None:
|
|
647
692
|
"""Reset stored message history while optionally retaining prompt templates."""
|
|
648
693
|
|
fast_agent/llm/memory.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Generic, List, Protocol, TypeVar
|
|
1
|
+
from typing import Generic, List, Optional, Protocol, TypeVar
|
|
2
2
|
|
|
3
3
|
# Define our own type variable for implementation use
|
|
4
4
|
MessageParamT = TypeVar("MessageParamT")
|
|
@@ -23,6 +23,8 @@ class Memory(Protocol, Generic[MessageParamT]):
|
|
|
23
23
|
|
|
24
24
|
def clear(self, clear_prompts: bool = False) -> None: ...
|
|
25
25
|
|
|
26
|
+
def pop(self, *, from_prompts: bool = False) -> Optional[MessageParamT]: ...
|
|
27
|
+
|
|
26
28
|
|
|
27
29
|
class SimpleMemory(Memory, Generic[MessageParamT]):
|
|
28
30
|
"""
|
|
@@ -108,6 +110,29 @@ class SimpleMemory(Memory, Generic[MessageParamT]):
|
|
|
108
110
|
if clear_prompts:
|
|
109
111
|
self.prompt_messages = []
|
|
110
112
|
|
|
113
|
+
def pop(self, *, from_prompts: bool = False) -> Optional[MessageParamT]:
|
|
114
|
+
"""
|
|
115
|
+
Remove and return the most recent message from history or prompt messages.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
from_prompts: If True, pop from prompt_messages instead of history
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
The removed message if available, otherwise None
|
|
122
|
+
"""
|
|
123
|
+
if from_prompts:
|
|
124
|
+
if not self.prompt_messages:
|
|
125
|
+
return None
|
|
126
|
+
return self.prompt_messages.pop()
|
|
127
|
+
|
|
128
|
+
if not self.history:
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
removed = self.history.pop()
|
|
132
|
+
# Recalculate cache positions now that the history shrank
|
|
133
|
+
self.conversation_cache_positions = self._calculate_cache_positions(len(self.history))
|
|
134
|
+
return removed
|
|
135
|
+
|
|
111
136
|
def should_apply_conversation_cache(self) -> bool:
|
|
112
137
|
"""
|
|
113
138
|
Determine if conversation caching should be applied based on walking algorithm.
|
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
|
|
@@ -244,10 +244,114 @@ class AnthropicLLM(FastAgentLLM[MessageParam, Message]):
|
|
|
244
244
|
"""Process the streaming response and display real-time token usage."""
|
|
245
245
|
# Track estimated output tokens by counting text chunks
|
|
246
246
|
estimated_tokens = 0
|
|
247
|
+
tool_streams: dict[int, dict[str, Any]] = {}
|
|
247
248
|
|
|
248
249
|
try:
|
|
249
250
|
# Process the raw event stream to get token counts
|
|
250
251
|
async for event in stream:
|
|
252
|
+
if (
|
|
253
|
+
event.type == "content_block_start"
|
|
254
|
+
and hasattr(event, "content_block")
|
|
255
|
+
and getattr(event.content_block, "type", None) == "tool_use"
|
|
256
|
+
):
|
|
257
|
+
content_block = event.content_block
|
|
258
|
+
tool_streams[event.index] = {
|
|
259
|
+
"name": content_block.name,
|
|
260
|
+
"id": content_block.id,
|
|
261
|
+
"buffer": [],
|
|
262
|
+
}
|
|
263
|
+
self._notify_tool_stream_listeners(
|
|
264
|
+
"start",
|
|
265
|
+
{
|
|
266
|
+
"tool_name": content_block.name,
|
|
267
|
+
"tool_use_id": content_block.id,
|
|
268
|
+
"index": event.index,
|
|
269
|
+
"streams_arguments": False, # Anthropic doesn't stream arguments
|
|
270
|
+
},
|
|
271
|
+
)
|
|
272
|
+
self.logger.info(
|
|
273
|
+
"Model started streaming tool input",
|
|
274
|
+
data={
|
|
275
|
+
"progress_action": ProgressAction.CALLING_TOOL,
|
|
276
|
+
"agent_name": self.name,
|
|
277
|
+
"model": model,
|
|
278
|
+
"tool_name": content_block.name,
|
|
279
|
+
"tool_use_id": content_block.id,
|
|
280
|
+
"tool_event": "start",
|
|
281
|
+
},
|
|
282
|
+
)
|
|
283
|
+
continue
|
|
284
|
+
|
|
285
|
+
if (
|
|
286
|
+
event.type == "content_block_delta"
|
|
287
|
+
and hasattr(event, "delta")
|
|
288
|
+
and event.delta.type == "input_json_delta"
|
|
289
|
+
):
|
|
290
|
+
info = tool_streams.get(event.index)
|
|
291
|
+
if info is not None:
|
|
292
|
+
chunk = event.delta.partial_json or ""
|
|
293
|
+
info["buffer"].append(chunk)
|
|
294
|
+
preview = chunk if len(chunk) <= 80 else chunk[:77] + "..."
|
|
295
|
+
self._notify_tool_stream_listeners(
|
|
296
|
+
"delta",
|
|
297
|
+
{
|
|
298
|
+
"tool_name": info.get("name"),
|
|
299
|
+
"tool_use_id": info.get("id"),
|
|
300
|
+
"index": event.index,
|
|
301
|
+
"chunk": chunk,
|
|
302
|
+
"streams_arguments": False,
|
|
303
|
+
},
|
|
304
|
+
)
|
|
305
|
+
self.logger.debug(
|
|
306
|
+
"Streaming tool input delta",
|
|
307
|
+
data={
|
|
308
|
+
"tool_name": info.get("name"),
|
|
309
|
+
"tool_use_id": info.get("id"),
|
|
310
|
+
"chunk": preview,
|
|
311
|
+
},
|
|
312
|
+
)
|
|
313
|
+
continue
|
|
314
|
+
|
|
315
|
+
if (
|
|
316
|
+
event.type == "content_block_stop"
|
|
317
|
+
and event.index in tool_streams
|
|
318
|
+
):
|
|
319
|
+
info = tool_streams.pop(event.index)
|
|
320
|
+
preview_raw = "".join(info.get("buffer", []))
|
|
321
|
+
if preview_raw:
|
|
322
|
+
preview = (
|
|
323
|
+
preview_raw if len(preview_raw) <= 120 else preview_raw[:117] + "..."
|
|
324
|
+
)
|
|
325
|
+
self.logger.debug(
|
|
326
|
+
"Completed tool input stream",
|
|
327
|
+
data={
|
|
328
|
+
"tool_name": info.get("name"),
|
|
329
|
+
"tool_use_id": info.get("id"),
|
|
330
|
+
"input_preview": preview,
|
|
331
|
+
},
|
|
332
|
+
)
|
|
333
|
+
self._notify_tool_stream_listeners(
|
|
334
|
+
"stop",
|
|
335
|
+
{
|
|
336
|
+
"tool_name": info.get("name"),
|
|
337
|
+
"tool_use_id": info.get("id"),
|
|
338
|
+
"index": event.index,
|
|
339
|
+
"streams_arguments": False,
|
|
340
|
+
},
|
|
341
|
+
)
|
|
342
|
+
self.logger.info(
|
|
343
|
+
"Model finished streaming tool input",
|
|
344
|
+
data={
|
|
345
|
+
"progress_action": ProgressAction.CALLING_TOOL,
|
|
346
|
+
"agent_name": self.name,
|
|
347
|
+
"model": model,
|
|
348
|
+
"tool_name": info.get("name"),
|
|
349
|
+
"tool_use_id": info.get("id"),
|
|
350
|
+
"tool_event": "stop",
|
|
351
|
+
},
|
|
352
|
+
)
|
|
353
|
+
continue
|
|
354
|
+
|
|
251
355
|
# Count tokens in real-time from content_block_delta events
|
|
252
356
|
if (
|
|
253
357
|
event.type == "content_block_delta"
|
|
@@ -258,6 +362,14 @@ class AnthropicLLM(FastAgentLLM[MessageParam, Message]):
|
|
|
258
362
|
estimated_tokens = self._update_streaming_progress(
|
|
259
363
|
event.delta.text, model, estimated_tokens
|
|
260
364
|
)
|
|
365
|
+
self._notify_tool_stream_listeners(
|
|
366
|
+
"text",
|
|
367
|
+
{
|
|
368
|
+
"chunk": event.delta.text,
|
|
369
|
+
"index": event.index,
|
|
370
|
+
"streams_arguments": False,
|
|
371
|
+
},
|
|
372
|
+
)
|
|
261
373
|
|
|
262
374
|
# Also check for final message_delta events with actual usage info
|
|
263
375
|
elif (
|