fast-agent-mcp 0.3.14__py3-none-any.whl → 0.3.16__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 +52 -4
- fast_agent/agents/llm_decorator.py +6 -0
- fast_agent/agents/mcp_agent.py +137 -13
- fast_agent/agents/tool_agent.py +33 -19
- fast_agent/agents/workflow/router_agent.py +2 -1
- fast_agent/cli/__main__.py +35 -0
- fast_agent/cli/commands/check_config.py +90 -2
- fast_agent/cli/commands/go.py +100 -36
- fast_agent/cli/constants.py +13 -1
- fast_agent/cli/main.py +1 -0
- fast_agent/config.py +41 -12
- 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 +115 -2
- fast_agent/core/logging/listeners.py +8 -0
- fast_agent/core/validation.py +31 -33
- fast_agent/human_input/form_fields.py +4 -1
- fast_agent/interfaces.py +12 -1
- fast_agent/llm/fastagent_llm.py +76 -0
- fast_agent/llm/memory.py +26 -1
- fast_agent/llm/model_database.py +2 -2
- fast_agent/llm/model_factory.py +4 -1
- fast_agent/llm/provider/anthropic/llm_anthropic.py +112 -0
- fast_agent/llm/provider/openai/llm_openai.py +184 -18
- fast_agent/llm/provider/openai/responses.py +133 -0
- fast_agent/mcp/prompt_message_extended.py +2 -2
- fast_agent/resources/setup/agent.py +2 -0
- fast_agent/resources/setup/fastagent.config.yaml +11 -4
- fast_agent/skills/__init__.py +9 -0
- fast_agent/skills/registry.py +200 -0
- fast_agent/tools/shell_runtime.py +404 -0
- fast_agent/ui/console_display.py +925 -73
- fast_agent/ui/elicitation_form.py +98 -24
- fast_agent/ui/elicitation_style.py +2 -2
- fast_agent/ui/enhanced_prompt.py +128 -26
- fast_agent/ui/history_display.py +20 -5
- fast_agent/ui/interactive_prompt.py +108 -3
- fast_agent/ui/markdown_truncator.py +942 -0
- fast_agent/ui/mcp_display.py +2 -2
- fast_agent/ui/plain_text_truncator.py +68 -0
- fast_agent/ui/streaming_buffer.py +449 -0
- {fast_agent_mcp-0.3.14.dist-info → fast_agent_mcp-0.3.16.dist-info}/METADATA +9 -7
- {fast_agent_mcp-0.3.14.dist-info → fast_agent_mcp-0.3.16.dist-info}/RECORD +49 -42
- {fast_agent_mcp-0.3.14.dist-info → fast_agent_mcp-0.3.16.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.3.14.dist-info → fast_agent_mcp-0.3.16.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.3.14.dist-info → fast_agent_mcp-0.3.16.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,
|
|
@@ -293,7 +315,9 @@ class FastAgent:
|
|
|
293
315
|
default: bool = False,
|
|
294
316
|
elicitation_handler: Optional[ElicitationFnT] = None,
|
|
295
317
|
api_key: str | None = None,
|
|
296
|
-
) -> Callable[
|
|
318
|
+
) -> Callable[
|
|
319
|
+
[Callable[P, Coroutine[Any, Any, R]]], Callable[P, Coroutine[Any, Any, R]]
|
|
320
|
+
]: ...
|
|
297
321
|
|
|
298
322
|
def custom(
|
|
299
323
|
self,
|
|
@@ -428,6 +452,21 @@ class FastAgent:
|
|
|
428
452
|
with tracer.start_as_current_span(self.name):
|
|
429
453
|
try:
|
|
430
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
|
+
|
|
431
470
|
# Apply quiet mode if requested
|
|
432
471
|
if quiet_mode:
|
|
433
472
|
cfg = self.app.context.config
|
|
@@ -474,6 +513,17 @@ class FastAgent:
|
|
|
474
513
|
# Create a wrapper with all agents for simplified access
|
|
475
514
|
wrapper = AgentApp(active_agents)
|
|
476
515
|
|
|
516
|
+
# Disable streaming if parallel agents are present
|
|
517
|
+
from fast_agent.agents.agent_types import AgentType
|
|
518
|
+
|
|
519
|
+
has_parallel = any(
|
|
520
|
+
agent.agent_type == AgentType.PARALLEL for agent in active_agents.values()
|
|
521
|
+
)
|
|
522
|
+
if has_parallel:
|
|
523
|
+
cfg = self.app.context.config
|
|
524
|
+
if cfg is not None and cfg.logger is not None:
|
|
525
|
+
cfg.logger.streaming = "none"
|
|
526
|
+
|
|
477
527
|
# Handle command line options that should be processed after agent initialization
|
|
478
528
|
|
|
479
529
|
# Handle --server option
|
|
@@ -608,6 +658,69 @@ class FastAgent:
|
|
|
608
658
|
except Exception:
|
|
609
659
|
pass
|
|
610
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
|
+
|
|
611
724
|
def _handle_error(self, e: Exception, error_type: Optional[str] = None) -> None:
|
|
612
725
|
"""
|
|
613
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/core/validation.py
CHANGED
|
@@ -200,6 +200,34 @@ def get_dependencies(
|
|
|
200
200
|
return deps
|
|
201
201
|
|
|
202
202
|
|
|
203
|
+
def get_agent_dependencies(agent_data: dict[str, Any]) -> set[str]:
|
|
204
|
+
deps: set[str] = set()
|
|
205
|
+
agent_dependency_attribute_names = {
|
|
206
|
+
AgentType.CHAIN: ("sequence",),
|
|
207
|
+
AgentType.EVALUATOR_OPTIMIZER: ("evaluator", "generator", "eval_optimizer_agents"),
|
|
208
|
+
AgentType.ITERATIVE_PLANNER: ("child_agents",),
|
|
209
|
+
AgentType.ORCHESTRATOR: ("child_agents",),
|
|
210
|
+
AgentType.PARALLEL: ("fan_out", "fan_in", "parallel_agents"),
|
|
211
|
+
AgentType.ROUTER: ("router_agents",),
|
|
212
|
+
}
|
|
213
|
+
agent_type = agent_data["type"]
|
|
214
|
+
dependency_names = agent_dependency_attribute_names.get(agent_type, None)
|
|
215
|
+
if dependency_names is None:
|
|
216
|
+
return deps
|
|
217
|
+
|
|
218
|
+
for dependency_name in dependency_names:
|
|
219
|
+
dependency_value = agent_data.get(dependency_name)
|
|
220
|
+
if dependency_value is None:
|
|
221
|
+
continue
|
|
222
|
+
if isinstance(dependency_value, str):
|
|
223
|
+
deps.add(dependency_value)
|
|
224
|
+
else:
|
|
225
|
+
# here, we have an implicit assumption that if it is not a None or a string, then it is a list
|
|
226
|
+
deps.update(dependency_value)
|
|
227
|
+
|
|
228
|
+
return deps
|
|
229
|
+
|
|
230
|
+
|
|
203
231
|
def get_dependencies_groups(
|
|
204
232
|
agents_dict: Dict[str, Dict[str, Any]], allow_cycles: bool = False
|
|
205
233
|
) -> List[List[str]]:
|
|
@@ -221,39 +249,9 @@ def get_dependencies_groups(
|
|
|
221
249
|
agent_names = list(agents_dict.keys())
|
|
222
250
|
|
|
223
251
|
# Dictionary to store dependencies for each agent
|
|
224
|
-
dependencies = {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
for name, agent_data in agents_dict.items():
|
|
228
|
-
agent_type = agent_data["type"] # This is a string from config
|
|
229
|
-
|
|
230
|
-
# Note: Compare string values from config with the Enum's string value
|
|
231
|
-
if agent_type == AgentType.PARALLEL.value:
|
|
232
|
-
# Parallel agents depend on their fan-out and fan-in agents
|
|
233
|
-
dependencies[name].update(agent_data.get("parallel_agents", []))
|
|
234
|
-
# Also add explicit fan_out dependencies if present
|
|
235
|
-
if "fan_out" in agent_data:
|
|
236
|
-
dependencies[name].update(agent_data["fan_out"])
|
|
237
|
-
# Add explicit fan_in dependency if present
|
|
238
|
-
if "fan_in" in agent_data and agent_data["fan_in"]:
|
|
239
|
-
dependencies[name].add(agent_data["fan_in"])
|
|
240
|
-
elif agent_type == AgentType.CHAIN.value:
|
|
241
|
-
# Chain agents depend on the agents in their sequence
|
|
242
|
-
dependencies[name].update(agent_data.get("sequence", []))
|
|
243
|
-
elif agent_type == AgentType.ROUTER.value:
|
|
244
|
-
# Router agents depend on the agents they route to
|
|
245
|
-
dependencies[name].update(agent_data.get("router_agents", []))
|
|
246
|
-
elif agent_type == AgentType.ORCHESTRATOR.value:
|
|
247
|
-
# Orchestrator agents depend on their child agents
|
|
248
|
-
dependencies[name].update(agent_data.get("child_agents", []))
|
|
249
|
-
elif agent_type == AgentType.EVALUATOR_OPTIMIZER.value:
|
|
250
|
-
# Evaluator-Optimizer agents depend on their evaluator and generator agents
|
|
251
|
-
if "evaluator" in agent_data:
|
|
252
|
-
dependencies[name].add(agent_data["evaluator"])
|
|
253
|
-
if "generator" in agent_data:
|
|
254
|
-
dependencies[name].add(agent_data["generator"])
|
|
255
|
-
# For backward compatibility - also check eval_optimizer_agents if present
|
|
256
|
-
dependencies[name].update(agent_data.get("eval_optimizer_agents", []))
|
|
252
|
+
dependencies = {
|
|
253
|
+
name: get_agent_dependencies(agent_data) for name, agent_data in agents_dict.items()
|
|
254
|
+
}
|
|
257
255
|
|
|
258
256
|
# Check for cycles if not allowed
|
|
259
257
|
if not allow_cycles:
|
|
@@ -29,6 +29,8 @@ class StringField:
|
|
|
29
29
|
schema["minLength"] = self.min_length
|
|
30
30
|
if self.max_length is not None:
|
|
31
31
|
schema["maxLength"] = self.max_length
|
|
32
|
+
if self.pattern is not None:
|
|
33
|
+
schema["pattern"] = self.pattern
|
|
32
34
|
if self.format:
|
|
33
35
|
schema["format"] = self.format
|
|
34
36
|
|
|
@@ -178,10 +180,11 @@ def string(
|
|
|
178
180
|
default: Optional[str] = None,
|
|
179
181
|
min_length: Optional[int] = None,
|
|
180
182
|
max_length: Optional[int] = None,
|
|
183
|
+
pattern: Optional[str] = None,
|
|
181
184
|
format: Optional[str] = None,
|
|
182
185
|
) -> StringField:
|
|
183
186
|
"""Create a string field."""
|
|
184
|
-
return StringField(title, description, default, min_length, max_length, format)
|
|
187
|
+
return StringField(title, description, default, min_length, max_length, pattern, format)
|
|
185
188
|
|
|
186
189
|
|
|
187
190
|
def email(
|
fast_agent/interfaces.py
CHANGED
|
@@ -8,6 +8,7 @@ without pulling in MCP-specific code, helping to avoid circular imports.
|
|
|
8
8
|
from typing import (
|
|
9
9
|
TYPE_CHECKING,
|
|
10
10
|
Any,
|
|
11
|
+
Callable,
|
|
11
12
|
Dict,
|
|
12
13
|
List,
|
|
13
14
|
Mapping,
|
|
@@ -83,10 +84,18 @@ class FastAgentLLMProtocol(Protocol):
|
|
|
83
84
|
self,
|
|
84
85
|
request_params: RequestParams | None = None,
|
|
85
86
|
) -> RequestParams: ...
|
|
86
|
-
|
|
87
|
+
|
|
88
|
+
def add_stream_listener(self, listener: Callable[[str], None]) -> Callable[[], None]: ...
|
|
89
|
+
|
|
90
|
+
def add_tool_stream_listener(
|
|
91
|
+
self, listener: Callable[[str, Dict[str, Any] | None], None]
|
|
92
|
+
) -> Callable[[], None]: ...
|
|
93
|
+
|
|
87
94
|
@property
|
|
88
95
|
def message_history(self) -> List[PromptMessageExtended]: ...
|
|
89
96
|
|
|
97
|
+
def pop_last_message(self) -> PromptMessageExtended | None: ...
|
|
98
|
+
|
|
90
99
|
@property
|
|
91
100
|
def usage_accumulator(self) -> UsageAccumulator | None: ...
|
|
92
101
|
|
|
@@ -120,6 +129,8 @@ class LlmAgentProtocol(Protocol):
|
|
|
120
129
|
|
|
121
130
|
def clear(self, *, clear_prompts: bool = False) -> None: ...
|
|
122
131
|
|
|
132
|
+
def pop_last_message(self) -> PromptMessageExtended | None: ...
|
|
133
|
+
|
|
123
134
|
|
|
124
135
|
class AgentProtocol(LlmAgentProtocol, Protocol):
|
|
125
136
|
"""Standard agent interface with flexible input types."""
|
fast_agent/llm/fastagent_llm.py
CHANGED
|
@@ -3,6 +3,7 @@ from contextvars import ContextVar
|
|
|
3
3
|
from typing import (
|
|
4
4
|
TYPE_CHECKING,
|
|
5
5
|
Any,
|
|
6
|
+
Callable,
|
|
6
7
|
Dict,
|
|
7
8
|
Generic,
|
|
8
9
|
List,
|
|
@@ -157,6 +158,8 @@ class FastAgentLLM(ContextDependent, FastAgentLLMProtocol, Generic[MessageParamT
|
|
|
157
158
|
|
|
158
159
|
# Initialize usage tracking
|
|
159
160
|
self._usage_accumulator = UsageAccumulator()
|
|
161
|
+
self._stream_listeners: set[Callable[[str], None]] = set()
|
|
162
|
+
self._tool_stream_listeners: set[Callable[[str, Dict[str, Any] | None], None]] = set()
|
|
160
163
|
|
|
161
164
|
def _initialize_default_params(self, kwargs: dict) -> RequestParams:
|
|
162
165
|
"""Initialize default parameters for the LLM.
|
|
@@ -483,6 +486,8 @@ class FastAgentLLM(ContextDependent, FastAgentLLMProtocol, Generic[MessageParamT
|
|
|
483
486
|
Returns:
|
|
484
487
|
Updated estimated token count
|
|
485
488
|
"""
|
|
489
|
+
self._notify_stream_listeners(content)
|
|
490
|
+
|
|
486
491
|
# Rough estimate: 1 token per 4 characters (OpenAI's typical ratio)
|
|
487
492
|
text_length = len(content)
|
|
488
493
|
additional_tokens = max(1, text_length // 4)
|
|
@@ -503,6 +508,64 @@ class FastAgentLLM(ContextDependent, FastAgentLLMProtocol, Generic[MessageParamT
|
|
|
503
508
|
|
|
504
509
|
return new_total
|
|
505
510
|
|
|
511
|
+
def add_stream_listener(self, listener: Callable[[str], None]) -> Callable[[], None]:
|
|
512
|
+
"""
|
|
513
|
+
Register a callback invoked with streaming text chunks.
|
|
514
|
+
|
|
515
|
+
Args:
|
|
516
|
+
listener: Callable receiving the text chunk emitted by the provider.
|
|
517
|
+
|
|
518
|
+
Returns:
|
|
519
|
+
A function that removes the listener when called.
|
|
520
|
+
"""
|
|
521
|
+
self._stream_listeners.add(listener)
|
|
522
|
+
|
|
523
|
+
def remove() -> None:
|
|
524
|
+
self._stream_listeners.discard(listener)
|
|
525
|
+
|
|
526
|
+
return remove
|
|
527
|
+
|
|
528
|
+
def _notify_stream_listeners(self, chunk: str) -> None:
|
|
529
|
+
"""Notify registered listeners with a streaming text chunk."""
|
|
530
|
+
if not chunk:
|
|
531
|
+
return
|
|
532
|
+
for listener in list(self._stream_listeners):
|
|
533
|
+
try:
|
|
534
|
+
listener(chunk)
|
|
535
|
+
except Exception:
|
|
536
|
+
self.logger.exception("Stream listener raised an exception")
|
|
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
|
+
|
|
506
569
|
def _log_chat_finished(self, model: Optional[str] = None) -> None:
|
|
507
570
|
"""Log a chat finished event"""
|
|
508
571
|
data = {
|
|
@@ -612,6 +675,19 @@ class FastAgentLLM(ContextDependent, FastAgentLLMProtocol, Generic[MessageParamT
|
|
|
612
675
|
"""
|
|
613
676
|
return self._message_history
|
|
614
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
|
+
|
|
615
691
|
def clear(self, *, clear_prompts: bool = False) -> None:
|
|
616
692
|
"""Reset stored message history while optionally retaining prompt templates."""
|
|
617
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
|
@@ -130,11 +130,9 @@ class ModelDatabase:
|
|
|
130
130
|
context_window=400000, max_output_tokens=128000, tokenizes=OPENAI_MULTIMODAL
|
|
131
131
|
)
|
|
132
132
|
|
|
133
|
-
# TODO update to 32000
|
|
134
133
|
ANTHROPIC_OPUS_4_VERSIONED = ModelParameters(
|
|
135
134
|
context_window=200000, max_output_tokens=32000, tokenizes=ANTHROPIC_MULTIMODAL
|
|
136
135
|
)
|
|
137
|
-
# TODO update to 64000
|
|
138
136
|
ANTHROPIC_SONNET_4_VERSIONED = ModelParameters(
|
|
139
137
|
context_window=200000, max_output_tokens=64000, tokenizes=ANTHROPIC_MULTIMODAL
|
|
140
138
|
)
|
|
@@ -237,6 +235,8 @@ class ModelDatabase:
|
|
|
237
235
|
"claude-opus-4-0": ANTHROPIC_OPUS_4_VERSIONED,
|
|
238
236
|
"claude-opus-4-1": ANTHROPIC_OPUS_4_VERSIONED,
|
|
239
237
|
"claude-opus-4-20250514": ANTHROPIC_OPUS_4_VERSIONED,
|
|
238
|
+
"claude-haiku-4-5-20251001": ANTHROPIC_SONNET_4_VERSIONED,
|
|
239
|
+
"claude-haiku-4-5": ANTHROPIC_SONNET_4_VERSIONED,
|
|
240
240
|
# DeepSeek Models
|
|
241
241
|
"deepseek-chat": DEEPSEEK_CHAT_STANDARD,
|
|
242
242
|
# Google Gemini Models (vanilla aliases and versioned)
|
fast_agent/llm/model_factory.py
CHANGED
|
@@ -86,6 +86,7 @@ class ModelFactory:
|
|
|
86
86
|
"claude-sonnet-4-0": Provider.ANTHROPIC,
|
|
87
87
|
"claude-sonnet-4-5-20250929": Provider.ANTHROPIC,
|
|
88
88
|
"claude-sonnet-4-5": Provider.ANTHROPIC,
|
|
89
|
+
"claude-haiku-4-5": Provider.ANTHROPIC,
|
|
89
90
|
"deepseek-chat": Provider.DEEPSEEK,
|
|
90
91
|
"gemini-2.0-flash": Provider.GOOGLE,
|
|
91
92
|
"gemini-2.5-flash-preview-05-20": Provider.GOOGLE,
|
|
@@ -109,9 +110,10 @@ class ModelFactory:
|
|
|
109
110
|
"sonnet35": "claude-3-5-sonnet-latest",
|
|
110
111
|
"sonnet37": "claude-3-7-sonnet-latest",
|
|
111
112
|
"claude": "claude-sonnet-4-0",
|
|
112
|
-
"haiku": "claude-
|
|
113
|
+
"haiku": "claude-haiku-4-5",
|
|
113
114
|
"haiku3": "claude-3-haiku-20240307",
|
|
114
115
|
"haiku35": "claude-3-5-haiku-latest",
|
|
116
|
+
"hauku45": "claude-haiku-4-5",
|
|
115
117
|
"opus": "claude-opus-4-1",
|
|
116
118
|
"opus4": "claude-opus-4-1",
|
|
117
119
|
"opus3": "claude-3-opus-latest",
|
|
@@ -319,6 +321,7 @@ class ModelFactory:
|
|
|
319
321
|
return GroqLLM
|
|
320
322
|
if provider == Provider.RESPONSES:
|
|
321
323
|
from fast_agent.llm.provider.openai.responses import ResponsesLLM
|
|
324
|
+
|
|
322
325
|
return ResponsesLLM
|
|
323
326
|
|
|
324
327
|
except Exception as e:
|