fast-agent-mcp 0.1.7__py3-none-any.whl → 0.1.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fast_agent_mcp-0.1.7.dist-info → fast_agent_mcp-0.1.9.dist-info}/METADATA +37 -9
- {fast_agent_mcp-0.1.7.dist-info → fast_agent_mcp-0.1.9.dist-info}/RECORD +53 -31
- {fast_agent_mcp-0.1.7.dist-info → fast_agent_mcp-0.1.9.dist-info}/entry_points.txt +1 -0
- mcp_agent/agents/agent.py +5 -11
- mcp_agent/core/agent_app.py +125 -44
- mcp_agent/core/decorators.py +3 -2
- mcp_agent/core/enhanced_prompt.py +106 -20
- mcp_agent/core/factory.py +28 -66
- mcp_agent/core/fastagent.py +13 -3
- mcp_agent/core/mcp_content.py +222 -0
- mcp_agent/core/prompt.py +132 -0
- mcp_agent/core/proxies.py +41 -36
- mcp_agent/human_input/handler.py +4 -1
- mcp_agent/logging/transport.py +30 -3
- mcp_agent/mcp/mcp_aggregator.py +27 -22
- mcp_agent/mcp/mime_utils.py +69 -0
- mcp_agent/mcp/prompt_message_multipart.py +64 -0
- mcp_agent/mcp/prompt_serialization.py +447 -0
- mcp_agent/mcp/prompts/__init__.py +0 -0
- mcp_agent/mcp/prompts/__main__.py +10 -0
- mcp_agent/mcp/prompts/prompt_server.py +508 -0
- mcp_agent/mcp/prompts/prompt_template.py +469 -0
- mcp_agent/mcp/resource_utils.py +203 -0
- mcp_agent/resources/examples/internal/agent.py +1 -1
- mcp_agent/resources/examples/internal/fastagent.config.yaml +2 -2
- mcp_agent/resources/examples/internal/sizer.py +0 -5
- mcp_agent/resources/examples/prompting/__init__.py +3 -0
- mcp_agent/resources/examples/prompting/agent.py +23 -0
- mcp_agent/resources/examples/prompting/fastagent.config.yaml +44 -0
- mcp_agent/resources/examples/prompting/image_server.py +56 -0
- mcp_agent/resources/examples/researcher/researcher-eval.py +1 -1
- mcp_agent/resources/examples/workflows/orchestrator.py +5 -4
- mcp_agent/resources/examples/workflows/router.py +0 -2
- mcp_agent/workflows/evaluator_optimizer/evaluator_optimizer.py +57 -87
- mcp_agent/workflows/llm/anthropic_utils.py +101 -0
- mcp_agent/workflows/llm/augmented_llm.py +155 -141
- mcp_agent/workflows/llm/augmented_llm_anthropic.py +135 -281
- mcp_agent/workflows/llm/augmented_llm_openai.py +175 -337
- mcp_agent/workflows/llm/augmented_llm_passthrough.py +104 -0
- mcp_agent/workflows/llm/augmented_llm_playback.py +109 -0
- mcp_agent/workflows/llm/model_factory.py +25 -6
- mcp_agent/workflows/llm/openai_utils.py +65 -0
- mcp_agent/workflows/llm/providers/__init__.py +8 -0
- mcp_agent/workflows/llm/providers/multipart_converter_anthropic.py +348 -0
- mcp_agent/workflows/llm/providers/multipart_converter_openai.py +426 -0
- mcp_agent/workflows/llm/providers/openai_multipart.py +197 -0
- mcp_agent/workflows/llm/providers/sampling_converter_anthropic.py +258 -0
- mcp_agent/workflows/llm/providers/sampling_converter_openai.py +229 -0
- mcp_agent/workflows/llm/sampling_format_converter.py +39 -0
- mcp_agent/workflows/orchestrator/orchestrator.py +62 -153
- mcp_agent/workflows/router/router_llm.py +18 -24
- mcp_agent/core/server_validation.py +0 -44
- mcp_agent/core/simulator_registry.py +0 -22
- mcp_agent/workflows/llm/enhanced_passthrough.py +0 -70
- {fast_agent_mcp-0.1.7.dist-info → fast_agent_mcp-0.1.9.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.1.7.dist-info → fast_agent_mcp-0.1.9.dist-info}/licenses/LICENSE +0 -0
@@ -2,13 +2,13 @@
|
|
2
2
|
Enhanced prompt functionality with advanced prompt_toolkit features.
|
3
3
|
"""
|
4
4
|
|
5
|
-
from typing import List
|
5
|
+
from typing import List, Optional
|
6
6
|
from importlib.metadata import version
|
7
7
|
from prompt_toolkit import PromptSession
|
8
8
|
from prompt_toolkit.formatted_text import HTML
|
9
9
|
from prompt_toolkit.history import InMemoryHistory
|
10
10
|
from prompt_toolkit.key_binding import KeyBindings
|
11
|
-
from prompt_toolkit.completion import Completer, Completion
|
11
|
+
from prompt_toolkit.completion import Completer, Completion, WordCompleter
|
12
12
|
from prompt_toolkit.lexers import PygmentsLexer
|
13
13
|
from prompt_toolkit.filters import Condition
|
14
14
|
from prompt_toolkit.styles import Style
|
@@ -330,6 +330,110 @@ async def get_enhanced_input(
|
|
330
330
|
# Log and gracefully handle other exceptions
|
331
331
|
print(f"\nInput error: {type(e).__name__}: {e}")
|
332
332
|
return "STOP"
|
333
|
+
finally:
|
334
|
+
# Ensure the prompt session is properly cleaned up
|
335
|
+
# This is especially important on Windows to prevent resource leaks
|
336
|
+
if session.app.is_running:
|
337
|
+
session.app.exit()
|
338
|
+
|
339
|
+
|
340
|
+
async def get_selection_input(
|
341
|
+
prompt_text: str,
|
342
|
+
options: List[str] = None,
|
343
|
+
default: str = None,
|
344
|
+
allow_cancel: bool = True,
|
345
|
+
complete_options: bool = True,
|
346
|
+
) -> Optional[str]:
|
347
|
+
"""
|
348
|
+
Display a selection prompt and return the user's selection.
|
349
|
+
|
350
|
+
Args:
|
351
|
+
prompt_text: Text to display as the prompt
|
352
|
+
options: List of valid options (for auto-completion)
|
353
|
+
default: Default value if user presses enter
|
354
|
+
allow_cancel: Whether to allow cancellation with empty input
|
355
|
+
complete_options: Whether to use the options for auto-completion
|
356
|
+
|
357
|
+
Returns:
|
358
|
+
Selected value, or None if cancelled
|
359
|
+
"""
|
360
|
+
try:
|
361
|
+
# Initialize completer if options provided and completion requested
|
362
|
+
completer = WordCompleter(options) if options and complete_options else None
|
363
|
+
|
364
|
+
# Create prompt session
|
365
|
+
prompt_session = PromptSession(completer=completer)
|
366
|
+
|
367
|
+
try:
|
368
|
+
# Get user input
|
369
|
+
selection = await prompt_session.prompt_async(
|
370
|
+
prompt_text, default=default or ""
|
371
|
+
)
|
372
|
+
|
373
|
+
# Handle cancellation
|
374
|
+
if allow_cancel and not selection.strip():
|
375
|
+
return None
|
376
|
+
|
377
|
+
return selection
|
378
|
+
finally:
|
379
|
+
# Ensure prompt session cleanup
|
380
|
+
if prompt_session.app.is_running:
|
381
|
+
prompt_session.app.exit()
|
382
|
+
except (KeyboardInterrupt, EOFError):
|
383
|
+
return None
|
384
|
+
except Exception as e:
|
385
|
+
rich_print(f"\n[red]Error getting selection: {e}[/red]")
|
386
|
+
return None
|
387
|
+
|
388
|
+
|
389
|
+
async def get_argument_input(
|
390
|
+
arg_name: str,
|
391
|
+
description: str = None,
|
392
|
+
required: bool = True,
|
393
|
+
) -> Optional[str]:
|
394
|
+
"""
|
395
|
+
Prompt for an argument value with formatting and help text.
|
396
|
+
|
397
|
+
Args:
|
398
|
+
arg_name: Name of the argument
|
399
|
+
description: Optional description of the argument
|
400
|
+
required: Whether this argument is required
|
401
|
+
|
402
|
+
Returns:
|
403
|
+
Input value, or None if cancelled/skipped
|
404
|
+
"""
|
405
|
+
# Format the prompt differently based on whether it's required
|
406
|
+
required_text = "(required)" if required else "(optional, press Enter to skip)"
|
407
|
+
|
408
|
+
# Show description if available
|
409
|
+
if description:
|
410
|
+
rich_print(f" [dim]{arg_name}: {description}[/dim]")
|
411
|
+
|
412
|
+
prompt_text = HTML(
|
413
|
+
f"Enter value for <ansibrightcyan>{arg_name}</ansibrightcyan> {required_text}: "
|
414
|
+
)
|
415
|
+
|
416
|
+
# Create prompt session
|
417
|
+
prompt_session = PromptSession()
|
418
|
+
|
419
|
+
try:
|
420
|
+
# Get user input
|
421
|
+
arg_value = await prompt_session.prompt_async(prompt_text)
|
422
|
+
|
423
|
+
# For optional arguments, empty input means skip
|
424
|
+
if not required and not arg_value:
|
425
|
+
return None
|
426
|
+
|
427
|
+
return arg_value
|
428
|
+
except (KeyboardInterrupt, EOFError):
|
429
|
+
return None
|
430
|
+
except Exception as e:
|
431
|
+
rich_print(f"\n[red]Error getting input: {e}[/red]")
|
432
|
+
return None
|
433
|
+
finally:
|
434
|
+
# Ensure prompt session cleanup
|
435
|
+
if prompt_session.app.is_running:
|
436
|
+
prompt_session.app.exit()
|
333
437
|
|
334
438
|
|
335
439
|
async def handle_special_commands(command, agent_app=None):
|
@@ -408,24 +512,6 @@ async def handle_special_commands(command, agent_app=None):
|
|
408
512
|
)
|
409
513
|
return True
|
410
514
|
|
411
|
-
elif command == "SELECT_PROMPT" or (
|
412
|
-
isinstance(command, str) and command.startswith("SELECT_PROMPT:")
|
413
|
-
):
|
414
|
-
# Handle prompt selection UI (previously named "list_prompts" action)
|
415
|
-
if agent_app:
|
416
|
-
# If it's a specific prompt, extract the name
|
417
|
-
prompt_name = None
|
418
|
-
if isinstance(command, str) and command.startswith("SELECT_PROMPT:"):
|
419
|
-
prompt_name = command.split(":", 1)[1].strip()
|
420
|
-
|
421
|
-
# Return a dictionary with a select_prompt action to be handled by the caller
|
422
|
-
return {"select_prompt": True, "prompt_name": prompt_name}
|
423
|
-
else:
|
424
|
-
rich_print(
|
425
|
-
"[yellow]Prompt selection is not available outside of an agent context[/yellow]"
|
426
|
-
)
|
427
|
-
return True
|
428
|
-
|
429
515
|
elif isinstance(command, str) and command.startswith("SWITCH:"):
|
430
516
|
agent_name = command.split(":", 1)[1]
|
431
517
|
if agent_name in available_agents:
|
mcp_agent/core/factory.py
CHANGED
@@ -34,10 +34,7 @@ T = TypeVar("T") # For the wrapper classes
|
|
34
34
|
|
35
35
|
|
36
36
|
def create_proxy(
|
37
|
-
app: MCPApp,
|
38
|
-
name: str,
|
39
|
-
instance: AgentOrWorkflow,
|
40
|
-
agent_type: str
|
37
|
+
app: MCPApp, name: str, instance: AgentOrWorkflow, agent_type: str
|
41
38
|
) -> BaseAgentProxy:
|
42
39
|
"""Create appropriate proxy type based on agent type and validate instance type
|
43
40
|
|
@@ -61,9 +58,7 @@ def create_proxy(
|
|
61
58
|
log_agent_load(app, name)
|
62
59
|
if agent_type == AgentType.BASIC.value:
|
63
60
|
if not isinstance(instance, Agent):
|
64
|
-
raise TypeError(
|
65
|
-
f"Expected Agent instance for {name}, got {type(instance)}"
|
66
|
-
)
|
61
|
+
raise TypeError(f"Expected Agent instance for {name}, got {type(instance)}")
|
67
62
|
return LLMAgentProxy(app, name, instance)
|
68
63
|
elif agent_type == AgentType.ORCHESTRATOR.value:
|
69
64
|
if not isinstance(instance, Orchestrator):
|
@@ -177,42 +172,18 @@ async def create_agents_by_type(
|
|
177
172
|
if agent_type == AgentType.BASIC:
|
178
173
|
# Get the agent name for special handling
|
179
174
|
agent_name = agent_data["config"].name
|
175
|
+
agent = Agent(config=config, context=app_instance.context)
|
180
176
|
|
181
|
-
#
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
# Create basic agent with configuration
|
189
|
-
agent = Agent(config=config, context=app_instance.context)
|
190
|
-
|
191
|
-
# Set up a PassthroughLLM directly
|
192
|
-
async with agent:
|
193
|
-
agent._llm = PassthroughLLM(
|
194
|
-
name=f"{config.name}_llm",
|
195
|
-
context=app_instance.context,
|
196
|
-
agent=agent,
|
197
|
-
default_request_params=config.default_request_params,
|
198
|
-
)
|
199
|
-
|
200
|
-
# Store the agent
|
201
|
-
instance = agent
|
202
|
-
else:
|
203
|
-
# Standard basic agent with LLM
|
204
|
-
agent = Agent(config=config, context=app_instance.context)
|
205
|
-
|
206
|
-
# Set up LLM with proper configuration
|
207
|
-
async with agent:
|
208
|
-
llm_factory = model_factory_func(
|
209
|
-
model=config.model,
|
210
|
-
request_params=config.default_request_params,
|
211
|
-
)
|
212
|
-
agent._llm = await agent.attach_llm(llm_factory)
|
177
|
+
# Set up LLM with proper configuration
|
178
|
+
async with agent:
|
179
|
+
llm_factory = model_factory_func(
|
180
|
+
model=config.model,
|
181
|
+
request_params=config.default_request_params,
|
182
|
+
)
|
183
|
+
agent._llm = await agent.attach_llm(llm_factory)
|
213
184
|
|
214
|
-
|
215
|
-
|
185
|
+
# Store the agent
|
186
|
+
instance = agent
|
216
187
|
|
217
188
|
elif agent_type == AgentType.ORCHESTRATOR:
|
218
189
|
# Get base params configured with model settings
|
@@ -276,12 +247,8 @@ async def create_agents_by_type(
|
|
276
247
|
|
277
248
|
elif agent_type == AgentType.EVALUATOR_OPTIMIZER:
|
278
249
|
# Get the referenced agents - unwrap from proxies
|
279
|
-
generator = unwrap_proxy(
|
280
|
-
|
281
|
-
)
|
282
|
-
evaluator = unwrap_proxy(
|
283
|
-
active_agents[agent_data["evaluator"]]
|
284
|
-
)
|
250
|
+
generator = unwrap_proxy(active_agents[agent_data["generator"]])
|
251
|
+
evaluator = unwrap_proxy(active_agents[agent_data["evaluator"]])
|
285
252
|
|
286
253
|
if not generator or not evaluator:
|
287
254
|
raise ValueError(
|
@@ -294,7 +261,9 @@ async def create_agents_by_type(
|
|
294
261
|
optimizer_model = None
|
295
262
|
if isinstance(generator, Agent):
|
296
263
|
optimizer_model = generator.config.model
|
297
|
-
elif hasattr(generator,
|
264
|
+
elif hasattr(generator, "_sequence") and hasattr(
|
265
|
+
generator, "_agent_proxies"
|
266
|
+
):
|
298
267
|
# For ChainProxy, use the config model directly
|
299
268
|
optimizer_model = config.model
|
300
269
|
|
@@ -311,9 +280,7 @@ async def create_agents_by_type(
|
|
311
280
|
|
312
281
|
elif agent_type == AgentType.ROUTER:
|
313
282
|
# Get the router's agents - unwrap proxies
|
314
|
-
router_agents = get_agent_instances(
|
315
|
-
agent_data["agents"], active_agents
|
316
|
-
)
|
283
|
+
router_agents = get_agent_instances(agent_data["agents"], active_agents)
|
317
284
|
|
318
285
|
# Create the router with proper configuration
|
319
286
|
llm_factory = model_factory_func(
|
@@ -376,20 +343,15 @@ async def create_agents_by_type(
|
|
376
343
|
"continue_with_final", True
|
377
344
|
)
|
378
345
|
# Set cumulative behavior from configuration
|
379
|
-
instance._cumulative = agent_data.get(
|
380
|
-
"cumulative", False
|
381
|
-
)
|
346
|
+
instance._cumulative = agent_data.get("cumulative", False)
|
382
347
|
|
383
348
|
elif agent_type == AgentType.PARALLEL:
|
384
|
-
# Get fan-out agents (could be basic agents or other parallels)
|
385
349
|
fan_out_agents = get_agent_instances(
|
386
350
|
agent_data["fan_out"], active_agents
|
387
351
|
)
|
388
352
|
|
389
353
|
# Get fan-in agent - unwrap proxy
|
390
|
-
fan_in_agent = unwrap_proxy(
|
391
|
-
active_agents[agent_data["fan_in"]]
|
392
|
-
)
|
354
|
+
fan_in_agent = unwrap_proxy(active_agents[agent_data["fan_in"]])
|
393
355
|
|
394
356
|
# Create the parallel workflow
|
395
357
|
llm_factory = model_factory_func(config.model)
|
@@ -416,7 +378,7 @@ async def create_agents_by_type(
|
|
416
378
|
|
417
379
|
|
418
380
|
async def create_basic_agents(
|
419
|
-
app_instance: MCPApp,
|
381
|
+
app_instance: MCPApp,
|
420
382
|
agents_dict: Dict[str, Dict[str, Any]],
|
421
383
|
model_factory_func: Callable,
|
422
384
|
) -> ProxyDict:
|
@@ -432,17 +394,17 @@ async def create_basic_agents(
|
|
432
394
|
Dictionary of initialized basic agents wrapped in appropriate proxies
|
433
395
|
"""
|
434
396
|
return await create_agents_by_type(
|
435
|
-
app_instance,
|
436
|
-
agents_dict,
|
437
|
-
AgentType.BASIC,
|
438
|
-
model_factory_func=model_factory_func
|
397
|
+
app_instance,
|
398
|
+
agents_dict,
|
399
|
+
AgentType.BASIC,
|
400
|
+
model_factory_func=model_factory_func,
|
439
401
|
)
|
440
402
|
|
441
403
|
|
442
404
|
async def create_agents_in_dependency_order(
|
443
|
-
app_instance: MCPApp,
|
405
|
+
app_instance: MCPApp,
|
444
406
|
agents_dict: Dict[str, Dict[str, Any]],
|
445
|
-
active_agents: ProxyDict,
|
407
|
+
active_agents: ProxyDict,
|
446
408
|
agent_type: AgentType,
|
447
409
|
model_factory_func: Callable,
|
448
410
|
) -> ProxyDict:
|
@@ -498,4 +460,4 @@ async def create_agents_in_dependency_order(
|
|
498
460
|
if agent_name in agent_result:
|
499
461
|
result_agents[agent_name] = agent_result[agent_name]
|
500
462
|
|
501
|
-
return result_agents
|
463
|
+
return result_agents
|
mcp_agent/core/fastagent.py
CHANGED
@@ -70,7 +70,12 @@ class FastAgent(ContextDependent):
|
|
70
70
|
Provides a simplified way to create and manage agents using decorators.
|
71
71
|
"""
|
72
72
|
|
73
|
-
def __init__(
|
73
|
+
def __init__(
|
74
|
+
self,
|
75
|
+
name: str,
|
76
|
+
config_path: Optional[str] = None,
|
77
|
+
ignore_unknown_args: bool = False,
|
78
|
+
):
|
74
79
|
"""
|
75
80
|
Initialize the decorator interface.
|
76
81
|
|
@@ -101,7 +106,12 @@ class FastAgent(ContextDependent):
|
|
101
106
|
action="store_true",
|
102
107
|
help="Disable progress display, tool and message logging for cleaner output",
|
103
108
|
)
|
104
|
-
|
109
|
+
|
110
|
+
if ignore_unknown_args:
|
111
|
+
known_args, _ = parser.parse_known_args()
|
112
|
+
self.args = known_args
|
113
|
+
else:
|
114
|
+
self.args = parser.parse_args()
|
105
115
|
|
106
116
|
# Quiet mode will be handled in _load_config()
|
107
117
|
|
@@ -372,7 +382,7 @@ class FastAgent(ContextDependent):
|
|
372
382
|
|
373
383
|
# Create wrapper with all agents
|
374
384
|
wrapper = AgentApp(agent_app, active_agents)
|
375
|
-
|
385
|
+
|
376
386
|
# Store reference to AgentApp in MCPApp for proxies to access
|
377
387
|
agent_app._agent_app = wrapper
|
378
388
|
|
@@ -0,0 +1,222 @@
|
|
1
|
+
"""
|
2
|
+
Helper functions for creating MCP content types with minimal code.
|
3
|
+
|
4
|
+
This module provides simple functions to create TextContent, ImageContent,
|
5
|
+
EmbeddedResource, and other MCP content types with minimal boilerplate.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import base64
|
9
|
+
from pathlib import Path
|
10
|
+
from typing import Literal, Optional, Union, List, Any
|
11
|
+
|
12
|
+
from mcp.types import (
|
13
|
+
TextContent,
|
14
|
+
ImageContent,
|
15
|
+
EmbeddedResource,
|
16
|
+
TextResourceContents,
|
17
|
+
BlobResourceContents,
|
18
|
+
)
|
19
|
+
|
20
|
+
from mcp_agent.mcp.mime_utils import (
|
21
|
+
guess_mime_type,
|
22
|
+
is_binary_content,
|
23
|
+
is_image_mime_type,
|
24
|
+
)
|
25
|
+
|
26
|
+
|
27
|
+
def MCPText(
|
28
|
+
text: str,
|
29
|
+
role: Literal["user", "assistant"] = "user",
|
30
|
+
annotations: Optional[dict] = None,
|
31
|
+
) -> dict:
|
32
|
+
"""
|
33
|
+
Create a message with text content.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
text: The text content
|
37
|
+
role: Role of the message, defaults to "user"
|
38
|
+
annotations: Optional annotations
|
39
|
+
|
40
|
+
Returns:
|
41
|
+
A dictionary with role and content that can be used in a prompt
|
42
|
+
"""
|
43
|
+
return {
|
44
|
+
"role": role,
|
45
|
+
"content": TextContent(type="text", text=text, annotations=annotations),
|
46
|
+
}
|
47
|
+
|
48
|
+
|
49
|
+
def MCPImage(
|
50
|
+
path: Union[str, Path] = None,
|
51
|
+
data: bytes = None,
|
52
|
+
mime_type: Optional[str] = None,
|
53
|
+
role: Literal["user", "assistant"] = "user",
|
54
|
+
annotations: Optional[dict] = None,
|
55
|
+
) -> dict:
|
56
|
+
"""
|
57
|
+
Create a message with image content.
|
58
|
+
|
59
|
+
Args:
|
60
|
+
path: Path to the image file
|
61
|
+
data: Raw image data bytes (alternative to path)
|
62
|
+
mime_type: Optional mime type, will be guessed from path if not provided
|
63
|
+
role: Role of the message, defaults to "user"
|
64
|
+
annotations: Optional annotations
|
65
|
+
|
66
|
+
Returns:
|
67
|
+
A dictionary with role and content that can be used in a prompt
|
68
|
+
"""
|
69
|
+
if path is None and data is None:
|
70
|
+
raise ValueError("Either path or data must be provided")
|
71
|
+
|
72
|
+
if path is not None and data is not None:
|
73
|
+
raise ValueError("Only one of path or data can be provided")
|
74
|
+
|
75
|
+
if path is not None:
|
76
|
+
path = Path(path)
|
77
|
+
if not mime_type:
|
78
|
+
mime_type = guess_mime_type(str(path))
|
79
|
+
with open(path, "rb") as f:
|
80
|
+
data = f.read()
|
81
|
+
|
82
|
+
if not mime_type:
|
83
|
+
mime_type = "image/png" # Default
|
84
|
+
|
85
|
+
b64_data = base64.b64encode(data).decode("ascii")
|
86
|
+
|
87
|
+
return {
|
88
|
+
"role": role,
|
89
|
+
"content": ImageContent(
|
90
|
+
type="image", data=b64_data, mimeType=mime_type, annotations=annotations
|
91
|
+
),
|
92
|
+
}
|
93
|
+
|
94
|
+
|
95
|
+
def MCPFile(
|
96
|
+
path: Union[str, Path],
|
97
|
+
mime_type: Optional[str] = None,
|
98
|
+
role: Literal["user", "assistant"] = "user",
|
99
|
+
annotations: Optional[dict] = None,
|
100
|
+
) -> dict:
|
101
|
+
"""
|
102
|
+
Create a message with an embedded resource from a file.
|
103
|
+
|
104
|
+
Args:
|
105
|
+
path: Path to the resource file
|
106
|
+
mime_type: Optional mime type, will be guessed from path if not provided
|
107
|
+
role: Role of the message, defaults to "user"
|
108
|
+
annotations: Optional annotations
|
109
|
+
|
110
|
+
Returns:
|
111
|
+
A dictionary with role and content that can be used in a prompt
|
112
|
+
"""
|
113
|
+
path = Path(path)
|
114
|
+
uri = f"file://{path.absolute()}"
|
115
|
+
|
116
|
+
if not mime_type:
|
117
|
+
mime_type = guess_mime_type(str(path))
|
118
|
+
|
119
|
+
# Determine if this is text or binary content
|
120
|
+
is_binary = is_binary_content(mime_type)
|
121
|
+
|
122
|
+
if is_binary:
|
123
|
+
# Read as binary
|
124
|
+
binary_data = path.read_bytes()
|
125
|
+
b64_data = base64.b64encode(binary_data).decode("ascii")
|
126
|
+
|
127
|
+
resource = BlobResourceContents(uri=uri, blob=b64_data, mimeType=mime_type)
|
128
|
+
else:
|
129
|
+
# Read as text
|
130
|
+
try:
|
131
|
+
text_data = path.read_text(encoding="utf-8")
|
132
|
+
resource = TextResourceContents(uri=uri, text=text_data, mimeType=mime_type)
|
133
|
+
except UnicodeDecodeError:
|
134
|
+
# Fallback to binary if text read fails
|
135
|
+
binary_data = path.read_bytes()
|
136
|
+
b64_data = base64.b64encode(binary_data).decode("ascii")
|
137
|
+
resource = BlobResourceContents(
|
138
|
+
uri=uri, blob=b64_data, mimeType=mime_type or "application/octet-stream"
|
139
|
+
)
|
140
|
+
|
141
|
+
return {
|
142
|
+
"role": role,
|
143
|
+
"content": EmbeddedResource(
|
144
|
+
type="resource", resource=resource, annotations=annotations
|
145
|
+
),
|
146
|
+
}
|
147
|
+
|
148
|
+
|
149
|
+
|
150
|
+
def MCPPrompt(
|
151
|
+
*content_items, role: Literal["user", "assistant"] = "user"
|
152
|
+
) -> List[dict]:
|
153
|
+
"""
|
154
|
+
Create one or more prompt messages with various content types.
|
155
|
+
|
156
|
+
This function intelligently creates different content types:
|
157
|
+
- Strings become TextContent
|
158
|
+
- File paths with image mime types become ImageContent
|
159
|
+
- File paths with text mime types or other mime types become EmbeddedResource
|
160
|
+
- Dicts with role and content are passed through unchanged
|
161
|
+
- Raw bytes become ImageContent
|
162
|
+
|
163
|
+
Args:
|
164
|
+
*content_items: Content items of various types
|
165
|
+
role: Role for all items (user or assistant)
|
166
|
+
|
167
|
+
Returns:
|
168
|
+
List of messages that can be used in a prompt
|
169
|
+
"""
|
170
|
+
result = []
|
171
|
+
|
172
|
+
for item in content_items:
|
173
|
+
if isinstance(item, dict) and "role" in item and "content" in item:
|
174
|
+
# Already a fully formed message
|
175
|
+
result.append(item)
|
176
|
+
elif isinstance(item, str) and not Path(item).exists():
|
177
|
+
# Simple text content (that's not a file path)
|
178
|
+
result.append(MCPText(item, role=role))
|
179
|
+
elif isinstance(item, Path) or isinstance(item, str):
|
180
|
+
# File path - determine the content type based on mime type
|
181
|
+
path_str = str(item)
|
182
|
+
mime_type = guess_mime_type(path_str)
|
183
|
+
|
184
|
+
if is_image_mime_type(mime_type):
|
185
|
+
# Image files (except SVG which is handled as text)
|
186
|
+
result.append(MCPImage(path=item, role=role))
|
187
|
+
else:
|
188
|
+
# All other file types (text documents, PDFs, SVGs, etc.)
|
189
|
+
result.append(MCPFile(path=item, role=role))
|
190
|
+
elif isinstance(item, bytes):
|
191
|
+
# Raw binary data, assume image
|
192
|
+
result.append(MCPImage(data=item, role=role))
|
193
|
+
else:
|
194
|
+
# Try to convert to string
|
195
|
+
result.append(MCPText(str(item), role=role))
|
196
|
+
|
197
|
+
return result
|
198
|
+
|
199
|
+
|
200
|
+
def User(*content_items) -> List[dict]:
|
201
|
+
"""Create user message(s) with various content types."""
|
202
|
+
return MCPPrompt(*content_items, role="user")
|
203
|
+
|
204
|
+
|
205
|
+
def Assistant(*content_items) -> List[dict]:
|
206
|
+
"""Create assistant message(s) with various content types."""
|
207
|
+
return MCPPrompt(*content_items, role="assistant")
|
208
|
+
|
209
|
+
|
210
|
+
def create_message(content: Any, role: Literal["user", "assistant"] = "user") -> dict:
|
211
|
+
"""
|
212
|
+
Create a single prompt message from content of various types.
|
213
|
+
|
214
|
+
Args:
|
215
|
+
content: Content of various types (str, Path, bytes, etc.)
|
216
|
+
role: Role of the message
|
217
|
+
|
218
|
+
Returns:
|
219
|
+
A dictionary with role and content that can be used in a prompt
|
220
|
+
"""
|
221
|
+
messages = MCPPrompt(content, role=role)
|
222
|
+
return messages[0] if messages else {}
|