fast-agent-mcp 0.0.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of fast-agent-mcp might be problematic. Click here for more details.

Files changed (100) hide show
  1. fast_agent_mcp-0.0.7.dist-info/METADATA +322 -0
  2. fast_agent_mcp-0.0.7.dist-info/RECORD +100 -0
  3. fast_agent_mcp-0.0.7.dist-info/WHEEL +4 -0
  4. fast_agent_mcp-0.0.7.dist-info/entry_points.txt +5 -0
  5. fast_agent_mcp-0.0.7.dist-info/licenses/LICENSE +201 -0
  6. mcp_agent/__init__.py +0 -0
  7. mcp_agent/agents/__init__.py +0 -0
  8. mcp_agent/agents/agent.py +277 -0
  9. mcp_agent/app.py +303 -0
  10. mcp_agent/cli/__init__.py +0 -0
  11. mcp_agent/cli/__main__.py +4 -0
  12. mcp_agent/cli/commands/bootstrap.py +221 -0
  13. mcp_agent/cli/commands/config.py +11 -0
  14. mcp_agent/cli/commands/setup.py +229 -0
  15. mcp_agent/cli/main.py +68 -0
  16. mcp_agent/cli/terminal.py +24 -0
  17. mcp_agent/config.py +334 -0
  18. mcp_agent/console.py +28 -0
  19. mcp_agent/context.py +251 -0
  20. mcp_agent/context_dependent.py +48 -0
  21. mcp_agent/core/fastagent.py +1013 -0
  22. mcp_agent/eval/__init__.py +0 -0
  23. mcp_agent/event_progress.py +88 -0
  24. mcp_agent/executor/__init__.py +0 -0
  25. mcp_agent/executor/decorator_registry.py +120 -0
  26. mcp_agent/executor/executor.py +293 -0
  27. mcp_agent/executor/task_registry.py +34 -0
  28. mcp_agent/executor/temporal.py +405 -0
  29. mcp_agent/executor/workflow.py +197 -0
  30. mcp_agent/executor/workflow_signal.py +325 -0
  31. mcp_agent/human_input/__init__.py +0 -0
  32. mcp_agent/human_input/handler.py +49 -0
  33. mcp_agent/human_input/types.py +58 -0
  34. mcp_agent/logging/__init__.py +0 -0
  35. mcp_agent/logging/events.py +123 -0
  36. mcp_agent/logging/json_serializer.py +163 -0
  37. mcp_agent/logging/listeners.py +216 -0
  38. mcp_agent/logging/logger.py +365 -0
  39. mcp_agent/logging/rich_progress.py +120 -0
  40. mcp_agent/logging/tracing.py +140 -0
  41. mcp_agent/logging/transport.py +461 -0
  42. mcp_agent/mcp/__init__.py +0 -0
  43. mcp_agent/mcp/gen_client.py +85 -0
  44. mcp_agent/mcp/mcp_activity.py +18 -0
  45. mcp_agent/mcp/mcp_agent_client_session.py +242 -0
  46. mcp_agent/mcp/mcp_agent_server.py +56 -0
  47. mcp_agent/mcp/mcp_aggregator.py +394 -0
  48. mcp_agent/mcp/mcp_connection_manager.py +330 -0
  49. mcp_agent/mcp/stdio.py +104 -0
  50. mcp_agent/mcp_server_registry.py +275 -0
  51. mcp_agent/progress_display.py +10 -0
  52. mcp_agent/resources/examples/decorator/main.py +26 -0
  53. mcp_agent/resources/examples/decorator/optimizer.py +78 -0
  54. mcp_agent/resources/examples/decorator/orchestrator.py +68 -0
  55. mcp_agent/resources/examples/decorator/parallel.py +81 -0
  56. mcp_agent/resources/examples/decorator/router.py +56 -0
  57. mcp_agent/resources/examples/decorator/tiny.py +22 -0
  58. mcp_agent/resources/examples/mcp_researcher/main-evalopt.py +53 -0
  59. mcp_agent/resources/examples/mcp_researcher/main.py +38 -0
  60. mcp_agent/telemetry/__init__.py +0 -0
  61. mcp_agent/telemetry/usage_tracking.py +18 -0
  62. mcp_agent/workflows/__init__.py +0 -0
  63. mcp_agent/workflows/embedding/__init__.py +0 -0
  64. mcp_agent/workflows/embedding/embedding_base.py +61 -0
  65. mcp_agent/workflows/embedding/embedding_cohere.py +49 -0
  66. mcp_agent/workflows/embedding/embedding_openai.py +46 -0
  67. mcp_agent/workflows/evaluator_optimizer/__init__.py +0 -0
  68. mcp_agent/workflows/evaluator_optimizer/evaluator_optimizer.py +359 -0
  69. mcp_agent/workflows/intent_classifier/__init__.py +0 -0
  70. mcp_agent/workflows/intent_classifier/intent_classifier_base.py +120 -0
  71. mcp_agent/workflows/intent_classifier/intent_classifier_embedding.py +134 -0
  72. mcp_agent/workflows/intent_classifier/intent_classifier_embedding_cohere.py +45 -0
  73. mcp_agent/workflows/intent_classifier/intent_classifier_embedding_openai.py +45 -0
  74. mcp_agent/workflows/intent_classifier/intent_classifier_llm.py +161 -0
  75. mcp_agent/workflows/intent_classifier/intent_classifier_llm_anthropic.py +60 -0
  76. mcp_agent/workflows/intent_classifier/intent_classifier_llm_openai.py +60 -0
  77. mcp_agent/workflows/llm/__init__.py +0 -0
  78. mcp_agent/workflows/llm/augmented_llm.py +645 -0
  79. mcp_agent/workflows/llm/augmented_llm_anthropic.py +539 -0
  80. mcp_agent/workflows/llm/augmented_llm_openai.py +615 -0
  81. mcp_agent/workflows/llm/llm_selector.py +345 -0
  82. mcp_agent/workflows/llm/model_factory.py +175 -0
  83. mcp_agent/workflows/orchestrator/__init__.py +0 -0
  84. mcp_agent/workflows/orchestrator/orchestrator.py +407 -0
  85. mcp_agent/workflows/orchestrator/orchestrator_models.py +154 -0
  86. mcp_agent/workflows/orchestrator/orchestrator_prompts.py +113 -0
  87. mcp_agent/workflows/parallel/__init__.py +0 -0
  88. mcp_agent/workflows/parallel/fan_in.py +350 -0
  89. mcp_agent/workflows/parallel/fan_out.py +187 -0
  90. mcp_agent/workflows/parallel/parallel_llm.py +141 -0
  91. mcp_agent/workflows/router/__init__.py +0 -0
  92. mcp_agent/workflows/router/router_base.py +276 -0
  93. mcp_agent/workflows/router/router_embedding.py +240 -0
  94. mcp_agent/workflows/router/router_embedding_cohere.py +59 -0
  95. mcp_agent/workflows/router/router_embedding_openai.py +59 -0
  96. mcp_agent/workflows/router/router_llm.py +301 -0
  97. mcp_agent/workflows/swarm/__init__.py +0 -0
  98. mcp_agent/workflows/swarm/swarm.py +320 -0
  99. mcp_agent/workflows/swarm/swarm_anthropic.py +42 -0
  100. mcp_agent/workflows/swarm/swarm_openai.py +41 -0
