fast-agent-mcp 0.2.21__py3-none-any.whl → 0.2.22__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of fast-agent-mcp might be problematic. Click here for more details.
- {fast_agent_mcp-0.2.21.dist-info → fast_agent_mcp-0.2.22.dist-info}/METADATA +9 -7
- {fast_agent_mcp-0.2.21.dist-info → fast_agent_mcp-0.2.22.dist-info}/RECORD +17 -15
- mcp_agent/cli/commands/go.py +49 -11
- mcp_agent/config.py +13 -0
- mcp_agent/core/request_params.py +6 -1
- mcp_agent/event_progress.py +1 -1
- mcp_agent/llm/augmented_llm.py +3 -9
- mcp_agent/llm/model_factory.py +8 -0
- mcp_agent/llm/provider_types.py +1 -0
- mcp_agent/llm/providers/augmented_llm_anthropic.py +1 -0
- mcp_agent/llm/providers/augmented_llm_openai.py +14 -1
- mcp_agent/llm/providers/augmented_llm_tensorzero.py +442 -0
- mcp_agent/llm/providers/multipart_converter_tensorzero.py +200 -0
- mcp_agent/mcp/mcp_connection_manager.py +46 -4
- {fast_agent_mcp-0.2.21.dist-info → fast_agent_mcp-0.2.22.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.2.21.dist-info → fast_agent_mcp-0.2.22.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.2.21.dist-info → fast_agent_mcp-0.2.22.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: fast-agent-mcp
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.22
|
4
4
|
Summary: Define, Prompt and Test MCP enabled Agents and Workflows
|
5
5
|
Author-email: Shaun Smith <fastagent@llmindset.co.uk>, Sarmad Qadri <sarmad@lastmileai.dev>
|
6
6
|
License: Apache License
|
@@ -213,7 +213,7 @@ Requires-Dist: a2a-types>=0.1.0
|
|
213
213
|
Requires-Dist: aiohttp>=3.11.13
|
214
214
|
Requires-Dist: anthropic>=0.49.0
|
215
215
|
Requires-Dist: fastapi>=0.115.6
|
216
|
-
Requires-Dist: mcp
|
216
|
+
Requires-Dist: mcp>=1.8.0
|
217
217
|
Requires-Dist: openai>=1.63.2
|
218
218
|
Requires-Dist: opentelemetry-distro>=0.50b0
|
219
219
|
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.29.0
|
@@ -224,6 +224,7 @@ Requires-Dist: pydantic-settings>=2.7.0
|
|
224
224
|
Requires-Dist: pydantic>=2.10.4
|
225
225
|
Requires-Dist: pyyaml>=6.0.2
|
226
226
|
Requires-Dist: rich>=13.9.4
|
227
|
+
Requires-Dist: tensorzero>=2025.4.7
|
227
228
|
Requires-Dist: typer>=0.15.1
|
228
229
|
Provides-Extra: dev
|
229
230
|
Requires-Dist: anthropic>=0.42.0; extra == 'dev'
|
@@ -260,6 +261,7 @@ The simple declarative syntax lets you concentrate on composing your Prompts and
|
|
260
261
|
`fast-agent` is multi-modal, supporting Images and PDFs for both Anthropic and OpenAI endpoints via Prompts, Resources and MCP Tool Call results. The inclusion of passthrough and playback LLMs enable rapid development and test of Python glue-code for your applications.
|
261
262
|
|
262
263
|
> [!IMPORTANT]
|
264
|
+
>
|
263
265
|
> `fast-agent` The fast-agent documentation repo is here: https://github.com/evalstate/fast-agent-docs. Please feel free to submit PRs for documentation, experience reports or other content you think others may find helpful. All help and feedback warmly received.
|
264
266
|
|
265
267
|
### Agent Application Development
|
@@ -277,12 +279,12 @@ Simple model selection makes testing Model <-> MCP Server interaction painless.
|
|
277
279
|
Start by installing the [uv package manager](https://docs.astral.sh/uv/) for Python. Then:
|
278
280
|
|
279
281
|
```bash
|
280
|
-
uv pip install fast-agent-mcp
|
282
|
+
uv pip install fast-agent-mcp # install fast-agent!
|
281
283
|
|
282
|
-
fast-agent setup
|
283
|
-
uv run agent.py
|
284
|
-
uv run agent.py --model=o3-mini.low
|
285
|
-
fast-agent quickstart workflow
|
284
|
+
uv run fast-agent setup # create an example agent and config files
|
285
|
+
uv run agent.py # run your first agent
|
286
|
+
uv run agent.py --model=o3-mini.low # specify a model
|
287
|
+
uv run fast-agent quickstart workflow # create "building effective agents" examples
|
286
288
|
```
|
287
289
|
|
288
290
|
Other quickstart examples include a Researcher Agent (with Evaluator-Optimizer workflow) and Data Analysis Agent (similar to the ChatGPT experience), demonstrating MCP Roots support.
|
@@ -1,10 +1,10 @@
|
|
1
1
|
mcp_agent/__init__.py,sha256=18T0AG0W9sJhTY38O9GFFOzliDhxx9p87CvRyti9zbw,1620
|
2
2
|
mcp_agent/app.py,sha256=WRsiUdwy_9IAnaGRDwuLm7pzgQpt2wgsg10vBOpfcwM,5539
|
3
|
-
mcp_agent/config.py,sha256=
|
3
|
+
mcp_agent/config.py,sha256=ZC4SiIVbxVn7-hUfv3RFj6fNrXxvci6gmUNCGM7vzs8,12624
|
4
4
|
mcp_agent/console.py,sha256=Gjf2QLFumwG1Lav__c07X_kZxxEUSkzV-1_-YbAwcwo,813
|
5
5
|
mcp_agent/context.py,sha256=Kb3s_0MolHx7AeTs1NVcY3ly-xFBd35o8LT7Srpx9is,7334
|
6
6
|
mcp_agent/context_dependent.py,sha256=QXfhw3RaQCKfscEEBRGuZ3sdMWqkgShz2jJ1ivGGX1I,1455
|
7
|
-
mcp_agent/event_progress.py,sha256=
|
7
|
+
mcp_agent/event_progress.py,sha256=b1VKlQQF2AgPMb6XHjlJAVoPdx8GuxRTUk2g-4lBNm0,2749
|
8
8
|
mcp_agent/mcp_server_registry.py,sha256=jUmCdfcpTitXm1-3TxpWsdRWY_8phdKNYgXwB16ZSVU,10100
|
9
9
|
mcp_agent/progress_display.py,sha256=GeJU9VUt6qKsFVymG688hCMVCsAygG9ifiiEb5IcbN4,361
|
10
10
|
mcp_agent/agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -23,7 +23,7 @@ mcp_agent/cli/__main__.py,sha256=AVZ7tQFhU_sDOGuUGJq8ujgKtcxsYJBJwHbVaaiRDlI,166
|
|
23
23
|
mcp_agent/cli/main.py,sha256=XjrgXMBaPKkVqAFo8T9LJz6Tp1-ivrKDOuNYWke99YA,3090
|
24
24
|
mcp_agent/cli/terminal.py,sha256=GRwD-RGW7saIz2IOWZn5vD6JjiArscELBThm1GTFkuI,1065
|
25
25
|
mcp_agent/cli/commands/check_config.py,sha256=9Ryxo_fLInm3YKdYv46yLrAJgnQtMisGreu6Kkriw2g,16677
|
26
|
-
mcp_agent/cli/commands/go.py,sha256=
|
26
|
+
mcp_agent/cli/commands/go.py,sha256=2UY8TSDwhh_-p-WYXrZz3pEv3-2eTdBl5Lxy3JyJV0E,6057
|
27
27
|
mcp_agent/cli/commands/quickstart.py,sha256=SM3CHMzDgvTxIpKjFuX9BrS_N1vRoXNBDaO90aWx1Rk,14586
|
28
28
|
mcp_agent/cli/commands/setup.py,sha256=eOEd4TL-b0DaDeSJMGOfNOsTEItoZ67W88eTP4aP-bo,6482
|
29
29
|
mcp_agent/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -38,7 +38,7 @@ mcp_agent/core/fastagent.py,sha256=WEEGz2WBAddDGNeWJwqwFIPLiQnLjaNxZLoMR0peyyU,2
|
|
38
38
|
mcp_agent/core/interactive_prompt.py,sha256=w3VyRzW4hzn0xhWZRwo_qRRAD5WVSrJYe8QDe1XZ55Y,24252
|
39
39
|
mcp_agent/core/mcp_content.py,sha256=2D7KHY9mG_vxoDwFLKvsPQV9VRIzHItM7V-jcEnACh8,8878
|
40
40
|
mcp_agent/core/prompt.py,sha256=qnintOUGEoDPYLI9bu9G2OlgVMCe5ZPUZilgMzydXhc,7919
|
41
|
-
mcp_agent/core/request_params.py,sha256=
|
41
|
+
mcp_agent/core/request_params.py,sha256=vRfAz9T6Ir-0oeJ4qEdO62LDOzoLwBuuXcBcdh6WPZ8,1576
|
42
42
|
mcp_agent/core/validation.py,sha256=RIBKFlh0GJg4rTcFQXoXp8A0sK1HpsCigKcYSK3gFaY,12090
|
43
43
|
mcp_agent/executor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
44
44
|
mcp_agent/executor/executor.py,sha256=E44p6d-o3OMRoP_dNs_cDnyti91LQ3P9eNU88mSi1kc,9462
|
@@ -48,26 +48,28 @@ mcp_agent/human_input/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
|
|
48
48
|
mcp_agent/human_input/handler.py,sha256=s712Z5ssTCwjL9-VKoIdP5CtgMh43YvepynYisiWTTA,3144
|
49
49
|
mcp_agent/human_input/types.py,sha256=RtWBOVzy8vnYoQrc36jRLn8z8N3C4pDPMBN5vF6qM5Y,1476
|
50
50
|
mcp_agent/llm/__init__.py,sha256=d8zgwG-bRFuwiMNMYkywg_qytk4P8lawyld_meuUmHI,68
|
51
|
-
mcp_agent/llm/augmented_llm.py,sha256=
|
51
|
+
mcp_agent/llm/augmented_llm.py,sha256=CqtSGo_QrHE73tz_DHMd0wdt2F41gwuUu5Bue51FNm4,24199
|
52
52
|
mcp_agent/llm/augmented_llm_passthrough.py,sha256=zHcctNpwg4EFJvD1x9Eg443SVX-uyzFphLikwF_yVE0,6288
|
53
53
|
mcp_agent/llm/augmented_llm_playback.py,sha256=6L_RWIK__R67oZK7u3Xt3hWy1T2LnHXIO-efqgP3tPw,4177
|
54
54
|
mcp_agent/llm/memory.py,sha256=HQ_c1QemOUjrkY6Z2omE6BG5fXga7y4jN7KCMOuGjPs,3345
|
55
|
-
mcp_agent/llm/model_factory.py,sha256=
|
55
|
+
mcp_agent/llm/model_factory.py,sha256=h3NJSa0yPa9iiLojEqBhIm9wgEBB46ZBibe44MnskHM,8089
|
56
56
|
mcp_agent/llm/prompt_utils.py,sha256=yWQHykoK13QRF7evHUKxVF0SpVLN-Bsft0Yixzvn0g0,4825
|
57
57
|
mcp_agent/llm/provider_key_manager.py,sha256=-K_FuibN6hdSnweT32lB8mKTfCARnbja6zYYs0ErTKg,2802
|
58
|
-
mcp_agent/llm/provider_types.py,sha256=
|
58
|
+
mcp_agent/llm/provider_types.py,sha256=oWwXTlyr6hIzU_QLJ5T-UwxZGo5e4Pjwtahz2cr1PHg,364
|
59
59
|
mcp_agent/llm/sampling_converter.py,sha256=C7wPBlmT0eD90XWabC22zkxsrVHKCrjwIwg6cG628cI,2926
|
60
60
|
mcp_agent/llm/sampling_format_converter.py,sha256=xGz4odHpOcP7--eFaJaFtUR8eR9jxZS7MnLH6J7n0EU,1263
|
61
61
|
mcp_agent/llm/providers/__init__.py,sha256=heVxtmuqFJOnjjxHz4bWSqTAxXoN1E8twC_gQ_yJpHk,265
|
62
62
|
mcp_agent/llm/providers/anthropic_utils.py,sha256=vYDN5G5jKMhD2CQg8veJYab7tvvzYkDMq8M1g_hUAQg,3275
|
63
|
-
mcp_agent/llm/providers/augmented_llm_anthropic.py,sha256=
|
63
|
+
mcp_agent/llm/providers/augmented_llm_anthropic.py,sha256=gK_IvllVBNJUUrSfpgFpdhM-d4liCt0MLq7d2lXS7RI,15510
|
64
64
|
mcp_agent/llm/providers/augmented_llm_deepseek.py,sha256=NiZK5nv91ZS2VgVFXpbsFNFYLsLcppcbo_RstlRMd7I,1145
|
65
65
|
mcp_agent/llm/providers/augmented_llm_generic.py,sha256=5Uq8ZBhcFuQTt7koP_5ykolREh2iWu8zKhNbh3pM9lQ,1210
|
66
66
|
mcp_agent/llm/providers/augmented_llm_google.py,sha256=N0a2fphVtkvNYxKQpEX6J4tlO1C_mRw4sw3LBXnrOeI,1130
|
67
|
-
mcp_agent/llm/providers/augmented_llm_openai.py,sha256=
|
67
|
+
mcp_agent/llm/providers/augmented_llm_openai.py,sha256=0C7BOB7i3xo0HsMCTagRSQ8Hsywb-31mot26OfohzCU,14478
|
68
68
|
mcp_agent/llm/providers/augmented_llm_openrouter.py,sha256=V_TlVKm92GHBxYIo6gpvH_6cAaIdppS25Tz6x5T7LW0,2341
|
69
|
+
mcp_agent/llm/providers/augmented_llm_tensorzero.py,sha256=Mol_Wzj_ZtccW-LMw0oFwWUt1m1yfofloay9QYNP23c,20729
|
69
70
|
mcp_agent/llm/providers/multipart_converter_anthropic.py,sha256=t5lHYGfFUacJldnrVtMNW-8gEMoto8Y7hJkDrnyZR-Y,16650
|
70
71
|
mcp_agent/llm/providers/multipart_converter_openai.py,sha256=XPIulWntNpZWNGWrc240StPzok2RqrDAV7OigDwQ1uU,15850
|
72
|
+
mcp_agent/llm/providers/multipart_converter_tensorzero.py,sha256=BFTdyVk42HZskDAuTHicfDTUJq89d1fz8C9nAOuHxlE,8646
|
71
73
|
mcp_agent/llm/providers/openai_multipart.py,sha256=qKBn7d3jSabnJmVgWweVzqh8q9mBqr09fsPmP92niAQ,6899
|
72
74
|
mcp_agent/llm/providers/openai_utils.py,sha256=T4bTCL9f7DsoS_zoKgQKv_FUv_4n98vgbvaUpdWZJr8,1875
|
73
75
|
mcp_agent/llm/providers/sampling_converter_anthropic.py,sha256=35WzBWkPklnuMlu5S6XsQIq0YL58NOy8Ja6A_l4m6eM,1612
|
@@ -85,7 +87,7 @@ mcp_agent/mcp/interfaces.py,sha256=PAou8znAl2HgtvfCpLQOZFbKra9F72OcVRfBJbboNX8,6
|
|
85
87
|
mcp_agent/mcp/logger_textio.py,sha256=vljC1BtNTCxBAda9ExqNB-FwVNUZIuJT3h1nWmCjMws,3172
|
86
88
|
mcp_agent/mcp/mcp_agent_client_session.py,sha256=Ng7epBXq8BEA_3m1GX5LqwafgNUAMSzBugwN6N0VUWQ,4364
|
87
89
|
mcp_agent/mcp/mcp_aggregator.py,sha256=lVSt0yp0CnaYjcHCWmluwBeFgl8JXHYEZk0MzXgrQzA,40110
|
88
|
-
mcp_agent/mcp/mcp_connection_manager.py,sha256=
|
90
|
+
mcp_agent/mcp/mcp_connection_manager.py,sha256=6jtjclh4YNJZsNwYnSWmQ6cPzapAwsRUxir1c_gVNfM,16051
|
89
91
|
mcp_agent/mcp/mime_utils.py,sha256=difepNR_gpb4MpMLkBRAoyhDk-AjXUHTiqKvT_VwS1o,1805
|
90
92
|
mcp_agent/mcp/prompt_message_multipart.py,sha256=BDwRdNwyWHb2q2bccDb2iR2VlORqVvkvoG3xYzcMpCE,4403
|
91
93
|
mcp_agent/mcp/prompt_render.py,sha256=k3v4BZDThGE2gGiOYVQtA6x8WTEdOuXIEnRafANhN1U,2996
|
@@ -143,8 +145,8 @@ mcp_agent/resources/examples/workflows/parallel.py,sha256=DQ5vY5-h8Qa5QHcYjsWXhZ
|
|
143
145
|
mcp_agent/resources/examples/workflows/router.py,sha256=E4x_-c3l4YW9w1i4ARcDtkdeqIdbWEGfsMzwLYpdbVc,1677
|
144
146
|
mcp_agent/resources/examples/workflows/short_story.txt,sha256=X3y_1AyhLFN2AKzCKvucJtDgAFIJfnlbsbGZO5bBWu0,1187
|
145
147
|
mcp_agent/ui/console_display.py,sha256=TVGDtJ37hc6UG0ei9g7ZPZZfFNeS1MYozt-Mx8HsPCk,9752
|
146
|
-
fast_agent_mcp-0.2.
|
147
|
-
fast_agent_mcp-0.2.
|
148
|
-
fast_agent_mcp-0.2.
|
149
|
-
fast_agent_mcp-0.2.
|
150
|
-
fast_agent_mcp-0.2.
|
148
|
+
fast_agent_mcp-0.2.22.dist-info/METADATA,sha256=HbiOG6NhC3IEfEyBsUDQnihh2LbU4CEZEe19X4Y7VTQ,30194
|
149
|
+
fast_agent_mcp-0.2.22.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
150
|
+
fast_agent_mcp-0.2.22.dist-info/entry_points.txt,sha256=bRniFM5zk3Kix5z7scX0gf9VnmGQ2Cz_Q1Gh7Ir4W00,186
|
151
|
+
fast_agent_mcp-0.2.22.dist-info/licenses/LICENSE,sha256=cN3FxDURL9XuzE5mhK9L2paZo82LTfjwCYVT7e3j0e4,10939
|
152
|
+
fast_agent_mcp-0.2.22.dist-info/RECORD,,
|
mcp_agent/cli/commands/go.py
CHANGED
@@ -18,8 +18,13 @@ async def _run_agent(
|
|
18
18
|
config_path: Optional[str] = None,
|
19
19
|
server_list: Optional[List[str]] = None,
|
20
20
|
model: Optional[str] = None,
|
21
|
+
message: Optional[str] = None,
|
22
|
+
prompt_file: Optional[str] = None
|
21
23
|
) -> None:
|
22
24
|
"""Async implementation to run an interactive agent."""
|
25
|
+
from pathlib import Path
|
26
|
+
|
27
|
+
from mcp_agent.mcp.prompts.prompt_load import load_prompt_multipart
|
23
28
|
|
24
29
|
# Create the FastAgent instance with CLI arg parsing enabled
|
25
30
|
# It will automatically parse args like --model, --quiet, etc.
|
@@ -27,6 +32,7 @@ async def _run_agent(
|
|
27
32
|
"name": name,
|
28
33
|
"config_path": config_path,
|
29
34
|
"ignore_unknown_args": True,
|
35
|
+
"parse_cli_args": False, # Don't parse CLI args, we're handling it ourselves
|
30
36
|
}
|
31
37
|
|
32
38
|
fast = FastAgent(**fast_kwargs)
|
@@ -38,10 +44,26 @@ async def _run_agent(
|
|
38
44
|
if model:
|
39
45
|
agent_kwargs["model"] = model
|
40
46
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
47
|
+
# Handle prompt file and message options
|
48
|
+
if message or prompt_file:
|
49
|
+
@fast.agent(**agent_kwargs)
|
50
|
+
async def cli_agent():
|
51
|
+
async with fast.run() as agent:
|
52
|
+
if message:
|
53
|
+
response = await agent.send(message)
|
54
|
+
# Print the response and exit
|
55
|
+
print(response)
|
56
|
+
elif prompt_file:
|
57
|
+
prompt = load_prompt_multipart(Path(prompt_file))
|
58
|
+
response = await agent.generate(prompt)
|
59
|
+
# Print the response text and exit
|
60
|
+
print(response.last_text())
|
61
|
+
else:
|
62
|
+
# Standard interactive mode
|
63
|
+
@fast.agent(**agent_kwargs)
|
64
|
+
async def cli_agent():
|
65
|
+
async with fast.run() as agent:
|
66
|
+
await agent.interactive()
|
45
67
|
|
46
68
|
# Run the agent
|
47
69
|
await cli_agent()
|
@@ -51,7 +73,9 @@ def run_async_agent(
|
|
51
73
|
instruction: str,
|
52
74
|
config_path: Optional[str] = None,
|
53
75
|
servers: Optional[str] = None,
|
54
|
-
model: Optional[str] = None
|
76
|
+
model: Optional[str] = None,
|
77
|
+
message: Optional[str] = None,
|
78
|
+
prompt_file: Optional[str] = None
|
55
79
|
):
|
56
80
|
"""Run the async agent function with proper loop handling."""
|
57
81
|
server_list = servers.split(',') if servers else None
|
@@ -75,7 +99,9 @@ def run_async_agent(
|
|
75
99
|
instruction=instruction,
|
76
100
|
config_path=config_path,
|
77
101
|
server_list=server_list,
|
78
|
-
model=model
|
102
|
+
model=model,
|
103
|
+
message=message,
|
104
|
+
prompt_file=prompt_file
|
79
105
|
))
|
80
106
|
finally:
|
81
107
|
try:
|
@@ -108,26 +134,38 @@ def go(
|
|
108
134
|
model: Optional[str] = typer.Option(
|
109
135
|
None, "--model", help="Override the default model (e.g., haiku, sonnet, gpt-4)"
|
110
136
|
),
|
137
|
+
message: Optional[str] = typer.Option(
|
138
|
+
None, "--message", "-m", help="Message to send to the agent (skips interactive mode)"
|
139
|
+
),
|
140
|
+
prompt_file: Optional[str] = typer.Option(
|
141
|
+
None, "--prompt-file", "-p", help="Path to a prompt file to use (either text or JSON)"
|
142
|
+
),
|
111
143
|
) -> None:
|
112
144
|
"""
|
113
145
|
Run an interactive agent directly from the command line.
|
114
146
|
|
115
|
-
|
147
|
+
Examples:
|
116
148
|
fast-agent go --model=haiku --instruction="You are a coding assistant" --servers=fetch,filesystem
|
149
|
+
fast-agent go --message="What is the weather today?" --model=haiku
|
150
|
+
fast-agent go --prompt-file=my-prompt.txt --model=haiku
|
117
151
|
|
118
152
|
This will start an interactive session with the agent, using the specified model
|
119
153
|
and instruction. It will use the default configuration from fastagent.config.yaml
|
120
154
|
unless --config-path is specified.
|
121
155
|
|
122
156
|
Common options:
|
123
|
-
--model
|
124
|
-
--quiet
|
125
|
-
--servers
|
157
|
+
--model Override the default model (e.g., --model=haiku)
|
158
|
+
--quiet Disable progress display and logging
|
159
|
+
--servers Comma-separated list of server names to enable from config
|
160
|
+
--message, -m Send a single message and exit
|
161
|
+
--prompt-file, -p Use a prompt file instead of interactive mode
|
126
162
|
"""
|
127
163
|
run_async_agent(
|
128
164
|
name=name,
|
129
165
|
instruction=instruction,
|
130
166
|
config_path=config_path,
|
131
167
|
servers=servers,
|
132
|
-
model=model
|
168
|
+
model=model,
|
169
|
+
message=message,
|
170
|
+
prompt_file=prompt_file
|
133
171
|
)
|
mcp_agent/config.py
CHANGED
@@ -198,6 +198,16 @@ class OpenTelemetrySettings(BaseModel):
|
|
198
198
|
"""Sample rate for tracing (1.0 = sample everything)"""
|
199
199
|
|
200
200
|
|
201
|
+
class TensorZeroSettings(BaseModel):
|
202
|
+
"""
|
203
|
+
Settings for using TensorZero via its OpenAI-compatible API.
|
204
|
+
"""
|
205
|
+
|
206
|
+
base_url: Optional[str] = None
|
207
|
+
api_key: Optional[str] = None
|
208
|
+
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
|
209
|
+
|
210
|
+
|
201
211
|
class LoggerSettings(BaseModel):
|
202
212
|
"""
|
203
213
|
Logger settings for the fast-agent application.
|
@@ -287,6 +297,9 @@ class Settings(BaseSettings):
|
|
287
297
|
generic: GenericSettings | None = None
|
288
298
|
"""Settings for using Generic models in the fast-agent application"""
|
289
299
|
|
300
|
+
tensorzero: Optional[TensorZeroSettings] = None
|
301
|
+
"""Settings for using TensorZero inference gateway"""
|
302
|
+
|
290
303
|
logger: LoggerSettings | None = LoggerSettings()
|
291
304
|
"""Logger settings for the fast-agent application"""
|
292
305
|
|
mcp_agent/core/request_params.py
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
Request parameters definitions for LLM interactions.
|
3
3
|
"""
|
4
4
|
|
5
|
-
from typing import Any, List
|
5
|
+
from typing import Any, Dict, List
|
6
6
|
|
7
7
|
from mcp import SamplingMessage
|
8
8
|
from mcp.types import CreateMessageRequestParams
|
@@ -48,3 +48,8 @@ class RequestParams(CreateMessageRequestParams):
|
|
48
48
|
"""
|
49
49
|
Override response format for structured calls. Prefer sending pydantic model - only use in exceptional circumstances
|
50
50
|
"""
|
51
|
+
|
52
|
+
template_vars: Dict[str, Any] = Field(default_factory=dict)
|
53
|
+
"""
|
54
|
+
Optional dictionary of template variables for dynamic templates. Currently only works for TensorZero inference backend
|
55
|
+
"""
|
mcp_agent/event_progress.py
CHANGED
mcp_agent/llm/augmented_llm.py
CHANGED
@@ -76,20 +76,14 @@ def deep_merge(dict1: Dict[Any, Any], dict2: Dict[Any, Any]) -> Dict[Any, Any]:
|
|
76
76
|
Dict: The updated `dict1`.
|
77
77
|
"""
|
78
78
|
for key in dict2:
|
79
|
-
if (
|
80
|
-
key in dict1
|
81
|
-
and isinstance(dict1[key], dict)
|
82
|
-
and isinstance(dict2[key], dict)
|
83
|
-
):
|
79
|
+
if key in dict1 and isinstance(dict1[key], dict) and isinstance(dict2[key], dict):
|
84
80
|
deep_merge(dict1[key], dict2[key])
|
85
81
|
else:
|
86
82
|
dict1[key] = dict2[key]
|
87
83
|
return dict1
|
88
84
|
|
89
85
|
|
90
|
-
class AugmentedLLM(
|
91
|
-
ContextDependent, AugmentedLLMProtocol, Generic[MessageParamT, MessageT]
|
92
|
-
):
|
86
|
+
class AugmentedLLM(ContextDependent, AugmentedLLMProtocol, Generic[MessageParamT, MessageT]):
|
93
87
|
# Common parameter names used across providers
|
94
88
|
PARAM_MESSAGES = "messages"
|
95
89
|
PARAM_MODEL = "model"
|
@@ -100,7 +94,7 @@ class AugmentedLLM(
|
|
100
94
|
PARAM_METADATA = "metadata"
|
101
95
|
PARAM_USE_HISTORY = "use_history"
|
102
96
|
PARAM_MAX_ITERATIONS = "max_iterations"
|
103
|
-
|
97
|
+
PARAM_TEMPLATE_VARS = "template_vars"
|
104
98
|
# Base set of fields that should always be excluded
|
105
99
|
BASE_EXCLUDE_FIELDS = {PARAM_METADATA}
|
106
100
|
|
mcp_agent/llm/model_factory.py
CHANGED
@@ -15,6 +15,7 @@ from mcp_agent.llm.providers.augmented_llm_generic import GenericAugmentedLLM
|
|
15
15
|
from mcp_agent.llm.providers.augmented_llm_google import GoogleAugmentedLLM
|
16
16
|
from mcp_agent.llm.providers.augmented_llm_openai import OpenAIAugmentedLLM
|
17
17
|
from mcp_agent.llm.providers.augmented_llm_openrouter import OpenRouterAugmentedLLM
|
18
|
+
from mcp_agent.llm.providers.augmented_llm_tensorzero import TensorZeroAugmentedLLM
|
18
19
|
from mcp_agent.mcp.interfaces import AugmentedLLMProtocol
|
19
20
|
|
20
21
|
# from mcp_agent.workflows.llm.augmented_llm_deepseek import DeekSeekAugmentedLLM
|
@@ -28,6 +29,7 @@ LLMClass = Union[
|
|
28
29
|
Type[PlaybackLLM],
|
29
30
|
Type[DeepSeekAugmentedLLM],
|
30
31
|
Type[OpenRouterAugmentedLLM],
|
32
|
+
Type[TensorZeroAugmentedLLM],
|
31
33
|
]
|
32
34
|
|
33
35
|
|
@@ -110,6 +112,7 @@ class ModelFactory:
|
|
110
112
|
Provider.GENERIC: GenericAugmentedLLM,
|
111
113
|
Provider.GOOGLE: GoogleAugmentedLLM, # type: ignore
|
112
114
|
Provider.OPENROUTER: OpenRouterAugmentedLLM,
|
115
|
+
Provider.TENSORZERO: TensorZeroAugmentedLLM,
|
113
116
|
}
|
114
117
|
|
115
118
|
# Mapping of special model names to their specific LLM classes
|
@@ -142,6 +145,11 @@ class ModelFactory:
|
|
142
145
|
provider = Provider(potential_provider)
|
143
146
|
model_parts = model_parts[1:]
|
144
147
|
|
148
|
+
if provider == Provider.TENSORZERO and not model_parts:
|
149
|
+
raise ModelConfigError(
|
150
|
+
f"TensorZero provider requires a function name after the provider "
|
151
|
+
f"(e.g., tensorzero.my-function), got: {model_string}"
|
152
|
+
)
|
145
153
|
# Join remaining parts as model name
|
146
154
|
model_name = ".".join(model_parts)
|
147
155
|
|
mcp_agent/llm/provider_types.py
CHANGED
@@ -62,6 +62,7 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
|
|
62
62
|
AugmentedLLM.PARAM_USE_HISTORY,
|
63
63
|
AugmentedLLM.PARAM_MAX_ITERATIONS,
|
64
64
|
AugmentedLLM.PARAM_PARALLEL_TOOL_CALLS,
|
65
|
+
AugmentedLLM.PARAM_TEMPLATE_VARS,
|
65
66
|
}
|
66
67
|
|
67
68
|
def __init__(self, *args, **kwargs) -> None:
|
@@ -56,6 +56,7 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
|
|
56
56
|
AugmentedLLM.PARAM_PARALLEL_TOOL_CALLS,
|
57
57
|
AugmentedLLM.PARAM_USE_HISTORY,
|
58
58
|
AugmentedLLM.PARAM_MAX_ITERATIONS,
|
59
|
+
AugmentedLLM.PARAM_TEMPLATE_VARS,
|
59
60
|
}
|
60
61
|
|
61
62
|
def __init__(self, provider: Provider = Provider.OPENAI, *args, **kwargs) -> None:
|
@@ -143,7 +144,7 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
|
|
143
144
|
function={
|
144
145
|
"name": tool.name,
|
145
146
|
"description": tool.description if tool.description else "",
|
146
|
-
"parameters": tool.inputSchema,
|
147
|
+
"parameters": self.adjust_schema(tool.inputSchema),
|
147
148
|
},
|
148
149
|
)
|
149
150
|
for tool in response.tools
|
@@ -350,3 +351,15 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
|
|
350
351
|
base_args, request_params, self.OPENAI_EXCLUDE_FIELDS.union(self.BASE_EXCLUDE_FIELDS)
|
351
352
|
)
|
352
353
|
return arguments
|
354
|
+
|
355
|
+
def adjust_schema(self, inputSchema: Dict) -> Dict:
|
356
|
+
# return inputSchema
|
357
|
+
if not Provider.OPENAI == self.provider:
|
358
|
+
return inputSchema
|
359
|
+
|
360
|
+
if "properties" in inputSchema:
|
361
|
+
return inputSchema
|
362
|
+
|
363
|
+
result = inputSchema.copy()
|
364
|
+
result["properties"] = {}
|
365
|
+
return result
|
@@ -0,0 +1,442 @@
|
|
1
|
+
import json
|
2
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
3
|
+
|
4
|
+
from mcp.types import (
|
5
|
+
CallToolRequest,
|
6
|
+
CallToolRequestParams,
|
7
|
+
CallToolResult,
|
8
|
+
EmbeddedResource,
|
9
|
+
ImageContent,
|
10
|
+
TextContent,
|
11
|
+
)
|
12
|
+
from tensorzero import AsyncTensorZeroGateway
|
13
|
+
from tensorzero.types import (
|
14
|
+
ChatInferenceResponse,
|
15
|
+
JsonInferenceResponse,
|
16
|
+
TensorZeroError,
|
17
|
+
)
|
18
|
+
|
19
|
+
from mcp_agent.agents.agent import Agent
|
20
|
+
from mcp_agent.core.exceptions import ModelConfigError
|
21
|
+
from mcp_agent.core.request_params import RequestParams
|
22
|
+
from mcp_agent.llm.augmented_llm import AugmentedLLM
|
23
|
+
from mcp_agent.llm.memory import Memory, SimpleMemory
|
24
|
+
from mcp_agent.llm.provider_types import Provider
|
25
|
+
from mcp_agent.llm.providers.multipart_converter_tensorzero import TensorZeroConverter
|
26
|
+
from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
|
27
|
+
|
28
|
+
|
29
|
+
class TensorZeroAugmentedLLM(AugmentedLLM[Dict[str, Any], Any]):
|
30
|
+
"""
|
31
|
+
AugmentedLLM implementation for TensorZero using its native API.
|
32
|
+
Uses the Converter pattern for message formatting.
|
33
|
+
Implements multi-turn tool calling logic, storing API dicts in history.
|
34
|
+
"""
|
35
|
+
|
36
|
+
def __init__(
|
37
|
+
self,
|
38
|
+
agent: Agent,
|
39
|
+
model: str,
|
40
|
+
request_params: Optional[RequestParams] = None,
|
41
|
+
**kwargs: Any,
|
42
|
+
):
|
43
|
+
self._t0_gateway: Optional[AsyncTensorZeroGateway] = None
|
44
|
+
self._t0_function_name: str = model
|
45
|
+
self._t0_episode_id: Optional[str] = kwargs.get("episode_id")
|
46
|
+
|
47
|
+
super().__init__(
|
48
|
+
agent=agent,
|
49
|
+
model=model,
|
50
|
+
provider=Provider.TENSORZERO,
|
51
|
+
request_params=request_params,
|
52
|
+
**kwargs,
|
53
|
+
)
|
54
|
+
|
55
|
+
self.history: Memory[Dict[str, Any]] = SimpleMemory[Dict[str, Any]]()
|
56
|
+
|
57
|
+
self.logger.info(
|
58
|
+
f"TensorZero LLM provider initialized for function '{self._t0_function_name}'. History type: {type(self.history)}"
|
59
|
+
)
|
60
|
+
|
61
|
+
@staticmethod
|
62
|
+
def block_to_dict(block: Any) -> Dict[str, Any]:
|
63
|
+
if hasattr(block, "model_dump"):
|
64
|
+
try:
|
65
|
+
dumped = block.model_dump(mode="json")
|
66
|
+
if dumped:
|
67
|
+
return dumped
|
68
|
+
except Exception:
|
69
|
+
pass
|
70
|
+
if hasattr(block, "__dict__"):
|
71
|
+
try:
|
72
|
+
block_vars = vars(block)
|
73
|
+
if block_vars:
|
74
|
+
return block_vars
|
75
|
+
except Exception:
|
76
|
+
pass
|
77
|
+
if isinstance(block, (str, int, float, bool, list, dict, type(None))):
|
78
|
+
return {"type": "raw", "content": block}
|
79
|
+
|
80
|
+
# Basic attribute extraction as fallback
|
81
|
+
d = {"type": getattr(block, "type", "unknown")}
|
82
|
+
for attr in ["id", "name", "text", "arguments"]:
|
83
|
+
if hasattr(block, attr):
|
84
|
+
d[attr] = getattr(block, attr)
|
85
|
+
if len(d) == 1 and d.get("type") == "unknown":
|
86
|
+
d["content"] = str(block)
|
87
|
+
return d
|
88
|
+
|
89
|
+
def _initialize_default_params(self, kwargs: dict) -> RequestParams:
|
90
|
+
func_name = kwargs.get("model", self._t0_function_name or "unknown_t0_function")
|
91
|
+
return RequestParams(
|
92
|
+
model=func_name,
|
93
|
+
systemPrompt=self.instruction,
|
94
|
+
maxTokens=4096,
|
95
|
+
use_history=True,
|
96
|
+
max_iterations=10, # Max iterations for tool use loop
|
97
|
+
parallel_tool_calls=True,
|
98
|
+
)
|
99
|
+
|
100
|
+
async def _initialize_gateway(self) -> AsyncTensorZeroGateway:
|
101
|
+
if self._t0_gateway is None:
|
102
|
+
self.logger.debug("Initializing AsyncTensorZeroGateway client...")
|
103
|
+
try:
|
104
|
+
base_url: Optional[str] = None
|
105
|
+
default_url = "http://localhost:3000"
|
106
|
+
|
107
|
+
if (
|
108
|
+
self.context
|
109
|
+
and self.context.config
|
110
|
+
and hasattr(self.context.config, "tensorzero")
|
111
|
+
and self.context.config.tensorzero
|
112
|
+
):
|
113
|
+
base_url = getattr(self.context.config.tensorzero, "base_url", None)
|
114
|
+
|
115
|
+
if not base_url:
|
116
|
+
if not self.context:
|
117
|
+
# Handle case where context itself is missing, log and use default
|
118
|
+
self.logger.warning(
|
119
|
+
f"LLM context not found. Cannot read TensorZero Gateway base URL configuration. "
|
120
|
+
f"Using default: {default_url}"
|
121
|
+
)
|
122
|
+
else:
|
123
|
+
self.logger.warning(
|
124
|
+
f"TensorZero Gateway base URL not configured in context.config.tensorzero.base_url. "
|
125
|
+
f"Using default: {default_url}"
|
126
|
+
)
|
127
|
+
|
128
|
+
base_url = default_url
|
129
|
+
|
130
|
+
self._t0_gateway = await AsyncTensorZeroGateway.build_http(gateway_url=base_url) # type: ignore
|
131
|
+
self.logger.info(f"TensorZero Gateway client initialized for URL: {base_url}")
|
132
|
+
except Exception as e:
|
133
|
+
self.logger.error(f"Failed to initialize TensorZero Gateway: {e}")
|
134
|
+
raise ModelConfigError(f"Failed to initialize TensorZero Gateway lazily: {e}")
|
135
|
+
|
136
|
+
return self._t0_gateway
|
137
|
+
|
138
|
+
async def _apply_prompt_provider_specific(
|
139
|
+
self,
|
140
|
+
multipart_messages: List[PromptMessageMultipart],
|
141
|
+
request_params: Optional[RequestParams] = None,
|
142
|
+
is_template: bool = False,
|
143
|
+
) -> PromptMessageMultipart:
|
144
|
+
gateway = await self._initialize_gateway()
|
145
|
+
merged_params = self.get_request_params(request_params)
|
146
|
+
|
147
|
+
# [1] Retrieve history
|
148
|
+
current_api_messages: List[Dict[str, Any]] = []
|
149
|
+
if merged_params.use_history:
|
150
|
+
try:
|
151
|
+
current_api_messages = self.history.get() or []
|
152
|
+
self.logger.debug(
|
153
|
+
f"Retrieved {len(current_api_messages)} API dict messages from history."
|
154
|
+
)
|
155
|
+
except Exception as e:
|
156
|
+
self.logger.error(f"Error retrieving history: {e}")
|
157
|
+
|
158
|
+
# [2] Convert *new* incoming PromptMessageMultipart messages to API dicts
|
159
|
+
for msg in multipart_messages:
|
160
|
+
msg_dict = TensorZeroConverter.convert_mcp_to_t0_message(msg)
|
161
|
+
if msg_dict:
|
162
|
+
current_api_messages.append(msg_dict)
|
163
|
+
|
164
|
+
t0_system_vars = self._prepare_t0_system_params(merged_params)
|
165
|
+
if t0_system_vars:
|
166
|
+
t0_api_input_dict = {"system": t0_system_vars}
|
167
|
+
else:
|
168
|
+
t0_api_input_dict = {}
|
169
|
+
available_tools: Optional[List[Dict[str, Any]]] = await self._prepare_t0_tools()
|
170
|
+
|
171
|
+
# [3] Initialize storage arrays for the text content of the assistant message reply and, optionally, tool calls and results, and begin inference loop
|
172
|
+
final_assistant_message: List[Union[TextContent, ImageContent, EmbeddedResource]] = []
|
173
|
+
last_executed_results: Optional[List[CallToolResult]] = None
|
174
|
+
|
175
|
+
for i in range(merged_params.max_iterations):
|
176
|
+
use_parallel_calls = merged_params.parallel_tool_calls if available_tools else False
|
177
|
+
current_t0_episode_id = self._t0_episode_id
|
178
|
+
|
179
|
+
try:
|
180
|
+
self.logger.debug(
|
181
|
+
f"Calling TensorZero inference (Iteration {i + 1}/{merged_params.max_iterations})..."
|
182
|
+
)
|
183
|
+
t0_api_input_dict["messages"] = current_api_messages # type: ignore
|
184
|
+
|
185
|
+
# [4] Call the TensorZero inference API
|
186
|
+
response_iter_or_completion = await gateway.inference(
|
187
|
+
function_name=self._t0_function_name,
|
188
|
+
input=t0_api_input_dict,
|
189
|
+
additional_tools=available_tools,
|
190
|
+
parallel_tool_calls=use_parallel_calls,
|
191
|
+
stream=False,
|
192
|
+
episode_id=current_t0_episode_id,
|
193
|
+
)
|
194
|
+
|
195
|
+
if not isinstance(
|
196
|
+
response_iter_or_completion, (ChatInferenceResponse, JsonInferenceResponse)
|
197
|
+
):
|
198
|
+
self.logger.error(
|
199
|
+
f"Unexpected TensorZero response type: {type(response_iter_or_completion)}"
|
200
|
+
)
|
201
|
+
final_assistant_message = [
|
202
|
+
TextContent(type="text", text="Unexpected response type")
|
203
|
+
]
|
204
|
+
break # Exit loop
|
205
|
+
|
206
|
+
# [5] quick check to confirm that episode_id is present and being used correctly by TensorZero
|
207
|
+
completion = response_iter_or_completion
|
208
|
+
if completion.episode_id: #
|
209
|
+
self._t0_episode_id = str(completion.episode_id)
|
210
|
+
if (
|
211
|
+
self._t0_episode_id != current_t0_episode_id
|
212
|
+
and current_t0_episode_id is not None
|
213
|
+
):
|
214
|
+
raise Exception(
|
215
|
+
f"Episode ID mismatch: {self._t0_episode_id} != {current_t0_episode_id}"
|
216
|
+
)
|
217
|
+
|
218
|
+
# [6] Adapt TensorZero inference response to a format compatible with the broader framework
|
219
|
+
(
|
220
|
+
content_parts_this_turn, # Text/Image content ONLY
|
221
|
+
executed_results_this_iter, # Results from THIS iteration
|
222
|
+
raw_tool_call_blocks,
|
223
|
+
) = await self._adapt_t0_native_completion(completion, available_tools)
|
224
|
+
|
225
|
+
last_executed_results = (
|
226
|
+
executed_results_this_iter # Track results from this iteration
|
227
|
+
)
|
228
|
+
|
229
|
+
# [7] If a text message was returned from the assistant, format that message using the multipart_converter_tensorzero.py helper methods and add this to the current list of API messages
|
230
|
+
assistant_api_content = []
|
231
|
+
for part in content_parts_this_turn:
|
232
|
+
api_part = TensorZeroConverter._convert_content_part(part)
|
233
|
+
if api_part:
|
234
|
+
assistant_api_content.append(api_part)
|
235
|
+
if raw_tool_call_blocks:
|
236
|
+
assistant_api_content.extend(
|
237
|
+
[self.block_to_dict(b) for b in raw_tool_call_blocks]
|
238
|
+
)
|
239
|
+
|
240
|
+
if assistant_api_content:
|
241
|
+
assistant_api_message_dict = {
|
242
|
+
"role": "assistant",
|
243
|
+
"content": assistant_api_content,
|
244
|
+
}
|
245
|
+
current_api_messages.append(assistant_api_message_dict)
|
246
|
+
elif executed_results_this_iter:
|
247
|
+
self.logger.debug(
|
248
|
+
"Assistant turn contained only tool calls, no API message added."
|
249
|
+
)
|
250
|
+
|
251
|
+
final_assistant_message = content_parts_this_turn
|
252
|
+
|
253
|
+
# [8] If there were no tool calls we're done. If not, format the tool results and add them to the current list of API messages
|
254
|
+
if not executed_results_this_iter:
|
255
|
+
self.logger.debug(f"Iteration {i + 1}: No tool calls detected. Finishing loop.")
|
256
|
+
break
|
257
|
+
else:
|
258
|
+
user_message_with_results = (
|
259
|
+
TensorZeroConverter.convert_tool_results_to_t0_user_message(
|
260
|
+
executed_results_this_iter
|
261
|
+
)
|
262
|
+
)
|
263
|
+
if user_message_with_results:
|
264
|
+
current_api_messages.append(user_message_with_results)
|
265
|
+
else:
|
266
|
+
self.logger.error("Converter failed to format tool results, breaking loop.")
|
267
|
+
break
|
268
|
+
|
269
|
+
# Check max iterations: TODO: implement logic in the future to handle this dynamically, checking for the presence of a tool call in the last iteration
|
270
|
+
if i == merged_params.max_iterations - 1:
|
271
|
+
self.logger.warning(f"Max iterations ({merged_params.max_iterations}) reached.")
|
272
|
+
break
|
273
|
+
|
274
|
+
# --- Error Handling for Inference Call ---
|
275
|
+
except TensorZeroError as e:
|
276
|
+
error_details = getattr(e, "detail", str(e.args[0] if e.args else e))
|
277
|
+
self.logger.error(f"TensorZero Error (HTTP {e.status_code}): {error_details}")
|
278
|
+
error_content = TextContent(type="text", text=f"TensorZero Error: {error_details}")
|
279
|
+
return PromptMessageMultipart(role="assistant", content=[error_content])
|
280
|
+
except Exception as e:
|
281
|
+
import traceback
|
282
|
+
|
283
|
+
self.logger.error(f"Unexpected Error: {e}\n{traceback.format_exc()}")
|
284
|
+
error_content = TextContent(type="text", text=f"Unexpected error: {e}")
|
285
|
+
return PromptMessageMultipart(role="assistant", content=[error_content])
|
286
|
+
|
287
|
+
# [9] Construct the final assistant message and update history
|
288
|
+
final_message_to_return = PromptMessageMultipart(
|
289
|
+
role="assistant", content=final_assistant_message
|
290
|
+
)
|
291
|
+
|
292
|
+
if merged_params.use_history:
|
293
|
+
try:
|
294
|
+
# Store the final list of API DICTIONARIES in history
|
295
|
+
self.history.set(current_api_messages)
|
296
|
+
self.logger.debug(
|
297
|
+
f"Updated self.history with {len(current_api_messages)} API message dicts."
|
298
|
+
)
|
299
|
+
except Exception as e:
|
300
|
+
self.logger.error(f"Failed to update self.history after loop: {e}")
|
301
|
+
|
302
|
+
# [10] Post final assistant message to display
|
303
|
+
display_text = final_message_to_return.all_text()
|
304
|
+
if display_text and display_text != "<no text>":
|
305
|
+
title = f"ASSISTANT/{self._t0_function_name}"
|
306
|
+
await self.show_assistant_message(message_text=display_text, title=title)
|
307
|
+
|
308
|
+
elif not final_assistant_message and last_executed_results:
|
309
|
+
self.logger.debug("Final assistant turn involved only tool calls, no text to display.")
|
310
|
+
|
311
|
+
return final_message_to_return
|
312
|
+
|
313
|
+
def _prepare_t0_system_params(self, merged_params: RequestParams) -> Dict[str, Any]:
|
314
|
+
"""Prepares the 'system' dictionary part of the main input."""
|
315
|
+
t0_func_input = merged_params.template_vars.copy()
|
316
|
+
|
317
|
+
metadata_args = None
|
318
|
+
if merged_params.metadata and isinstance(merged_params.metadata, dict):
|
319
|
+
metadata_args = merged_params.metadata.get("tensorzero_arguments")
|
320
|
+
if isinstance(metadata_args, dict):
|
321
|
+
t0_func_input.update(metadata_args)
|
322
|
+
self.logger.debug(f"Merged tensorzero_arguments from metadata: {metadata_args}")
|
323
|
+
return t0_func_input
|
324
|
+
|
325
|
+
async def _prepare_t0_tools(self) -> Optional[List[Dict[str, Any]]]:
|
326
|
+
"""Fetches and formats tools for the additional_tools parameter."""
|
327
|
+
formatted_tools: List[Dict[str, Any]] = []
|
328
|
+
try:
|
329
|
+
tools_response = await self.aggregator.list_tools()
|
330
|
+
if tools_response and hasattr(tools_response, "tools") and tools_response.tools:
|
331
|
+
for mcp_tool in tools_response.tools:
|
332
|
+
if (
|
333
|
+
not isinstance(mcp_tool.inputSchema, dict)
|
334
|
+
or mcp_tool.inputSchema.get("type") != "object"
|
335
|
+
):
|
336
|
+
self.logger.warning(
|
337
|
+
f"Tool '{mcp_tool.name}' has invalid parameters schema. Skipping."
|
338
|
+
)
|
339
|
+
continue
|
340
|
+
t0_tool_dict = {
|
341
|
+
"name": mcp_tool.name,
|
342
|
+
"description": mcp_tool.description if mcp_tool.description else "",
|
343
|
+
"parameters": mcp_tool.inputSchema,
|
344
|
+
}
|
345
|
+
formatted_tools.append(t0_tool_dict)
|
346
|
+
return formatted_tools if formatted_tools else None
|
347
|
+
except Exception as e:
|
348
|
+
self.logger.error(f"Failed to fetch or format tools: {e}")
|
349
|
+
return None
|
350
|
+
|
351
|
+
async def _adapt_t0_native_completion(
|
352
|
+
self,
|
353
|
+
completion: Union[ChatInferenceResponse, JsonInferenceResponse],
|
354
|
+
available_tools_for_display: Optional[List[Dict[str, Any]]] = None,
|
355
|
+
) -> Tuple[
|
356
|
+
List[Union[TextContent, ImageContent, EmbeddedResource]], # Text/Image content ONLY
|
357
|
+
List[CallToolResult], # Executed results
|
358
|
+
List[Any], # Raw tool_call blocks
|
359
|
+
]:
|
360
|
+
content_parts_this_turn: List[Union[TextContent, ImageContent, EmbeddedResource]] = []
|
361
|
+
executed_tool_results: List[CallToolResult] = []
|
362
|
+
raw_tool_call_blocks_from_t0: List[Any] = []
|
363
|
+
|
364
|
+
if isinstance(completion, ChatInferenceResponse) and hasattr(completion, "content"):
|
365
|
+
for block in completion.content:
|
366
|
+
block_type = getattr(block, "type", "UNKNOWN")
|
367
|
+
|
368
|
+
if block_type == "text":
|
369
|
+
text_val = getattr(block, "text", None)
|
370
|
+
if text_val is not None:
|
371
|
+
content_parts_this_turn.append(TextContent(type="text", text=text_val))
|
372
|
+
|
373
|
+
elif block_type == "tool_call":
|
374
|
+
raw_tool_call_blocks_from_t0.append(block)
|
375
|
+
tool_use_id = getattr(block, "id", None)
|
376
|
+
tool_name = getattr(block, "name", None)
|
377
|
+
tool_input_raw = getattr(block, "arguments", None)
|
378
|
+
tool_input = {}
|
379
|
+
if isinstance(tool_input_raw, dict):
|
380
|
+
tool_input = tool_input_raw
|
381
|
+
elif isinstance(tool_input_raw, str):
|
382
|
+
try:
|
383
|
+
tool_input = json.loads(tool_input_raw)
|
384
|
+
except json.JSONDecodeError:
|
385
|
+
tool_input = {}
|
386
|
+
elif tool_input_raw is not None:
|
387
|
+
tool_input = {}
|
388
|
+
|
389
|
+
if tool_use_id and tool_name:
|
390
|
+
self.show_tool_call(
|
391
|
+
available_tools_for_display, tool_name, json.dumps(tool_input)
|
392
|
+
)
|
393
|
+
mcp_tool_request = CallToolRequest(
|
394
|
+
method="tools/call",
|
395
|
+
params=CallToolRequestParams(name=tool_name, arguments=tool_input),
|
396
|
+
)
|
397
|
+
try:
|
398
|
+
result: CallToolResult = await self.call_tool(
|
399
|
+
mcp_tool_request, tool_use_id
|
400
|
+
)
|
401
|
+
setattr(result, "_t0_tool_use_id_temp", tool_use_id)
|
402
|
+
setattr(result, "_t0_tool_name_temp", tool_name)
|
403
|
+
setattr(result, "_t0_is_error_temp", False)
|
404
|
+
executed_tool_results.append(result)
|
405
|
+
self.show_oai_tool_result(str(result))
|
406
|
+
except Exception as e:
|
407
|
+
self.logger.error(
|
408
|
+
f"Error executing tool {tool_name} (id: {tool_use_id}): {e}"
|
409
|
+
)
|
410
|
+
error_text = f"Error executing tool {tool_name}: {str(e)}"
|
411
|
+
error_result = CallToolResult(
|
412
|
+
isError=True, content=[TextContent(type="text", text=error_text)]
|
413
|
+
)
|
414
|
+
setattr(error_result, "_t0_tool_use_id_temp", tool_use_id)
|
415
|
+
setattr(error_result, "_t0_tool_name_temp", tool_name)
|
416
|
+
setattr(error_result, "_t0_is_error_temp", True)
|
417
|
+
executed_tool_results.append(error_result)
|
418
|
+
self.show_oai_tool_result(f"ERROR: {error_text}")
|
419
|
+
|
420
|
+
elif block_type == "thought":
|
421
|
+
thought_text = getattr(block, "text", None)
|
422
|
+
self.logger.debug(f"TensorZero thought: {thought_text}")
|
423
|
+
else:
|
424
|
+
self.logger.warning(
|
425
|
+
f"TensorZero Adapt: Skipping unknown block type: {block_type}"
|
426
|
+
)
|
427
|
+
|
428
|
+
elif isinstance(completion, JsonInferenceResponse):
|
429
|
+
# `completion.output.raw` should always be present unless the LLM provider returns unexpected data
|
430
|
+
if completion.output.raw:
|
431
|
+
content_parts_this_turn.append(TextContent(type="text", text=completion.output.raw))
|
432
|
+
|
433
|
+
return content_parts_this_turn, executed_tool_results, raw_tool_call_blocks_from_t0
|
434
|
+
|
435
|
+
async def shutdown(self):
|
436
|
+
"""Close the TensorZero gateway client if initialized."""
|
437
|
+
if self._t0_gateway:
|
438
|
+
try:
|
439
|
+
await self._t0_gateway.close()
|
440
|
+
self.logger.debug("TensorZero Gateway client closed.")
|
441
|
+
except Exception as e:
|
442
|
+
self.logger.error(f"Error closing TensorZero Gateway client: {e}")
|
@@ -0,0 +1,200 @@
|
|
1
|
+
import json
|
2
|
+
from typing import Any, Dict, List, Optional, Union
|
3
|
+
|
4
|
+
from mcp.types import (
|
5
|
+
CallToolResult,
|
6
|
+
EmbeddedResource,
|
7
|
+
ImageContent,
|
8
|
+
TextContent,
|
9
|
+
)
|
10
|
+
|
11
|
+
from mcp_agent.logging.logger import get_logger
|
12
|
+
from mcp_agent.mcp.helpers.content_helpers import (
|
13
|
+
get_text,
|
14
|
+
)
|
15
|
+
from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
|
16
|
+
|
17
|
+
_logger = get_logger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
class TensorZeroConverter:
|
21
|
+
"""Converts MCP message types to/from TensorZero API format."""
|
22
|
+
|
23
|
+
@staticmethod
|
24
|
+
def _convert_content_part(
|
25
|
+
part: Union[TextContent, ImageContent, EmbeddedResource],
|
26
|
+
) -> Optional[Dict[str, Any]]:
|
27
|
+
"""Converts a single MCP content part to a T0 content block dictionary."""
|
28
|
+
if isinstance(part, TextContent):
|
29
|
+
text = get_text(part)
|
30
|
+
if text is not None:
|
31
|
+
return {"type": "text", "text": text}
|
32
|
+
elif isinstance(part, ImageContent):
|
33
|
+
# Handle Base64: needs data, mimeType (and mimeType must not be empty)
|
34
|
+
if hasattr(part, "data") and part.data and hasattr(part, "mimeType") and part.mimeType:
|
35
|
+
_logger.debug(
|
36
|
+
f"Converting ImageContent as base64 for T0 native: mime={part.mimeType}, data_len={len(part.data) if isinstance(part.data, str) else 'N/A'}"
|
37
|
+
)
|
38
|
+
supported_mime_types = ["image/jpeg", "image/png", "image/webp"]
|
39
|
+
mime_type = getattr(part, "mimeType", "")
|
40
|
+
|
41
|
+
# Use the provided mime_type if supported, otherwise default to png
|
42
|
+
if mime_type not in supported_mime_types:
|
43
|
+
_logger.warning(
|
44
|
+
f"Unsupported mimeType '{mime_type}' for T0 base64 image, defaulting to image/png."
|
45
|
+
)
|
46
|
+
mime_type = "image/png"
|
47
|
+
|
48
|
+
return {
|
49
|
+
"type": "image",
|
50
|
+
"mime_type": mime_type, # Note: T0 uses mime_type, not media_type
|
51
|
+
"data": getattr(part, "data", ""), # Data is direct property
|
52
|
+
}
|
53
|
+
else:
|
54
|
+
# Log cases where it's an ImageContent but doesn't fit Base64 criteria
|
55
|
+
_logger.warning(
|
56
|
+
f"Skipping ImageContent: Missing required base64 fields (mimeType/data), or mimeType is empty: {part}"
|
57
|
+
)
|
58
|
+
|
59
|
+
elif isinstance(part, EmbeddedResource):
|
60
|
+
_logger.warning(f"Skipping EmbeddedResource, T0 conversion not implemented: {part}")
|
61
|
+
else:
|
62
|
+
_logger.error(
|
63
|
+
f"Unsupported content part type for T0 conversion: {type(part)}"
|
64
|
+
) # Changed to error
|
65
|
+
|
66
|
+
return None # Return None if no block was successfully created
|
67
|
+
|
68
|
+
@staticmethod
|
69
|
+
def _get_text_from_call_tool_result(result: CallToolResult) -> str:
|
70
|
+
"""Helper to extract combined text from a CallToolResult's content list."""
|
71
|
+
texts = []
|
72
|
+
if result.content:
|
73
|
+
for part in result.content:
|
74
|
+
text = get_text(part)
|
75
|
+
if text:
|
76
|
+
texts.append(text)
|
77
|
+
return "\n".join(texts)
|
78
|
+
|
79
|
+
@staticmethod
|
80
|
+
def convert_tool_results_to_t0_user_message(
|
81
|
+
results: List[CallToolResult],
|
82
|
+
) -> Optional[Dict[str, Any]]:
|
83
|
+
"""Formats CallToolResult list into T0's tool_result blocks within a user message dict."""
|
84
|
+
t0_tool_result_blocks = []
|
85
|
+
for result in results:
|
86
|
+
tool_use_id = getattr(result, "_t0_tool_use_id_temp", None)
|
87
|
+
tool_name = getattr(result, "_t0_tool_name_temp", None)
|
88
|
+
|
89
|
+
if tool_use_id and tool_name:
|
90
|
+
result_content_str = TensorZeroConverter._get_text_from_call_tool_result(result)
|
91
|
+
try:
|
92
|
+
# Attempt to treat result as JSON if possible, else use raw string
|
93
|
+
try:
|
94
|
+
json_result = json.loads(result_content_str)
|
95
|
+
except json.JSONDecodeError:
|
96
|
+
json_result = result_content_str # Fallback to string if not valid JSON
|
97
|
+
except Exception as e:
|
98
|
+
_logger.error(f"Unexpected error processing tool result content: {e}")
|
99
|
+
json_result = str(result_content_str) # Safest fallback
|
100
|
+
|
101
|
+
t0_block = {
|
102
|
+
"type": "tool_result",
|
103
|
+
"id": tool_use_id,
|
104
|
+
"name": tool_name,
|
105
|
+
"result": json_result, # T0 expects the result directly
|
106
|
+
}
|
107
|
+
t0_tool_result_blocks.append(t0_block)
|
108
|
+
|
109
|
+
# Clean up temporary attributes
|
110
|
+
try:
|
111
|
+
delattr(result, "_t0_tool_use_id_temp")
|
112
|
+
delattr(result, "_t0_tool_name_temp")
|
113
|
+
if hasattr(result, "_t0_is_error_temp"):
|
114
|
+
delattr(result, "_t0_is_error_temp")
|
115
|
+
except AttributeError:
|
116
|
+
pass
|
117
|
+
else:
|
118
|
+
_logger.warning(
|
119
|
+
f"Could not find id/name temp attributes for CallToolResult: {result}"
|
120
|
+
)
|
121
|
+
|
122
|
+
if not t0_tool_result_blocks:
|
123
|
+
return None
|
124
|
+
|
125
|
+
return {"role": "user", "content": t0_tool_result_blocks}
|
126
|
+
|
127
|
+
@staticmethod
|
128
|
+
def convert_mcp_to_t0_message(msg: PromptMessageMultipart) -> Optional[Dict[str, Any]]:
|
129
|
+
"""
|
130
|
+
Converts a single PromptMessageMultipart to a T0 API message dictionary.
|
131
|
+
Handles Text, Image, and embedded CallToolResult content.
|
132
|
+
Skips system messages.
|
133
|
+
"""
|
134
|
+
if msg.role == "system":
|
135
|
+
return None
|
136
|
+
|
137
|
+
t0_content_blocks = []
|
138
|
+
contains_tool_result = False
|
139
|
+
|
140
|
+
for part in msg.content:
|
141
|
+
# Use the corrected _convert_content_part
|
142
|
+
converted_block = TensorZeroConverter._convert_content_part(part)
|
143
|
+
if converted_block:
|
144
|
+
t0_content_blocks.append(converted_block)
|
145
|
+
elif isinstance(part, CallToolResult):
|
146
|
+
# Existing logic for handling embedded CallToolResult (seems compatible with T0 tool_result spec)
|
147
|
+
contains_tool_result = True
|
148
|
+
tool_use_id = getattr(part, "_t0_tool_use_id_temp", None)
|
149
|
+
tool_name = getattr(part, "_t0_tool_name_temp", None)
|
150
|
+
if tool_use_id and tool_name:
|
151
|
+
result_content_str = TensorZeroConverter._get_text_from_call_tool_result(part)
|
152
|
+
# Try to format result as JSON object/string
|
153
|
+
try:
|
154
|
+
json_result = json.loads(result_content_str)
|
155
|
+
except json.JSONDecodeError:
|
156
|
+
json_result = result_content_str # Fallback
|
157
|
+
except Exception as e:
|
158
|
+
_logger.error(f"Error processing embedded tool result: {e}")
|
159
|
+
json_result = str(result_content_str)
|
160
|
+
|
161
|
+
t0_content_blocks.append(
|
162
|
+
{
|
163
|
+
"type": "tool_result",
|
164
|
+
"id": tool_use_id,
|
165
|
+
"name": tool_name,
|
166
|
+
"result": json_result,
|
167
|
+
}
|
168
|
+
)
|
169
|
+
# Clean up temp attributes
|
170
|
+
try:
|
171
|
+
delattr(part, "_t0_tool_use_id_temp")
|
172
|
+
delattr(part, "_t0_tool_name_temp")
|
173
|
+
except AttributeError:
|
174
|
+
pass
|
175
|
+
else:
|
176
|
+
_logger.warning(
|
177
|
+
f"Found embedded CallToolResult without required temp attributes: {part}"
|
178
|
+
)
|
179
|
+
# Note: The _convert_content_part handles logging for other skipped/unsupported types
|
180
|
+
|
181
|
+
if not t0_content_blocks:
|
182
|
+
return None
|
183
|
+
|
184
|
+
# Determine role - logic remains the same
|
185
|
+
valid_role = msg.role if msg.role in ["user", "assistant"] else "user"
|
186
|
+
if contains_tool_result and all(
|
187
|
+
block.get("type") == "tool_result" for block in t0_content_blocks
|
188
|
+
):
|
189
|
+
final_role = "user"
|
190
|
+
if valid_role != final_role:
|
191
|
+
_logger.debug(f"Overriding role to '{final_role}' for tool result message.")
|
192
|
+
else:
|
193
|
+
final_role = valid_role
|
194
|
+
if valid_role != msg.role:
|
195
|
+
_logger.warning(f"Mapping message role '{msg.role}' to '{valid_role}' for T0.")
|
196
|
+
|
197
|
+
return {"role": final_role, "content": t0_content_blocks}
|
198
|
+
|
199
|
+
# Add methods here if needed to convert *from* T0 format back to MCP types
|
200
|
+
# e.g., adapt_t0_response_to_mcp(...) - this logic stays in the LLM class for now
|
@@ -15,6 +15,7 @@ from typing import (
|
|
15
15
|
|
16
16
|
from anyio import Event, Lock, create_task_group
|
17
17
|
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
|
18
|
+
from httpx import HTTPStatusError
|
18
19
|
from mcp import ClientSession
|
19
20
|
from mcp.client.sse import sse_client
|
20
21
|
from mcp.client.stdio import (
|
@@ -162,14 +163,26 @@ async def _server_lifecycle_task(server_conn: ServerConnection) -> None:
|
|
162
163
|
transport_context = server_conn._transport_context_factory()
|
163
164
|
|
164
165
|
async with transport_context as (read_stream, write_stream):
|
165
|
-
# try:
|
166
166
|
server_conn.create_session(read_stream, write_stream)
|
167
167
|
|
168
168
|
async with server_conn.session:
|
169
169
|
await server_conn.initialize_session()
|
170
|
-
|
171
170
|
await server_conn.wait_for_shutdown_request()
|
172
171
|
|
172
|
+
except HTTPStatusError as http_exc:
|
173
|
+
logger.error(
|
174
|
+
f"{server_name}: Lifecycle task encountered HTTP error: {http_exc}",
|
175
|
+
exc_info=True,
|
176
|
+
data={
|
177
|
+
"progress_action": ProgressAction.FATAL_ERROR,
|
178
|
+
"server_name": server_name,
|
179
|
+
},
|
180
|
+
)
|
181
|
+
server_conn._error_occurred = True
|
182
|
+
server_conn._error_message = f"HTTP Error: {http_exc.response.status_code} {http_exc.response.reason_phrase} for URL: {http_exc.request.url}"
|
183
|
+
server_conn._initialized_event.set()
|
184
|
+
# No raise - let get_server handle it with a friendly message
|
185
|
+
|
173
186
|
except Exception as exc:
|
174
187
|
logger.error(
|
175
188
|
f"{server_name}: Lifecycle task encountered an error: {exc}",
|
@@ -180,7 +193,27 @@ async def _server_lifecycle_task(server_conn: ServerConnection) -> None:
|
|
180
193
|
},
|
181
194
|
)
|
182
195
|
server_conn._error_occurred = True
|
183
|
-
|
196
|
+
|
197
|
+
if "ExceptionGroup" in type(exc).__name__ and hasattr(exc, "exceptions"):
|
198
|
+
# Handle ExceptionGroup better by extracting the actual errors
|
199
|
+
error_messages = []
|
200
|
+
for subexc in exc.exceptions:
|
201
|
+
if isinstance(subexc, HTTPStatusError):
|
202
|
+
# Special handling for HTTP errors to make them more user-friendly
|
203
|
+
error_messages.append(
|
204
|
+
f"HTTP Error: {subexc.response.status_code} {subexc.response.reason_phrase} for URL: {subexc.request.url}"
|
205
|
+
)
|
206
|
+
else:
|
207
|
+
error_messages.append(f"Error: {type(subexc).__name__}: {subexc}")
|
208
|
+
if hasattr(subexc, "__cause__") and subexc.__cause__:
|
209
|
+
error_messages.append(
|
210
|
+
f"Caused by: {type(subexc.__cause__).__name__}: {subexc.__cause__}"
|
211
|
+
)
|
212
|
+
server_conn._error_message = error_messages
|
213
|
+
else:
|
214
|
+
# For regular exceptions, keep the traceback but format it more cleanly
|
215
|
+
server_conn._error_message = traceback.format_exception(exc)
|
216
|
+
|
184
217
|
# If there's an error, we should also set the event so that
|
185
218
|
# 'get_server' won't hang
|
186
219
|
server_conn._initialized_event.set()
|
@@ -277,6 +310,7 @@ class MCPConnectionManager(ContextDependent):
|
|
277
310
|
config.headers,
|
278
311
|
sse_read_timeout=config.read_transport_sse_timeout_seconds,
|
279
312
|
)
|
313
|
+
|
280
314
|
else:
|
281
315
|
raise ValueError(f"Unsupported transport: {config.transport}")
|
282
316
|
|
@@ -333,9 +367,17 @@ class MCPConnectionManager(ContextDependent):
|
|
333
367
|
# Check if the server is healthy after initialization
|
334
368
|
if not server_conn.is_healthy():
|
335
369
|
error_msg = server_conn._error_message or "Unknown error"
|
370
|
+
|
371
|
+
# Format the error message for better display
|
372
|
+
if isinstance(error_msg, list):
|
373
|
+
# Join the list with newlines for better readability
|
374
|
+
formatted_error = "\n".join(error_msg)
|
375
|
+
else:
|
376
|
+
formatted_error = str(error_msg)
|
377
|
+
|
336
378
|
raise ServerInitializationError(
|
337
379
|
f"MCP Server: '{server_name}': Failed to initialize - see details. Check fastagent.config.yaml?",
|
338
|
-
|
380
|
+
formatted_error,
|
339
381
|
)
|
340
382
|
|
341
383
|
return server_conn
|
File without changes
|
File without changes
|
File without changes
|