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,509 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OAuth v2.1 integration helpers for MCP client transports.
|
|
3
|
+
|
|
4
|
+
Provides token storage (in-memory and OS keyring), a local callback server
|
|
5
|
+
with paste-URL fallback, and a builder for OAuthClientProvider that can be
|
|
6
|
+
passed to SSE/HTTP transports as the `auth` parameter.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
15
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
16
|
+
from urllib.parse import parse_qs, urlparse
|
|
17
|
+
|
|
18
|
+
from mcp.client.auth import OAuthClientProvider, TokenStorage
|
|
19
|
+
from mcp.shared.auth import (
|
|
20
|
+
OAuthClientInformationFull,
|
|
21
|
+
OAuthClientMetadata,
|
|
22
|
+
OAuthToken,
|
|
23
|
+
)
|
|
24
|
+
from pydantic import AnyUrl
|
|
25
|
+
|
|
26
|
+
from fast_agent.core.logging.logger import get_logger
|
|
27
|
+
from fast_agent.ui import console
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from fast_agent.config import MCPServerSettings
|
|
31
|
+
|
|
32
|
+
logger = get_logger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class InMemoryTokenStorage(TokenStorage):
|
|
36
|
+
"""Non-persistent token storage (process memory only)."""
|
|
37
|
+
|
|
38
|
+
def __init__(self) -> None:
|
|
39
|
+
self._tokens: OAuthToken | None = None
|
|
40
|
+
self._client_info: OAuthClientInformationFull | None = None
|
|
41
|
+
|
|
42
|
+
async def get_tokens(self) -> OAuthToken | None:
|
|
43
|
+
return self._tokens
|
|
44
|
+
|
|
45
|
+
async def set_tokens(self, tokens: OAuthToken) -> None:
|
|
46
|
+
self._tokens = tokens
|
|
47
|
+
|
|
48
|
+
async def get_client_info(self) -> OAuthClientInformationFull | None:
|
|
49
|
+
return self._client_info
|
|
50
|
+
|
|
51
|
+
async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
|
|
52
|
+
self._client_info = client_info
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class _CallbackResult:
|
|
57
|
+
authorization_code: str | None = None
|
|
58
|
+
state: str | None = None
|
|
59
|
+
error: str | None = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class _CallbackHandler(BaseHTTPRequestHandler):
|
|
63
|
+
"""HTTP handler to capture OAuth callback query params."""
|
|
64
|
+
|
|
65
|
+
def __init__(self, *args, result: _CallbackResult, expected_path: str, **kwargs):
|
|
66
|
+
self._result = result
|
|
67
|
+
self._expected_path = expected_path.rstrip("/") or "/callback"
|
|
68
|
+
super().__init__(*args, **kwargs)
|
|
69
|
+
|
|
70
|
+
def do_GET(self) -> None: # noqa: N802 - http.server signature
|
|
71
|
+
parsed = urlparse(self.path)
|
|
72
|
+
|
|
73
|
+
# Only accept the configured callback path
|
|
74
|
+
if (parsed.path.rstrip("/") or "/callback") != self._expected_path:
|
|
75
|
+
self.send_response(404)
|
|
76
|
+
self.end_headers()
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
params = parse_qs(parsed.query)
|
|
80
|
+
if "code" in params:
|
|
81
|
+
self._result.authorization_code = params["code"][0]
|
|
82
|
+
self._result.state = params.get("state", [None])[0]
|
|
83
|
+
self.send_response(200)
|
|
84
|
+
self.send_header("Content-Type", "text/html")
|
|
85
|
+
self.end_headers()
|
|
86
|
+
self.wfile.write(
|
|
87
|
+
b"""
|
|
88
|
+
<html><body>
|
|
89
|
+
<h1>Authorization Successful</h1>
|
|
90
|
+
<p>You can close this window.</p>
|
|
91
|
+
<script>setTimeout(() => window.close(), 1000);</script>
|
|
92
|
+
</body></html>
|
|
93
|
+
"""
|
|
94
|
+
)
|
|
95
|
+
elif "error" in params:
|
|
96
|
+
self._result.error = params["error"][0]
|
|
97
|
+
self.send_response(400)
|
|
98
|
+
self.send_header("Content-Type", "text/html")
|
|
99
|
+
self.end_headers()
|
|
100
|
+
self.wfile.write(
|
|
101
|
+
f"""
|
|
102
|
+
<html><body>
|
|
103
|
+
<h1>Authorization Failed</h1>
|
|
104
|
+
<p>Error: {self._result.error}</p>
|
|
105
|
+
</body></html>
|
|
106
|
+
""".encode()
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
self.send_response(404)
|
|
110
|
+
self.end_headers()
|
|
111
|
+
|
|
112
|
+
def log_message(self, format: str, *args: Any) -> None: # silence default logging
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class _CallbackServer:
|
|
117
|
+
"""Simple background HTTP server to receive a single OAuth callback."""
|
|
118
|
+
|
|
119
|
+
def __init__(self, port: int, path: str) -> None:
|
|
120
|
+
self._port = port
|
|
121
|
+
self._path = path.rstrip("/") or "/callback"
|
|
122
|
+
self._result = _CallbackResult()
|
|
123
|
+
self._server: HTTPServer | None = None
|
|
124
|
+
self._thread: threading.Thread | None = None
|
|
125
|
+
|
|
126
|
+
def _make_handler(self) -> Callable[..., BaseHTTPRequestHandler]:
|
|
127
|
+
result = self._result
|
|
128
|
+
expected_path = self._path
|
|
129
|
+
|
|
130
|
+
def handler(*args, **kwargs):
|
|
131
|
+
return _CallbackHandler(*args, result=result, expected_path=expected_path, **kwargs)
|
|
132
|
+
|
|
133
|
+
return handler
|
|
134
|
+
|
|
135
|
+
def start(self) -> None:
|
|
136
|
+
self._server = HTTPServer(("localhost", self._port), self._make_handler())
|
|
137
|
+
self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
|
|
138
|
+
self._thread.start()
|
|
139
|
+
logger.info(f"OAuth callback server listening on http://localhost:{self._port}{self._path}")
|
|
140
|
+
|
|
141
|
+
def stop(self) -> None:
|
|
142
|
+
if self._server:
|
|
143
|
+
try:
|
|
144
|
+
self._server.shutdown()
|
|
145
|
+
self._server.server_close()
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
148
|
+
if self._thread:
|
|
149
|
+
self._thread.join(timeout=1)
|
|
150
|
+
|
|
151
|
+
def wait(self, timeout_seconds: int = 300) -> tuple[str, str | None]:
|
|
152
|
+
start = time.time()
|
|
153
|
+
while time.time() - start < timeout_seconds:
|
|
154
|
+
if self._result.authorization_code:
|
|
155
|
+
return self._result.authorization_code, self._result.state
|
|
156
|
+
if self._result.error:
|
|
157
|
+
raise RuntimeError(f"OAuth error: {self._result.error}")
|
|
158
|
+
time.sleep(0.1)
|
|
159
|
+
raise TimeoutError("Timeout waiting for OAuth callback")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _derive_base_server_url(url: str | None) -> str | None:
|
|
163
|
+
"""Derive the base server URL for OAuth discovery from an MCP endpoint URL.
|
|
164
|
+
|
|
165
|
+
- Strips a trailing "/mcp" or "/sse" path segment
|
|
166
|
+
- Ignores query and fragment parts entirely
|
|
167
|
+
"""
|
|
168
|
+
if not url:
|
|
169
|
+
return None
|
|
170
|
+
try:
|
|
171
|
+
from urllib.parse import urlparse, urlunparse
|
|
172
|
+
|
|
173
|
+
parsed = urlparse(url)
|
|
174
|
+
# Normalize path without trailing slash
|
|
175
|
+
path = parsed.path or ""
|
|
176
|
+
path = path[:-1] if path.endswith("/") else path
|
|
177
|
+
# Remove one trailing segment if it is mcp or sse
|
|
178
|
+
for suffix in ("/mcp", "/sse"):
|
|
179
|
+
if path.endswith(suffix):
|
|
180
|
+
path = path[: -len(suffix)]
|
|
181
|
+
break
|
|
182
|
+
# Ensure path is at least '/'
|
|
183
|
+
if not path:
|
|
184
|
+
path = "/"
|
|
185
|
+
# Rebuild URL without query/fragment
|
|
186
|
+
clean = parsed._replace(path=path, params="", query="", fragment="")
|
|
187
|
+
base = urlunparse(clean)
|
|
188
|
+
# Drop trailing slash except for root
|
|
189
|
+
if base.endswith("/") and base.count("/") > 2:
|
|
190
|
+
base = base[:-1]
|
|
191
|
+
return base
|
|
192
|
+
except Exception:
|
|
193
|
+
return url
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def compute_server_identity(server_config: MCPServerSettings) -> str:
|
|
197
|
+
"""Compute a stable identity for token storage.
|
|
198
|
+
|
|
199
|
+
Prefer the normalized base server URL; fall back to configured name, then 'default'.
|
|
200
|
+
"""
|
|
201
|
+
base = _derive_base_server_url(server_config.url)
|
|
202
|
+
if base:
|
|
203
|
+
return base
|
|
204
|
+
if server_config.name:
|
|
205
|
+
return server_config.name
|
|
206
|
+
return "default"
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def keyring_has_token(server_config: MCPServerSettings) -> bool:
|
|
210
|
+
"""Check if keyring has a token stored for this server."""
|
|
211
|
+
try:
|
|
212
|
+
import keyring
|
|
213
|
+
|
|
214
|
+
identity = compute_server_identity(server_config)
|
|
215
|
+
token_key = f"oauth:tokens:{identity}"
|
|
216
|
+
return keyring.get_password("fast-agent-mcp", token_key) is not None
|
|
217
|
+
except Exception:
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
async def _print_authorization_link(auth_url: str, warn_if_no_keyring: bool = False) -> None:
|
|
222
|
+
"""Emit a clickable authorization link using rich console markup.
|
|
223
|
+
|
|
224
|
+
If warn_if_no_keyring is True and the OS keyring backend is unavailable,
|
|
225
|
+
print a warning to indicate tokens won't be persisted.
|
|
226
|
+
"""
|
|
227
|
+
console.console.print("[bold]Open this link to authorize:[/bold]", markup=True)
|
|
228
|
+
console.console.print(f"[link={auth_url}]{auth_url}[/link]")
|
|
229
|
+
if warn_if_no_keyring:
|
|
230
|
+
try:
|
|
231
|
+
import keyring # type: ignore
|
|
232
|
+
|
|
233
|
+
backend = keyring.get_keyring()
|
|
234
|
+
try:
|
|
235
|
+
from keyring.backends.fail import Keyring as FailKeyring # type: ignore
|
|
236
|
+
|
|
237
|
+
if isinstance(backend, FailKeyring):
|
|
238
|
+
console.console.print(
|
|
239
|
+
"[yellow]Warning:[/yellow] Keyring backend not available — tokens will not be persisted."
|
|
240
|
+
)
|
|
241
|
+
except Exception:
|
|
242
|
+
# If we cannot detect the fail backend, do nothing
|
|
243
|
+
pass
|
|
244
|
+
except Exception:
|
|
245
|
+
console.console.print(
|
|
246
|
+
"[yellow]Warning:[/yellow] Keyring backend not available — tokens will not be persisted."
|
|
247
|
+
)
|
|
248
|
+
logger.info("OAuth authorization URL emitted to console")
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class KeyringTokenStorage(TokenStorage):
|
|
252
|
+
"""Token storage backed by the OS keychain using 'keyring'."""
|
|
253
|
+
|
|
254
|
+
def __init__(self, service_name: str, server_identity: str) -> None:
|
|
255
|
+
self._service = service_name
|
|
256
|
+
self._identity = server_identity
|
|
257
|
+
|
|
258
|
+
@property
|
|
259
|
+
def _token_key(self) -> str:
|
|
260
|
+
return f"oauth:tokens:{self._identity}"
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def _client_key(self) -> str:
|
|
264
|
+
return f"oauth:client_info:{self._identity}"
|
|
265
|
+
|
|
266
|
+
async def get_tokens(self) -> OAuthToken | None:
|
|
267
|
+
try:
|
|
268
|
+
import keyring
|
|
269
|
+
|
|
270
|
+
payload = keyring.get_password(self._service, self._token_key)
|
|
271
|
+
if not payload:
|
|
272
|
+
return None
|
|
273
|
+
return OAuthToken.model_validate_json(payload)
|
|
274
|
+
except Exception:
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
async def set_tokens(self, tokens: OAuthToken) -> None:
|
|
278
|
+
try:
|
|
279
|
+
import keyring
|
|
280
|
+
|
|
281
|
+
keyring.set_password(self._service, self._token_key, tokens.model_dump_json())
|
|
282
|
+
# Update index
|
|
283
|
+
add_identity_to_index(self._service, self._identity)
|
|
284
|
+
except Exception:
|
|
285
|
+
pass
|
|
286
|
+
|
|
287
|
+
async def get_client_info(self) -> OAuthClientInformationFull | None:
|
|
288
|
+
try:
|
|
289
|
+
import keyring
|
|
290
|
+
|
|
291
|
+
payload = keyring.get_password(self._service, self._client_key)
|
|
292
|
+
if not payload:
|
|
293
|
+
return None
|
|
294
|
+
return OAuthClientInformationFull.model_validate_json(payload)
|
|
295
|
+
except Exception:
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
|
|
299
|
+
try:
|
|
300
|
+
import keyring
|
|
301
|
+
|
|
302
|
+
keyring.set_password(self._service, self._client_key, client_info.model_dump_json())
|
|
303
|
+
except Exception:
|
|
304
|
+
pass
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# --- Keyring index helpers (to enable cross-platform token enumeration) ---
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _index_username() -> str:
|
|
311
|
+
return "oauth:index"
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _read_index(service: str) -> set[str]:
|
|
315
|
+
try:
|
|
316
|
+
import json
|
|
317
|
+
|
|
318
|
+
import keyring
|
|
319
|
+
|
|
320
|
+
raw = keyring.get_password(service, _index_username())
|
|
321
|
+
if not raw:
|
|
322
|
+
return set()
|
|
323
|
+
data = json.loads(raw)
|
|
324
|
+
if isinstance(data, list):
|
|
325
|
+
return set([str(x) for x in data])
|
|
326
|
+
return set()
|
|
327
|
+
except Exception:
|
|
328
|
+
return set()
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _write_index(service: str, identities: set[str]) -> None:
|
|
332
|
+
try:
|
|
333
|
+
import json
|
|
334
|
+
|
|
335
|
+
import keyring
|
|
336
|
+
|
|
337
|
+
payload = json.dumps(sorted(list(identities)))
|
|
338
|
+
keyring.set_password(service, _index_username(), payload)
|
|
339
|
+
except Exception:
|
|
340
|
+
pass
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def add_identity_to_index(service: str, identity: str) -> None:
|
|
344
|
+
identities = _read_index(service)
|
|
345
|
+
if identity not in identities:
|
|
346
|
+
identities.add(identity)
|
|
347
|
+
_write_index(service, identities)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def remove_identity_from_index(service: str, identity: str) -> None:
|
|
351
|
+
identities = _read_index(service)
|
|
352
|
+
if identity in identities:
|
|
353
|
+
identities.remove(identity)
|
|
354
|
+
_write_index(service, identities)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def list_keyring_tokens(service: str = "fast-agent-mcp") -> list[str]:
|
|
358
|
+
"""List identities with stored tokens in keyring (using our index).
|
|
359
|
+
|
|
360
|
+
Returns only identities that currently have a corresponding token entry.
|
|
361
|
+
"""
|
|
362
|
+
try:
|
|
363
|
+
import keyring
|
|
364
|
+
|
|
365
|
+
identities = _read_index(service)
|
|
366
|
+
present: list[str] = []
|
|
367
|
+
for ident in sorted(identities):
|
|
368
|
+
tok_key = f"oauth:tokens:{ident}"
|
|
369
|
+
if keyring.get_password(service, tok_key):
|
|
370
|
+
present.append(ident)
|
|
371
|
+
return present
|
|
372
|
+
except Exception:
|
|
373
|
+
return []
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def clear_keyring_token(identity: str, service: str = "fast-agent-mcp") -> bool:
|
|
377
|
+
"""Remove token+client info for identity and update the index.
|
|
378
|
+
|
|
379
|
+
Returns True if anything was removed.
|
|
380
|
+
"""
|
|
381
|
+
removed = False
|
|
382
|
+
try:
|
|
383
|
+
import keyring
|
|
384
|
+
|
|
385
|
+
tok_key = f"oauth:tokens:{identity}"
|
|
386
|
+
cli_key = f"oauth:client_info:{identity}"
|
|
387
|
+
try:
|
|
388
|
+
keyring.delete_password(service, tok_key)
|
|
389
|
+
removed = True
|
|
390
|
+
except Exception:
|
|
391
|
+
pass
|
|
392
|
+
try:
|
|
393
|
+
keyring.delete_password(service, cli_key)
|
|
394
|
+
removed = True or removed
|
|
395
|
+
except Exception:
|
|
396
|
+
pass
|
|
397
|
+
if removed:
|
|
398
|
+
remove_identity_from_index(service, identity)
|
|
399
|
+
except Exception:
|
|
400
|
+
return False
|
|
401
|
+
return removed
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def build_oauth_provider(server_config: MCPServerSettings) -> OAuthClientProvider | None:
|
|
405
|
+
"""
|
|
406
|
+
Build an OAuthClientProvider for the given server config if applicable.
|
|
407
|
+
|
|
408
|
+
Returns None for unsupported transports, or when disabled via config.
|
|
409
|
+
"""
|
|
410
|
+
# Only for SSE/HTTP transports
|
|
411
|
+
if server_config.transport not in ("sse", "http"):
|
|
412
|
+
return None
|
|
413
|
+
|
|
414
|
+
# Determine if OAuth should be enabled. Default to True if no auth block provided
|
|
415
|
+
enable_oauth = True
|
|
416
|
+
redirect_port = 3030
|
|
417
|
+
redirect_path = "/callback"
|
|
418
|
+
scope_value: str | None = None
|
|
419
|
+
persist_mode: str = "keyring"
|
|
420
|
+
|
|
421
|
+
if server_config.auth is not None:
|
|
422
|
+
try:
|
|
423
|
+
enable_oauth = getattr(server_config.auth, "oauth", True)
|
|
424
|
+
redirect_port = getattr(server_config.auth, "redirect_port", 3030)
|
|
425
|
+
redirect_path = getattr(server_config.auth, "redirect_path", "/callback")
|
|
426
|
+
scope_field = getattr(server_config.auth, "scope", None)
|
|
427
|
+
persist_mode = getattr(server_config.auth, "persist", "keyring")
|
|
428
|
+
if isinstance(scope_field, list):
|
|
429
|
+
scope_value = " ".join(scope_field)
|
|
430
|
+
elif isinstance(scope_field, str):
|
|
431
|
+
scope_value = scope_field
|
|
432
|
+
except Exception:
|
|
433
|
+
logger.debug("Malformed auth configuration; using defaults.")
|
|
434
|
+
|
|
435
|
+
if not enable_oauth:
|
|
436
|
+
return None
|
|
437
|
+
|
|
438
|
+
base_url = _derive_base_server_url(server_config.url)
|
|
439
|
+
if not base_url:
|
|
440
|
+
# No usable URL -> cannot build provider
|
|
441
|
+
return None
|
|
442
|
+
|
|
443
|
+
# Construct client metadata with minimal defaults
|
|
444
|
+
redirect_uri = f"http://localhost:{redirect_port}{redirect_path}"
|
|
445
|
+
metadata_kwargs: dict[str, Any] = {
|
|
446
|
+
"client_name": "fast-agent",
|
|
447
|
+
"redirect_uris": [AnyUrl(redirect_uri)],
|
|
448
|
+
"grant_types": ["authorization_code", "refresh_token"],
|
|
449
|
+
"response_types": ["code"],
|
|
450
|
+
}
|
|
451
|
+
if scope_value:
|
|
452
|
+
metadata_kwargs["scope"] = scope_value
|
|
453
|
+
|
|
454
|
+
client_metadata = OAuthClientMetadata.model_validate(metadata_kwargs)
|
|
455
|
+
|
|
456
|
+
# Local callback server handler
|
|
457
|
+
async def _redirect_handler(authorization_url: str) -> None:
|
|
458
|
+
# Warn if persisting to keyring but no backend is available
|
|
459
|
+
await _print_authorization_link(
|
|
460
|
+
authorization_url,
|
|
461
|
+
warn_if_no_keyring=(persist_mode == "keyring"),
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
async def _callback_handler() -> tuple[str, str | None]:
|
|
465
|
+
# Try local HTTP capture first
|
|
466
|
+
try:
|
|
467
|
+
server = _CallbackServer(port=redirect_port, path=redirect_path)
|
|
468
|
+
server.start()
|
|
469
|
+
try:
|
|
470
|
+
code, state = server.wait(timeout_seconds=300)
|
|
471
|
+
return code, state
|
|
472
|
+
finally:
|
|
473
|
+
server.stop()
|
|
474
|
+
except Exception as e:
|
|
475
|
+
# Fallback to paste-URL flow
|
|
476
|
+
logger.info(f"OAuth local callback server unavailable, fallback to paste flow: {e}")
|
|
477
|
+
try:
|
|
478
|
+
import sys
|
|
479
|
+
|
|
480
|
+
print("Paste the full callback URL after authorization:", file=sys.stderr)
|
|
481
|
+
callback_url = input("Callback URL: ").strip()
|
|
482
|
+
except Exception as ee:
|
|
483
|
+
raise RuntimeError(f"Failed to read callback URL from user: {ee}")
|
|
484
|
+
|
|
485
|
+
params = parse_qs(urlparse(callback_url).query)
|
|
486
|
+
code = params.get("code", [None])[0]
|
|
487
|
+
state = params.get("state", [None])[0]
|
|
488
|
+
if not code:
|
|
489
|
+
raise RuntimeError("Callback URL missing authorization code")
|
|
490
|
+
return code, state
|
|
491
|
+
|
|
492
|
+
# Choose storage
|
|
493
|
+
storage: TokenStorage
|
|
494
|
+
if persist_mode == "keyring":
|
|
495
|
+
identity = compute_server_identity(server_config)
|
|
496
|
+
# Update index on write via storage methods; creation here doesn't modify index yet.
|
|
497
|
+
storage = KeyringTokenStorage(service_name="fast-agent-mcp", server_identity=identity)
|
|
498
|
+
else:
|
|
499
|
+
storage = InMemoryTokenStorage()
|
|
500
|
+
|
|
501
|
+
provider = OAuthClientProvider(
|
|
502
|
+
server_url=base_url,
|
|
503
|
+
client_metadata=client_metadata,
|
|
504
|
+
storage=storage,
|
|
505
|
+
redirect_handler=_redirect_handler,
|
|
506
|
+
callback_handler=_callback_handler,
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
return provider
|
fast_agent/mcp/prompt.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Prompt class for easily creating and working with MCP prompt content.
|
|
3
|
+
|
|
4
|
+
This implementation lives in the fast_agent namespace as part of the
|
|
5
|
+
migration away from fast_agent. A compatibility shim remains at
|
|
6
|
+
fast_agent.core.prompt importing this Prompt.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Literal, Union
|
|
11
|
+
|
|
12
|
+
from mcp import CallToolRequest
|
|
13
|
+
from mcp.types import ContentBlock, PromptMessage
|
|
14
|
+
|
|
15
|
+
from fast_agent.mcp.mcp_content import Assistant, MCPPrompt, User
|
|
16
|
+
from fast_agent.types import LlmStopReason, PromptMessageExtended
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Prompt:
|
|
20
|
+
"""
|
|
21
|
+
A helper class for working with MCP prompt content.
|
|
22
|
+
|
|
23
|
+
This class provides static methods to create:
|
|
24
|
+
- PromptMessage instances
|
|
25
|
+
- PromptMessageExtended instances
|
|
26
|
+
- Lists of messages for conversations
|
|
27
|
+
|
|
28
|
+
All methods intelligently handle various content types:
|
|
29
|
+
- Strings become TextContent
|
|
30
|
+
- Image file paths become ImageContent
|
|
31
|
+
- Other file paths become EmbeddedResource
|
|
32
|
+
- TextContent objects are used directly
|
|
33
|
+
- ImageContent objects are used directly
|
|
34
|
+
- EmbeddedResource objects are used directly
|
|
35
|
+
- Pre-formatted messages pass through unchanged
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def user(
|
|
40
|
+
cls,
|
|
41
|
+
*content_items: Union[
|
|
42
|
+
str, Path, bytes, dict, ContentBlock, PromptMessage, PromptMessageExtended
|
|
43
|
+
],
|
|
44
|
+
) -> PromptMessageExtended:
|
|
45
|
+
"""
|
|
46
|
+
Create a user PromptMessageExtended with various content items.
|
|
47
|
+
"""
|
|
48
|
+
# Handle PromptMessage and PromptMessageExtended directly
|
|
49
|
+
if len(content_items) == 1:
|
|
50
|
+
item = content_items[0]
|
|
51
|
+
if isinstance(item, PromptMessage):
|
|
52
|
+
return PromptMessageExtended(role="user", content=[item.content])
|
|
53
|
+
elif isinstance(item, PromptMessageExtended):
|
|
54
|
+
# Keep the content but change role to user
|
|
55
|
+
return PromptMessageExtended(role="user", content=item.content)
|
|
56
|
+
|
|
57
|
+
# Use the content factory for other types
|
|
58
|
+
messages = User(*content_items)
|
|
59
|
+
return PromptMessageExtended(role="user", content=[msg["content"] for msg in messages])
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def assistant(
|
|
63
|
+
cls,
|
|
64
|
+
*content_items: Union[
|
|
65
|
+
str, Path, bytes, dict, ContentBlock, PromptMessage, PromptMessageExtended
|
|
66
|
+
],
|
|
67
|
+
stop_reason: LlmStopReason | None = None,
|
|
68
|
+
tool_calls: dict[str, CallToolRequest] | None = None,
|
|
69
|
+
) -> PromptMessageExtended:
|
|
70
|
+
"""
|
|
71
|
+
Create an assistant PromptMessageExtended with various content items.
|
|
72
|
+
"""
|
|
73
|
+
# Handle PromptMessage and PromptMessageExtended directly
|
|
74
|
+
if len(content_items) == 1:
|
|
75
|
+
item = content_items[0]
|
|
76
|
+
if isinstance(item, PromptMessage):
|
|
77
|
+
return PromptMessageExtended(
|
|
78
|
+
role="assistant",
|
|
79
|
+
content=[item.content],
|
|
80
|
+
stop_reason=stop_reason,
|
|
81
|
+
tool_calls=tool_calls,
|
|
82
|
+
)
|
|
83
|
+
elif isinstance(item, PromptMessageExtended):
|
|
84
|
+
# Keep the content but change role to assistant
|
|
85
|
+
return PromptMessageExtended(
|
|
86
|
+
role="assistant",
|
|
87
|
+
content=item.content,
|
|
88
|
+
stop_reason=stop_reason,
|
|
89
|
+
tool_calls=tool_calls,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Use the content factory for other types
|
|
93
|
+
messages = Assistant(*content_items)
|
|
94
|
+
return PromptMessageExtended(
|
|
95
|
+
role="assistant",
|
|
96
|
+
content=[msg["content"] for msg in messages],
|
|
97
|
+
stop_reason=stop_reason,
|
|
98
|
+
tool_calls=tool_calls,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def message(
|
|
103
|
+
cls,
|
|
104
|
+
*content_items: Union[
|
|
105
|
+
str, Path, bytes, dict, ContentBlock, PromptMessage, PromptMessageExtended
|
|
106
|
+
],
|
|
107
|
+
role: Literal["user", "assistant"] = "user",
|
|
108
|
+
) -> PromptMessageExtended:
|
|
109
|
+
"""
|
|
110
|
+
Create a PromptMessageExtended with the specified role and content items.
|
|
111
|
+
"""
|
|
112
|
+
# Handle PromptMessage and PromptMessageExtended directly
|
|
113
|
+
if len(content_items) == 1:
|
|
114
|
+
item = content_items[0]
|
|
115
|
+
if isinstance(item, PromptMessage):
|
|
116
|
+
return PromptMessageExtended(role=role, content=[item.content])
|
|
117
|
+
elif isinstance(item, PromptMessageExtended):
|
|
118
|
+
# Keep the content but change role as specified
|
|
119
|
+
return PromptMessageExtended(role=role, content=item.content)
|
|
120
|
+
|
|
121
|
+
# Use the content factory for other types
|
|
122
|
+
messages = MCPPrompt(*content_items, role=role)
|
|
123
|
+
return PromptMessageExtended(
|
|
124
|
+
role=messages[0]["role"] if messages else role,
|
|
125
|
+
content=[msg["content"] for msg in messages],
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
def conversation(cls, *messages) -> list[PromptMessage]:
|
|
130
|
+
"""
|
|
131
|
+
Create a list of PromptMessages from various inputs.
|
|
132
|
+
"""
|
|
133
|
+
result = []
|
|
134
|
+
|
|
135
|
+
for item in messages:
|
|
136
|
+
if isinstance(item, PromptMessageExtended):
|
|
137
|
+
# Convert PromptMessageExtended to a list of PromptMessages
|
|
138
|
+
result.extend(item.from_multipart())
|
|
139
|
+
elif isinstance(item, dict) and "role" in item and "content" in item:
|
|
140
|
+
# Convert a single message dict to PromptMessage
|
|
141
|
+
result.append(PromptMessage(**item))
|
|
142
|
+
elif isinstance(item, list):
|
|
143
|
+
# Process each item in the list
|
|
144
|
+
for msg in item:
|
|
145
|
+
if isinstance(msg, dict) and "role" in msg and "content" in msg:
|
|
146
|
+
result.append(PromptMessage(**msg))
|
|
147
|
+
# Ignore other types
|
|
148
|
+
|
|
149
|
+
return result
|
|
150
|
+
|
|
151
|
+
@classmethod
|
|
152
|
+
def from_multipart(cls, multipart: list[PromptMessageExtended]) -> list[PromptMessage]:
|
|
153
|
+
"""
|
|
154
|
+
Convert a list of PromptMessageExtended objects to PromptMessages.
|
|
155
|
+
"""
|
|
156
|
+
result = []
|
|
157
|
+
for mp in multipart:
|
|
158
|
+
result.extend(mp.from_multipart())
|
|
159
|
+
return result
|