fast-agent-mcp 0.4.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fast_agent/__init__.py +183 -0
- fast_agent/acp/__init__.py +19 -0
- fast_agent/acp/acp_aware_mixin.py +304 -0
- fast_agent/acp/acp_context.py +437 -0
- fast_agent/acp/content_conversion.py +136 -0
- fast_agent/acp/filesystem_runtime.py +427 -0
- fast_agent/acp/permission_store.py +269 -0
- fast_agent/acp/server/__init__.py +5 -0
- fast_agent/acp/server/agent_acp_server.py +1472 -0
- fast_agent/acp/slash_commands.py +1050 -0
- fast_agent/acp/terminal_runtime.py +408 -0
- fast_agent/acp/tool_permission_adapter.py +125 -0
- fast_agent/acp/tool_permissions.py +474 -0
- fast_agent/acp/tool_progress.py +814 -0
- fast_agent/agents/__init__.py +85 -0
- fast_agent/agents/agent_types.py +64 -0
- fast_agent/agents/llm_agent.py +350 -0
- fast_agent/agents/llm_decorator.py +1139 -0
- fast_agent/agents/mcp_agent.py +1337 -0
- fast_agent/agents/tool_agent.py +271 -0
- fast_agent/agents/workflow/agents_as_tools_agent.py +849 -0
- fast_agent/agents/workflow/chain_agent.py +212 -0
- fast_agent/agents/workflow/evaluator_optimizer.py +380 -0
- fast_agent/agents/workflow/iterative_planner.py +652 -0
- fast_agent/agents/workflow/maker_agent.py +379 -0
- fast_agent/agents/workflow/orchestrator_models.py +218 -0
- fast_agent/agents/workflow/orchestrator_prompts.py +248 -0
- fast_agent/agents/workflow/parallel_agent.py +250 -0
- fast_agent/agents/workflow/router_agent.py +353 -0
- fast_agent/cli/__init__.py +0 -0
- fast_agent/cli/__main__.py +73 -0
- fast_agent/cli/commands/acp.py +159 -0
- fast_agent/cli/commands/auth.py +404 -0
- fast_agent/cli/commands/check_config.py +783 -0
- fast_agent/cli/commands/go.py +514 -0
- fast_agent/cli/commands/quickstart.py +557 -0
- fast_agent/cli/commands/serve.py +143 -0
- fast_agent/cli/commands/server_helpers.py +114 -0
- fast_agent/cli/commands/setup.py +174 -0
- fast_agent/cli/commands/url_parser.py +190 -0
- fast_agent/cli/constants.py +40 -0
- fast_agent/cli/main.py +115 -0
- fast_agent/cli/terminal.py +24 -0
- fast_agent/config.py +798 -0
- fast_agent/constants.py +41 -0
- fast_agent/context.py +279 -0
- fast_agent/context_dependent.py +50 -0
- fast_agent/core/__init__.py +92 -0
- fast_agent/core/agent_app.py +448 -0
- fast_agent/core/core_app.py +137 -0
- fast_agent/core/direct_decorators.py +784 -0
- fast_agent/core/direct_factory.py +620 -0
- fast_agent/core/error_handling.py +27 -0
- fast_agent/core/exceptions.py +90 -0
- fast_agent/core/executor/__init__.py +0 -0
- fast_agent/core/executor/executor.py +280 -0
- fast_agent/core/executor/task_registry.py +32 -0
- fast_agent/core/executor/workflow_signal.py +324 -0
- fast_agent/core/fastagent.py +1186 -0
- fast_agent/core/logging/__init__.py +5 -0
- fast_agent/core/logging/events.py +138 -0
- fast_agent/core/logging/json_serializer.py +164 -0
- fast_agent/core/logging/listeners.py +309 -0
- fast_agent/core/logging/logger.py +278 -0
- fast_agent/core/logging/transport.py +481 -0
- fast_agent/core/prompt.py +9 -0
- fast_agent/core/prompt_templates.py +183 -0
- fast_agent/core/validation.py +326 -0
- fast_agent/event_progress.py +62 -0
- fast_agent/history/history_exporter.py +49 -0
- fast_agent/human_input/__init__.py +47 -0
- fast_agent/human_input/elicitation_handler.py +123 -0
- fast_agent/human_input/elicitation_state.py +33 -0
- fast_agent/human_input/form_elements.py +59 -0
- fast_agent/human_input/form_fields.py +256 -0
- fast_agent/human_input/simple_form.py +113 -0
- fast_agent/human_input/types.py +40 -0
- fast_agent/interfaces.py +310 -0
- fast_agent/llm/__init__.py +9 -0
- fast_agent/llm/cancellation.py +22 -0
- fast_agent/llm/fastagent_llm.py +931 -0
- fast_agent/llm/internal/passthrough.py +161 -0
- fast_agent/llm/internal/playback.py +129 -0
- fast_agent/llm/internal/silent.py +41 -0
- fast_agent/llm/internal/slow.py +38 -0
- fast_agent/llm/memory.py +275 -0
- fast_agent/llm/model_database.py +490 -0
- fast_agent/llm/model_factory.py +388 -0
- fast_agent/llm/model_info.py +102 -0
- fast_agent/llm/prompt_utils.py +155 -0
- fast_agent/llm/provider/anthropic/anthropic_utils.py +84 -0
- fast_agent/llm/provider/anthropic/cache_planner.py +56 -0
- fast_agent/llm/provider/anthropic/llm_anthropic.py +796 -0
- fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +462 -0
- fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
- fast_agent/llm/provider/bedrock/llm_bedrock.py +2207 -0
- fast_agent/llm/provider/bedrock/multipart_converter_bedrock.py +84 -0
- fast_agent/llm/provider/google/google_converter.py +466 -0
- fast_agent/llm/provider/google/llm_google_native.py +681 -0
- fast_agent/llm/provider/openai/llm_aliyun.py +31 -0
- fast_agent/llm/provider/openai/llm_azure.py +143 -0
- fast_agent/llm/provider/openai/llm_deepseek.py +76 -0
- fast_agent/llm/provider/openai/llm_generic.py +35 -0
- fast_agent/llm/provider/openai/llm_google_oai.py +32 -0
- fast_agent/llm/provider/openai/llm_groq.py +42 -0
- fast_agent/llm/provider/openai/llm_huggingface.py +85 -0
- fast_agent/llm/provider/openai/llm_openai.py +1195 -0
- fast_agent/llm/provider/openai/llm_openai_compatible.py +138 -0
- fast_agent/llm/provider/openai/llm_openrouter.py +45 -0
- fast_agent/llm/provider/openai/llm_tensorzero_openai.py +128 -0
- fast_agent/llm/provider/openai/llm_xai.py +38 -0
- fast_agent/llm/provider/openai/multipart_converter_openai.py +561 -0
- fast_agent/llm/provider/openai/openai_multipart.py +169 -0
- fast_agent/llm/provider/openai/openai_utils.py +67 -0
- fast_agent/llm/provider/openai/responses.py +133 -0
- fast_agent/llm/provider_key_manager.py +139 -0
- fast_agent/llm/provider_types.py +34 -0
- fast_agent/llm/request_params.py +61 -0
- fast_agent/llm/sampling_converter.py +98 -0
- fast_agent/llm/stream_types.py +9 -0
- fast_agent/llm/usage_tracking.py +445 -0
- fast_agent/mcp/__init__.py +56 -0
- fast_agent/mcp/common.py +26 -0
- fast_agent/mcp/elicitation_factory.py +84 -0
- fast_agent/mcp/elicitation_handlers.py +164 -0
- fast_agent/mcp/gen_client.py +83 -0
- fast_agent/mcp/helpers/__init__.py +36 -0
- fast_agent/mcp/helpers/content_helpers.py +352 -0
- fast_agent/mcp/helpers/server_config_helpers.py +25 -0
- fast_agent/mcp/hf_auth.py +147 -0
- fast_agent/mcp/interfaces.py +92 -0
- fast_agent/mcp/logger_textio.py +108 -0
- fast_agent/mcp/mcp_agent_client_session.py +411 -0
- fast_agent/mcp/mcp_aggregator.py +2175 -0
- fast_agent/mcp/mcp_connection_manager.py +723 -0
- fast_agent/mcp/mcp_content.py +262 -0
- fast_agent/mcp/mime_utils.py +108 -0
- fast_agent/mcp/oauth_client.py +509 -0
- fast_agent/mcp/prompt.py +159 -0
- fast_agent/mcp/prompt_message_extended.py +155 -0
- fast_agent/mcp/prompt_render.py +84 -0
- fast_agent/mcp/prompt_serialization.py +580 -0
- fast_agent/mcp/prompts/__init__.py +0 -0
- fast_agent/mcp/prompts/__main__.py +7 -0
- fast_agent/mcp/prompts/prompt_constants.py +18 -0
- fast_agent/mcp/prompts/prompt_helpers.py +238 -0
- fast_agent/mcp/prompts/prompt_load.py +186 -0
- fast_agent/mcp/prompts/prompt_server.py +552 -0
- fast_agent/mcp/prompts/prompt_template.py +438 -0
- fast_agent/mcp/resource_utils.py +215 -0
- fast_agent/mcp/sampling.py +200 -0
- fast_agent/mcp/server/__init__.py +4 -0
- fast_agent/mcp/server/agent_server.py +613 -0
- fast_agent/mcp/skybridge.py +44 -0
- fast_agent/mcp/sse_tracking.py +287 -0
- fast_agent/mcp/stdio_tracking_simple.py +59 -0
- fast_agent/mcp/streamable_http_tracking.py +309 -0
- fast_agent/mcp/tool_execution_handler.py +137 -0
- fast_agent/mcp/tool_permission_handler.py +88 -0
- fast_agent/mcp/transport_tracking.py +634 -0
- fast_agent/mcp/types.py +24 -0
- fast_agent/mcp/ui_agent.py +48 -0
- fast_agent/mcp/ui_mixin.py +209 -0
- fast_agent/mcp_server_registry.py +89 -0
- fast_agent/py.typed +0 -0
- fast_agent/resources/examples/data-analysis/analysis-campaign.py +189 -0
- fast_agent/resources/examples/data-analysis/analysis.py +68 -0
- fast_agent/resources/examples/data-analysis/fastagent.config.yaml +41 -0
- fast_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +1471 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +297 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
- fast_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
- fast_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
- fast_agent/resources/examples/mcp/elicitations/forms_demo.py +107 -0
- fast_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
- fast_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
- fast_agent/resources/examples/mcp/elicitations/tool_call.py +21 -0
- fast_agent/resources/examples/mcp/state-transfer/agent_one.py +18 -0
- fast_agent/resources/examples/mcp/state-transfer/agent_two.py +18 -0
- fast_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +27 -0
- fast_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +15 -0
- fast_agent/resources/examples/researcher/fastagent.config.yaml +61 -0
- fast_agent/resources/examples/researcher/researcher-eval.py +53 -0
- fast_agent/resources/examples/researcher/researcher-imp.py +189 -0
- fast_agent/resources/examples/researcher/researcher.py +36 -0
- fast_agent/resources/examples/tensorzero/.env.sample +2 -0
- fast_agent/resources/examples/tensorzero/Makefile +31 -0
- fast_agent/resources/examples/tensorzero/README.md +56 -0
- fast_agent/resources/examples/tensorzero/agent.py +35 -0
- fast_agent/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
- fast_agent/resources/examples/tensorzero/demo_images/crab.png +0 -0
- fast_agent/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
- fast_agent/resources/examples/tensorzero/docker-compose.yml +105 -0
- fast_agent/resources/examples/tensorzero/fastagent.config.yaml +19 -0
- fast_agent/resources/examples/tensorzero/image_demo.py +67 -0
- fast_agent/resources/examples/tensorzero/mcp_server/Dockerfile +25 -0
- fast_agent/resources/examples/tensorzero/mcp_server/entrypoint.sh +35 -0
- fast_agent/resources/examples/tensorzero/mcp_server/mcp_server.py +31 -0
- fast_agent/resources/examples/tensorzero/mcp_server/pyproject.toml +11 -0
- fast_agent/resources/examples/tensorzero/simple_agent.py +25 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/system_schema.json +29 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +11 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +35 -0
- fast_agent/resources/examples/workflows/agents_as_tools_extended.py +73 -0
- fast_agent/resources/examples/workflows/agents_as_tools_simple.py +50 -0
- fast_agent/resources/examples/workflows/chaining.py +37 -0
- fast_agent/resources/examples/workflows/evaluator.py +77 -0
- fast_agent/resources/examples/workflows/fastagent.config.yaml +26 -0
- fast_agent/resources/examples/workflows/graded_report.md +89 -0
- fast_agent/resources/examples/workflows/human_input.py +28 -0
- fast_agent/resources/examples/workflows/maker.py +156 -0
- fast_agent/resources/examples/workflows/orchestrator.py +70 -0
- fast_agent/resources/examples/workflows/parallel.py +56 -0
- fast_agent/resources/examples/workflows/router.py +69 -0
- fast_agent/resources/examples/workflows/short_story.md +13 -0
- fast_agent/resources/examples/workflows/short_story.txt +19 -0
- fast_agent/resources/setup/.gitignore +30 -0
- fast_agent/resources/setup/agent.py +28 -0
- fast_agent/resources/setup/fastagent.config.yaml +65 -0
- fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
- fast_agent/resources/setup/pyproject.toml.tmpl +23 -0
- fast_agent/skills/__init__.py +9 -0
- fast_agent/skills/registry.py +235 -0
- fast_agent/tools/elicitation.py +369 -0
- fast_agent/tools/shell_runtime.py +402 -0
- fast_agent/types/__init__.py +59 -0
- fast_agent/types/conversation_summary.py +294 -0
- fast_agent/types/llm_stop_reason.py +78 -0
- fast_agent/types/message_search.py +249 -0
- fast_agent/ui/__init__.py +38 -0
- fast_agent/ui/console.py +59 -0
- fast_agent/ui/console_display.py +1080 -0
- fast_agent/ui/elicitation_form.py +946 -0
- fast_agent/ui/elicitation_style.py +59 -0
- fast_agent/ui/enhanced_prompt.py +1400 -0
- fast_agent/ui/history_display.py +734 -0
- fast_agent/ui/interactive_prompt.py +1199 -0
- fast_agent/ui/markdown_helpers.py +104 -0
- fast_agent/ui/markdown_truncator.py +1004 -0
- fast_agent/ui/mcp_display.py +857 -0
- fast_agent/ui/mcp_ui_utils.py +235 -0
- fast_agent/ui/mermaid_utils.py +169 -0
- fast_agent/ui/message_primitives.py +50 -0
- fast_agent/ui/notification_tracker.py +205 -0
- fast_agent/ui/plain_text_truncator.py +68 -0
- fast_agent/ui/progress_display.py +10 -0
- fast_agent/ui/rich_progress.py +195 -0
- fast_agent/ui/streaming.py +774 -0
- fast_agent/ui/streaming_buffer.py +449 -0
- fast_agent/ui/tool_display.py +422 -0
- fast_agent/ui/usage_display.py +204 -0
- fast_agent/utils/__init__.py +5 -0
- fast_agent/utils/reasoning_stream_parser.py +77 -0
- fast_agent/utils/time.py +22 -0
- fast_agent/workflow_telemetry.py +261 -0
- fast_agent_mcp-0.4.7.dist-info/METADATA +788 -0
- fast_agent_mcp-0.4.7.dist-info/RECORD +261 -0
- fast_agent_mcp-0.4.7.dist-info/WHEEL +4 -0
- fast_agent_mcp-0.4.7.dist-info/entry_points.txt +7 -0
- fast_agent_mcp-0.4.7.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,931 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import inspect
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
from abc import abstractmethod
|
|
7
|
+
from contextvars import ContextVar
|
|
8
|
+
from typing import (
|
|
9
|
+
TYPE_CHECKING,
|
|
10
|
+
Any,
|
|
11
|
+
Awaitable,
|
|
12
|
+
Callable,
|
|
13
|
+
Generic,
|
|
14
|
+
Type,
|
|
15
|
+
TypeVar,
|
|
16
|
+
Union,
|
|
17
|
+
cast,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from mcp import Tool
|
|
21
|
+
from mcp.types import (
|
|
22
|
+
GetPromptResult,
|
|
23
|
+
PromptMessage,
|
|
24
|
+
TextContent,
|
|
25
|
+
)
|
|
26
|
+
from openai import NotGiven
|
|
27
|
+
from openai.lib._parsing import type_to_response_format_param as _type_to_response_format
|
|
28
|
+
from pydantic_core import from_json
|
|
29
|
+
from rich import print as rich_print
|
|
30
|
+
|
|
31
|
+
from fast_agent.constants import (
|
|
32
|
+
CONTROL_MESSAGE_SAVE_HISTORY,
|
|
33
|
+
DEFAULT_MAX_ITERATIONS,
|
|
34
|
+
FAST_AGENT_TIMING,
|
|
35
|
+
)
|
|
36
|
+
from fast_agent.context_dependent import ContextDependent
|
|
37
|
+
from fast_agent.core.exceptions import AgentConfigError, ProviderKeyError, ServerConfigError
|
|
38
|
+
from fast_agent.core.logging.logger import get_logger
|
|
39
|
+
from fast_agent.core.prompt import Prompt
|
|
40
|
+
from fast_agent.event_progress import ProgressAction
|
|
41
|
+
from fast_agent.interfaces import (
|
|
42
|
+
FastAgentLLMProtocol,
|
|
43
|
+
ModelT,
|
|
44
|
+
)
|
|
45
|
+
from fast_agent.llm.memory import Memory, SimpleMemory
|
|
46
|
+
from fast_agent.llm.model_database import ModelDatabase
|
|
47
|
+
from fast_agent.llm.provider_types import Provider
|
|
48
|
+
from fast_agent.llm.stream_types import StreamChunk
|
|
49
|
+
from fast_agent.llm.usage_tracking import TurnUsage, UsageAccumulator
|
|
50
|
+
from fast_agent.mcp.helpers.content_helpers import get_text
|
|
51
|
+
from fast_agent.types import PromptMessageExtended, RequestParams
|
|
52
|
+
|
|
53
|
+
# Define type variables locally
|
|
54
|
+
MessageParamT = TypeVar("MessageParamT")
|
|
55
|
+
MessageT = TypeVar("MessageT")
|
|
56
|
+
|
|
57
|
+
# Forward reference for type annotations
|
|
58
|
+
if TYPE_CHECKING:
|
|
59
|
+
from fast_agent.context import Context
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# Context variable for storing MCP metadata
|
|
63
|
+
_mcp_metadata_var: ContextVar[dict[str, Any] | None] = ContextVar("mcp_metadata", default=None)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def deep_merge(dict1: dict[Any, Any], dict2: dict[Any, Any]) -> dict[Any, Any]:
|
|
67
|
+
"""
|
|
68
|
+
Recursively merges `dict2` into `dict1` in place.
|
|
69
|
+
|
|
70
|
+
If a key exists in both dictionaries and their values are dictionaries,
|
|
71
|
+
the function merges them recursively. Otherwise, the value from `dict2`
|
|
72
|
+
overwrites or is added to `dict1`.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
dict1 (Dict): The dictionary to be updated.
|
|
76
|
+
dict2 (Dict): The dictionary to merge into `dict1`.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Dict: The updated `dict1`.
|
|
80
|
+
"""
|
|
81
|
+
for key in dict2:
|
|
82
|
+
if key in dict1 and isinstance(dict1[key], dict) and isinstance(dict2[key], dict):
|
|
83
|
+
deep_merge(dict1[key], dict2[key])
|
|
84
|
+
else:
|
|
85
|
+
dict1[key] = dict2[key]
|
|
86
|
+
return dict1
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class FastAgentLLM(ContextDependent, FastAgentLLMProtocol, Generic[MessageParamT, MessageT]):
|
|
90
|
+
# Common parameter names used across providers
|
|
91
|
+
PARAM_MESSAGES = "messages"
|
|
92
|
+
PARAM_MODEL = "model"
|
|
93
|
+
PARAM_MAX_TOKENS = "maxTokens"
|
|
94
|
+
PARAM_SYSTEM_PROMPT = "systemPrompt"
|
|
95
|
+
PARAM_STOP_SEQUENCES = "stopSequences"
|
|
96
|
+
PARAM_PARALLEL_TOOL_CALLS = "parallel_tool_calls"
|
|
97
|
+
PARAM_METADATA = "metadata"
|
|
98
|
+
PARAM_USE_HISTORY = "use_history"
|
|
99
|
+
PARAM_MAX_ITERATIONS = "max_iterations"
|
|
100
|
+
PARAM_TEMPLATE_VARS = "template_vars"
|
|
101
|
+
PARAM_MCP_METADATA = "mcp_metadata"
|
|
102
|
+
|
|
103
|
+
# Base set of fields that should always be excluded
|
|
104
|
+
BASE_EXCLUDE_FIELDS = {PARAM_METADATA}
|
|
105
|
+
|
|
106
|
+
"""
|
|
107
|
+
Implementation of the Llm Protocol - intended be subclassed for Provider
|
|
108
|
+
or behaviour specific reasons. Contains convenience and template methods.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(
|
|
112
|
+
self,
|
|
113
|
+
provider: Provider,
|
|
114
|
+
instruction: str | None = None,
|
|
115
|
+
name: str | None = None,
|
|
116
|
+
request_params: RequestParams | None = None,
|
|
117
|
+
context: Union["Context", None] = None,
|
|
118
|
+
model: str | None = None,
|
|
119
|
+
api_key: str | None = None,
|
|
120
|
+
**kwargs: dict[str, Any],
|
|
121
|
+
) -> None:
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
provider: LLM API Provider
|
|
126
|
+
instruction: System prompt for the LLM
|
|
127
|
+
name: Name for the LLM (usually attached Agent name)
|
|
128
|
+
request_params: RequestParams to configure LLM behaviour
|
|
129
|
+
context: Application context
|
|
130
|
+
model: Optional model name override
|
|
131
|
+
**kwargs: Additional provider-specific parameters
|
|
132
|
+
"""
|
|
133
|
+
# Extract request_params before super() call
|
|
134
|
+
self._init_request_params = request_params
|
|
135
|
+
super().__init__(context=context, **kwargs)
|
|
136
|
+
self.logger = get_logger(__name__)
|
|
137
|
+
self.executor = self.context.executor
|
|
138
|
+
self.name: str = name or "fast-agent"
|
|
139
|
+
self.instruction = instruction
|
|
140
|
+
self._provider = provider
|
|
141
|
+
# memory contains provider specific API types.
|
|
142
|
+
self.history: Memory[MessageParamT] = SimpleMemory[MessageParamT]()
|
|
143
|
+
|
|
144
|
+
# Initialize the display component
|
|
145
|
+
from fast_agent.ui.console_display import ConsoleDisplay
|
|
146
|
+
|
|
147
|
+
self.display = ConsoleDisplay(config=self.context.config)
|
|
148
|
+
|
|
149
|
+
# Initialize default parameters, passing model info
|
|
150
|
+
model_kwargs = kwargs.copy()
|
|
151
|
+
if model:
|
|
152
|
+
model_kwargs["model"] = model
|
|
153
|
+
self.default_request_params = self._initialize_default_params(model_kwargs)
|
|
154
|
+
|
|
155
|
+
# Merge with provided params if any
|
|
156
|
+
if self._init_request_params:
|
|
157
|
+
self.default_request_params = self._merge_request_params(
|
|
158
|
+
self.default_request_params, self._init_request_params
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Cache effective model name for type-safe access
|
|
162
|
+
self._model_name: str | None = self.default_request_params.model
|
|
163
|
+
|
|
164
|
+
self.verb = kwargs.get("verb")
|
|
165
|
+
|
|
166
|
+
self._init_api_key = api_key
|
|
167
|
+
|
|
168
|
+
# Initialize usage tracking
|
|
169
|
+
self._usage_accumulator = UsageAccumulator()
|
|
170
|
+
self._stream_listeners: set[Callable[[StreamChunk], None]] = set()
|
|
171
|
+
self._tool_stream_listeners: set[Callable[[str, dict[str, Any] | None], None]] = set()
|
|
172
|
+
self.retry_count = self._resolve_retry_count()
|
|
173
|
+
self.retry_backoff_seconds: float = 10.0
|
|
174
|
+
|
|
175
|
+
def _initialize_default_params(self, kwargs: dict) -> RequestParams:
|
|
176
|
+
"""Initialize default parameters for the LLM.
|
|
177
|
+
Should be overridden by provider implementations to set provider-specific defaults."""
|
|
178
|
+
# Get model-aware default max tokens
|
|
179
|
+
model = kwargs.get("model")
|
|
180
|
+
max_tokens = ModelDatabase.get_default_max_tokens(model)
|
|
181
|
+
|
|
182
|
+
return RequestParams(
|
|
183
|
+
model=model,
|
|
184
|
+
maxTokens=max_tokens,
|
|
185
|
+
systemPrompt=self.instruction,
|
|
186
|
+
parallel_tool_calls=True,
|
|
187
|
+
max_iterations=DEFAULT_MAX_ITERATIONS,
|
|
188
|
+
use_history=True,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
async def _execute_with_retry(
|
|
194
|
+
self,
|
|
195
|
+
func: Callable[..., Awaitable[Any]],
|
|
196
|
+
*args: Any,
|
|
197
|
+
on_final_error: Callable[[Exception], Awaitable[Any] | Any] | None = None,
|
|
198
|
+
**kwargs: Any,
|
|
199
|
+
) -> Any:
|
|
200
|
+
"""
|
|
201
|
+
Executes a function with robust retry logic for transient API errors.
|
|
202
|
+
"""
|
|
203
|
+
retries = max(0, int(self.retry_count))
|
|
204
|
+
|
|
205
|
+
def _is_fatal_error(e: Exception) -> bool:
|
|
206
|
+
if isinstance(e, (KeyboardInterrupt, AgentConfigError, ServerConfigError)):
|
|
207
|
+
return True
|
|
208
|
+
if isinstance(e, ProviderKeyError):
|
|
209
|
+
msg = str(e).lower()
|
|
210
|
+
# Retry on Rate Limits (429, Quota, Overloaded)
|
|
211
|
+
keywords = ["429", "503", "quota", "exhausted", "overloaded", "unavailable", "timeout"]
|
|
212
|
+
if any(k in msg for k in keywords):
|
|
213
|
+
return False
|
|
214
|
+
return True
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
last_error = None
|
|
218
|
+
|
|
219
|
+
for attempt in range(retries + 1):
|
|
220
|
+
try:
|
|
221
|
+
# Await the async function
|
|
222
|
+
return await func(*args, **kwargs)
|
|
223
|
+
except Exception as e:
|
|
224
|
+
if _is_fatal_error(e):
|
|
225
|
+
raise e
|
|
226
|
+
|
|
227
|
+
last_error = e
|
|
228
|
+
if attempt < retries:
|
|
229
|
+
wait_time = self.retry_backoff_seconds * (attempt + 1)
|
|
230
|
+
|
|
231
|
+
# Try to import progress_display safely
|
|
232
|
+
try:
|
|
233
|
+
from fast_agent.ui.progress_display import progress_display
|
|
234
|
+
with progress_display.paused():
|
|
235
|
+
rich_print(f"\n[yellow]⚠ Provider Error: {str(e)[:300]}...[/yellow]")
|
|
236
|
+
rich_print(f"[dim]⟳ Retrying in {wait_time}s... (Attempt {attempt+1}/{retries})[/dim]")
|
|
237
|
+
except ImportError:
|
|
238
|
+
print(f"⚠ Provider Error: {str(e)[:300]}...")
|
|
239
|
+
print(f"⟳ Retrying in {wait_time}s... (Attempt {attempt+1}/{retries})")
|
|
240
|
+
|
|
241
|
+
await asyncio.sleep(wait_time)
|
|
242
|
+
|
|
243
|
+
if last_error:
|
|
244
|
+
handler = on_final_error or getattr(self, "_handle_retry_failure", None)
|
|
245
|
+
if handler:
|
|
246
|
+
handled = handler(last_error)
|
|
247
|
+
if inspect.isawaitable(handled):
|
|
248
|
+
handled = await handled
|
|
249
|
+
if handled is not None:
|
|
250
|
+
return handled
|
|
251
|
+
|
|
252
|
+
raise last_error
|
|
253
|
+
|
|
254
|
+
# This line satisfies Pylance that we never implicitly return None
|
|
255
|
+
raise RuntimeError("Retry loop finished without success or exception")
|
|
256
|
+
|
|
257
|
+
def _handle_retry_failure(self, error: Exception) -> Any | None:
|
|
258
|
+
"""
|
|
259
|
+
Optional hook for providers to convert an exhausted retry into a user-facing response.
|
|
260
|
+
|
|
261
|
+
Return a non-None value to short-circuit raising the final exception.
|
|
262
|
+
"""
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
def _resolve_retry_count(self) -> int:
|
|
266
|
+
"""Resolve retries from config first, then env, defaulting to 0."""
|
|
267
|
+
config_retries = None
|
|
268
|
+
try:
|
|
269
|
+
config_retries = getattr(self.context.config, "llm_retries", None)
|
|
270
|
+
except Exception:
|
|
271
|
+
config_retries = None
|
|
272
|
+
|
|
273
|
+
if config_retries is not None:
|
|
274
|
+
try:
|
|
275
|
+
return int(config_retries)
|
|
276
|
+
except (TypeError, ValueError):
|
|
277
|
+
pass
|
|
278
|
+
|
|
279
|
+
env_retries = os.getenv("FAST_AGENT_RETRIES")
|
|
280
|
+
if env_retries is not None:
|
|
281
|
+
try:
|
|
282
|
+
return int(env_retries)
|
|
283
|
+
except (TypeError, ValueError):
|
|
284
|
+
pass
|
|
285
|
+
|
|
286
|
+
return 0
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
async def generate(
|
|
290
|
+
self,
|
|
291
|
+
messages: list[PromptMessageExtended],
|
|
292
|
+
request_params: RequestParams | None = None,
|
|
293
|
+
tools: list[Tool] | None = None,
|
|
294
|
+
) -> PromptMessageExtended:
|
|
295
|
+
"""
|
|
296
|
+
Generate a completion using normalized message lists.
|
|
297
|
+
|
|
298
|
+
This is the primary LLM interface that works directly with
|
|
299
|
+
list[PromptMessageExtended] for efficient internal usage.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
messages: List of PromptMessageExtended objects
|
|
303
|
+
request_params: Optional parameters to configure the LLM request
|
|
304
|
+
tools: Optional list of tools available to the LLM
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
A PromptMessageExtended containing the Assistant response
|
|
308
|
+
|
|
309
|
+
Raises:
|
|
310
|
+
asyncio.CancelledError: If the operation is cancelled via task.cancel()
|
|
311
|
+
"""
|
|
312
|
+
# TODO -- create a "fast-agent" control role rather than magic strings
|
|
313
|
+
|
|
314
|
+
if messages[-1].first_text().startswith(CONTROL_MESSAGE_SAVE_HISTORY):
|
|
315
|
+
parts: list[str] = messages[-1].first_text().split(" ", 1)
|
|
316
|
+
if len(parts) > 1:
|
|
317
|
+
filename: str = parts[1].strip()
|
|
318
|
+
else:
|
|
319
|
+
from datetime import datetime
|
|
320
|
+
|
|
321
|
+
timestamp = datetime.now().strftime("%y_%m_%d_%H_%M")
|
|
322
|
+
filename = f"{timestamp}-conversation.json"
|
|
323
|
+
await self._save_history(filename, messages)
|
|
324
|
+
return Prompt.assistant(f"History saved to {filename}")
|
|
325
|
+
|
|
326
|
+
# Store MCP metadata in context variable
|
|
327
|
+
final_request_params = self.get_request_params(request_params)
|
|
328
|
+
if final_request_params.mcp_metadata:
|
|
329
|
+
_mcp_metadata_var.set(final_request_params.mcp_metadata)
|
|
330
|
+
|
|
331
|
+
# The caller supplies the full conversation to send
|
|
332
|
+
full_history = messages
|
|
333
|
+
|
|
334
|
+
# Track timing for this generation
|
|
335
|
+
start_time = time.perf_counter()
|
|
336
|
+
assistant_response: PromptMessageExtended = await self._execute_with_retry(
|
|
337
|
+
self._apply_prompt_provider_specific,
|
|
338
|
+
full_history,
|
|
339
|
+
request_params,
|
|
340
|
+
tools
|
|
341
|
+
)
|
|
342
|
+
end_time = time.perf_counter()
|
|
343
|
+
duration_ms = round((end_time - start_time) * 1000, 2)
|
|
344
|
+
|
|
345
|
+
# Add timing data to channels only if not already present
|
|
346
|
+
# (preserves original timing when loading saved history)
|
|
347
|
+
channels = dict(assistant_response.channels or {})
|
|
348
|
+
if FAST_AGENT_TIMING not in channels:
|
|
349
|
+
timing_data = {
|
|
350
|
+
"start_time": start_time,
|
|
351
|
+
"end_time": end_time,
|
|
352
|
+
"duration_ms": duration_ms,
|
|
353
|
+
}
|
|
354
|
+
channels[FAST_AGENT_TIMING] = [TextContent(type="text", text=json.dumps(timing_data))]
|
|
355
|
+
assistant_response.channels = channels
|
|
356
|
+
|
|
357
|
+
self.usage_accumulator.count_tools(len(assistant_response.tool_calls or {}))
|
|
358
|
+
|
|
359
|
+
return assistant_response
|
|
360
|
+
|
|
361
|
+
@abstractmethod
|
|
362
|
+
async def _apply_prompt_provider_specific(
|
|
363
|
+
self,
|
|
364
|
+
multipart_messages: list["PromptMessageExtended"],
|
|
365
|
+
request_params: RequestParams | None = None,
|
|
366
|
+
tools: list[Tool] | None = None,
|
|
367
|
+
is_template: bool = False,
|
|
368
|
+
) -> PromptMessageExtended:
|
|
369
|
+
"""
|
|
370
|
+
Provider-specific implementation of apply_prompt_template.
|
|
371
|
+
This default implementation handles basic text content for any LLM type.
|
|
372
|
+
Provider-specific subclasses should override this method to handle
|
|
373
|
+
multimodal content appropriately.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
multipart_messages: List of PromptMessageExtended objects parsed from the prompt template
|
|
377
|
+
request_params: Optional parameters to configure the LLM request
|
|
378
|
+
tools: Optional list of tools available to the LLM
|
|
379
|
+
is_template: Whether this is a template application
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
String representation of the assistant's response if generated,
|
|
383
|
+
or the last assistant message in the prompt
|
|
384
|
+
"""
|
|
385
|
+
|
|
386
|
+
async def structured(
|
|
387
|
+
self,
|
|
388
|
+
messages: list[PromptMessageExtended],
|
|
389
|
+
model: Type[ModelT],
|
|
390
|
+
request_params: RequestParams | None = None,
|
|
391
|
+
) -> tuple[ModelT | None, PromptMessageExtended]:
|
|
392
|
+
"""
|
|
393
|
+
Generate a structured response using normalized message lists.
|
|
394
|
+
|
|
395
|
+
This is the primary LLM interface for structured output that works directly with
|
|
396
|
+
list[PromptMessageExtended] for efficient internal usage.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
messages: List of PromptMessageExtended objects
|
|
400
|
+
model: The Pydantic model class to parse the response into
|
|
401
|
+
request_params: Optional parameters to configure the LLM request
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
Tuple of (parsed model instance or None, assistant response message)
|
|
405
|
+
"""
|
|
406
|
+
|
|
407
|
+
# Store MCP metadata in context variable
|
|
408
|
+
final_request_params = self.get_request_params(request_params)
|
|
409
|
+
|
|
410
|
+
# TODO -- this doesn't need to go here anymore.
|
|
411
|
+
if final_request_params.mcp_metadata:
|
|
412
|
+
_mcp_metadata_var.set(final_request_params.mcp_metadata)
|
|
413
|
+
|
|
414
|
+
full_history = messages
|
|
415
|
+
|
|
416
|
+
# Track timing for this structured generation
|
|
417
|
+
start_time = time.perf_counter()
|
|
418
|
+
result_or_response = await self._execute_with_retry(
|
|
419
|
+
self._apply_prompt_provider_specific_structured,
|
|
420
|
+
full_history,
|
|
421
|
+
model,
|
|
422
|
+
request_params,
|
|
423
|
+
on_final_error=self._handle_retry_failure,
|
|
424
|
+
)
|
|
425
|
+
if isinstance(result_or_response, PromptMessageExtended):
|
|
426
|
+
result, assistant_response = self._structured_from_multipart(result_or_response, model)
|
|
427
|
+
else:
|
|
428
|
+
result, assistant_response = result_or_response
|
|
429
|
+
end_time = time.perf_counter()
|
|
430
|
+
duration_ms = round((end_time - start_time) * 1000, 2)
|
|
431
|
+
|
|
432
|
+
# Add timing data to channels only if not already present
|
|
433
|
+
# (preserves original timing when loading saved history)
|
|
434
|
+
channels = dict(assistant_response.channels or {})
|
|
435
|
+
if FAST_AGENT_TIMING not in channels:
|
|
436
|
+
timing_data = {
|
|
437
|
+
"start_time": start_time,
|
|
438
|
+
"end_time": end_time,
|
|
439
|
+
"duration_ms": duration_ms,
|
|
440
|
+
}
|
|
441
|
+
channels[FAST_AGENT_TIMING] = [TextContent(type="text", text=json.dumps(timing_data))]
|
|
442
|
+
assistant_response.channels = channels
|
|
443
|
+
|
|
444
|
+
return result, assistant_response
|
|
445
|
+
|
|
446
|
+
@staticmethod
|
|
447
|
+
def model_to_response_format(
|
|
448
|
+
model: Type[Any],
|
|
449
|
+
) -> Any:
|
|
450
|
+
"""
|
|
451
|
+
Convert a pydantic model to the appropriate response format schema.
|
|
452
|
+
This allows for reuse in multiple provider implementations.
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
model: The pydantic model class to convert to a schema
|
|
456
|
+
|
|
457
|
+
Returns:
|
|
458
|
+
Provider-agnostic schema representation or NotGiven if conversion fails
|
|
459
|
+
"""
|
|
460
|
+
return _type_to_response_format(model)
|
|
461
|
+
|
|
462
|
+
@staticmethod
|
|
463
|
+
def model_to_schema_str(
|
|
464
|
+
model: Type[Any],
|
|
465
|
+
) -> str:
|
|
466
|
+
"""
|
|
467
|
+
Convert a pydantic model to a schema string representation.
|
|
468
|
+
This provides a simpler interface for provider implementations
|
|
469
|
+
that need a string representation.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
model: The pydantic model class to convert to a schema
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
Schema as a string, or empty string if conversion fails
|
|
476
|
+
"""
|
|
477
|
+
import json
|
|
478
|
+
|
|
479
|
+
try:
|
|
480
|
+
schema = model.model_json_schema()
|
|
481
|
+
return json.dumps(schema)
|
|
482
|
+
except Exception:
|
|
483
|
+
return ""
|
|
484
|
+
|
|
485
|
+
async def _apply_prompt_provider_specific_structured(
|
|
486
|
+
self,
|
|
487
|
+
multipart_messages: list[PromptMessageExtended],
|
|
488
|
+
model: Type[ModelT],
|
|
489
|
+
request_params: RequestParams | None = None,
|
|
490
|
+
) -> tuple[ModelT | None, PromptMessageExtended]:
|
|
491
|
+
"""Base class attempts to parse JSON - subclasses can use provider specific functionality"""
|
|
492
|
+
|
|
493
|
+
request_params = self.get_request_params(request_params)
|
|
494
|
+
|
|
495
|
+
if not request_params.response_format:
|
|
496
|
+
schema = self.model_to_response_format(model)
|
|
497
|
+
if schema is not NotGiven:
|
|
498
|
+
request_params.response_format = schema
|
|
499
|
+
|
|
500
|
+
result: PromptMessageExtended = await self._apply_prompt_provider_specific(
|
|
501
|
+
multipart_messages, request_params
|
|
502
|
+
)
|
|
503
|
+
return self._structured_from_multipart(result, model)
|
|
504
|
+
|
|
505
|
+
def _structured_from_multipart(
|
|
506
|
+
self, message: PromptMessageExtended, model: Type[ModelT]
|
|
507
|
+
) -> tuple[ModelT | None, PromptMessageExtended]:
|
|
508
|
+
"""Parse the content of a PromptMessage and return the structured model and message itself"""
|
|
509
|
+
try:
|
|
510
|
+
text = get_text(message.content[-1]) or ""
|
|
511
|
+
text = self._prepare_structured_text(text)
|
|
512
|
+
json_data = from_json(text, allow_partial=True)
|
|
513
|
+
validated_model = model.model_validate(json_data)
|
|
514
|
+
return cast("ModelT", validated_model), message
|
|
515
|
+
except ValueError as e:
|
|
516
|
+
logger = get_logger(__name__)
|
|
517
|
+
logger.warning(f"Failed to parse structured response: {str(e)}")
|
|
518
|
+
return None, message
|
|
519
|
+
|
|
520
|
+
def _prepare_structured_text(self, text: str) -> str:
|
|
521
|
+
"""Hook for subclasses to adjust structured output text before parsing."""
|
|
522
|
+
return text
|
|
523
|
+
|
|
524
|
+
def record_templates(self, templates: list[PromptMessageExtended]) -> None:
|
|
525
|
+
"""Hook for providers that need template visibility (e.g., caching)."""
|
|
526
|
+
return
|
|
527
|
+
|
|
528
|
+
def _precall(self, multipart_messages: list[PromptMessageExtended]) -> None:
|
|
529
|
+
"""Pre-call hook to modify the message before sending it to the provider."""
|
|
530
|
+
# No-op placeholder; history is managed by the agent
|
|
531
|
+
|
|
532
|
+
def chat_turn(self) -> int:
|
|
533
|
+
"""Return the current chat turn number"""
|
|
534
|
+
return 1 + len(self._usage_accumulator.turns)
|
|
535
|
+
|
|
536
|
+
def prepare_provider_arguments(
|
|
537
|
+
self,
|
|
538
|
+
base_args: dict,
|
|
539
|
+
request_params: RequestParams,
|
|
540
|
+
exclude_fields: set | None = None,
|
|
541
|
+
) -> dict:
|
|
542
|
+
"""
|
|
543
|
+
Prepare arguments for provider API calls by merging request parameters.
|
|
544
|
+
|
|
545
|
+
Args:
|
|
546
|
+
base_args: Base arguments dictionary with provider-specific required parameters
|
|
547
|
+
params: The RequestParams object containing all parameters
|
|
548
|
+
exclude_fields: Set of field names to exclude from params. If None, uses BASE_EXCLUDE_FIELDS.
|
|
549
|
+
|
|
550
|
+
Returns:
|
|
551
|
+
Complete arguments dictionary with all applicable parameters
|
|
552
|
+
"""
|
|
553
|
+
# Start with base arguments
|
|
554
|
+
arguments = base_args.copy()
|
|
555
|
+
|
|
556
|
+
# Combine base exclusions with provider-specific exclusions
|
|
557
|
+
final_exclude_fields = self.BASE_EXCLUDE_FIELDS.copy()
|
|
558
|
+
if exclude_fields:
|
|
559
|
+
final_exclude_fields.update(exclude_fields)
|
|
560
|
+
|
|
561
|
+
# Add all fields from params that aren't explicitly excluded
|
|
562
|
+
# Ensure model_dump only includes set fields if that's the desired behavior,
|
|
563
|
+
# or adjust exclude_unset=True/False as needed.
|
|
564
|
+
# Default Pydantic v2 model_dump is exclude_unset=False
|
|
565
|
+
params_dict = request_params.model_dump(exclude=final_exclude_fields)
|
|
566
|
+
|
|
567
|
+
for key, value in params_dict.items():
|
|
568
|
+
# Only add if not None and not already in base_args (base_args take precedence)
|
|
569
|
+
# or if None is a valid value for the provider, this logic might need adjustment.
|
|
570
|
+
if value is not None and key not in arguments:
|
|
571
|
+
arguments[key] = value
|
|
572
|
+
elif value is not None and key in arguments and arguments[key] is None:
|
|
573
|
+
# Allow overriding a None in base_args with a set value from params
|
|
574
|
+
arguments[key] = value
|
|
575
|
+
|
|
576
|
+
# Finally, add any metadata fields as a last layer of overrides
|
|
577
|
+
# This ensures metadata can override anything previously set if keys conflict.
|
|
578
|
+
if request_params.metadata:
|
|
579
|
+
arguments.update(request_params.metadata)
|
|
580
|
+
|
|
581
|
+
return arguments
|
|
582
|
+
|
|
583
|
+
def _merge_request_params(
|
|
584
|
+
self, default_params: RequestParams, provided_params: RequestParams
|
|
585
|
+
) -> RequestParams:
|
|
586
|
+
"""Merge default and provided request parameters"""
|
|
587
|
+
|
|
588
|
+
merged = deep_merge(
|
|
589
|
+
default_params.model_dump(),
|
|
590
|
+
provided_params.model_dump(exclude_unset=True),
|
|
591
|
+
)
|
|
592
|
+
final_params = RequestParams(**merged)
|
|
593
|
+
|
|
594
|
+
return final_params
|
|
595
|
+
|
|
596
|
+
def get_request_params(
|
|
597
|
+
self,
|
|
598
|
+
request_params: RequestParams | None = None,
|
|
599
|
+
) -> RequestParams:
|
|
600
|
+
"""
|
|
601
|
+
Get request parameters with merged-in defaults and overrides.
|
|
602
|
+
Args:
|
|
603
|
+
request_params: The request parameters to use as overrides.
|
|
604
|
+
default: The default request parameters to use as the base.
|
|
605
|
+
If unspecified, self.default_request_params will be used.
|
|
606
|
+
"""
|
|
607
|
+
|
|
608
|
+
# If user provides overrides, merge them with defaults
|
|
609
|
+
if request_params:
|
|
610
|
+
return self._merge_request_params(self.default_request_params, request_params)
|
|
611
|
+
|
|
612
|
+
return self.default_request_params.model_copy()
|
|
613
|
+
|
|
614
|
+
@classmethod
|
|
615
|
+
def convert_message_to_message_param(
|
|
616
|
+
cls, message: MessageT, **kwargs: dict[str, Any]
|
|
617
|
+
) -> MessageParamT:
|
|
618
|
+
"""Convert a response object to an input parameter object to allow LLM calls to be chained."""
|
|
619
|
+
# Many LLM implementations will allow the same type for input and output messages
|
|
620
|
+
return cast("MessageParamT", message)
|
|
621
|
+
|
|
622
|
+
def _finalize_turn_usage(self, turn_usage: "TurnUsage") -> None:
|
|
623
|
+
"""Set tool call count on TurnUsage and add to accumulator."""
|
|
624
|
+
self._usage_accumulator.add_turn(turn_usage)
|
|
625
|
+
|
|
626
|
+
def _log_chat_progress(self, chat_turn: int | None = None, model: str | None = None) -> None:
|
|
627
|
+
"""Log a chat progress event"""
|
|
628
|
+
# Determine action type based on verb
|
|
629
|
+
if hasattr(self, "verb") and self.verb:
|
|
630
|
+
# Use verb directly regardless of type
|
|
631
|
+
act = self.verb
|
|
632
|
+
else:
|
|
633
|
+
act = ProgressAction.CHATTING
|
|
634
|
+
|
|
635
|
+
data = {
|
|
636
|
+
"progress_action": act,
|
|
637
|
+
"model": model,
|
|
638
|
+
"agent_name": self.name,
|
|
639
|
+
"chat_turn": chat_turn if chat_turn is not None else None,
|
|
640
|
+
}
|
|
641
|
+
self.logger.debug("Chat in progress", data=data)
|
|
642
|
+
|
|
643
|
+
def _update_streaming_progress(self, content: str, model: str, estimated_tokens: int) -> int:
|
|
644
|
+
"""Update streaming progress with token estimation and formatting.
|
|
645
|
+
|
|
646
|
+
Args:
|
|
647
|
+
content: The text content from the streaming event
|
|
648
|
+
model: The model name
|
|
649
|
+
estimated_tokens: Current token count to update
|
|
650
|
+
|
|
651
|
+
Returns:
|
|
652
|
+
Updated estimated token count
|
|
653
|
+
"""
|
|
654
|
+
# Rough estimate: 1 token per 4 characters (OpenAI's typical ratio)
|
|
655
|
+
text_length = len(content)
|
|
656
|
+
additional_tokens = max(1, text_length // 4)
|
|
657
|
+
new_total = estimated_tokens + additional_tokens
|
|
658
|
+
|
|
659
|
+
# Format token count for display
|
|
660
|
+
token_str = str(new_total).rjust(5)
|
|
661
|
+
|
|
662
|
+
# Emit progress event
|
|
663
|
+
data = {
|
|
664
|
+
"progress_action": ProgressAction.STREAMING,
|
|
665
|
+
"model": model,
|
|
666
|
+
"agent_name": self.name,
|
|
667
|
+
"chat_turn": self.chat_turn(),
|
|
668
|
+
"details": token_str.strip(), # Token count goes in details for STREAMING action
|
|
669
|
+
}
|
|
670
|
+
self.logger.info("Streaming progress", data=data)
|
|
671
|
+
|
|
672
|
+
return new_total
|
|
673
|
+
|
|
674
|
+
def add_stream_listener(self, listener: Callable[[StreamChunk], None]) -> Callable[[], None]:
|
|
675
|
+
"""
|
|
676
|
+
Register a callback invoked with streaming text chunks.
|
|
677
|
+
|
|
678
|
+
Args:
|
|
679
|
+
listener: Callable receiving a StreamChunk emitted by the provider.
|
|
680
|
+
|
|
681
|
+
Returns:
|
|
682
|
+
A function that removes the listener when called.
|
|
683
|
+
"""
|
|
684
|
+
self._stream_listeners.add(listener)
|
|
685
|
+
|
|
686
|
+
def remove() -> None:
|
|
687
|
+
self._stream_listeners.discard(listener)
|
|
688
|
+
|
|
689
|
+
return remove
|
|
690
|
+
|
|
691
|
+
def _notify_stream_listeners(self, chunk: StreamChunk) -> None:
|
|
692
|
+
"""Notify registered listeners with a streaming chunk."""
|
|
693
|
+
if not chunk.text:
|
|
694
|
+
return
|
|
695
|
+
for listener in list(self._stream_listeners):
|
|
696
|
+
try:
|
|
697
|
+
listener(chunk)
|
|
698
|
+
except Exception:
|
|
699
|
+
self.logger.exception("Stream listener raised an exception")
|
|
700
|
+
|
|
701
|
+
def add_tool_stream_listener(
|
|
702
|
+
self, listener: Callable[[str, dict[str, Any] | None], None]
|
|
703
|
+
) -> Callable[[], None]:
|
|
704
|
+
"""Register a callback invoked with tool streaming events.
|
|
705
|
+
|
|
706
|
+
Args:
|
|
707
|
+
listener: Callable receiving event_type (str) and optional info dict.
|
|
708
|
+
|
|
709
|
+
Returns:
|
|
710
|
+
A function that removes the listener when called.
|
|
711
|
+
"""
|
|
712
|
+
|
|
713
|
+
self._tool_stream_listeners.add(listener)
|
|
714
|
+
|
|
715
|
+
def remove() -> None:
|
|
716
|
+
self._tool_stream_listeners.discard(listener)
|
|
717
|
+
|
|
718
|
+
return remove
|
|
719
|
+
|
|
720
|
+
def _notify_tool_stream_listeners(
|
|
721
|
+
self, event_type: str, payload: dict[str, Any] | None = None
|
|
722
|
+
) -> None:
|
|
723
|
+
"""Notify listeners about tool streaming lifecycle events."""
|
|
724
|
+
|
|
725
|
+
data = payload or {}
|
|
726
|
+
for listener in list(self._tool_stream_listeners):
|
|
727
|
+
try:
|
|
728
|
+
listener(event_type, data)
|
|
729
|
+
except Exception:
|
|
730
|
+
self.logger.exception("Tool stream listener raised an exception")
|
|
731
|
+
|
|
732
|
+
def _log_chat_finished(self, model: str | None = None) -> None:
|
|
733
|
+
"""Log a chat finished event"""
|
|
734
|
+
data = {
|
|
735
|
+
"progress_action": ProgressAction.READY,
|
|
736
|
+
"model": model,
|
|
737
|
+
"agent_name": self.name,
|
|
738
|
+
}
|
|
739
|
+
self.logger.debug("Chat finished", data=data)
|
|
740
|
+
|
|
741
|
+
def _convert_prompt_messages(self, prompt_messages: list[PromptMessage]) -> list[MessageParamT]:
|
|
742
|
+
"""
|
|
743
|
+
Convert prompt messages to this LLM's specific message format.
|
|
744
|
+
To be implemented by concrete LLM classes.
|
|
745
|
+
"""
|
|
746
|
+
raise NotImplementedError("Must be implemented by subclass")
|
|
747
|
+
|
|
748
|
+
def _convert_to_provider_format(
|
|
749
|
+
self, messages: list[PromptMessageExtended]
|
|
750
|
+
) -> list[MessageParamT]:
|
|
751
|
+
"""
|
|
752
|
+
Convert provided messages to provider-specific format.
|
|
753
|
+
Called fresh on EVERY API call - no caching.
|
|
754
|
+
|
|
755
|
+
Args:
|
|
756
|
+
messages: List of PromptMessageExtended
|
|
757
|
+
|
|
758
|
+
Returns:
|
|
759
|
+
List of provider-specific message objects
|
|
760
|
+
"""
|
|
761
|
+
return self._convert_extended_messages_to_provider(messages)
|
|
762
|
+
|
|
763
|
+
@abstractmethod
|
|
764
|
+
def _convert_extended_messages_to_provider(
|
|
765
|
+
self, messages: list[PromptMessageExtended]
|
|
766
|
+
) -> list[MessageParamT]:
|
|
767
|
+
"""
|
|
768
|
+
Convert PromptMessageExtended list to provider-specific format.
|
|
769
|
+
Must be implemented by each provider.
|
|
770
|
+
|
|
771
|
+
Args:
|
|
772
|
+
messages: List of PromptMessageExtended objects
|
|
773
|
+
|
|
774
|
+
Returns:
|
|
775
|
+
List of provider-specific message parameter objects
|
|
776
|
+
"""
|
|
777
|
+
raise NotImplementedError("Must be implemented by subclass")
|
|
778
|
+
|
|
779
|
+
async def show_prompt_loaded(
|
|
780
|
+
self,
|
|
781
|
+
prompt_name: str,
|
|
782
|
+
description: str | None = None,
|
|
783
|
+
message_count: int = 0,
|
|
784
|
+
arguments: dict[str, str] | None = None,
|
|
785
|
+
) -> None:
|
|
786
|
+
"""
|
|
787
|
+
Display information about a loaded prompt template.
|
|
788
|
+
|
|
789
|
+
Args:
|
|
790
|
+
prompt_name: The name of the prompt
|
|
791
|
+
description: Optional description of the prompt
|
|
792
|
+
message_count: Number of messages in the prompt
|
|
793
|
+
arguments: Optional dictionary of arguments passed to the prompt
|
|
794
|
+
"""
|
|
795
|
+
await self.display.show_prompt_loaded(
|
|
796
|
+
prompt_name=prompt_name,
|
|
797
|
+
description=description,
|
|
798
|
+
message_count=message_count,
|
|
799
|
+
agent_name=self.name,
|
|
800
|
+
arguments=arguments,
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
async def apply_prompt_template(self, prompt_result: GetPromptResult, prompt_name: str) -> str:
|
|
804
|
+
"""
|
|
805
|
+
Apply a prompt template by adding it to the conversation history.
|
|
806
|
+
If the last message in the prompt is from a user, automatically
|
|
807
|
+
generate an assistant response.
|
|
808
|
+
|
|
809
|
+
Args:
|
|
810
|
+
prompt_result: The GetPromptResult containing prompt messages
|
|
811
|
+
prompt_name: The name of the prompt being applied
|
|
812
|
+
|
|
813
|
+
Returns:
|
|
814
|
+
String representation of the assistant's response if generated,
|
|
815
|
+
or the last assistant message in the prompt
|
|
816
|
+
"""
|
|
817
|
+
from fast_agent.types import PromptMessageExtended
|
|
818
|
+
|
|
819
|
+
# Check if we have any messages
|
|
820
|
+
if not prompt_result.messages:
|
|
821
|
+
return "Prompt contains no messages"
|
|
822
|
+
|
|
823
|
+
# Extract arguments if they were stored in the result
|
|
824
|
+
arguments = getattr(prompt_result, "arguments", None)
|
|
825
|
+
|
|
826
|
+
# Display information about the loaded prompt
|
|
827
|
+
await self.show_prompt_loaded(
|
|
828
|
+
prompt_name=prompt_name,
|
|
829
|
+
description=prompt_result.description,
|
|
830
|
+
message_count=len(prompt_result.messages),
|
|
831
|
+
arguments=arguments,
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
# Convert to PromptMessageExtended objects and delegate
|
|
835
|
+
multipart_messages = PromptMessageExtended.parse_get_prompt_result(prompt_result)
|
|
836
|
+
result = await self._apply_prompt_provider_specific(
|
|
837
|
+
multipart_messages, None, is_template=True
|
|
838
|
+
)
|
|
839
|
+
return result.first_text()
|
|
840
|
+
|
|
841
|
+
async def _save_history(self, filename: str, messages: list[PromptMessageExtended]) -> None:
|
|
842
|
+
"""
|
|
843
|
+
Save the Message History to a file in a format determined by the file extension.
|
|
844
|
+
|
|
845
|
+
Uses JSON format for .json files (MCP SDK compatible format) and
|
|
846
|
+
delimited text format for other extensions.
|
|
847
|
+
"""
|
|
848
|
+
from fast_agent.mcp.prompt_serialization import save_messages
|
|
849
|
+
|
|
850
|
+
# Drop control messages like ***SAVE_HISTORY before persisting
|
|
851
|
+
filtered = [
|
|
852
|
+
msg.model_copy(deep=True)
|
|
853
|
+
for msg in messages
|
|
854
|
+
if not msg.first_text().startswith(CONTROL_MESSAGE_SAVE_HISTORY)
|
|
855
|
+
]
|
|
856
|
+
|
|
857
|
+
# Save messages using the unified save function that auto-detects format
|
|
858
|
+
save_messages(filtered, filename)
|
|
859
|
+
|
|
860
|
+
@property
|
|
861
|
+
def message_history(self) -> list[PromptMessageExtended]:
|
|
862
|
+
"""
|
|
863
|
+
Return the agent's message history as PromptMessageExtended objects.
|
|
864
|
+
|
|
865
|
+
This history can be used to transfer state between agents or for
|
|
866
|
+
analysis and debugging purposes.
|
|
867
|
+
|
|
868
|
+
Returns:
|
|
869
|
+
List of PromptMessageExtended objects representing the conversation history
|
|
870
|
+
"""
|
|
871
|
+
return []
|
|
872
|
+
|
|
873
|
+
def pop_last_message(self) -> PromptMessageExtended | None:
|
|
874
|
+
"""Remove and return the most recent message from the conversation history."""
|
|
875
|
+
return None
|
|
876
|
+
|
|
877
|
+
def clear(self, *, clear_prompts: bool = False) -> None:
|
|
878
|
+
"""Reset stored message history while optionally retaining prompt templates."""
|
|
879
|
+
|
|
880
|
+
self.history.clear(clear_prompts=clear_prompts)
|
|
881
|
+
|
|
882
|
+
def _api_key(self):
|
|
883
|
+
if self._init_api_key:
|
|
884
|
+
return self._init_api_key
|
|
885
|
+
|
|
886
|
+
from fast_agent.llm.provider_key_manager import ProviderKeyManager
|
|
887
|
+
|
|
888
|
+
assert self.provider
|
|
889
|
+
return ProviderKeyManager.get_api_key(self.provider.value, self.context.config)
|
|
890
|
+
|
|
891
|
+
@property
|
|
892
|
+
def usage_accumulator(self):
|
|
893
|
+
return self._usage_accumulator
|
|
894
|
+
|
|
895
|
+
def get_usage_summary(self) -> dict:
|
|
896
|
+
"""
|
|
897
|
+
Get a summary of usage statistics for this LLM instance.
|
|
898
|
+
|
|
899
|
+
Returns:
|
|
900
|
+
Dictionary containing usage statistics including tokens, cache metrics,
|
|
901
|
+
and context window utilization.
|
|
902
|
+
"""
|
|
903
|
+
return self._usage_accumulator.get_summary()
|
|
904
|
+
|
|
905
|
+
@property
|
|
906
|
+
def provider(self) -> Provider:
|
|
907
|
+
"""
|
|
908
|
+
Return the LLM provider type.
|
|
909
|
+
|
|
910
|
+
Returns:
|
|
911
|
+
The Provider enum value representing the LLM provider
|
|
912
|
+
"""
|
|
913
|
+
return self._provider
|
|
914
|
+
|
|
915
|
+
@property
|
|
916
|
+
def model_name(self) -> str | None:
|
|
917
|
+
"""Return the effective model name, if set."""
|
|
918
|
+
return self._model_name
|
|
919
|
+
|
|
920
|
+
@property
|
|
921
|
+
def model_info(self):
|
|
922
|
+
"""Return resolved model information with capabilities.
|
|
923
|
+
|
|
924
|
+
Uses a lightweight resolver backed by the ModelDatabase and provides
|
|
925
|
+
text/document/vision flags, context window, etc.
|
|
926
|
+
"""
|
|
927
|
+
from fast_agent.llm.model_info import ModelInfo
|
|
928
|
+
|
|
929
|
+
if not self._model_name:
|
|
930
|
+
return None
|
|
931
|
+
return ModelInfo.from_name(self._model_name, self._provider)
|