fast-agent-mcp 0.2.44__py3-none-any.whl → 0.2.45__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.44.dist-info → fast_agent_mcp-0.2.45.dist-info}/METADATA +4 -4
- {fast_agent_mcp-0.2.44.dist-info → fast_agent_mcp-0.2.45.dist-info}/RECORD +17 -17
- mcp_agent/agents/workflow/evaluator_optimizer.py +39 -63
- mcp_agent/agents/workflow/router_agent.py +46 -21
- mcp_agent/context.py +4 -0
- mcp_agent/core/direct_decorators.py +4 -5
- mcp_agent/core/enhanced_prompt.py +4 -5
- mcp_agent/human_input/elicitation_form.py +16 -13
- mcp_agent/llm/augmented_llm.py +1 -2
- mcp_agent/llm/augmented_llm_passthrough.py +0 -11
- mcp_agent/llm/providers/augmented_llm_anthropic.py +254 -95
- mcp_agent/resources/examples/workflows/evaluator.py +2 -2
- mcp_agent/resources/examples/workflows/router.py +1 -1
- mcp_agent/ui/console_display.py +19 -9
- {fast_agent_mcp-0.2.44.dist-info → fast_agent_mcp-0.2.45.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.2.44.dist-info → fast_agent_mcp-0.2.45.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.2.44.dist-info → fast_agent_mcp-0.2.45.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.45
|
|
4
4
|
Summary: Define, Prompt and Test MCP enabled Agents and Workflows
|
|
5
5
|
Author-email: Shaun Smith <fastagent@llmindset.co.uk>
|
|
6
6
|
License: Apache License
|
|
@@ -222,10 +222,10 @@ Requires-Dist: mcp==1.12.0
|
|
|
222
222
|
Requires-Dist: openai>=1.93.0
|
|
223
223
|
Requires-Dist: opentelemetry-distro>=0.50b0
|
|
224
224
|
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.29.0
|
|
225
|
-
Requires-Dist: opentelemetry-instrumentation-anthropic>=0.
|
|
225
|
+
Requires-Dist: opentelemetry-instrumentation-anthropic>=0.42.0; python_version >= '3.10' and python_version < '4.0'
|
|
226
226
|
Requires-Dist: opentelemetry-instrumentation-google-genai>=0.2b0
|
|
227
|
-
Requires-Dist: opentelemetry-instrumentation-mcp>=0.
|
|
228
|
-
Requires-Dist: opentelemetry-instrumentation-openai>=0.
|
|
227
|
+
Requires-Dist: opentelemetry-instrumentation-mcp>=0.42.0; python_version >= '3.10' and python_version < '4.0'
|
|
228
|
+
Requires-Dist: opentelemetry-instrumentation-openai>=0.42.0; python_version >= '3.10' and python_version < '4.0'
|
|
229
229
|
Requires-Dist: prompt-toolkit>=3.0.50
|
|
230
230
|
Requires-Dist: pydantic-settings>=2.7.0
|
|
231
231
|
Requires-Dist: pydantic>=2.10.4
|
|
@@ -2,7 +2,7 @@ mcp_agent/__init__.py,sha256=18T0AG0W9sJhTY38O9GFFOzliDhxx9p87CvRyti9zbw,1620
|
|
|
2
2
|
mcp_agent/app.py,sha256=3mtHP1nRQcRaKhhxgTmCOv00alh70nT7UxNA8bN47QE,5560
|
|
3
3
|
mcp_agent/config.py,sha256=RjwgvR-Sys4JIzhNyEsaS_NarCe157RenJLOsioUtDk,18980
|
|
4
4
|
mcp_agent/console.py,sha256=Gjf2QLFumwG1Lav__c07X_kZxxEUSkzV-1_-YbAwcwo,813
|
|
5
|
-
mcp_agent/context.py,sha256=
|
|
5
|
+
mcp_agent/context.py,sha256=lzz_Fyf9lz9BBAUt1bRVBlyyHjLkyeuyIziAi4qXYUk,7639
|
|
6
6
|
mcp_agent/context_dependent.py,sha256=QXfhw3RaQCKfscEEBRGuZ3sdMWqkgShz2jJ1ivGGX1I,1455
|
|
7
7
|
mcp_agent/event_progress.py,sha256=d7T1hQ1D289MYh2Z5bMPB4JqjGqTOzveJuOHE03B_Xo,3720
|
|
8
8
|
mcp_agent/mcp_server_registry.py,sha256=lmz-aES-l7Gbg4itDF0iCmpso_KD8bVazVKSVzjwNE4,12398
|
|
@@ -12,12 +12,12 @@ mcp_agent/agents/agent.py,sha256=EAYlcP1qqI1D0_CS808I806z1048FBjZQxxpcCZPeIU,315
|
|
|
12
12
|
mcp_agent/agents/base_agent.py,sha256=VCBWJ-l1zMQiBuONGzqcbqPUQfXK4oq-pBB5lJrMgQ0,32318
|
|
13
13
|
mcp_agent/agents/workflow/__init__.py,sha256=HloteEW6kalvgR0XewpiFAqaQlMPlPJYg5p3K33IUzI,25
|
|
14
14
|
mcp_agent/agents/workflow/chain_agent.py,sha256=eIlImirrSXkqBJmPuAJgOKis81Cl6lZEGM0-6IyaUV8,6105
|
|
15
|
-
mcp_agent/agents/workflow/evaluator_optimizer.py,sha256=
|
|
15
|
+
mcp_agent/agents/workflow/evaluator_optimizer.py,sha256=LT81m2B7fxgBZY0CorXFOZJbVhM5fnjDjfrcywO5UrM,12210
|
|
16
16
|
mcp_agent/agents/workflow/orchestrator_agent.py,sha256=lArV7wHwPYepSuxe0ybTGJRJv85iebRI4ZOY_m8kMZQ,21593
|
|
17
17
|
mcp_agent/agents/workflow/orchestrator_models.py,sha256=5P_aXADVT4Et8qT4e1cb9RelmHX5dCRrzu8j8T41Kdg,7230
|
|
18
18
|
mcp_agent/agents/workflow/orchestrator_prompts.py,sha256=EXKEI174sshkZyPPEnWbwwNafzSPuA39MXL7iqG9cWc,9106
|
|
19
19
|
mcp_agent/agents/workflow/parallel_agent.py,sha256=JaQFp35nmAdoBRLAwx8BfnK7kirVq9PMw24LQ3ZEzoc,7705
|
|
20
|
-
mcp_agent/agents/workflow/router_agent.py,sha256=
|
|
20
|
+
mcp_agent/agents/workflow/router_agent.py,sha256=DYxld96C_xy2TXtZDHPB0CaJqyX-7p0LpsfjkZV6-3o,10517
|
|
21
21
|
mcp_agent/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
22
|
mcp_agent/cli/__main__.py,sha256=KyZnfXkml0KsOnfy8T9JDYNVNynKix9cslwuafmKNbc,1089
|
|
23
23
|
mcp_agent/cli/constants.py,sha256=KawdkaN289nVB02DKPB4IVUJ8-fohIUD0gLfOp0P7B8,551
|
|
@@ -32,9 +32,9 @@ mcp_agent/cli/commands/url_parser.py,sha256=5VdtcHRHzi67YignStVbz7u-rcvNNErw9oJL
|
|
|
32
32
|
mcp_agent/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
33
33
|
mcp_agent/core/agent_app.py,sha256=SolGwejEmv9XtsTsmiMkNKPia7RN1VcHXm6JoEo4hvQ,16187
|
|
34
34
|
mcp_agent/core/agent_types.py,sha256=7zVzAFWjvh5dDV3TuDwmO9LAWmDjYnZd3eeLH-wvvIQ,1705
|
|
35
|
-
mcp_agent/core/direct_decorators.py,sha256=
|
|
35
|
+
mcp_agent/core/direct_decorators.py,sha256=zWnfs4sxONw_7UOno62ERb0gezMgV_AIOfxb0tRQg3w,18939
|
|
36
36
|
mcp_agent/core/direct_factory.py,sha256=d_HvbAxyv2WrM07zyCpLXFVn7eArXk1LZmLKS49hzJo,19537
|
|
37
|
-
mcp_agent/core/enhanced_prompt.py,sha256=
|
|
37
|
+
mcp_agent/core/enhanced_prompt.py,sha256=ZIeJCeW7rcGMBZ2OdEQwqOmRT0wNSp0hO2-dZRSnnLE,36068
|
|
38
38
|
mcp_agent/core/error_handling.py,sha256=xoyS2kLe0eG0bj2eSJCJ2odIhGUve2SbDR7jP-A-uRw,624
|
|
39
39
|
mcp_agent/core/exceptions.py,sha256=ENAD_qGG67foxy6vDkIvc-lgopIUQy6O7zvNPpPXaQg,2289
|
|
40
40
|
mcp_agent/core/fastagent.py,sha256=qA64fwmJ6TVFEvmj6l-oTWPD28Js5zJvdDnPjH-agzQ,25117
|
|
@@ -49,15 +49,15 @@ mcp_agent/executor/executor.py,sha256=E44p6d-o3OMRoP_dNs_cDnyti91LQ3P9eNU88mSi1k
|
|
|
49
49
|
mcp_agent/executor/task_registry.py,sha256=PCALFeYtkQrPBg4RBJnlA0aDI8nHclrNkHGUS4kV3W8,1242
|
|
50
50
|
mcp_agent/executor/workflow_signal.py,sha256=Cg1uZBk3fn8kXhPOg-wINNuVaf3v9pvLD6NbqWy5Z6E,11142
|
|
51
51
|
mcp_agent/human_input/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
52
|
-
mcp_agent/human_input/elicitation_form.py,sha256=
|
|
52
|
+
mcp_agent/human_input/elicitation_form.py,sha256=VgS-DXlwYTU4qDntok4Pqt8qfl1w_-Xby5PTlfNerug,28324
|
|
53
53
|
mcp_agent/human_input/elicitation_forms.py,sha256=w8XQ1GfZX8Jw-VB4jnDI0Im4mF-T9Ts8mT2zRZBtL6M,3824
|
|
54
54
|
mcp_agent/human_input/elicitation_handler.py,sha256=YfVhIhSBc9wuszPS4zoHho4n1pwmIoq13huN4MSRkIs,3305
|
|
55
55
|
mcp_agent/human_input/elicitation_state.py,sha256=Unl9uhEybUqACCUimnETdfUprJNpYDMq3DdbbHw5oAw,1175
|
|
56
56
|
mcp_agent/human_input/handler.py,sha256=s712Z5ssTCwjL9-VKoIdP5CtgMh43YvepynYisiWTTA,3144
|
|
57
57
|
mcp_agent/human_input/types.py,sha256=RtWBOVzy8vnYoQrc36jRLn8z8N3C4pDPMBN5vF6qM5Y,1476
|
|
58
58
|
mcp_agent/llm/__init__.py,sha256=d8zgwG-bRFuwiMNMYkywg_qytk4P8lawyld_meuUmHI,68
|
|
59
|
-
mcp_agent/llm/augmented_llm.py,sha256=
|
|
60
|
-
mcp_agent/llm/augmented_llm_passthrough.py,sha256
|
|
59
|
+
mcp_agent/llm/augmented_llm.py,sha256=UiOTOAaNVnc03yuLBbBZstnaOG6Q8XLwkyiHPA3yxEk,27434
|
|
60
|
+
mcp_agent/llm/augmented_llm_passthrough.py,sha256=bu0DJkjyFPzBZEU7f6MHnOp__9BCYl56tFd5nZVhSeY,8808
|
|
61
61
|
mcp_agent/llm/augmented_llm_playback.py,sha256=BQeBXRpO-xGAY9wIJxyde6xpHmZEdQPLd32frF8t3QQ,4916
|
|
62
62
|
mcp_agent/llm/augmented_llm_silent.py,sha256=IUnK_1Byy4D9TG0Pj46LFeNezgSTQ8d6MQIHWAImBwE,1846
|
|
63
63
|
mcp_agent/llm/augmented_llm_slow.py,sha256=DDSD8bL2flmQrVHZm-UDs7sR8aHRWkDOcOW-mX_GPok,2067
|
|
@@ -73,7 +73,7 @@ mcp_agent/llm/usage_tracking.py,sha256=rF6v8QQDam8QbvlP4jzHljKqvuNHExeYDLkUMI86c
|
|
|
73
73
|
mcp_agent/llm/providers/__init__.py,sha256=heVxtmuqFJOnjjxHz4bWSqTAxXoN1E8twC_gQ_yJpHk,265
|
|
74
74
|
mcp_agent/llm/providers/anthropic_utils.py,sha256=vYDN5G5jKMhD2CQg8veJYab7tvvzYkDMq8M1g_hUAQg,3275
|
|
75
75
|
mcp_agent/llm/providers/augmented_llm_aliyun.py,sha256=XylkJKZ9theSVUxJKOZkf1244hgzng4Ng4Dr209Qb-w,1101
|
|
76
|
-
mcp_agent/llm/providers/augmented_llm_anthropic.py,sha256=
|
|
76
|
+
mcp_agent/llm/providers/augmented_llm_anthropic.py,sha256=l32pJ3yo0oVv7ELaoi1aCSV2TDGhBuYf0AKwW0lgjPs,30359
|
|
77
77
|
mcp_agent/llm/providers/augmented_llm_azure.py,sha256=sBVWgY88F4OsdRSHl71BAT2p3XPvuZp844z1ubwcV7U,6098
|
|
78
78
|
mcp_agent/llm/providers/augmented_llm_bedrock.py,sha256=nfby1udL07zPTOLlN_tFxd1h0JRioo2oIW7v4iP4Bnk,82267
|
|
79
79
|
mcp_agent/llm/providers/augmented_llm_deepseek.py,sha256=zI9a90dwT4r6E1f_xp4K50Cj9sD7y7kNRgjo0s1pd5w,3804
|
|
@@ -150,20 +150,20 @@ mcp_agent/resources/examples/researcher/researcher-eval.py,sha256=CR9m4lyoXijS1w
|
|
|
150
150
|
mcp_agent/resources/examples/researcher/researcher-imp.py,sha256=oJxSVnLbZfIn71QbQR1E6j_m_UBrOOGP4SVljXErHLQ,7879
|
|
151
151
|
mcp_agent/resources/examples/researcher/researcher.py,sha256=SZfExi-FfwYETzGt2O3caS3L5E6EemV3IUrJHyzZqHI,1333
|
|
152
152
|
mcp_agent/resources/examples/workflows/chaining.py,sha256=tY0kA0U8s2rceAO4ogZFtpQEkiUWcrYnYDgHu_-4G50,889
|
|
153
|
-
mcp_agent/resources/examples/workflows/evaluator.py,sha256=
|
|
153
|
+
mcp_agent/resources/examples/workflows/evaluator.py,sha256=XJXrk5r1hrJzfZAMtQ7WIggy6qPttMJG1yqxYELO7C4,3101
|
|
154
154
|
mcp_agent/resources/examples/workflows/fastagent.config.yaml,sha256=qaxk-p7Pl7JepdL3a7BTl0CIp4LHCXies7pFdVWS9xk,783
|
|
155
155
|
mcp_agent/resources/examples/workflows/graded_report.md,sha256=QVF38xEtDIO1a2P-xv2hlBEG6KKYughtFkzDhY2NpzE,2726
|
|
156
156
|
mcp_agent/resources/examples/workflows/human_input.py,sha256=_I6nS6xYo8IHAmvzsUYOxqVGb4G6BTyJXPAmS3fNcBU,621
|
|
157
157
|
mcp_agent/resources/examples/workflows/orchestrator.py,sha256=5Jxfe0s3ai4qRpVXm3DaqtkDvjeWD2CcysW36J6JA-M,2521
|
|
158
158
|
mcp_agent/resources/examples/workflows/parallel.py,sha256=OTWhX33uum_rspAzrSf3uCTj7b67Kvw3UUU8nsosz4g,1839
|
|
159
|
-
mcp_agent/resources/examples/workflows/router.py,sha256=
|
|
159
|
+
mcp_agent/resources/examples/workflows/router.py,sha256=vmw8aBitByi5PRFIvjYWWn2GUtAPiwzl7juN3kmRqvw,2033
|
|
160
160
|
mcp_agent/resources/examples/workflows/short_story.md,sha256=XN9I2kzCcMmke3dE5F2lyRH5iFUZUQ8Sy-hS3rm_Wlc,1153
|
|
161
161
|
mcp_agent/resources/examples/workflows/short_story.txt,sha256=X3y_1AyhLFN2AKzCKvucJtDgAFIJfnlbsbGZO5bBWu0,1187
|
|
162
162
|
mcp_agent/tools/tool_definition.py,sha256=L3Pxl-uLEXqlVoo-bYuFTFALeI-2pIU44YgFhsTKEtM,398
|
|
163
|
-
mcp_agent/ui/console_display.py,sha256=
|
|
163
|
+
mcp_agent/ui/console_display.py,sha256=cv-dvpJT7-zd7d8nqlcDLKfYg9_pDEQQOt7rGnDvj54,26057
|
|
164
164
|
mcp_agent/ui/console_display_legacy.py,sha256=sm2v61-IPVafbF7uUaOyhO2tW_zgFWOjNS83IEWqGgI,14931
|
|
165
|
-
fast_agent_mcp-0.2.
|
|
166
|
-
fast_agent_mcp-0.2.
|
|
167
|
-
fast_agent_mcp-0.2.
|
|
168
|
-
fast_agent_mcp-0.2.
|
|
169
|
-
fast_agent_mcp-0.2.
|
|
165
|
+
fast_agent_mcp-0.2.45.dist-info/METADATA,sha256=xdue1zGI50YjprDUaZUnOlO4vW-S02iGdK6dKh5Cq74,31041
|
|
166
|
+
fast_agent_mcp-0.2.45.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
167
|
+
fast_agent_mcp-0.2.45.dist-info/entry_points.txt,sha256=QaX5kLdI0VdMPRdPUF1nkG_WdLUTNjp_icW6e3EhNYU,232
|
|
168
|
+
fast_agent_mcp-0.2.45.dist-info/licenses/LICENSE,sha256=Gx1L3axA4PnuK4FxsbX87jQ1opoOkSFfHHSytW6wLUU,10935
|
|
169
|
+
fast_agent_mcp-0.2.45.dist-info/RECORD,,
|
|
@@ -33,16 +33,14 @@ class QualityRating(str, Enum):
|
|
|
33
33
|
GOOD = "GOOD" # Minor improvements possible
|
|
34
34
|
EXCELLENT = "EXCELLENT" # No improvements needed
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"EXCELLENT": 3,
|
|
45
|
-
}[self._value_]
|
|
36
|
+
|
|
37
|
+
# Separate mapping for quality ratings to numerical values
|
|
38
|
+
QUALITY_RATING_VALUES = {
|
|
39
|
+
QualityRating.POOR: 0,
|
|
40
|
+
QualityRating.FAIR: 1,
|
|
41
|
+
QualityRating.GOOD: 2,
|
|
42
|
+
QualityRating.EXCELLENT: 3,
|
|
43
|
+
}
|
|
46
44
|
|
|
47
45
|
|
|
48
46
|
class EvaluationResult(BaseModel):
|
|
@@ -140,7 +138,7 @@ class EvaluatorOptimizerAgent(BaseAgent):
|
|
|
140
138
|
|
|
141
139
|
# Evaluate current response
|
|
142
140
|
eval_prompt = self._build_eval_prompt(
|
|
143
|
-
request=request, response=response.
|
|
141
|
+
request=request, response=response.last_text(), iteration=refinement_count
|
|
144
142
|
)
|
|
145
143
|
|
|
146
144
|
# Create evaluation message and get structured evaluation result
|
|
@@ -171,7 +169,7 @@ class EvaluatorOptimizerAgent(BaseAgent):
|
|
|
171
169
|
logger.debug(f"Evaluation result: {evaluation_result.rating}")
|
|
172
170
|
|
|
173
171
|
# Track best response based on rating
|
|
174
|
-
if evaluation_result.rating
|
|
172
|
+
if QUALITY_RATING_VALUES[evaluation_result.rating] > QUALITY_RATING_VALUES[best_rating]:
|
|
175
173
|
best_rating = evaluation_result.rating
|
|
176
174
|
best_response = response
|
|
177
175
|
logger.debug(f"New best response (rating: {best_rating})")
|
|
@@ -183,14 +181,17 @@ class EvaluatorOptimizerAgent(BaseAgent):
|
|
|
183
181
|
best_response = response
|
|
184
182
|
break
|
|
185
183
|
|
|
186
|
-
if
|
|
184
|
+
if (
|
|
185
|
+
QUALITY_RATING_VALUES[evaluation_result.rating]
|
|
186
|
+
>= QUALITY_RATING_VALUES[self.min_rating]
|
|
187
|
+
):
|
|
187
188
|
logger.debug(f"Acceptable quality reached ({evaluation_result.rating})")
|
|
188
189
|
break
|
|
189
190
|
|
|
190
191
|
# Generate refined response
|
|
191
192
|
refinement_prompt = self._build_refinement_prompt(
|
|
192
193
|
request=request,
|
|
193
|
-
response=response.
|
|
194
|
+
response=response.last_text(), ## only if there is no history?
|
|
194
195
|
feedback=evaluation_result,
|
|
195
196
|
iteration=refinement_count,
|
|
196
197
|
)
|
|
@@ -270,48 +271,21 @@ class EvaluatorOptimizerAgent(BaseAgent):
|
|
|
270
271
|
return f"""
|
|
271
272
|
You are an expert evaluator for content quality. Your task is to evaluate a response against the user's original request.
|
|
272
273
|
|
|
273
|
-
Evaluate the response for iteration {iteration + 1} and provide
|
|
274
|
+
Evaluate the response for iteration {iteration + 1} and provide feedback on its quality and areas for improvement.
|
|
274
275
|
|
|
276
|
+
```
|
|
275
277
|
<fastagent:data>
|
|
276
|
-
<fastagent:request>
|
|
278
|
+
<fastagent:request>
|
|
277
279
|
{request}
|
|
278
|
-
</fastagent:request>
|
|
280
|
+
</fastagent:request>
|
|
279
281
|
|
|
280
|
-
<fastagent:response>
|
|
282
|
+
<fastagent:response>
|
|
281
283
|
{response}
|
|
282
|
-
</fastagent:response>
|
|
284
|
+
</fastagent:response>
|
|
283
285
|
</fastagent:data>
|
|
284
286
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
{{
|
|
289
|
-
"rating": "RATING",
|
|
290
|
-
"feedback": "DETAILED FEEDBACK",
|
|
291
|
-
"needs_improvement": BOOLEAN,
|
|
292
|
-
"focus_areas": ["FOCUS_AREA_1", "FOCUS_AREA_2", "FOCUS_AREA_3"]
|
|
293
|
-
}}
|
|
294
|
-
|
|
295
|
-
Where:
|
|
296
|
-
- RATING: Must be one of: "EXCELLENT", "GOOD", "FAIR", or "POOR"
|
|
297
|
-
- EXCELLENT: No improvements needed
|
|
298
|
-
- GOOD: Only minor improvements possible
|
|
299
|
-
- FAIR: Several improvements needed
|
|
300
|
-
- POOR: Major improvements needed
|
|
301
|
-
- DETAILED FEEDBACK: Specific, actionable feedback (as a single string)
|
|
302
|
-
- BOOLEAN: true or false (lowercase, no quotes) indicating if further improvement is needed
|
|
303
|
-
- FOCUS_AREAS: Array of 1-3 specific areas to focus on (empty array if no improvement needed)
|
|
304
|
-
|
|
305
|
-
Example of valid response (DO NOT include the triple backticks in your response):
|
|
306
|
-
{{
|
|
307
|
-
"rating": "GOOD",
|
|
308
|
-
"feedback": "The response is clear but could use more supporting evidence.",
|
|
309
|
-
"needs_improvement": true,
|
|
310
|
-
"focus_areas": ["Add more examples", "Include data points"]
|
|
311
|
-
}}
|
|
312
|
-
|
|
313
|
-
IMPORTANT: Your response should be ONLY the JSON object without any code fences, explanations, or other text.
|
|
314
|
-
</fastagent:instruction>
|
|
287
|
+
```
|
|
288
|
+
|
|
315
289
|
"""
|
|
316
290
|
|
|
317
291
|
def _build_refinement_prompt(
|
|
@@ -333,28 +307,27 @@ IMPORTANT: Your response should be ONLY the JSON object without any code fences,
|
|
|
333
307
|
Returns:
|
|
334
308
|
Formatted refinement prompt
|
|
335
309
|
"""
|
|
336
|
-
|
|
310
|
+
|
|
311
|
+
# Format focus areas as bulleted list with each item on a separate line
|
|
312
|
+
if feedback.focus_areas:
|
|
313
|
+
focus_areas = "\n".join(f" * {area}" for area in feedback.focus_areas)
|
|
314
|
+
else:
|
|
315
|
+
focus_areas = "None specified"
|
|
337
316
|
|
|
338
317
|
return f"""
|
|
339
|
-
You are tasked with improving
|
|
318
|
+
You are tasked with improving your previous response based on expert feedback. This is iteration {iteration + 1} of the refinement process.
|
|
340
319
|
|
|
341
320
|
Your goal is to address all feedback points while maintaining accuracy and relevance to the original request.
|
|
342
321
|
|
|
343
|
-
|
|
344
|
-
<fastagent:request>
|
|
345
|
-
{request}
|
|
346
|
-
</fastagent:request>
|
|
347
|
-
|
|
348
|
-
<fastagent:previous-response>
|
|
349
|
-
{response}
|
|
350
|
-
</fastagent:previous-response>
|
|
322
|
+
```
|
|
351
323
|
|
|
352
324
|
<fastagent:feedback>
|
|
353
|
-
<rating>{feedback.rating}</rating>
|
|
354
|
-
<details>{feedback.feedback}</details>
|
|
355
|
-
<focus-areas>
|
|
325
|
+
<rating>{feedback.rating.name}</rating>
|
|
326
|
+
<details>{feedback.feedback}</details>
|
|
327
|
+
<focus-areas>
|
|
328
|
+
{focus_areas}
|
|
329
|
+
</focus-areas>
|
|
356
330
|
</fastagent:feedback>
|
|
357
|
-
</fastagent:data>
|
|
358
331
|
|
|
359
332
|
<fastagent:instruction>
|
|
360
333
|
Create an improved version of the response that:
|
|
@@ -365,4 +338,7 @@ Create an improved version of the response that:
|
|
|
365
338
|
|
|
366
339
|
Provide your complete improved response without explanations or commentary.
|
|
367
340
|
</fastagent:instruction>
|
|
341
|
+
|
|
342
|
+
```
|
|
343
|
+
|
|
368
344
|
"""
|
|
@@ -39,7 +39,7 @@ Follow these guidelines:
|
|
|
39
39
|
"""
|
|
40
40
|
|
|
41
41
|
# Default routing instruction with placeholders for context (AgentCard JSON)
|
|
42
|
-
|
|
42
|
+
ROUTING_AGENT_INSTRUCTION = """
|
|
43
43
|
Select from the following agents to handle the request:
|
|
44
44
|
<fastagent:agents>
|
|
45
45
|
[
|
|
@@ -100,7 +100,7 @@ class RouterAgent(BaseAgent):
|
|
|
100
100
|
self.routing_instruction = routing_instruction
|
|
101
101
|
self.agent_map = {agent.name: agent for agent in agents}
|
|
102
102
|
|
|
103
|
-
# Set up base router request parameters
|
|
103
|
+
# Set up base router request parameters with just the base instruction for now
|
|
104
104
|
base_params = {"systemPrompt": ROUTING_SYSTEM_INSTRUCTION, "use_history": False}
|
|
105
105
|
|
|
106
106
|
if default_request_params:
|
|
@@ -120,6 +120,18 @@ class RouterAgent(BaseAgent):
|
|
|
120
120
|
if not getattr(agent, "initialized", False):
|
|
121
121
|
await agent.initialize()
|
|
122
122
|
|
|
123
|
+
complete_routing_instruction = await self._generate_routing_instruction(
|
|
124
|
+
self.agents, self.routing_instruction
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Update the system prompt to include the routing instruction with agent cards
|
|
128
|
+
combined_system_prompt = (
|
|
129
|
+
ROUTING_SYSTEM_INSTRUCTION + "\n\n" + complete_routing_instruction
|
|
130
|
+
)
|
|
131
|
+
self._default_request_params.systemPrompt = combined_system_prompt
|
|
132
|
+
self.instruction = combined_system_prompt
|
|
133
|
+
self._routing_instruction_generated = True
|
|
134
|
+
|
|
123
135
|
self.initialized = True
|
|
124
136
|
|
|
125
137
|
async def shutdown(self) -> None:
|
|
@@ -133,6 +145,36 @@ class RouterAgent(BaseAgent):
|
|
|
133
145
|
except Exception as e:
|
|
134
146
|
logger.warning(f"Error shutting down agent: {str(e)}")
|
|
135
147
|
|
|
148
|
+
@staticmethod
|
|
149
|
+
async def _generate_routing_instruction(
|
|
150
|
+
agents: List[Agent], routing_instruction: Optional[str] = None
|
|
151
|
+
) -> str:
|
|
152
|
+
"""
|
|
153
|
+
Generate the complete routing instruction with agent cards.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
agents: List of agents to include in routing instruction
|
|
157
|
+
routing_instruction: Optional custom routing instruction template
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Complete routing instruction with agent cards formatted
|
|
161
|
+
"""
|
|
162
|
+
# Generate agent descriptions
|
|
163
|
+
agent_descriptions = []
|
|
164
|
+
for agent in agents:
|
|
165
|
+
agent_card: AgentCard = await agent.agent_card()
|
|
166
|
+
agent_descriptions.append(
|
|
167
|
+
agent_card.model_dump_json(
|
|
168
|
+
include={"name", "description", "skills"}, exclude_none=True
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
context = ",\n".join(agent_descriptions)
|
|
173
|
+
|
|
174
|
+
# Format the routing instruction
|
|
175
|
+
instruction_template = routing_instruction or ROUTING_AGENT_INSTRUCTION
|
|
176
|
+
return instruction_template.format(context=context)
|
|
177
|
+
|
|
136
178
|
async def attach_llm(
|
|
137
179
|
self,
|
|
138
180
|
llm_factory: type[AugmentedLLMProtocol] | Callable[..., AugmentedLLMProtocol],
|
|
@@ -227,27 +269,10 @@ class RouterAgent(BaseAgent):
|
|
|
227
269
|
agent=self.agents[0].name, confidence="high", reasoning="Only one agent available"
|
|
228
270
|
), None
|
|
229
271
|
|
|
230
|
-
# Generate agent descriptions for the context
|
|
231
|
-
agent_descriptions = []
|
|
232
|
-
for agent in self.agents:
|
|
233
|
-
agent_card: AgentCard = await agent.agent_card()
|
|
234
|
-
agent_descriptions.append(
|
|
235
|
-
agent_card.model_dump_json(
|
|
236
|
-
include={"name", "description", "skills"}, exclude_none=True
|
|
237
|
-
)
|
|
238
|
-
)
|
|
239
|
-
|
|
240
|
-
context = ",\n".join(agent_descriptions)
|
|
241
|
-
|
|
242
|
-
# Format the routing prompt
|
|
243
|
-
routing_instruction = self.routing_instruction or DEFAULT_ROUTING_INSTRUCTION
|
|
244
|
-
routing_instruction = routing_instruction.format(context=context)
|
|
245
|
-
|
|
246
272
|
assert self._llm
|
|
247
|
-
|
|
248
|
-
mutated.add_text(routing_instruction)
|
|
273
|
+
# No need to add routing instruction here - it's already in the system prompt
|
|
249
274
|
response, _ = await self._llm.structured(
|
|
250
|
-
[
|
|
275
|
+
[message],
|
|
251
276
|
RoutingResponse,
|
|
252
277
|
self._default_request_params,
|
|
253
278
|
)
|
mcp_agent/context.py
CHANGED
|
@@ -10,7 +10,11 @@ from typing import TYPE_CHECKING, Any, Optional, Union
|
|
|
10
10
|
from mcp import ServerSession
|
|
11
11
|
from opentelemetry import trace
|
|
12
12
|
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
13
|
+
|
|
14
|
+
# from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor
|
|
13
15
|
from opentelemetry.instrumentation.google_genai import GoogleGenAiSdkInstrumentor
|
|
16
|
+
|
|
17
|
+
# from opentelemetry.instrumentation.mcp import McpInstrumentor
|
|
14
18
|
from opentelemetry.instrumentation.openai import OpenAIInstrumentor
|
|
15
19
|
from opentelemetry.propagate import set_global_textmap
|
|
16
20
|
from opentelemetry.sdk.resources import Resource
|
|
@@ -23,6 +23,9 @@ from typing import (
|
|
|
23
23
|
from mcp.client.session import ElicitationFnT
|
|
24
24
|
|
|
25
25
|
from mcp_agent.agents.agent import AgentConfig
|
|
26
|
+
from mcp_agent.agents.workflow.router_agent import (
|
|
27
|
+
ROUTING_SYSTEM_INSTRUCTION,
|
|
28
|
+
)
|
|
26
29
|
from mcp_agent.core.agent_types import AgentType
|
|
27
30
|
from mcp_agent.core.request_params import RequestParams
|
|
28
31
|
|
|
@@ -397,10 +400,6 @@ def router(
|
|
|
397
400
|
Returns:
|
|
398
401
|
A decorator that registers the router with proper type annotations
|
|
399
402
|
"""
|
|
400
|
-
default_instruction = """
|
|
401
|
-
You are a router that determines which specialized agent should handle a given query.
|
|
402
|
-
Analyze the query and select the most appropriate agent to handle it.
|
|
403
|
-
"""
|
|
404
403
|
|
|
405
404
|
return cast(
|
|
406
405
|
"Callable[[AgentCallable[P, R]], DecoratedRouterProtocol[P, R]]",
|
|
@@ -408,7 +407,7 @@ def router(
|
|
|
408
407
|
self,
|
|
409
408
|
AgentType.ROUTER,
|
|
410
409
|
name=name,
|
|
411
|
-
instruction=instruction or
|
|
410
|
+
instruction=instruction or ROUTING_SYSTEM_INSTRUCTION,
|
|
412
411
|
servers=servers,
|
|
413
412
|
model=model,
|
|
414
413
|
use_history=use_history,
|
|
@@ -511,13 +511,12 @@ def create_keybindings(on_toggle_multiline=None, app=None, agent_provider=None,
|
|
|
511
511
|
rich_print("\n[green]✓ Copied to clipboard[/green]")
|
|
512
512
|
return
|
|
513
513
|
|
|
514
|
-
rich_print("\n[yellow]No assistant messages found[/yellow]")
|
|
515
514
|
else:
|
|
516
|
-
|
|
517
|
-
except Exception
|
|
518
|
-
|
|
515
|
+
pass
|
|
516
|
+
except Exception:
|
|
517
|
+
pass
|
|
519
518
|
else:
|
|
520
|
-
|
|
519
|
+
pass
|
|
521
520
|
|
|
522
521
|
return kb
|
|
523
522
|
|
|
@@ -5,6 +5,7 @@ from typing import Any, Dict, Optional
|
|
|
5
5
|
|
|
6
6
|
from mcp.types import ElicitRequestedSchema
|
|
7
7
|
from prompt_toolkit import Application
|
|
8
|
+
from prompt_toolkit.application.current import get_app
|
|
8
9
|
from prompt_toolkit.buffer import Buffer
|
|
9
10
|
from prompt_toolkit.filters import Condition
|
|
10
11
|
from prompt_toolkit.formatted_text import FormattedText
|
|
@@ -272,29 +273,32 @@ class ElicitationForm:
|
|
|
272
273
|
keep_focused_window_visible=True,
|
|
273
274
|
)
|
|
274
275
|
|
|
275
|
-
#
|
|
276
|
-
title_bar = Window(
|
|
277
|
-
FormattedTextControl(FormattedText([("class:title", "Elicitation Request")])),
|
|
278
|
-
height=1,
|
|
279
|
-
style="class:dialog.title",
|
|
280
|
-
)
|
|
281
|
-
|
|
282
|
-
# Combine title, sticky headers, and scrollable content
|
|
276
|
+
# Combine sticky headers and scrollable content (no separate title bar needed)
|
|
283
277
|
full_content = HSplit(
|
|
284
278
|
[
|
|
285
|
-
|
|
286
|
-
Window(height=1), # Spacing after title
|
|
279
|
+
Window(height=1), # Top spacing
|
|
287
280
|
sticky_headers, # Headers stay fixed at top
|
|
288
281
|
scrollable_content, # Form fields can scroll
|
|
289
282
|
]
|
|
290
283
|
)
|
|
291
284
|
|
|
292
|
-
# Create dialog frame
|
|
285
|
+
# Create dialog frame with title
|
|
293
286
|
dialog = Frame(
|
|
294
287
|
body=full_content,
|
|
288
|
+
title="Elicitation Request",
|
|
295
289
|
style="class:dialog",
|
|
296
290
|
)
|
|
297
291
|
|
|
292
|
+
# Apply width constraints by putting Frame in VSplit with flexible spacers
|
|
293
|
+
# This prevents console display interference and constrains the Frame border
|
|
294
|
+
constrained_dialog = VSplit(
|
|
295
|
+
[
|
|
296
|
+
Window(width=10), # Smaller left spacer
|
|
297
|
+
dialog,
|
|
298
|
+
Window(width=10), # Smaller right spacer
|
|
299
|
+
]
|
|
300
|
+
)
|
|
301
|
+
|
|
298
302
|
# Key bindings
|
|
299
303
|
kb = KeyBindings()
|
|
300
304
|
|
|
@@ -370,7 +374,7 @@ class ElicitationForm:
|
|
|
370
374
|
# Add toolbar to the layout
|
|
371
375
|
root_layout = HSplit(
|
|
372
376
|
[
|
|
373
|
-
|
|
377
|
+
constrained_dialog, # The width-constrained dialog
|
|
374
378
|
self._toolbar_window,
|
|
375
379
|
]
|
|
376
380
|
)
|
|
@@ -588,7 +592,6 @@ class ElicitationForm:
|
|
|
588
592
|
|
|
589
593
|
def _is_in_multiline_field(self) -> bool:
|
|
590
594
|
"""Check if currently focused field is a multiline field."""
|
|
591
|
-
from prompt_toolkit.application.current import get_app
|
|
592
595
|
|
|
593
596
|
focused = get_app().layout.current_control
|
|
594
597
|
|
mcp_agent/llm/augmented_llm.py
CHANGED
|
@@ -212,8 +212,6 @@ class AugmentedLLM(ContextDependent, AugmentedLLMProtocol, Generic[MessageParamT
|
|
|
212
212
|
# note - check changes here are mirrored in structured(). i've thought hard about
|
|
213
213
|
# a strategy to reduce duplication etc, but aiming for simple but imperfect for the moment
|
|
214
214
|
|
|
215
|
-
# We never expect this for structured() calls - this is for interactive use - developers
|
|
216
|
-
# can do this programatically
|
|
217
215
|
# TODO -- create a "fast-agent" control role rather than magic strings
|
|
218
216
|
|
|
219
217
|
if multipart_messages[-1].first_text().startswith("***SAVE_HISTORY"):
|
|
@@ -235,6 +233,7 @@ class AugmentedLLM(ContextDependent, AugmentedLLMProtocol, Generic[MessageParamT
|
|
|
235
233
|
|
|
236
234
|
# add generic error and termination reason handling/rollback
|
|
237
235
|
self._message_history.append(assistant_response)
|
|
236
|
+
|
|
238
237
|
return assistant_response
|
|
239
238
|
|
|
240
239
|
@abstractmethod
|
|
@@ -164,15 +164,10 @@ class PassthroughLLM(AugmentedLLM):
|
|
|
164
164
|
request_params: RequestParams | None = None,
|
|
165
165
|
is_template: bool = False,
|
|
166
166
|
) -> PromptMessageMultipart:
|
|
167
|
-
print(
|
|
168
|
-
f"DEBUG: PassthroughLLM _apply_prompt_provider_specific called with {len(multipart_messages)} messages, is_template={is_template}"
|
|
169
|
-
)
|
|
170
|
-
|
|
171
167
|
# Add messages to history with proper is_prompt flag
|
|
172
168
|
self.history.extend(multipart_messages, is_prompt=is_template)
|
|
173
169
|
|
|
174
170
|
last_message = multipart_messages[-1]
|
|
175
|
-
print(f"DEBUG: Last message role: {last_message.role}, text: '{last_message.first_text()}'")
|
|
176
171
|
|
|
177
172
|
if self.is_tool_call(last_message):
|
|
178
173
|
result = Prompt.assistant(await self.generate_str(last_message.first_text()))
|
|
@@ -209,14 +204,8 @@ class PassthroughLLM(AugmentedLLM):
|
|
|
209
204
|
else:
|
|
210
205
|
# TODO -- improve when we support Audio/Multimodal gen models e.g. gemini . This should really just return the input as "assistant"...
|
|
211
206
|
concatenated: str = "\n".join(message.all_text() for message in multipart_messages)
|
|
212
|
-
print(
|
|
213
|
-
f"DEBUG: PassthroughLLM generating response: '{concatenated}' (is_template={is_template})"
|
|
214
|
-
)
|
|
215
207
|
await self.show_assistant_message(concatenated)
|
|
216
208
|
result = Prompt.assistant(concatenated)
|
|
217
|
-
print(f"DEBUG: PassthroughLLM created result: {result}")
|
|
218
|
-
print(f"DEBUG: Result first_text(): {result.first_text()}")
|
|
219
|
-
print(f"DEBUG: Result content: {result.content}")
|
|
220
209
|
|
|
221
210
|
# Track usage for this passthrough "turn"
|
|
222
211
|
try:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import json
|
|
2
|
+
from typing import TYPE_CHECKING, Any, List, Tuple, Type
|
|
2
3
|
|
|
3
4
|
from mcp.types import TextContent
|
|
4
5
|
|
|
@@ -33,6 +34,7 @@ from anthropic.types import (
|
|
|
33
34
|
from mcp.types import (
|
|
34
35
|
CallToolRequest,
|
|
35
36
|
CallToolRequestParams,
|
|
37
|
+
CallToolResult,
|
|
36
38
|
ContentBlock,
|
|
37
39
|
)
|
|
38
40
|
from rich.text import Text
|
|
@@ -99,6 +101,184 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
|
|
|
99
101
|
cache_mode = self.context.config.anthropic.cache_mode
|
|
100
102
|
return cache_mode
|
|
101
103
|
|
|
104
|
+
async def _prepare_tools(self, structured_model: Type[ModelT] | None = None) -> List[ToolParam]:
|
|
105
|
+
"""Prepare tools based on whether we're in structured output mode."""
|
|
106
|
+
if structured_model:
|
|
107
|
+
# JSON mode - create a single tool for structured output
|
|
108
|
+
return [
|
|
109
|
+
ToolParam(
|
|
110
|
+
name="return_structured_output",
|
|
111
|
+
description="Return the response in the required JSON format",
|
|
112
|
+
input_schema=structured_model.model_json_schema(),
|
|
113
|
+
)
|
|
114
|
+
]
|
|
115
|
+
else:
|
|
116
|
+
# Regular mode - use tools from aggregator
|
|
117
|
+
tool_list: ListToolsResult = await self.aggregator.list_tools()
|
|
118
|
+
return [
|
|
119
|
+
ToolParam(
|
|
120
|
+
name=tool.name,
|
|
121
|
+
description=tool.description or "",
|
|
122
|
+
input_schema=tool.inputSchema,
|
|
123
|
+
)
|
|
124
|
+
for tool in tool_list.tools
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
def _apply_system_cache(self, base_args: dict, cache_mode: str) -> None:
|
|
128
|
+
"""Apply cache control to system prompt if cache mode allows it."""
|
|
129
|
+
if cache_mode != "off" and base_args["system"]:
|
|
130
|
+
if isinstance(base_args["system"], str):
|
|
131
|
+
base_args["system"] = [
|
|
132
|
+
{
|
|
133
|
+
"type": "text",
|
|
134
|
+
"text": base_args["system"],
|
|
135
|
+
"cache_control": {"type": "ephemeral"},
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
self.logger.debug(
|
|
139
|
+
"Applied cache_control to system prompt (caches tools+system in one block)"
|
|
140
|
+
)
|
|
141
|
+
else:
|
|
142
|
+
self.logger.debug(f"System prompt is not a string: {type(base_args['system'])}")
|
|
143
|
+
|
|
144
|
+
async def _apply_conversation_cache(self, messages: List[MessageParam], cache_mode: str) -> int:
|
|
145
|
+
"""Apply conversation caching if in auto mode. Returns number of cache blocks applied."""
|
|
146
|
+
applied_count = 0
|
|
147
|
+
if cache_mode == "auto" and self.history.should_apply_conversation_cache():
|
|
148
|
+
cache_updates = self.history.get_conversation_cache_updates()
|
|
149
|
+
|
|
150
|
+
# Remove cache control from old positions
|
|
151
|
+
if cache_updates["remove"]:
|
|
152
|
+
self.history.remove_cache_control_from_messages(messages, cache_updates["remove"])
|
|
153
|
+
self.logger.debug(
|
|
154
|
+
f"Removed conversation cache_control from positions {cache_updates['remove']}"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Add cache control to new positions
|
|
158
|
+
if cache_updates["add"]:
|
|
159
|
+
applied_count = self.history.add_cache_control_to_messages(
|
|
160
|
+
messages, cache_updates["add"]
|
|
161
|
+
)
|
|
162
|
+
if applied_count > 0:
|
|
163
|
+
self.history.apply_conversation_cache_updates(cache_updates)
|
|
164
|
+
self.logger.debug(
|
|
165
|
+
f"Applied conversation cache_control to positions {cache_updates['add']} ({applied_count} blocks)"
|
|
166
|
+
)
|
|
167
|
+
else:
|
|
168
|
+
self.logger.debug(
|
|
169
|
+
f"Failed to apply conversation cache_control to positions {cache_updates['add']}"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
return applied_count
|
|
173
|
+
|
|
174
|
+
async def _process_structured_output(
|
|
175
|
+
self,
|
|
176
|
+
content_block: Any,
|
|
177
|
+
) -> Tuple[str, CallToolResult, TextContent]:
|
|
178
|
+
"""
|
|
179
|
+
Process a structured output tool call from Anthropic.
|
|
180
|
+
|
|
181
|
+
This handles the special case where Anthropic's model was forced to use
|
|
182
|
+
a 'return_structured_output' tool via tool_choice. The tool input contains
|
|
183
|
+
the JSON data we want, so we extract it and format it for display.
|
|
184
|
+
|
|
185
|
+
Even though we don't call an external tool, we must create a CallToolResult
|
|
186
|
+
to satisfy Anthropic's API requirement that every tool_use has a corresponding
|
|
187
|
+
tool_result in the next message.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Tuple of (tool_use_id, tool_result, content_block) for the structured data
|
|
191
|
+
"""
|
|
192
|
+
tool_args = content_block.input
|
|
193
|
+
tool_use_id = content_block.id
|
|
194
|
+
|
|
195
|
+
# Show the formatted JSON response to the user
|
|
196
|
+
json_response = json.dumps(tool_args, indent=2)
|
|
197
|
+
await self.show_assistant_message(json_response)
|
|
198
|
+
|
|
199
|
+
# Create the content for responses
|
|
200
|
+
structured_content = TextContent(type="text", text=json.dumps(tool_args))
|
|
201
|
+
|
|
202
|
+
# Create a CallToolResult to satisfy Anthropic's API requirements
|
|
203
|
+
# This represents the "result" of our structured output "tool"
|
|
204
|
+
tool_result = CallToolResult(isError=False, content=[structured_content])
|
|
205
|
+
|
|
206
|
+
return tool_use_id, tool_result, structured_content
|
|
207
|
+
|
|
208
|
+
async def _process_regular_tool_call(
|
|
209
|
+
self,
|
|
210
|
+
content_block: Any,
|
|
211
|
+
available_tools: List[ToolParam],
|
|
212
|
+
is_first_tool: bool,
|
|
213
|
+
message_text: str | Text,
|
|
214
|
+
) -> Tuple[str, CallToolResult]:
|
|
215
|
+
"""
|
|
216
|
+
Process a regular MCP tool call.
|
|
217
|
+
|
|
218
|
+
This handles actual tool execution via the MCP aggregator.
|
|
219
|
+
"""
|
|
220
|
+
tool_name = content_block.name
|
|
221
|
+
tool_args = content_block.input
|
|
222
|
+
tool_use_id = content_block.id
|
|
223
|
+
|
|
224
|
+
if is_first_tool:
|
|
225
|
+
await self.show_assistant_message(message_text, tool_name)
|
|
226
|
+
|
|
227
|
+
self.show_tool_call(available_tools, tool_name, tool_args)
|
|
228
|
+
tool_call_request = CallToolRequest(
|
|
229
|
+
method="tools/call",
|
|
230
|
+
params=CallToolRequestParams(name=tool_name, arguments=tool_args),
|
|
231
|
+
)
|
|
232
|
+
result = await self.call_tool(request=tool_call_request, tool_call_id=tool_use_id)
|
|
233
|
+
self.show_tool_result(result)
|
|
234
|
+
return tool_use_id, result
|
|
235
|
+
|
|
236
|
+
async def _process_tool_calls(
|
|
237
|
+
self,
|
|
238
|
+
tool_uses: List[Any],
|
|
239
|
+
available_tools: List[ToolParam],
|
|
240
|
+
message_text: str | Text,
|
|
241
|
+
structured_model: Type[ModelT] | None = None,
|
|
242
|
+
) -> Tuple[List[Tuple[str, CallToolResult]], List[ContentBlock]]:
|
|
243
|
+
"""
|
|
244
|
+
Process tool calls, handling both structured output and regular MCP tools.
|
|
245
|
+
|
|
246
|
+
For structured output mode:
|
|
247
|
+
- Extracts JSON data from the forced 'return_structured_output' tool
|
|
248
|
+
- Does NOT create fake CallToolResults
|
|
249
|
+
- Returns the JSON content directly
|
|
250
|
+
|
|
251
|
+
For regular tools:
|
|
252
|
+
- Calls actual MCP tools via the aggregator
|
|
253
|
+
- Returns real CallToolResults
|
|
254
|
+
"""
|
|
255
|
+
tool_results = []
|
|
256
|
+
responses = []
|
|
257
|
+
|
|
258
|
+
for tool_idx, content_block in enumerate(tool_uses):
|
|
259
|
+
tool_name = content_block.name
|
|
260
|
+
is_first_tool = tool_idx == 0
|
|
261
|
+
|
|
262
|
+
if tool_name == "return_structured_output" and structured_model:
|
|
263
|
+
# Structured output: extract JSON, don't call external tools
|
|
264
|
+
(
|
|
265
|
+
tool_use_id,
|
|
266
|
+
tool_result,
|
|
267
|
+
structured_content,
|
|
268
|
+
) = await self._process_structured_output(content_block)
|
|
269
|
+
responses.append(structured_content)
|
|
270
|
+
# Add to tool_results to satisfy Anthropic's API requirement for tool_result messages
|
|
271
|
+
tool_results.append((tool_use_id, tool_result))
|
|
272
|
+
else:
|
|
273
|
+
# Regular tool: call external MCP tool
|
|
274
|
+
tool_use_id, tool_result = await self._process_regular_tool_call(
|
|
275
|
+
content_block, available_tools, is_first_tool, message_text
|
|
276
|
+
)
|
|
277
|
+
tool_results.append((tool_use_id, tool_result))
|
|
278
|
+
responses.extend(tool_result.content)
|
|
279
|
+
|
|
280
|
+
return tool_results, responses
|
|
281
|
+
|
|
102
282
|
async def _process_stream(self, stream: AsyncMessageStream, model: str) -> Message:
|
|
103
283
|
"""Process the streaming response and display real-time token usage."""
|
|
104
284
|
# Track estimated output tokens by counting text chunks
|
|
@@ -150,6 +330,7 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
|
|
|
150
330
|
self,
|
|
151
331
|
message_param,
|
|
152
332
|
request_params: RequestParams | None = None,
|
|
333
|
+
structured_model: Type[ModelT] | None = None,
|
|
153
334
|
) -> list[ContentBlock]:
|
|
154
335
|
"""
|
|
155
336
|
Process a query using an LLM and available tools.
|
|
@@ -181,15 +362,7 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
|
|
|
181
362
|
cache_mode = self._get_cache_mode()
|
|
182
363
|
self.logger.debug(f"Anthropic cache_mode: {cache_mode}")
|
|
183
364
|
|
|
184
|
-
|
|
185
|
-
available_tools: List[ToolParam] = [
|
|
186
|
-
ToolParam(
|
|
187
|
-
name=tool.name,
|
|
188
|
-
description=tool.description or "",
|
|
189
|
-
input_schema=tool.inputSchema,
|
|
190
|
-
)
|
|
191
|
-
for tool in tool_list.tools
|
|
192
|
-
]
|
|
365
|
+
available_tools = await self._prepare_tools(structured_model)
|
|
193
366
|
|
|
194
367
|
responses: List[ContentBlock] = []
|
|
195
368
|
|
|
@@ -209,59 +382,25 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
|
|
|
209
382
|
"tools": available_tools,
|
|
210
383
|
}
|
|
211
384
|
|
|
212
|
-
#
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if isinstance(base_args["system"], str):
|
|
216
|
-
base_args["system"] = [
|
|
217
|
-
{
|
|
218
|
-
"type": "text",
|
|
219
|
-
"text": base_args["system"],
|
|
220
|
-
"cache_control": {"type": "ephemeral"},
|
|
221
|
-
}
|
|
222
|
-
]
|
|
223
|
-
self.logger.debug(
|
|
224
|
-
"Applied cache_control to system prompt (caches tools+system in one block)"
|
|
225
|
-
)
|
|
226
|
-
else:
|
|
227
|
-
self.logger.debug(f"System prompt is not a string: {type(base_args['system'])}")
|
|
385
|
+
# Add tool_choice for structured output mode
|
|
386
|
+
if structured_model:
|
|
387
|
+
base_args["tool_choice"] = {"type": "tool", "name": "return_structured_output"}
|
|
228
388
|
|
|
229
|
-
# Apply
|
|
230
|
-
|
|
231
|
-
cache_updates = self.history.get_conversation_cache_updates()
|
|
389
|
+
# Apply cache control to system prompt
|
|
390
|
+
self._apply_system_cache(base_args, cache_mode)
|
|
232
391
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
self.history.remove_cache_control_from_messages(
|
|
236
|
-
messages, cache_updates["remove"]
|
|
237
|
-
)
|
|
238
|
-
self.logger.debug(
|
|
239
|
-
f"Removed conversation cache_control from positions {cache_updates['remove']}"
|
|
240
|
-
)
|
|
392
|
+
# Apply conversation caching
|
|
393
|
+
applied_count = await self._apply_conversation_cache(messages, cache_mode)
|
|
241
394
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
395
|
+
# Verify we don't exceed Anthropic's 4 cache block limit
|
|
396
|
+
if applied_count > 0:
|
|
397
|
+
total_cache_blocks = applied_count
|
|
398
|
+
if cache_mode != "off" and base_args["system"]:
|
|
399
|
+
total_cache_blocks += 1 # tools+system cache block
|
|
400
|
+
if total_cache_blocks > 4:
|
|
401
|
+
self.logger.warning(
|
|
402
|
+
f"Total cache blocks ({total_cache_blocks}) exceeds Anthropic limit of 4"
|
|
246
403
|
)
|
|
247
|
-
if applied_count > 0:
|
|
248
|
-
self.history.apply_conversation_cache_updates(cache_updates)
|
|
249
|
-
self.logger.debug(
|
|
250
|
-
f"Applied conversation cache_control to positions {cache_updates['add']} ({applied_count} blocks)"
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
# Verify we don't exceed Anthropic's 4 cache block limit
|
|
254
|
-
total_cache_blocks = applied_count
|
|
255
|
-
if cache_mode != "off" and base_args["system"]:
|
|
256
|
-
total_cache_blocks += 1 # tools+system cache block
|
|
257
|
-
if total_cache_blocks > 4:
|
|
258
|
-
self.logger.warning(
|
|
259
|
-
f"Total cache blocks ({total_cache_blocks}) exceeds Anthropic limit of 4"
|
|
260
|
-
)
|
|
261
|
-
else:
|
|
262
|
-
self.logger.debug(
|
|
263
|
-
f"Failed to apply conversation cache_control to positions {cache_updates['add']}"
|
|
264
|
-
)
|
|
265
404
|
|
|
266
405
|
if params.maxTokens is not None:
|
|
267
406
|
base_args["max_tokens"] = params.maxTokens
|
|
@@ -387,34 +526,22 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
|
|
|
387
526
|
style="dim green italic",
|
|
388
527
|
)
|
|
389
528
|
|
|
390
|
-
# Process all tool calls
|
|
391
|
-
tool_results =
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
tool_args = content_block.input
|
|
396
|
-
tool_use_id = content_block.id
|
|
397
|
-
|
|
398
|
-
if tool_idx == 0: # Only show message for first tool use
|
|
399
|
-
await self.show_assistant_message(message_text, tool_name)
|
|
400
|
-
|
|
401
|
-
self.show_tool_call(available_tools, tool_name, tool_args)
|
|
402
|
-
tool_call_request = CallToolRequest(
|
|
403
|
-
method="tools/call",
|
|
404
|
-
params=CallToolRequestParams(name=tool_name, arguments=tool_args),
|
|
405
|
-
)
|
|
406
|
-
# TODO -- support MCP isError etc.
|
|
407
|
-
result = await self.call_tool(
|
|
408
|
-
request=tool_call_request, tool_call_id=tool_use_id
|
|
409
|
-
)
|
|
410
|
-
self.show_tool_result(result)
|
|
411
|
-
|
|
412
|
-
# Add each result to our collection
|
|
413
|
-
tool_results.append((tool_use_id, result))
|
|
414
|
-
responses.extend(result.content)
|
|
529
|
+
# Process all tool calls using the helper method
|
|
530
|
+
tool_results, tool_responses = await self._process_tool_calls(
|
|
531
|
+
tool_uses, available_tools, message_text, structured_model
|
|
532
|
+
)
|
|
533
|
+
responses.extend(tool_responses)
|
|
415
534
|
|
|
535
|
+
# Always add tool_results_message first (required by Anthropic API)
|
|
416
536
|
messages.append(AnthropicConverter.create_tool_results_message(tool_results))
|
|
417
537
|
|
|
538
|
+
# For structured output, we have our result and should exit after sending tool_result
|
|
539
|
+
if structured_model and any(
|
|
540
|
+
tool.name == "return_structured_output" for tool in tool_uses
|
|
541
|
+
):
|
|
542
|
+
self.logger.debug("Structured output received, breaking iteration loop")
|
|
543
|
+
break
|
|
544
|
+
|
|
418
545
|
# Only save the new conversation messages to history if use_history is true
|
|
419
546
|
# Keep the prompt messages separate
|
|
420
547
|
if params.use_history:
|
|
@@ -501,19 +628,51 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
|
|
|
501
628
|
) -> Tuple[ModelT | None, PromptMessageMultipart]: # noqa: F821
|
|
502
629
|
request_params = self.get_request_params(request_params)
|
|
503
630
|
|
|
504
|
-
#
|
|
505
|
-
multipart_messages[-1]
|
|
506
|
-
"""YOU MUST RESPOND IN THE FOLLOWING FORMAT:
|
|
507
|
-
{schema}
|
|
508
|
-
RESPOND ONLY WITH THE JSON, NO PREAMBLE, CODE FENCES OR 'properties' ARE PERMISSABLE """.format(
|
|
509
|
-
schema=model.model_json_schema()
|
|
510
|
-
)
|
|
511
|
-
)
|
|
631
|
+
# Check the last message role
|
|
632
|
+
last_message = multipart_messages[-1]
|
|
512
633
|
|
|
513
|
-
|
|
514
|
-
|
|
634
|
+
# Add all previous messages to history (or all messages if last is from assistant)
|
|
635
|
+
messages_to_add = (
|
|
636
|
+
multipart_messages[:-1] if last_message.role == "user" else multipart_messages
|
|
515
637
|
)
|
|
516
|
-
|
|
638
|
+
converted = []
|
|
639
|
+
|
|
640
|
+
for msg in messages_to_add:
|
|
641
|
+
anthropic_msg = AnthropicConverter.convert_to_anthropic(msg)
|
|
642
|
+
converted.append(anthropic_msg)
|
|
643
|
+
|
|
644
|
+
self.history.extend(converted, is_prompt=False)
|
|
645
|
+
|
|
646
|
+
if last_message.role == "user":
|
|
647
|
+
self.logger.debug("Last message in prompt is from user, generating structured response")
|
|
648
|
+
message_param = AnthropicConverter.convert_to_anthropic(last_message)
|
|
649
|
+
|
|
650
|
+
# Call _anthropic_completion with the structured model
|
|
651
|
+
response_content = await self._anthropic_completion(
|
|
652
|
+
message_param, request_params, structured_model=model
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
# Extract the structured data from the response
|
|
656
|
+
for content in response_content:
|
|
657
|
+
if content.type == "text":
|
|
658
|
+
try:
|
|
659
|
+
# Parse the JSON response from the tool
|
|
660
|
+
data = json.loads(content.text)
|
|
661
|
+
parsed_model = model(**data)
|
|
662
|
+
# Create assistant response
|
|
663
|
+
assistant_response = Prompt.assistant(content)
|
|
664
|
+
return parsed_model, assistant_response
|
|
665
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
666
|
+
self.logger.error(f"Failed to parse structured output: {e}")
|
|
667
|
+
assistant_response = Prompt.assistant(content)
|
|
668
|
+
return None, assistant_response
|
|
669
|
+
|
|
670
|
+
# If no valid response found
|
|
671
|
+
return None, Prompt.assistant()
|
|
672
|
+
else:
|
|
673
|
+
# For assistant messages: Return the last message content
|
|
674
|
+
self.logger.debug("Last message in prompt is from assistant, returning it directly")
|
|
675
|
+
return None, last_message
|
|
517
676
|
|
|
518
677
|
def _show_usage(self, raw_usage: Usage, turn_usage: TurnUsage) -> None:
|
|
519
678
|
# Print raw usage for debugging
|
|
@@ -18,7 +18,7 @@ fast = FastAgent("Evaluator-Optimizer")
|
|
|
18
18
|
candidate details, and company information. Tailor the response to the company and job requirements.
|
|
19
19
|
""",
|
|
20
20
|
servers=["fetch"],
|
|
21
|
-
model="
|
|
21
|
+
model="gpt-4.1-nano",
|
|
22
22
|
use_history=True,
|
|
23
23
|
)
|
|
24
24
|
# Define evaluator agent
|
|
@@ -40,7 +40,7 @@ fast = FastAgent("Evaluator-Optimizer")
|
|
|
40
40
|
Summarize your evaluation as a structured response with:
|
|
41
41
|
- Overall quality rating.
|
|
42
42
|
- Specific feedback and areas for improvement.""",
|
|
43
|
-
model="
|
|
43
|
+
model="sonnet",
|
|
44
44
|
)
|
|
45
45
|
# Define the evaluator-optimizer workflow
|
|
46
46
|
@fast.evaluator_optimizer(
|
mcp_agent/ui/console_display.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
from json import JSONDecodeError
|
|
1
2
|
from typing import Optional, Union
|
|
2
3
|
|
|
3
4
|
from mcp.types import CallToolResult
|
|
5
|
+
from rich.json import JSON
|
|
4
6
|
from rich.panel import Panel
|
|
5
7
|
from rich.text import Text
|
|
6
8
|
|
|
@@ -52,13 +54,15 @@ class ConsoleDisplay:
|
|
|
52
54
|
elif len(content) == 1 and is_text_content(content[0]):
|
|
53
55
|
text_content = get_text(content[0])
|
|
54
56
|
char_count = len(text_content) if text_content else 0
|
|
55
|
-
status = f"Text Only
|
|
57
|
+
status = f"Text Only {char_count} chars"
|
|
56
58
|
else:
|
|
57
59
|
text_count = sum(1 for item in content if is_text_content(item))
|
|
58
60
|
if text_count == len(content):
|
|
59
61
|
status = f"{len(content)} Text Blocks" if len(content) > 1 else "1 Text Block"
|
|
60
62
|
else:
|
|
61
|
-
status =
|
|
63
|
+
status = (
|
|
64
|
+
f"{len(content)} Content Blocks" if len(content) > 1 else "1 Content Block"
|
|
65
|
+
)
|
|
62
66
|
|
|
63
67
|
# Combined separator and status line
|
|
64
68
|
left = f"[{block_color}]▎[/{block_color}][{text_color}]▶[/{text_color}]{f' [{block_color}]{name}[/{block_color}]' if name else ''}"
|
|
@@ -357,16 +361,22 @@ class ConsoleDisplay:
|
|
|
357
361
|
right = f"[dim]{model}[/dim]" if model else ""
|
|
358
362
|
self._create_combined_separator_status(left, right)
|
|
359
363
|
|
|
360
|
-
# Display content as markdown if it looks like markdown, otherwise as text
|
|
361
364
|
if isinstance(message_text, str):
|
|
362
365
|
content = message_text
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
366
|
+
|
|
367
|
+
# Try to detect and pretty print JSON
|
|
368
|
+
try:
|
|
369
|
+
import json
|
|
370
|
+
|
|
371
|
+
json.loads(content)
|
|
372
|
+
json = JSON(message_text)
|
|
373
|
+
console.console.print(json, markup=self._markup)
|
|
374
|
+
except (JSONDecodeError, TypeError, ValueError):
|
|
375
|
+
# Not JSON, treat as markdown
|
|
376
|
+
md = Markdown(content, code_theme=CODE_STYLE)
|
|
377
|
+
console.console.print(md, markup=self._markup)
|
|
368
378
|
else:
|
|
369
|
-
# Handle Text objects directly
|
|
379
|
+
# Handle Rich Text objects directly
|
|
370
380
|
console.console.print(message_text, markup=self._markup)
|
|
371
381
|
|
|
372
382
|
# Bottom separator with server list: ─ [server1] [server2] ────────
|
|
File without changes
|
|
File without changes
|
|
File without changes
|