@@ -0,0 +1,345 @@
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
@@ -0,0 +1,175 @@
1
+ from dataclasses import dataclass
2
+ from enum import Enum, auto
3
+ from typing import Optional, Type, Dict, Union, Callable
4
+
5
+ from mcp_agent.agents.agent import Agent
6
+ from mcp_agent.workflows.llm.augmented_llm_anthropic import AnthropicAugmentedLLM
7
+ from mcp_agent.workflows.llm.augmented_llm_openai import OpenAIAugmentedLLM
8
+ from mcp_agent.workflows.llm.augmented_llm import RequestParams
9
+
10
+ # Type alias for LLM classes
11
+ LLMClass = Union[Type[AnthropicAugmentedLLM], Type[OpenAIAugmentedLLM]]
12
+
13
+
14
+ class Provider(Enum):
15
+ """Supported LLM providers"""
16
+
17
+ ANTHROPIC = auto()
18
+ OPENAI = auto()
19
+
20
+
21
+ class ReasoningEffort(Enum):
22
+ """Optional reasoning effort levels"""
23
+
24
+ LOW = "low"
25
+ MEDIUM = "medium"
26
+ HIGH = "high"
27
+
28
+
29
+ @dataclass
30
+ class ModelConfig:
31
+ """Configuration for a specific model"""
32
+
33
+ provider: Provider
34
+ model_name: str
35
+ reasoning_effort: Optional[ReasoningEffort] = None
36
+
37
+
38
+ class ModelFactory:
39
+ """Factory for creating LLM instances based on model specifications"""
40
+
41
+ # Mapping of provider strings to enum values
42
+ PROVIDER_MAP = {
43
+ "anthropic": Provider.ANTHROPIC,
44
+ "openai": Provider.OPENAI,
45
+ }
46
+
47
+ # Mapping of effort strings to enum values
48
+ EFFORT_MAP = {
49
+ "low": ReasoningEffort.LOW,
50
+ "medium": ReasoningEffort.MEDIUM,
51
+ "high": ReasoningEffort.HIGH,
52
+ }
53
+
54
+ # TODO -- add context window size information for display/mmanagement
55
+ # TODO -- add audio supporting got-4o-audio-preview
56
+ # Mapping of model names to their default providers
57
+ DEFAULT_PROVIDERS = {
58
+ "gpt-4o": Provider.OPENAI,
59
+ "gpt-4o-mini": Provider.OPENAI,
60
+ "o1-mini": Provider.OPENAI,
61
+ "o1": Provider.OPENAI,
62
+ "o1-preview": Provider.OPENAI,
63
+ "o3-mini": Provider.OPENAI,
64
+ "claude-3-haiku-20240307": Provider.ANTHROPIC,
65
+ "claude-3-5-sonnet-20240620": Provider.ANTHROPIC,
66
+ "claude-3-5-sonnet-20241022": Provider.ANTHROPIC,
67
+ "claude-3-5-sonnet-latest": Provider.ANTHROPIC,
68
+ "claude-3-opus-20240229": Provider.ANTHROPIC,
69
+ "claude-3-opus-latest": Provider.ANTHROPIC,
70
+ }
71
+
72
+ MODEL_ALIASES = {
73
+ "sonnet": "claude-3-5-sonnet-latest",
74
+ "sonnet35": "claude-3-5-sonnet-latest",
75
+ "claude": "claude-3-5-sonnet-latest",
76
+ "haiku": "claude-3-haiku-20240307",
77
+ "haiku3": "claude-3-haiku-20240307",
78
+ "opus": "claude-3-opus-latest",
79
+ "opus3": "claude-3-opus-latest",
80
+ }
81
+
82
+ # Mapping of providers to their LLM classes
83
+ PROVIDER_CLASSES: Dict[Provider, LLMClass] = {
84
+ Provider.ANTHROPIC: AnthropicAugmentedLLM,
85
+ Provider.OPENAI: OpenAIAugmentedLLM,
86
+ }
87
+
88
+ @classmethod
89
+ def parse_model_string(cls, model_string: str) -> ModelConfig:
90
+ """Parse a model string into a ModelConfig object"""
91
+ # Check if model string is an alias
92
+ model_string = cls.MODEL_ALIASES.get(model_string, model_string)
93
+ parts = model_string.split(".")
94
+
95
+ # Start with all parts as the model name
96
+ model_parts = parts.copy()
97
+ provider = None
98
+ reasoning_effort = None
99
+
100
+ # Check last part for reasoning effort
101
+ if len(parts) > 1 and parts[-1].lower() in cls.EFFORT_MAP:
102
+ reasoning_effort = cls.EFFORT_MAP[parts[-1].lower()]
103
+ model_parts = model_parts[:-1]
104
+
105
+ # Check first part for provider
106
+ if len(model_parts) > 1:
107
+ potential_provider = model_parts[0]
108
+ if potential_provider in cls.PROVIDER_MAP:
109
+ provider = cls.PROVIDER_MAP[potential_provider]
110
+ model_parts = model_parts[1:]
111
+
112
+ # Join remaining parts as model name
113
+ model_name = ".".join(model_parts)
114
+
115
+ # If no provider was found in the string, look it up in defaults
116
+ if provider is None:
117
+ provider = cls.DEFAULT_PROVIDERS.get(model_name)
118
+ if provider is None:
119
+ raise ValueError(f"Unknown model: {model_name}")
120
+
121
+ return ModelConfig(
122
+ provider=provider, model_name=model_name, reasoning_effort=reasoning_effort
123
+ )
124
+
125
+ @classmethod
126
+ def create_factory(
127
+ cls, model_string: str, request_params: Optional[RequestParams] = None
128
+ ) -> Callable[..., LLMClass]:
129
+ """
130
+ Creates a factory function that follows the attach_llm protocol.
131
+
132
+ Args:
133
+ model_string: The model specification string (e.g. "gpt-4o.high")
134
+ request_params: Optional parameters to configure LLM behavior
135
+
136
+ Returns:
137
+ A callable that takes an agent parameter and returns an LLM instance
138
+ """
139
+ # Parse configuration up front
140
+ config = cls.parse_model_string(model_string)
141
+ llm_class = cls.PROVIDER_CLASSES[config.provider]
142
+
143
+ # Create a factory function matching the attach_llm protocol
144
+ def factory(agent: Agent, **kwargs) -> LLMClass:
145
+ # Create merged params with parsed model name
146
+ factory_params = (
147
+ request_params.model_copy() if request_params else RequestParams()
148
+ )
149
+ factory_params.model = (
150
+ config.model_name
151
+ ) # Use the parsed model name, not the alias
152
+
153
+ # Merge with any provided default_request_params
154
+ if "default_request_params" in kwargs and kwargs["default_request_params"]:
155
+ params_dict = factory_params.model_dump()
156
+ params_dict.update(
157
+ kwargs["default_request_params"].model_dump(exclude_unset=True)
158
+ )
159
+ factory_params = RequestParams(**params_dict)
160
+ factory_params.model = (
161
+ config.model_name
162
+ ) # Ensure parsed model name isn't overwritten
163
+
164
+ llm = llm_class(
165
+ agent=agent,
166
+ model=config.model_name,
167
+ reasoning_effort=config.reasoning_effort.value
168
+ if config.reasoning_effort
169
+ else None,
170
+ request_params=factory_params,
171
+ name=kwargs.get("name"),
172
+ )
173
+ return llm
174
+
175
+ return factory
File without changes