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,84 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from fast_agent.types import PromptMessageExtended
|
|
4
|
+
|
|
5
|
+
# Bedrock message format types
|
|
6
|
+
BedrockMessageParam = dict[str, Any]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BedrockConverter:
|
|
10
|
+
"""Converts MCP message types to Bedrock API format."""
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def convert_to_bedrock(multipart_msg: PromptMessageExtended) -> BedrockMessageParam:
|
|
14
|
+
"""
|
|
15
|
+
Convert a PromptMessageExtended message to Bedrock API format.
|
|
16
|
+
|
|
17
|
+
This is a wrapper around the instance method _convert_multipart_to_bedrock_message
|
|
18
|
+
to provide a static interface similar to AnthropicConverter.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
multipart_msg: The PromptMessageExtended message to convert
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
A Bedrock API message parameter dictionary
|
|
25
|
+
"""
|
|
26
|
+
# Simple conversion without needing BedrockLLM instance
|
|
27
|
+
bedrock_msg = {"role": multipart_msg.role, "content": []}
|
|
28
|
+
|
|
29
|
+
# Handle tool results first (if present)
|
|
30
|
+
if multipart_msg.tool_results:
|
|
31
|
+
import json
|
|
32
|
+
|
|
33
|
+
from mcp.types import TextContent
|
|
34
|
+
|
|
35
|
+
# Check if any tool ID indicates system prompt format
|
|
36
|
+
has_system_prompt_tools = any(
|
|
37
|
+
tool_id.startswith("system_prompt_") for tool_id in multipart_msg.tool_results.keys()
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if has_system_prompt_tools:
|
|
41
|
+
# For system prompt models: format as human-readable text
|
|
42
|
+
tool_result_parts = []
|
|
43
|
+
for tool_id, tool_result in multipart_msg.tool_results.items():
|
|
44
|
+
result_text = "".join(
|
|
45
|
+
part.text for part in tool_result.content if isinstance(part, TextContent)
|
|
46
|
+
)
|
|
47
|
+
result_payload = {
|
|
48
|
+
"tool_name": tool_id,
|
|
49
|
+
"status": "error" if tool_result.isError else "success",
|
|
50
|
+
"result": result_text,
|
|
51
|
+
}
|
|
52
|
+
tool_result_parts.append(json.dumps(result_payload))
|
|
53
|
+
|
|
54
|
+
if tool_result_parts:
|
|
55
|
+
full_result_text = f"Tool Results:\n{', '.join(tool_result_parts)}"
|
|
56
|
+
bedrock_msg["content"].append({"type": "text", "text": full_result_text})
|
|
57
|
+
else:
|
|
58
|
+
# For Nova/Anthropic models: use structured tool_result format
|
|
59
|
+
for tool_id, tool_result in multipart_msg.tool_results.items():
|
|
60
|
+
result_content_blocks = []
|
|
61
|
+
if tool_result.content:
|
|
62
|
+
for part in tool_result.content:
|
|
63
|
+
if isinstance(part, TextContent):
|
|
64
|
+
result_content_blocks.append({"text": part.text})
|
|
65
|
+
|
|
66
|
+
if not result_content_blocks:
|
|
67
|
+
result_content_blocks.append({"text": "[No content in tool result]"})
|
|
68
|
+
|
|
69
|
+
bedrock_msg["content"].append(
|
|
70
|
+
{
|
|
71
|
+
"type": "tool_result",
|
|
72
|
+
"tool_use_id": tool_id,
|
|
73
|
+
"content": result_content_blocks,
|
|
74
|
+
"status": "error" if tool_result.isError else "success",
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Handle regular content
|
|
79
|
+
from mcp.types import TextContent
|
|
80
|
+
for content_item in multipart_msg.content:
|
|
81
|
+
if isinstance(content_item, TextContent):
|
|
82
|
+
bedrock_msg["content"].append({"type": "text", "text": content_item.text})
|
|
83
|
+
|
|
84
|
+
return bedrock_msg
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
# Import necessary types from google.genai
|
|
5
|
+
from google.genai import types
|
|
6
|
+
from mcp import Tool
|
|
7
|
+
from mcp.types import (
|
|
8
|
+
BlobResourceContents,
|
|
9
|
+
CallToolRequest,
|
|
10
|
+
CallToolRequestParams,
|
|
11
|
+
CallToolResult,
|
|
12
|
+
ContentBlock,
|
|
13
|
+
EmbeddedResource,
|
|
14
|
+
ImageContent,
|
|
15
|
+
ResourceLink,
|
|
16
|
+
TextContent,
|
|
17
|
+
TextResourceContents,
|
|
18
|
+
)
|
|
19
|
+
from pydantic import AnyUrl
|
|
20
|
+
|
|
21
|
+
from fast_agent.mcp.helpers.content_helpers import (
|
|
22
|
+
get_image_data,
|
|
23
|
+
get_text,
|
|
24
|
+
is_image_content,
|
|
25
|
+
is_resource_content,
|
|
26
|
+
is_resource_link,
|
|
27
|
+
is_text_content,
|
|
28
|
+
)
|
|
29
|
+
from fast_agent.types import PromptMessageExtended, RequestParams
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class GoogleConverter:
|
|
33
|
+
"""
|
|
34
|
+
Converts between fast-agent and google.genai data structures.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def _clean_schema_for_google(self, schema: dict[str, Any]) -> dict[str, Any]:
|
|
38
|
+
"""
|
|
39
|
+
Recursively removes unsupported JSON schema keywords for google.genai.types.Schema.
|
|
40
|
+
Specifically removes 'additionalProperties', '$schema', 'exclusiveMaximum', and 'exclusiveMinimum'.
|
|
41
|
+
Also resolves $ref references and inlines $defs.
|
|
42
|
+
"""
|
|
43
|
+
# First, resolve any $ref references in the schema
|
|
44
|
+
schema = self._resolve_refs(schema, schema)
|
|
45
|
+
|
|
46
|
+
cleaned_schema = {}
|
|
47
|
+
unsupported_keys = {
|
|
48
|
+
"additionalProperties",
|
|
49
|
+
"$schema",
|
|
50
|
+
"exclusiveMaximum",
|
|
51
|
+
"exclusiveMinimum",
|
|
52
|
+
"$defs", # Remove $defs after resolving references
|
|
53
|
+
}
|
|
54
|
+
supported_string_formats = {"enum", "date-time"}
|
|
55
|
+
|
|
56
|
+
for key, value in schema.items():
|
|
57
|
+
if key in unsupported_keys:
|
|
58
|
+
continue # Skip this key
|
|
59
|
+
|
|
60
|
+
# Rewrite unsupported 'const' to a safe form for Gemini tools
|
|
61
|
+
# - For string const, convert to enum [value]
|
|
62
|
+
# - For non-string const (booleans/numbers), drop the constraint
|
|
63
|
+
if key == "const":
|
|
64
|
+
if isinstance(value, str):
|
|
65
|
+
cleaned_schema["enum"] = [value]
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
if (
|
|
69
|
+
key == "format"
|
|
70
|
+
and schema.get("type") == "string"
|
|
71
|
+
and value not in supported_string_formats
|
|
72
|
+
):
|
|
73
|
+
continue # Remove unsupported string formats
|
|
74
|
+
|
|
75
|
+
if isinstance(value, dict):
|
|
76
|
+
cleaned_schema[key] = self._clean_schema_for_google(value)
|
|
77
|
+
elif isinstance(value, list):
|
|
78
|
+
cleaned_schema[key] = [
|
|
79
|
+
self._clean_schema_for_google(item) if isinstance(item, dict) else item
|
|
80
|
+
for item in value
|
|
81
|
+
]
|
|
82
|
+
else:
|
|
83
|
+
cleaned_schema[key] = value
|
|
84
|
+
return cleaned_schema
|
|
85
|
+
|
|
86
|
+
def _resolve_refs(self, schema: dict[str, Any], root_schema: dict[str, Any]) -> dict[str, Any]:
|
|
87
|
+
"""
|
|
88
|
+
Resolve $ref references in a JSON schema by inlining the referenced definitions.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
schema: The current schema fragment being processed
|
|
92
|
+
root_schema: The root schema containing $defs
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Schema with $ref references resolved
|
|
96
|
+
"""
|
|
97
|
+
if not isinstance(schema, dict):
|
|
98
|
+
return schema
|
|
99
|
+
|
|
100
|
+
# If this is a $ref, resolve it
|
|
101
|
+
if "$ref" in schema:
|
|
102
|
+
ref_path = schema["$ref"]
|
|
103
|
+
if ref_path.startswith("#/"):
|
|
104
|
+
# Parse the reference path (e.g., "#/$defs/HumanInputRequest")
|
|
105
|
+
path_parts = ref_path[2:].split("/") # Remove "#/" and split
|
|
106
|
+
|
|
107
|
+
# Navigate to the referenced definition
|
|
108
|
+
ref_target = root_schema
|
|
109
|
+
for part in path_parts:
|
|
110
|
+
if part in ref_target:
|
|
111
|
+
ref_target = ref_target[part]
|
|
112
|
+
else:
|
|
113
|
+
# If reference not found, return the original schema
|
|
114
|
+
return schema
|
|
115
|
+
|
|
116
|
+
# Return the resolved definition (recursively resolve any nested refs)
|
|
117
|
+
return self._resolve_refs(ref_target, root_schema)
|
|
118
|
+
|
|
119
|
+
# Otherwise, recursively process all values in the schema
|
|
120
|
+
resolved = {}
|
|
121
|
+
for key, value in schema.items():
|
|
122
|
+
if isinstance(value, dict):
|
|
123
|
+
resolved[key] = self._resolve_refs(value, root_schema)
|
|
124
|
+
elif isinstance(value, list):
|
|
125
|
+
resolved[key] = [
|
|
126
|
+
self._resolve_refs(item, root_schema) if isinstance(item, dict) else item
|
|
127
|
+
for item in value
|
|
128
|
+
]
|
|
129
|
+
else:
|
|
130
|
+
resolved[key] = value
|
|
131
|
+
|
|
132
|
+
return resolved
|
|
133
|
+
|
|
134
|
+
def convert_to_google_content(
|
|
135
|
+
self, messages: list[PromptMessageExtended]
|
|
136
|
+
) -> list[types.Content]:
|
|
137
|
+
"""
|
|
138
|
+
Converts a list of fast-agent PromptMessageExtended to google.genai types.Content.
|
|
139
|
+
Handles different roles and content types (text, images, etc.).
|
|
140
|
+
"""
|
|
141
|
+
google_contents: list[types.Content] = []
|
|
142
|
+
for message in messages:
|
|
143
|
+
parts: list[types.Part] = []
|
|
144
|
+
for part_content in message.content: # renamed part to part_content to avoid conflict
|
|
145
|
+
if is_text_content(part_content):
|
|
146
|
+
parts.append(types.Part.from_text(text=get_text(part_content) or ""))
|
|
147
|
+
elif is_image_content(part_content):
|
|
148
|
+
assert isinstance(part_content, ImageContent)
|
|
149
|
+
image_bytes = base64.b64decode(get_image_data(part_content) or "")
|
|
150
|
+
parts.append(
|
|
151
|
+
types.Part.from_bytes(mime_type=part_content.mimeType, data=image_bytes)
|
|
152
|
+
)
|
|
153
|
+
elif is_resource_content(part_content):
|
|
154
|
+
assert isinstance(part_content, EmbeddedResource)
|
|
155
|
+
if "application/pdf" == part_content.resource.mimeType and isinstance(
|
|
156
|
+
part_content.resource, BlobResourceContents
|
|
157
|
+
):
|
|
158
|
+
pdf_bytes = base64.b64decode(part_content.resource.blob)
|
|
159
|
+
parts.append(
|
|
160
|
+
types.Part.from_bytes(
|
|
161
|
+
mime_type=part_content.resource.mimeType or "application/pdf",
|
|
162
|
+
data=pdf_bytes,
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
elif part_content.resource.mimeType and part_content.resource.mimeType.startswith(
|
|
166
|
+
"video/"
|
|
167
|
+
):
|
|
168
|
+
# Handle video content
|
|
169
|
+
if isinstance(part_content.resource, BlobResourceContents):
|
|
170
|
+
video_bytes = base64.b64decode(part_content.resource.blob)
|
|
171
|
+
parts.append(
|
|
172
|
+
types.Part.from_bytes(
|
|
173
|
+
mime_type=part_content.resource.mimeType,
|
|
174
|
+
data=video_bytes,
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
else:
|
|
178
|
+
# Handle non-blob video resources (YouTube URLs, File API URIs, etc.)
|
|
179
|
+
# Google supports YouTube URLs and File API URIs directly via file_data
|
|
180
|
+
uri_str = getattr(part_content.resource, "uri", None)
|
|
181
|
+
mime_str = getattr(part_content.resource, "mimeType", "video/mp4")
|
|
182
|
+
|
|
183
|
+
if uri_str:
|
|
184
|
+
# Use file_data for YouTube URLs and File API URIs
|
|
185
|
+
# Google accepts: YouTube URLs, gs:// URIs, and uploaded file URIs
|
|
186
|
+
parts.append(
|
|
187
|
+
types.Part.from_uri(
|
|
188
|
+
file_uri=str(uri_str),
|
|
189
|
+
mime_type=mime_str
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
else:
|
|
193
|
+
# Fallback if no URI is available
|
|
194
|
+
parts.append(
|
|
195
|
+
types.Part.from_text(
|
|
196
|
+
text=f"[Video Resource: No URI provided, MIME: {mime_str}]"
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
else:
|
|
200
|
+
# Check if the resource itself has text content
|
|
201
|
+
# Use get_text helper to extract text from various content types
|
|
202
|
+
resource_text = get_text(part_content.resource)
|
|
203
|
+
|
|
204
|
+
if resource_text is not None:
|
|
205
|
+
parts.append(types.Part.from_text(text=resource_text))
|
|
206
|
+
else:
|
|
207
|
+
# Fallback for other binary types or types without direct text
|
|
208
|
+
uri_str = getattr(part_content.resource, "uri", "unknown_uri")
|
|
209
|
+
mime_str = getattr(part_content.resource, "mimeType", "unknown_mime")
|
|
210
|
+
parts.append(
|
|
211
|
+
types.Part.from_text(
|
|
212
|
+
text=f"[Resource: {uri_str}, MIME: {mime_str}]"
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
elif is_resource_link(part_content):
|
|
216
|
+
# Handle ResourceLink - metadata reference to a resource
|
|
217
|
+
assert isinstance(part_content, ResourceLink)
|
|
218
|
+
mime = part_content.mimeType
|
|
219
|
+
uri_str = str(part_content.uri) if part_content.uri else None
|
|
220
|
+
|
|
221
|
+
# For media types (video/audio/image), use Part.from_uri() to let Google fetch
|
|
222
|
+
if uri_str and mime and (
|
|
223
|
+
mime.startswith("video/")
|
|
224
|
+
or mime.startswith("audio/")
|
|
225
|
+
or mime.startswith("image/")
|
|
226
|
+
):
|
|
227
|
+
parts.append(types.Part.from_uri(file_uri=uri_str, mime_type=mime))
|
|
228
|
+
else:
|
|
229
|
+
# Fallback to text representation for non-media types
|
|
230
|
+
text = get_text(part_content)
|
|
231
|
+
if text:
|
|
232
|
+
parts.append(types.Part.from_text(text=text))
|
|
233
|
+
|
|
234
|
+
if parts:
|
|
235
|
+
google_role = (
|
|
236
|
+
"user"
|
|
237
|
+
if message.role == "user"
|
|
238
|
+
else ("model" if message.role == "assistant" else "tool")
|
|
239
|
+
)
|
|
240
|
+
google_contents.append(types.Content(role=google_role, parts=parts))
|
|
241
|
+
return google_contents
|
|
242
|
+
|
|
243
|
+
def convert_to_google_tools(self, tools: list[Tool]) -> list[types.Tool]:
|
|
244
|
+
"""
|
|
245
|
+
Converts a list of fast-agent ToolDefinition to google.genai types.Tool.
|
|
246
|
+
"""
|
|
247
|
+
google_tools: list[types.Tool] = []
|
|
248
|
+
for tool in tools:
|
|
249
|
+
cleaned_input_schema = self._clean_schema_for_google(tool.inputSchema)
|
|
250
|
+
function_declaration = types.FunctionDeclaration(
|
|
251
|
+
name=tool.name,
|
|
252
|
+
description=tool.description if tool.description else "",
|
|
253
|
+
parameters=types.Schema(**cleaned_input_schema),
|
|
254
|
+
)
|
|
255
|
+
google_tools.append(types.Tool(function_declarations=[function_declaration]))
|
|
256
|
+
return google_tools
|
|
257
|
+
|
|
258
|
+
def convert_from_google_content(
|
|
259
|
+
self, content: types.Content
|
|
260
|
+
) -> list[ContentBlock | CallToolRequestParams]:
|
|
261
|
+
"""
|
|
262
|
+
Converts google.genai types.Content from a model response to a list of
|
|
263
|
+
fast-agent content types or tool call requests.
|
|
264
|
+
"""
|
|
265
|
+
fast_agent_parts: list[ContentBlock | CallToolRequestParams] = []
|
|
266
|
+
|
|
267
|
+
if content is None or not hasattr(content, "parts") or content.parts is None:
|
|
268
|
+
return [] # Google API response 'content' object is None. Cannot extract parts.
|
|
269
|
+
|
|
270
|
+
for part in content.parts:
|
|
271
|
+
if part.text:
|
|
272
|
+
fast_agent_parts.append(TextContent(type="text", text=part.text))
|
|
273
|
+
elif part.function_call:
|
|
274
|
+
fast_agent_parts.append(
|
|
275
|
+
CallToolRequestParams(
|
|
276
|
+
name=part.function_call.name,
|
|
277
|
+
arguments=part.function_call.args,
|
|
278
|
+
)
|
|
279
|
+
)
|
|
280
|
+
return fast_agent_parts
|
|
281
|
+
|
|
282
|
+
def convert_from_google_function_call(
|
|
283
|
+
self, function_call: types.FunctionCall
|
|
284
|
+
) -> CallToolRequest:
|
|
285
|
+
"""
|
|
286
|
+
Converts a single google.genai types.FunctionCall to a fast-agent CallToolRequest.
|
|
287
|
+
"""
|
|
288
|
+
return CallToolRequest(
|
|
289
|
+
method="tools/call",
|
|
290
|
+
params=CallToolRequestParams(
|
|
291
|
+
name=function_call.name,
|
|
292
|
+
arguments=function_call.args,
|
|
293
|
+
),
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
def convert_function_results_to_google(
|
|
297
|
+
self, tool_results: list[tuple[str, CallToolResult]]
|
|
298
|
+
) -> list[types.Content]:
|
|
299
|
+
"""
|
|
300
|
+
Converts a list of fast-agent tool results to google.genai types.Content
|
|
301
|
+
with role 'tool'. Handles multimodal content in tool results.
|
|
302
|
+
"""
|
|
303
|
+
google_tool_response_contents: list[types.Content] = []
|
|
304
|
+
for tool_name, tool_result in tool_results:
|
|
305
|
+
current_content_parts: list[types.Part] = []
|
|
306
|
+
textual_outputs: list[str] = []
|
|
307
|
+
media_parts: list[types.Part] = []
|
|
308
|
+
|
|
309
|
+
for item in tool_result.content:
|
|
310
|
+
if is_text_content(item):
|
|
311
|
+
textual_outputs.append(get_text(item) or "") # Ensure no None is added
|
|
312
|
+
elif is_image_content(item):
|
|
313
|
+
assert isinstance(item, ImageContent)
|
|
314
|
+
try:
|
|
315
|
+
image_bytes = base64.b64decode(get_image_data(item) or "")
|
|
316
|
+
media_parts.append(
|
|
317
|
+
types.Part.from_bytes(data=image_bytes, mime_type=item.mimeType)
|
|
318
|
+
)
|
|
319
|
+
except Exception as e:
|
|
320
|
+
textual_outputs.append(f"[Error processing image from tool result: {e}]")
|
|
321
|
+
elif is_resource_content(item):
|
|
322
|
+
assert isinstance(item, EmbeddedResource)
|
|
323
|
+
if (
|
|
324
|
+
"application/pdf" == item.resource.mimeType
|
|
325
|
+
and hasattr(item.resource, "blob")
|
|
326
|
+
and isinstance(item.resource, BlobResourceContents)
|
|
327
|
+
):
|
|
328
|
+
try:
|
|
329
|
+
pdf_bytes = base64.b64decode(item.resource.blob)
|
|
330
|
+
media_parts.append(
|
|
331
|
+
types.Part.from_bytes(
|
|
332
|
+
data=pdf_bytes,
|
|
333
|
+
mime_type=item.resource.mimeType or "application/pdf",
|
|
334
|
+
)
|
|
335
|
+
)
|
|
336
|
+
except Exception as e:
|
|
337
|
+
textual_outputs.append(f"[Error processing PDF from tool result: {e}]")
|
|
338
|
+
else:
|
|
339
|
+
# Check if the resource itself has text content
|
|
340
|
+
# Use get_text helper to extract text from various content types
|
|
341
|
+
resource_text = get_text(item.resource)
|
|
342
|
+
|
|
343
|
+
if resource_text is not None:
|
|
344
|
+
textual_outputs.append(resource_text)
|
|
345
|
+
else:
|
|
346
|
+
uri_str = getattr(item.resource, "uri", "unknown_uri")
|
|
347
|
+
mime_str = getattr(item.resource, "mimeType", "unknown_mime")
|
|
348
|
+
textual_outputs.append(
|
|
349
|
+
f"[Unhandled Resource in Tool: {uri_str}, MIME: {mime_str}]"
|
|
350
|
+
)
|
|
351
|
+
elif is_resource_link(item):
|
|
352
|
+
# Handle ResourceLink in tool results
|
|
353
|
+
assert isinstance(item, ResourceLink)
|
|
354
|
+
mime = item.mimeType
|
|
355
|
+
uri_str = str(item.uri) if item.uri else None
|
|
356
|
+
|
|
357
|
+
# For media types, use Part.from_uri() to let Google fetch
|
|
358
|
+
if uri_str and mime and (
|
|
359
|
+
mime.startswith("video/")
|
|
360
|
+
or mime.startswith("audio/")
|
|
361
|
+
or mime.startswith("image/")
|
|
362
|
+
):
|
|
363
|
+
media_parts.append(types.Part.from_uri(file_uri=uri_str, mime_type=mime))
|
|
364
|
+
else:
|
|
365
|
+
# Fallback to text representation for non-media types
|
|
366
|
+
text = get_text(item)
|
|
367
|
+
if text:
|
|
368
|
+
textual_outputs.append(text)
|
|
369
|
+
# Add handling for other content types if needed, for now they are skipped or become unhandled resource text
|
|
370
|
+
|
|
371
|
+
function_response_payload: dict[str, Any] = {"tool_name": tool_name}
|
|
372
|
+
if textual_outputs:
|
|
373
|
+
function_response_payload["text_content"] = "\n".join(textual_outputs)
|
|
374
|
+
|
|
375
|
+
# Only add media_parts if there are some, otherwise Gemini might error on empty parts for function response
|
|
376
|
+
if media_parts:
|
|
377
|
+
# Create the main FunctionResponse part
|
|
378
|
+
fn_response_part = types.Part.from_function_response(
|
|
379
|
+
name=tool_name, response=function_response_payload
|
|
380
|
+
)
|
|
381
|
+
current_content_parts.append(fn_response_part)
|
|
382
|
+
current_content_parts.extend(
|
|
383
|
+
media_parts
|
|
384
|
+
) # Add media parts after the main response part
|
|
385
|
+
else: # If no media parts, the textual output (if any) is the sole content of the function response
|
|
386
|
+
fn_response_part = types.Part.from_function_response(
|
|
387
|
+
name=tool_name, response=function_response_payload
|
|
388
|
+
)
|
|
389
|
+
current_content_parts.append(fn_response_part)
|
|
390
|
+
|
|
391
|
+
google_tool_response_contents.append(
|
|
392
|
+
types.Content(role="tool", parts=current_content_parts)
|
|
393
|
+
)
|
|
394
|
+
return google_tool_response_contents
|
|
395
|
+
|
|
396
|
+
def convert_request_params_to_google_config(
|
|
397
|
+
self, request_params: RequestParams
|
|
398
|
+
) -> types.GenerateContentConfig:
|
|
399
|
+
"""
|
|
400
|
+
Converts fast-agent RequestParams to google.genai types.GenerateContentConfig.
|
|
401
|
+
"""
|
|
402
|
+
config_args: dict[str, Any] = {}
|
|
403
|
+
if request_params.temperature is not None:
|
|
404
|
+
config_args["temperature"] = request_params.temperature
|
|
405
|
+
if request_params.maxTokens is not None:
|
|
406
|
+
config_args["max_output_tokens"] = request_params.maxTokens
|
|
407
|
+
if hasattr(request_params, "topK") and request_params.topK is not None:
|
|
408
|
+
config_args["top_k"] = request_params.topK
|
|
409
|
+
if hasattr(request_params, "topP") and request_params.topP is not None:
|
|
410
|
+
config_args["top_p"] = request_params.topP
|
|
411
|
+
if hasattr(request_params, "stopSequences") and request_params.stopSequences is not None:
|
|
412
|
+
config_args["stop_sequences"] = request_params.stopSequences
|
|
413
|
+
if (
|
|
414
|
+
hasattr(request_params, "presencePenalty")
|
|
415
|
+
and request_params.presencePenalty is not None
|
|
416
|
+
):
|
|
417
|
+
config_args["presence_penalty"] = request_params.presencePenalty
|
|
418
|
+
if (
|
|
419
|
+
hasattr(request_params, "frequencyPenalty")
|
|
420
|
+
and request_params.frequencyPenalty is not None
|
|
421
|
+
):
|
|
422
|
+
config_args["frequency_penalty"] = request_params.frequencyPenalty
|
|
423
|
+
if request_params.systemPrompt is not None:
|
|
424
|
+
config_args["system_instruction"] = request_params.systemPrompt
|
|
425
|
+
return types.GenerateContentConfig(**config_args)
|
|
426
|
+
|
|
427
|
+
def convert_from_google_content_list(
|
|
428
|
+
self, contents: list[types.Content]
|
|
429
|
+
) -> list[PromptMessageExtended]:
|
|
430
|
+
"""
|
|
431
|
+
Converts a list of google.genai types.Content to a list of fast-agent PromptMessageExtended.
|
|
432
|
+
"""
|
|
433
|
+
return [self._convert_from_google_content(content) for content in contents]
|
|
434
|
+
|
|
435
|
+
def _convert_from_google_content(self, content: types.Content) -> PromptMessageExtended:
|
|
436
|
+
"""
|
|
437
|
+
Converts a single google.genai types.Content to a fast-agent PromptMessageExtended.
|
|
438
|
+
"""
|
|
439
|
+
# Official fix for GitHub issue #207: Handle None content or content.parts
|
|
440
|
+
if content is None or not hasattr(content, "parts") or content.parts is None:
|
|
441
|
+
return PromptMessageExtended(role="assistant", content=[])
|
|
442
|
+
|
|
443
|
+
if content.role == "model" and any(part.function_call for part in content.parts):
|
|
444
|
+
return PromptMessageExtended(role="assistant", content=[])
|
|
445
|
+
|
|
446
|
+
fast_agent_parts: list[ContentBlock] = []
|
|
447
|
+
for part in content.parts:
|
|
448
|
+
if part.text:
|
|
449
|
+
fast_agent_parts.append(TextContent(type="text", text=part.text))
|
|
450
|
+
elif part.function_response:
|
|
451
|
+
response_text = str(part.function_response.response)
|
|
452
|
+
fast_agent_parts.append(TextContent(type="text", text=response_text))
|
|
453
|
+
elif part.file_data:
|
|
454
|
+
fast_agent_parts.append(
|
|
455
|
+
EmbeddedResource(
|
|
456
|
+
type="resource",
|
|
457
|
+
resource=TextResourceContents(
|
|
458
|
+
uri=AnyUrl(part.file_data.file_uri or ""),
|
|
459
|
+
mimeType=part.file_data.mime_type,
|
|
460
|
+
text=f"[Resource: {part.file_data.file_uri}, MIME: {part.file_data.mime_type}]",
|
|
461
|
+
),
|
|
462
|
+
)
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
fast_agent_role = "user" if content.role == "user" else "assistant"
|
|
466
|
+
return PromptMessageExtended(role=fast_agent_role, content=fast_agent_parts)
|