fast-agent-mcp 0.1.11__py3-none-any.whl → 0.1.12__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.1.11.dist-info → fast_agent_mcp-0.1.12.dist-info}/METADATA +1 -1
- {fast_agent_mcp-0.1.11.dist-info → fast_agent_mcp-0.1.12.dist-info}/RECORD +39 -38
- mcp_agent/agents/agent.py +1 -24
- mcp_agent/app.py +0 -5
- mcp_agent/context.py +0 -2
- mcp_agent/core/agent_app.py +1 -1
- mcp_agent/core/agent_types.py +29 -2
- mcp_agent/core/decorators.py +1 -2
- mcp_agent/core/error_handling.py +1 -1
- mcp_agent/core/factory.py +2 -3
- mcp_agent/core/mcp_content.py +2 -3
- mcp_agent/core/request_params.py +43 -0
- mcp_agent/core/types.py +4 -2
- mcp_agent/core/validation.py +14 -15
- mcp_agent/logging/transport.py +2 -2
- mcp_agent/mcp/interfaces.py +37 -3
- mcp_agent/mcp/mcp_agent_client_session.py +1 -1
- mcp_agent/mcp/mcp_aggregator.py +5 -6
- mcp_agent/mcp/sampling.py +60 -53
- mcp_agent/mcp_server/__init__.py +1 -1
- mcp_agent/resources/examples/prompting/__init__.py +1 -1
- mcp_agent/ui/console_display.py +2 -2
- mcp_agent/workflows/evaluator_optimizer/evaluator_optimizer.py +2 -2
- mcp_agent/workflows/llm/augmented_llm.py +42 -102
- mcp_agent/workflows/llm/augmented_llm_anthropic.py +4 -3
- mcp_agent/workflows/llm/augmented_llm_openai.py +4 -3
- mcp_agent/workflows/llm/augmented_llm_passthrough.py +33 -4
- mcp_agent/workflows/llm/model_factory.py +1 -1
- mcp_agent/workflows/llm/prompt_utils.py +42 -28
- mcp_agent/workflows/llm/providers/multipart_converter_anthropic.py +244 -140
- mcp_agent/workflows/llm/providers/multipart_converter_openai.py +230 -185
- mcp_agent/workflows/llm/providers/sampling_converter_anthropic.py +5 -204
- mcp_agent/workflows/llm/providers/sampling_converter_openai.py +9 -207
- mcp_agent/workflows/llm/sampling_converter.py +124 -0
- mcp_agent/workflows/llm/sampling_format_converter.py +0 -17
- mcp_agent/workflows/router/router_base.py +10 -10
- mcp_agent/workflows/llm/llm_selector.py +0 -345
- {fast_agent_mcp-0.1.11.dist-info → fast_agent_mcp-0.1.12.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.1.11.dist-info → fast_agent_mcp-0.1.12.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.1.11.dist-info → fast_agent_mcp-0.1.12.dist-info}/licenses/LICENSE +0 -0
@@ -265,16 +265,16 @@ class Router(ABC, ContextDependent):
|
|
265
265
|
# Check if we have any content (description or tools)
|
266
266
|
has_description = bool(category.description)
|
267
267
|
has_tools = bool(category.tools)
|
268
|
-
|
268
|
+
|
269
269
|
# If no content at all, use self-closing tag
|
270
270
|
if not has_description and not has_tools:
|
271
271
|
return f'<fastagent:server-category name="{category.name}" />'
|
272
|
-
|
272
|
+
|
273
273
|
# Otherwise, build the content
|
274
274
|
description_section = ""
|
275
275
|
if has_description:
|
276
276
|
description_section = f"\n<fastagent:description>{category.description}</fastagent:description>"
|
277
|
-
|
277
|
+
|
278
278
|
# Add tools section if we have tool information
|
279
279
|
if has_tools:
|
280
280
|
tools = self._format_tools(category.tools)
|
@@ -293,11 +293,11 @@ class Router(ABC, ContextDependent):
|
|
293
293
|
# Check if we have any content (description or servers)
|
294
294
|
has_description = bool(category.description)
|
295
295
|
has_servers = bool(category.servers)
|
296
|
-
|
296
|
+
|
297
297
|
# If no content at all, use self-closing tag
|
298
298
|
if not has_description and not has_servers:
|
299
299
|
return f'<fastagent:agent-category name="{category.name}" />'
|
300
|
-
|
300
|
+
|
301
301
|
# Build description section if needed
|
302
302
|
description_section = ""
|
303
303
|
if has_description:
|
@@ -314,13 +314,13 @@ class Router(ABC, ContextDependent):
|
|
314
314
|
# Check if this server has any content
|
315
315
|
has_server_description = bool(server.description)
|
316
316
|
has_server_tools = bool(server.tools)
|
317
|
-
|
317
|
+
|
318
318
|
# Use self-closing tag if server has no content
|
319
319
|
if not has_server_description and not has_server_tools:
|
320
320
|
server_section = f'<fastagent:server name="{server.name}" />'
|
321
321
|
server_sections.append(server_section)
|
322
322
|
continue
|
323
|
-
|
323
|
+
|
324
324
|
# Build server description if needed
|
325
325
|
server_desc_section = ""
|
326
326
|
if has_server_description:
|
@@ -342,7 +342,7 @@ class Router(ABC, ContextDependent):
|
|
342
342
|
# Just description, no tools
|
343
343
|
server_section = f"""<fastagent:server name="{server.name}">{server_desc_section}
|
344
344
|
</fastagent:server>"""
|
345
|
-
|
345
|
+
|
346
346
|
server_sections.append(server_section)
|
347
347
|
|
348
348
|
servers = "\n".join(server_sections)
|
@@ -357,11 +357,11 @@ class Router(ABC, ContextDependent):
|
|
357
357
|
"""Format a function category into a readable string."""
|
358
358
|
# Check if we have a description
|
359
359
|
has_description = bool(category.description)
|
360
|
-
|
360
|
+
|
361
361
|
# If no description, use self-closing tag
|
362
362
|
if not has_description:
|
363
363
|
return f'<fastagent:function-category name="{category.name}" />'
|
364
|
-
|
364
|
+
|
365
365
|
# Include description
|
366
366
|
return f"""<fastagent:function-category name="{category.name}">
|
367
367
|
<fastagent:description>{category.description}</fastagent:description>
|
@@ -1,345 +0,0 @@
|
|
1
|
-
import json
|
2
|
-
from difflib import SequenceMatcher
|
3
|
-
from importlib import resources
|
4
|
-
from typing import Dict, List
|
5
|
-
|
6
|
-
from numpy import average
|
7
|
-
from pydantic import BaseModel, ConfigDict, Field, TypeAdapter
|
8
|
-
|
9
|
-
from mcp.types import ModelHint, ModelPreferences
|
10
|
-
|
11
|
-
|
12
|
-
class ModelBenchmarks(BaseModel):
|
13
|
-
"""
|
14
|
-
Performance benchmarks for comparing different models.
|
15
|
-
"""
|
16
|
-
|
17
|
-
__pydantic_extra__: dict[str, float] = Field(
|
18
|
-
init=False
|
19
|
-
) # Enforces that extra fields are floats
|
20
|
-
|
21
|
-
quality_score: float | None = None
|
22
|
-
"""A blended quality score for the model."""
|
23
|
-
|
24
|
-
mmlu_score: float | None = None
|
25
|
-
gsm8k_score: float | None = None
|
26
|
-
bbh_score: float | None = None
|
27
|
-
|
28
|
-
model_config = ConfigDict(extra="allow")
|
29
|
-
|
30
|
-
|
31
|
-
class ModelLatency(BaseModel):
|
32
|
-
"""
|
33
|
-
Latency benchmarks for comparing different models.
|
34
|
-
"""
|
35
|
-
|
36
|
-
time_to_first_token_ms: float = Field(gt=0)
|
37
|
-
"""
|
38
|
-
Median Time to first token in milliseconds.
|
39
|
-
"""
|
40
|
-
|
41
|
-
tokens_per_second: float = Field(gt=0)
|
42
|
-
"""
|
43
|
-
Median output tokens per second.
|
44
|
-
"""
|
45
|
-
|
46
|
-
|
47
|
-
class ModelCost(BaseModel):
|
48
|
-
"""
|
49
|
-
Cost benchmarks for comparing different models.
|
50
|
-
"""
|
51
|
-
|
52
|
-
blended_cost_per_1m: float | None = None
|
53
|
-
"""
|
54
|
-
Blended cost mixing input/output cost per 1M tokens.
|
55
|
-
"""
|
56
|
-
|
57
|
-
input_cost_per_1m: float | None = None
|
58
|
-
"""
|
59
|
-
Cost per 1M input tokens.
|
60
|
-
"""
|
61
|
-
|
62
|
-
output_cost_per_1m: float | None = None
|
63
|
-
"""
|
64
|
-
Cost per 1M output tokens.
|
65
|
-
"""
|
66
|
-
|
67
|
-
|
68
|
-
class ModelMetrics(BaseModel):
|
69
|
-
"""
|
70
|
-
Model metrics for comparing different models.
|
71
|
-
"""
|
72
|
-
|
73
|
-
cost: ModelCost
|
74
|
-
speed: ModelLatency
|
75
|
-
intelligence: ModelBenchmarks
|
76
|
-
|
77
|
-
|
78
|
-
class ModelInfo(BaseModel):
|
79
|
-
"""
|
80
|
-
LLM metadata, including performance benchmarks.
|
81
|
-
"""
|
82
|
-
|
83
|
-
name: str
|
84
|
-
description: str | None = None
|
85
|
-
provider: str
|
86
|
-
metrics: ModelMetrics
|
87
|
-
|
88
|
-
|
89
|
-
class ModelSelector:
|
90
|
-
"""
|
91
|
-
A heuristic-based selector to choose the best model from a list of models.
|
92
|
-
|
93
|
-
Because LLMs can vary along multiple dimensions, choosing the "best" model is
|
94
|
-
rarely straightforward. Different models excel in different areas—some are
|
95
|
-
faster but less capable, others are more capable but more expensive, and so
|
96
|
-
on.
|
97
|
-
|
98
|
-
MCP's ModelPreferences interface allows servers to express their priorities across multiple
|
99
|
-
dimensions to help clients make an appropriate selection for their use case.
|
100
|
-
"""
|
101
|
-
|
102
|
-
def __init__(
|
103
|
-
self,
|
104
|
-
models: List[ModelInfo] = None,
|
105
|
-
benchmark_weights: Dict[str, float] | None = None,
|
106
|
-
):
|
107
|
-
if not models:
|
108
|
-
self.models = load_default_models()
|
109
|
-
else:
|
110
|
-
self.models = models
|
111
|
-
|
112
|
-
if benchmark_weights:
|
113
|
-
self.benchmark_weights = benchmark_weights
|
114
|
-
else:
|
115
|
-
# Defaults for how much to value each benchmark metric (must add to 1)
|
116
|
-
self.benchmark_weights = {"mmlu": 0.4, "gsm8k": 0.3, "bbh": 0.3}
|
117
|
-
|
118
|
-
if abs(sum(self.benchmark_weights.values()) - 1.0) > 1e-6:
|
119
|
-
raise ValueError("Benchmark weights must sum to 1.0")
|
120
|
-
|
121
|
-
self.max_values = self._calculate_max_scores(self.models)
|
122
|
-
self.models_by_provider = self._models_by_provider(self.models)
|
123
|
-
|
124
|
-
def select_best_model(
|
125
|
-
self, model_preferences: ModelPreferences, provider: str | None = None
|
126
|
-
) -> ModelInfo:
|
127
|
-
"""
|
128
|
-
Select the best model from a given list of models based on the given model preferences.
|
129
|
-
"""
|
130
|
-
|
131
|
-
models: List[ModelInfo] = []
|
132
|
-
if provider:
|
133
|
-
models = self.models_by_provider[provider]
|
134
|
-
else:
|
135
|
-
models = self.models
|
136
|
-
|
137
|
-
if not models:
|
138
|
-
raise ValueError(f"No models available for selection. Provider={provider}")
|
139
|
-
|
140
|
-
candidate_models = models
|
141
|
-
# First check the model hints
|
142
|
-
if model_preferences.hints:
|
143
|
-
candidate_models = []
|
144
|
-
for model in models:
|
145
|
-
for hint in model_preferences.hints:
|
146
|
-
if self._check_model_hint(model, hint):
|
147
|
-
candidate_models.append(model)
|
148
|
-
|
149
|
-
if not candidate_models:
|
150
|
-
# If no hints match, we'll use all models and let the benchmark weights decide
|
151
|
-
candidate_models = models
|
152
|
-
|
153
|
-
scores = []
|
154
|
-
|
155
|
-
# Next, we'll use the benchmark weights to decide the best model
|
156
|
-
for model in candidate_models:
|
157
|
-
cost_score = self._calculate_cost_score(
|
158
|
-
model, model_preferences, max_cost=self.max_values["max_cost"]
|
159
|
-
)
|
160
|
-
speed_score = self._calculate_speed_score(
|
161
|
-
model,
|
162
|
-
max_tokens_per_second=self.max_values["max_tokens_per_second"],
|
163
|
-
max_time_to_first_token_ms=self.max_values[
|
164
|
-
"max_time_to_first_token_ms"
|
165
|
-
],
|
166
|
-
)
|
167
|
-
intelligence_score = self._calculate_intelligence_score(
|
168
|
-
model, self.max_values
|
169
|
-
)
|
170
|
-
|
171
|
-
model_score = (
|
172
|
-
(model_preferences.costPriority or 0) * cost_score
|
173
|
-
+ (model_preferences.speedPriority or 0) * speed_score
|
174
|
-
+ (model_preferences.intelligencePriority or 0) * intelligence_score
|
175
|
-
)
|
176
|
-
scores.append((model_score, model))
|
177
|
-
|
178
|
-
return max(scores, key=lambda x: x[0])[1]
|
179
|
-
|
180
|
-
def _models_by_provider(
|
181
|
-
self, models: List[ModelInfo]
|
182
|
-
) -> Dict[str, List[ModelInfo]]:
|
183
|
-
"""
|
184
|
-
Group models by provider.
|
185
|
-
"""
|
186
|
-
provider_models: Dict[str, List[ModelInfo]] = {}
|
187
|
-
for model in models:
|
188
|
-
if model.provider not in provider_models:
|
189
|
-
provider_models[model.provider] = []
|
190
|
-
provider_models[model.provider].append(model)
|
191
|
-
return provider_models
|
192
|
-
|
193
|
-
def _check_model_hint(self, model: ModelInfo, hint: ModelHint) -> bool:
|
194
|
-
"""
|
195
|
-
Check if a model matches a specific hint.
|
196
|
-
"""
|
197
|
-
|
198
|
-
name_match = True
|
199
|
-
if hint.name:
|
200
|
-
name_match = _fuzzy_match(hint.name, model.name)
|
201
|
-
|
202
|
-
provider_match = True
|
203
|
-
provider: str | None = getattr(hint, "provider", None)
|
204
|
-
if provider:
|
205
|
-
provider_match = _fuzzy_match(provider, model.provider)
|
206
|
-
|
207
|
-
# This can be extended to check for more hints
|
208
|
-
return name_match and provider_match
|
209
|
-
|
210
|
-
def _calculate_total_cost(self, model: ModelInfo, io_ratio: float = 3.0) -> float:
|
211
|
-
"""
|
212
|
-
Calculate a single cost metric of a model based on input/output token costs,
|
213
|
-
and a ratio of input to output tokens.
|
214
|
-
|
215
|
-
Args:
|
216
|
-
model: The model to calculate the cost for.
|
217
|
-
io_ratio: The estimated ratio of input to output tokens. Defaults to 3.0.
|
218
|
-
"""
|
219
|
-
|
220
|
-
if model.metrics.cost.blended_cost_per_1m is not None:
|
221
|
-
return model.metrics.cost.blended_cost_per_1m
|
222
|
-
|
223
|
-
input_cost = model.metrics.cost.input_cost_per_1m
|
224
|
-
output_cost = model.metrics.cost.output_cost_per_1m
|
225
|
-
|
226
|
-
total_cost = (input_cost * io_ratio + output_cost) / (1 + io_ratio)
|
227
|
-
return total_cost
|
228
|
-
|
229
|
-
def _calculate_cost_score(
|
230
|
-
self,
|
231
|
-
model: ModelInfo,
|
232
|
-
model_preferences: ModelPreferences,
|
233
|
-
max_cost: float,
|
234
|
-
) -> float:
|
235
|
-
"""Normalized 0->1 cost score for a model."""
|
236
|
-
total_cost = self._calculate_total_cost(model, model_preferences)
|
237
|
-
return 1 - (total_cost / max_cost)
|
238
|
-
|
239
|
-
def _calculate_intelligence_score(
|
240
|
-
self, model: ModelInfo, max_values: Dict[str, float]
|
241
|
-
) -> float:
|
242
|
-
"""
|
243
|
-
Return a normalized 0->1 intelligence score for a model based on its benchmark metrics.
|
244
|
-
"""
|
245
|
-
scores = []
|
246
|
-
weights = []
|
247
|
-
|
248
|
-
benchmark_dict: Dict[str, float] = model.metrics.intelligence.model_dump()
|
249
|
-
use_weights = True
|
250
|
-
for bench, score in benchmark_dict.items():
|
251
|
-
key = f"max_{bench}"
|
252
|
-
if score is not None and key in max_values:
|
253
|
-
scores.append(score / max_values[key])
|
254
|
-
if bench in self.benchmark_weights:
|
255
|
-
weights.append(self.benchmark_weights[bench])
|
256
|
-
else:
|
257
|
-
# If a benchmark doesn't have a weight, don't use weights at all, we'll just average the scores
|
258
|
-
use_weights = False
|
259
|
-
|
260
|
-
if not scores:
|
261
|
-
return 0
|
262
|
-
elif use_weights:
|
263
|
-
return average(scores, weights=weights)
|
264
|
-
else:
|
265
|
-
return average(scores)
|
266
|
-
|
267
|
-
def _calculate_speed_score(
|
268
|
-
self,
|
269
|
-
model: ModelInfo,
|
270
|
-
max_tokens_per_second: float,
|
271
|
-
max_time_to_first_token_ms: float,
|
272
|
-
) -> float:
|
273
|
-
"""Normalized 0->1 cost score for a model."""
|
274
|
-
|
275
|
-
time_to_first_token_score = 1 - (
|
276
|
-
model.metrics.speed.time_to_first_token_ms / max_time_to_first_token_ms
|
277
|
-
)
|
278
|
-
|
279
|
-
tokens_per_second_score = (
|
280
|
-
model.metrics.speed.tokens_per_second / max_tokens_per_second
|
281
|
-
)
|
282
|
-
|
283
|
-
latency_score = average(
|
284
|
-
[time_to_first_token_score, tokens_per_second_score], weights=[0.4, 0.6]
|
285
|
-
)
|
286
|
-
return latency_score
|
287
|
-
|
288
|
-
def _calculate_max_scores(self, models: List[ModelInfo]) -> Dict[str, float]:
|
289
|
-
"""
|
290
|
-
Of all the models, calculate the maximum value for each benchmark metric.
|
291
|
-
"""
|
292
|
-
max_dict: Dict[str, float] = {}
|
293
|
-
|
294
|
-
max_dict["max_cost"] = max(self._calculate_total_cost(m) for m in models)
|
295
|
-
max_dict["max_tokens_per_second"] = max(
|
296
|
-
max(m.metrics.speed.tokens_per_second for m in models), 1e-6
|
297
|
-
)
|
298
|
-
max_dict["max_time_to_first_token_ms"] = max(
|
299
|
-
max(m.metrics.speed.time_to_first_token_ms for m in models), 1e-6
|
300
|
-
)
|
301
|
-
|
302
|
-
# Find the maximum value for each model performance benchmark
|
303
|
-
for model in models:
|
304
|
-
benchmark_dict: Dict[str, float] = model.metrics.intelligence.model_dump()
|
305
|
-
for bench, score in benchmark_dict.items():
|
306
|
-
if score is None:
|
307
|
-
continue
|
308
|
-
|
309
|
-
key = f"max_{bench}"
|
310
|
-
if key in max_dict:
|
311
|
-
max_dict[key] = max(max_dict[key], score)
|
312
|
-
else:
|
313
|
-
max_dict[key] = score
|
314
|
-
|
315
|
-
return max_dict
|
316
|
-
|
317
|
-
|
318
|
-
def load_default_models() -> List[ModelInfo]:
|
319
|
-
"""
|
320
|
-
We use ArtificialAnalysis benchmarks for determining the best model.
|
321
|
-
"""
|
322
|
-
with (
|
323
|
-
resources.files("mcp_agent.data")
|
324
|
-
.joinpath("artificial_analysis_llm_benchmarks.json")
|
325
|
-
.open() as file
|
326
|
-
):
|
327
|
-
data = json.load(file) # Array of ModelInfo objects
|
328
|
-
adapter = TypeAdapter(List[ModelInfo])
|
329
|
-
return adapter.validate_python(data)
|
330
|
-
|
331
|
-
|
332
|
-
def _fuzzy_match(str1: str, str2: str, threshold: float = 0.8) -> bool:
|
333
|
-
"""
|
334
|
-
Fuzzy match two strings
|
335
|
-
|
336
|
-
Args:
|
337
|
-
str1: First string to compare
|
338
|
-
str2: Second string to compare
|
339
|
-
threshold: Minimum similarity ratio to consider a match (0.0 to 1.0)
|
340
|
-
|
341
|
-
Returns:
|
342
|
-
bool: True if strings match above threshold, False otherwise
|
343
|
-
"""
|
344
|
-
sequence_ratio = SequenceMatcher(None, str1.lower(), str2.lower()).ratio()
|
345
|
-
return sequence_ratio >= threshold
|
File without changes
|
File without changes
|
File without changes
|