fastmcp 2.14.4__py3-none-any.whl → 3.0.0b1__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.
- fastmcp/_vendor/__init__.py +1 -0
- fastmcp/_vendor/docket_di/README.md +7 -0
- fastmcp/_vendor/docket_di/__init__.py +163 -0
- fastmcp/cli/cli.py +112 -28
- fastmcp/cli/install/claude_code.py +1 -5
- fastmcp/cli/install/claude_desktop.py +1 -5
- fastmcp/cli/install/cursor.py +1 -5
- fastmcp/cli/install/gemini_cli.py +1 -5
- fastmcp/cli/install/mcp_json.py +1 -6
- fastmcp/cli/run.py +146 -5
- fastmcp/client/__init__.py +7 -9
- fastmcp/client/auth/oauth.py +18 -17
- fastmcp/client/client.py +100 -870
- fastmcp/client/elicitation.py +1 -1
- fastmcp/client/mixins/__init__.py +13 -0
- fastmcp/client/mixins/prompts.py +295 -0
- fastmcp/client/mixins/resources.py +325 -0
- fastmcp/client/mixins/task_management.py +157 -0
- fastmcp/client/mixins/tools.py +397 -0
- fastmcp/client/sampling/handlers/anthropic.py +2 -2
- fastmcp/client/sampling/handlers/openai.py +1 -1
- fastmcp/client/tasks.py +3 -3
- fastmcp/client/telemetry.py +47 -0
- fastmcp/client/transports/__init__.py +38 -0
- fastmcp/client/transports/base.py +82 -0
- fastmcp/client/transports/config.py +170 -0
- fastmcp/client/transports/http.py +145 -0
- fastmcp/client/transports/inference.py +154 -0
- fastmcp/client/transports/memory.py +90 -0
- fastmcp/client/transports/sse.py +89 -0
- fastmcp/client/transports/stdio.py +543 -0
- fastmcp/contrib/component_manager/README.md +4 -10
- fastmcp/contrib/component_manager/__init__.py +1 -2
- fastmcp/contrib/component_manager/component_manager.py +95 -160
- fastmcp/contrib/component_manager/example.py +1 -1
- fastmcp/contrib/mcp_mixin/example.py +4 -4
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +11 -4
- fastmcp/decorators.py +41 -0
- fastmcp/dependencies.py +12 -1
- fastmcp/exceptions.py +4 -0
- fastmcp/experimental/server/openapi/__init__.py +18 -15
- fastmcp/mcp_config.py +13 -4
- fastmcp/prompts/__init__.py +6 -3
- fastmcp/prompts/function_prompt.py +465 -0
- fastmcp/prompts/prompt.py +321 -271
- fastmcp/resources/__init__.py +5 -3
- fastmcp/resources/function_resource.py +335 -0
- fastmcp/resources/resource.py +325 -115
- fastmcp/resources/template.py +215 -43
- fastmcp/resources/types.py +27 -12
- fastmcp/server/__init__.py +2 -2
- fastmcp/server/auth/__init__.py +14 -0
- fastmcp/server/auth/auth.py +30 -10
- fastmcp/server/auth/authorization.py +190 -0
- fastmcp/server/auth/oauth_proxy/__init__.py +14 -0
- fastmcp/server/auth/oauth_proxy/consent.py +361 -0
- fastmcp/server/auth/oauth_proxy/models.py +178 -0
- fastmcp/server/auth/{oauth_proxy.py → oauth_proxy/proxy.py} +24 -778
- fastmcp/server/auth/oauth_proxy/ui.py +277 -0
- fastmcp/server/auth/oidc_proxy.py +2 -2
- fastmcp/server/auth/providers/auth0.py +24 -94
- fastmcp/server/auth/providers/aws.py +26 -95
- fastmcp/server/auth/providers/azure.py +41 -129
- fastmcp/server/auth/providers/descope.py +18 -49
- fastmcp/server/auth/providers/discord.py +25 -86
- fastmcp/server/auth/providers/github.py +23 -87
- fastmcp/server/auth/providers/google.py +24 -87
- fastmcp/server/auth/providers/introspection.py +60 -79
- fastmcp/server/auth/providers/jwt.py +30 -67
- fastmcp/server/auth/providers/oci.py +47 -110
- fastmcp/server/auth/providers/scalekit.py +23 -61
- fastmcp/server/auth/providers/supabase.py +18 -47
- fastmcp/server/auth/providers/workos.py +34 -127
- fastmcp/server/context.py +372 -419
- fastmcp/server/dependencies.py +541 -251
- fastmcp/server/elicitation.py +20 -18
- fastmcp/server/event_store.py +3 -3
- fastmcp/server/http.py +16 -6
- fastmcp/server/lifespan.py +198 -0
- fastmcp/server/low_level.py +92 -2
- fastmcp/server/middleware/__init__.py +5 -1
- fastmcp/server/middleware/authorization.py +312 -0
- fastmcp/server/middleware/caching.py +101 -54
- fastmcp/server/middleware/middleware.py +6 -9
- fastmcp/server/middleware/ping.py +70 -0
- fastmcp/server/middleware/tool_injection.py +2 -2
- fastmcp/server/mixins/__init__.py +7 -0
- fastmcp/server/mixins/lifespan.py +217 -0
- fastmcp/server/mixins/mcp_operations.py +392 -0
- fastmcp/server/mixins/transport.py +342 -0
- fastmcp/server/openapi/__init__.py +41 -21
- fastmcp/server/openapi/components.py +16 -339
- fastmcp/server/openapi/routing.py +34 -118
- fastmcp/server/openapi/server.py +67 -392
- fastmcp/server/providers/__init__.py +71 -0
- fastmcp/server/providers/aggregate.py +261 -0
- fastmcp/server/providers/base.py +578 -0
- fastmcp/server/providers/fastmcp_provider.py +674 -0
- fastmcp/server/providers/filesystem.py +226 -0
- fastmcp/server/providers/filesystem_discovery.py +327 -0
- fastmcp/server/providers/local_provider/__init__.py +11 -0
- fastmcp/server/providers/local_provider/decorators/__init__.py +15 -0
- fastmcp/server/providers/local_provider/decorators/prompts.py +256 -0
- fastmcp/server/providers/local_provider/decorators/resources.py +240 -0
- fastmcp/server/providers/local_provider/decorators/tools.py +315 -0
- fastmcp/server/providers/local_provider/local_provider.py +465 -0
- fastmcp/server/providers/openapi/__init__.py +39 -0
- fastmcp/server/providers/openapi/components.py +332 -0
- fastmcp/server/providers/openapi/provider.py +405 -0
- fastmcp/server/providers/openapi/routing.py +109 -0
- fastmcp/server/providers/proxy.py +867 -0
- fastmcp/server/providers/skills/__init__.py +59 -0
- fastmcp/server/providers/skills/_common.py +101 -0
- fastmcp/server/providers/skills/claude_provider.py +44 -0
- fastmcp/server/providers/skills/directory_provider.py +153 -0
- fastmcp/server/providers/skills/skill_provider.py +432 -0
- fastmcp/server/providers/skills/vendor_providers.py +142 -0
- fastmcp/server/providers/wrapped_provider.py +140 -0
- fastmcp/server/proxy.py +34 -700
- fastmcp/server/sampling/run.py +341 -2
- fastmcp/server/sampling/sampling_tool.py +4 -3
- fastmcp/server/server.py +1214 -2171
- fastmcp/server/tasks/__init__.py +2 -1
- fastmcp/server/tasks/capabilities.py +13 -1
- fastmcp/server/tasks/config.py +66 -3
- fastmcp/server/tasks/handlers.py +65 -273
- fastmcp/server/tasks/keys.py +4 -6
- fastmcp/server/tasks/requests.py +474 -0
- fastmcp/server/tasks/routing.py +76 -0
- fastmcp/server/tasks/subscriptions.py +20 -11
- fastmcp/server/telemetry.py +131 -0
- fastmcp/server/transforms/__init__.py +244 -0
- fastmcp/server/transforms/namespace.py +193 -0
- fastmcp/server/transforms/prompts_as_tools.py +175 -0
- fastmcp/server/transforms/resources_as_tools.py +190 -0
- fastmcp/server/transforms/tool_transform.py +96 -0
- fastmcp/server/transforms/version_filter.py +124 -0
- fastmcp/server/transforms/visibility.py +526 -0
- fastmcp/settings.py +34 -96
- fastmcp/telemetry.py +122 -0
- fastmcp/tools/__init__.py +10 -3
- fastmcp/tools/function_parsing.py +201 -0
- fastmcp/tools/function_tool.py +467 -0
- fastmcp/tools/tool.py +215 -362
- fastmcp/tools/tool_transform.py +38 -21
- fastmcp/utilities/async_utils.py +69 -0
- fastmcp/utilities/components.py +152 -91
- fastmcp/utilities/inspect.py +8 -20
- fastmcp/utilities/json_schema.py +12 -5
- fastmcp/utilities/json_schema_type.py +17 -15
- fastmcp/utilities/lifespan.py +56 -0
- fastmcp/utilities/logging.py +12 -4
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
- fastmcp/utilities/openapi/parser.py +3 -3
- fastmcp/utilities/pagination.py +80 -0
- fastmcp/utilities/skills.py +253 -0
- fastmcp/utilities/tests.py +0 -16
- fastmcp/utilities/timeout.py +47 -0
- fastmcp/utilities/types.py +1 -1
- fastmcp/utilities/versions.py +285 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/METADATA +8 -5
- fastmcp-3.0.0b1.dist-info/RECORD +228 -0
- fastmcp/client/transports.py +0 -1170
- fastmcp/contrib/component_manager/component_service.py +0 -209
- fastmcp/prompts/prompt_manager.py +0 -117
- fastmcp/resources/resource_manager.py +0 -338
- fastmcp/server/tasks/converters.py +0 -206
- fastmcp/server/tasks/protocol.py +0 -359
- fastmcp/tools/tool_manager.py +0 -170
- fastmcp/utilities/mcp_config.py +0 -56
- fastmcp-2.14.4.dist-info/RECORD +0 -161
- /fastmcp/server/{openapi → providers/openapi}/README.md +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/WHEEL +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/sampling/run.py
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import inspect
|
|
6
|
+
import json
|
|
7
|
+
from collections.abc import Callable, Sequence
|
|
6
8
|
from dataclasses import dataclass
|
|
7
|
-
from typing import TYPE_CHECKING, Generic
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Generic, Literal, cast
|
|
8
10
|
|
|
9
11
|
from mcp.types import (
|
|
10
12
|
ClientCapabilities,
|
|
@@ -14,6 +16,7 @@ from mcp.types import (
|
|
|
14
16
|
ModelPreferences,
|
|
15
17
|
SamplingCapability,
|
|
16
18
|
SamplingMessage,
|
|
19
|
+
SamplingMessageContentBlock,
|
|
17
20
|
SamplingToolsCapability,
|
|
18
21
|
TextContent,
|
|
19
22
|
ToolChoice,
|
|
@@ -22,18 +25,25 @@ from mcp.types import (
|
|
|
22
25
|
)
|
|
23
26
|
from mcp.types import CreateMessageRequestParams as SamplingParams
|
|
24
27
|
from mcp.types import Tool as SDKTool
|
|
28
|
+
from pydantic import ValidationError
|
|
25
29
|
from typing_extensions import TypeVar
|
|
26
30
|
|
|
31
|
+
from fastmcp import settings
|
|
27
32
|
from fastmcp.exceptions import ToolError
|
|
28
33
|
from fastmcp.server.sampling.sampling_tool import SamplingTool
|
|
34
|
+
from fastmcp.utilities.json_schema import compress_schema
|
|
29
35
|
from fastmcp.utilities.logging import get_logger
|
|
36
|
+
from fastmcp.utilities.types import get_cached_typeadapter
|
|
30
37
|
|
|
31
38
|
logger = get_logger(__name__)
|
|
32
39
|
|
|
33
40
|
if TYPE_CHECKING:
|
|
34
41
|
from fastmcp.server.context import Context
|
|
35
42
|
|
|
36
|
-
ResultT = TypeVar("ResultT"
|
|
43
|
+
ResultT = TypeVar("ResultT")
|
|
44
|
+
|
|
45
|
+
# Simplified tool choice type - just the mode string instead of the full MCP object
|
|
46
|
+
ToolChoiceOption = Literal["auto", "required", "none"]
|
|
37
47
|
|
|
38
48
|
|
|
39
49
|
@dataclass
|
|
@@ -299,3 +309,332 @@ async def execute_tools(
|
|
|
299
309
|
)
|
|
300
310
|
|
|
301
311
|
return tool_results
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# --- Helper functions for sampling ---
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def prepare_messages(
|
|
318
|
+
messages: str | Sequence[str | SamplingMessage],
|
|
319
|
+
) -> list[SamplingMessage]:
|
|
320
|
+
"""Convert various message formats to a list of SamplingMessage objects."""
|
|
321
|
+
if isinstance(messages, str):
|
|
322
|
+
return [
|
|
323
|
+
SamplingMessage(
|
|
324
|
+
content=TextContent(text=messages, type="text"), role="user"
|
|
325
|
+
)
|
|
326
|
+
]
|
|
327
|
+
else:
|
|
328
|
+
return [
|
|
329
|
+
SamplingMessage(content=TextContent(text=m, type="text"), role="user")
|
|
330
|
+
if isinstance(m, str)
|
|
331
|
+
else m
|
|
332
|
+
for m in messages
|
|
333
|
+
]
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def prepare_tools(
|
|
337
|
+
tools: Sequence[SamplingTool | Callable[..., Any]] | None,
|
|
338
|
+
) -> list[SamplingTool] | None:
|
|
339
|
+
"""Convert tools to SamplingTool objects."""
|
|
340
|
+
if tools is None:
|
|
341
|
+
return None
|
|
342
|
+
|
|
343
|
+
sampling_tools: list[SamplingTool] = []
|
|
344
|
+
for t in tools:
|
|
345
|
+
if isinstance(t, SamplingTool):
|
|
346
|
+
sampling_tools.append(t)
|
|
347
|
+
elif callable(t):
|
|
348
|
+
sampling_tools.append(SamplingTool.from_function(t))
|
|
349
|
+
else:
|
|
350
|
+
raise TypeError(f"Expected SamplingTool or callable, got {type(t)}")
|
|
351
|
+
|
|
352
|
+
return sampling_tools if sampling_tools else None
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def extract_tool_calls(
|
|
356
|
+
response: CreateMessageResult | CreateMessageResultWithTools,
|
|
357
|
+
) -> list[ToolUseContent]:
|
|
358
|
+
"""Extract tool calls from a response."""
|
|
359
|
+
content = response.content
|
|
360
|
+
if isinstance(content, list):
|
|
361
|
+
return [c for c in content if isinstance(c, ToolUseContent)]
|
|
362
|
+
elif isinstance(content, ToolUseContent):
|
|
363
|
+
return [content]
|
|
364
|
+
return []
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def create_final_response_tool(result_type: type) -> SamplingTool:
|
|
368
|
+
"""Create a synthetic 'final_response' tool for structured output.
|
|
369
|
+
|
|
370
|
+
This tool is used to capture structured responses from the LLM.
|
|
371
|
+
The tool's schema is derived from the result_type.
|
|
372
|
+
"""
|
|
373
|
+
type_adapter = get_cached_typeadapter(result_type)
|
|
374
|
+
schema = type_adapter.json_schema()
|
|
375
|
+
schema = compress_schema(schema, prune_titles=True)
|
|
376
|
+
|
|
377
|
+
# Tool parameters must be object-shaped. Wrap primitives in {"value": <schema>}
|
|
378
|
+
if schema.get("type") != "object":
|
|
379
|
+
schema = {
|
|
380
|
+
"type": "object",
|
|
381
|
+
"properties": {"value": schema},
|
|
382
|
+
"required": ["value"],
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
# The fn just returns the input as-is (validation happens in the loop)
|
|
386
|
+
def final_response(**kwargs: Any) -> dict[str, Any]:
|
|
387
|
+
return kwargs
|
|
388
|
+
|
|
389
|
+
return SamplingTool(
|
|
390
|
+
name="final_response",
|
|
391
|
+
description=(
|
|
392
|
+
"Call this tool to provide your final response. "
|
|
393
|
+
"Use this when you have completed the task and are ready to return the result."
|
|
394
|
+
),
|
|
395
|
+
parameters=schema,
|
|
396
|
+
fn=final_response,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
# --- Implementation functions for Context methods ---
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
async def sample_step_impl(
|
|
404
|
+
context: Context,
|
|
405
|
+
messages: str | Sequence[str | SamplingMessage],
|
|
406
|
+
*,
|
|
407
|
+
system_prompt: str | None = None,
|
|
408
|
+
temperature: float | None = None,
|
|
409
|
+
max_tokens: int | None = None,
|
|
410
|
+
model_preferences: ModelPreferences | str | list[str] | None = None,
|
|
411
|
+
tools: Sequence[SamplingTool | Callable[..., Any]] | None = None,
|
|
412
|
+
tool_choice: ToolChoiceOption | str | None = None,
|
|
413
|
+
auto_execute_tools: bool = True,
|
|
414
|
+
mask_error_details: bool | None = None,
|
|
415
|
+
) -> SampleStep:
|
|
416
|
+
"""Implementation of Context.sample_step().
|
|
417
|
+
|
|
418
|
+
Make a single LLM sampling call. This is a stateless function that makes
|
|
419
|
+
exactly one LLM call and optionally executes any requested tools.
|
|
420
|
+
"""
|
|
421
|
+
# Convert messages to SamplingMessage objects
|
|
422
|
+
current_messages = prepare_messages(messages)
|
|
423
|
+
|
|
424
|
+
# Convert tools to SamplingTools
|
|
425
|
+
sampling_tools = prepare_tools(tools)
|
|
426
|
+
sdk_tools: list[SDKTool] | None = (
|
|
427
|
+
[t._to_sdk_tool() for t in sampling_tools] if sampling_tools else None
|
|
428
|
+
)
|
|
429
|
+
tool_map: dict[str, SamplingTool] = (
|
|
430
|
+
{t.name: t for t in sampling_tools} if sampling_tools else {}
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
# Determine whether to use fallback handler or client
|
|
434
|
+
use_fallback = determine_handler_mode(context, bool(sampling_tools))
|
|
435
|
+
|
|
436
|
+
# Build tool choice
|
|
437
|
+
effective_tool_choice: ToolChoice | None = None
|
|
438
|
+
if tool_choice is not None:
|
|
439
|
+
if tool_choice not in ("auto", "required", "none"):
|
|
440
|
+
raise ValueError(
|
|
441
|
+
f"Invalid tool_choice: {tool_choice!r}. "
|
|
442
|
+
"Must be 'auto', 'required', or 'none'."
|
|
443
|
+
)
|
|
444
|
+
effective_tool_choice = ToolChoice(
|
|
445
|
+
mode=cast(Literal["auto", "required", "none"], tool_choice)
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
# Effective max_tokens
|
|
449
|
+
effective_max_tokens = max_tokens if max_tokens is not None else 512
|
|
450
|
+
|
|
451
|
+
# Make the LLM call
|
|
452
|
+
if use_fallback:
|
|
453
|
+
response = await call_sampling_handler(
|
|
454
|
+
context,
|
|
455
|
+
current_messages,
|
|
456
|
+
system_prompt=system_prompt,
|
|
457
|
+
temperature=temperature,
|
|
458
|
+
max_tokens=effective_max_tokens,
|
|
459
|
+
model_preferences=model_preferences,
|
|
460
|
+
sdk_tools=sdk_tools,
|
|
461
|
+
tool_choice=effective_tool_choice,
|
|
462
|
+
)
|
|
463
|
+
else:
|
|
464
|
+
response = await context.session.create_message(
|
|
465
|
+
messages=current_messages,
|
|
466
|
+
system_prompt=system_prompt,
|
|
467
|
+
temperature=temperature,
|
|
468
|
+
max_tokens=effective_max_tokens,
|
|
469
|
+
model_preferences=_parse_model_preferences(model_preferences),
|
|
470
|
+
tools=sdk_tools,
|
|
471
|
+
tool_choice=effective_tool_choice,
|
|
472
|
+
related_request_id=context.request_id,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
# Check if this is a tool use response
|
|
476
|
+
is_tool_use_response = (
|
|
477
|
+
isinstance(response, CreateMessageResultWithTools)
|
|
478
|
+
and response.stopReason == "toolUse"
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
# Always include the assistant response in history
|
|
482
|
+
current_messages.append(SamplingMessage(role="assistant", content=response.content))
|
|
483
|
+
|
|
484
|
+
# If not a tool use, return immediately
|
|
485
|
+
if not is_tool_use_response:
|
|
486
|
+
return SampleStep(response=response, history=current_messages)
|
|
487
|
+
|
|
488
|
+
# If not executing tools, return with assistant message but no tool results
|
|
489
|
+
if not auto_execute_tools:
|
|
490
|
+
return SampleStep(response=response, history=current_messages)
|
|
491
|
+
|
|
492
|
+
# Execute tools and add results to history
|
|
493
|
+
step_tool_calls = extract_tool_calls(response)
|
|
494
|
+
if step_tool_calls:
|
|
495
|
+
effective_mask = (
|
|
496
|
+
mask_error_details
|
|
497
|
+
if mask_error_details is not None
|
|
498
|
+
else settings.mask_error_details
|
|
499
|
+
)
|
|
500
|
+
tool_results: list[ToolResultContent] = await execute_tools(
|
|
501
|
+
step_tool_calls, tool_map, mask_error_details=effective_mask
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
if tool_results:
|
|
505
|
+
current_messages.append(
|
|
506
|
+
SamplingMessage(
|
|
507
|
+
role="user",
|
|
508
|
+
content=cast(list[SamplingMessageContentBlock], tool_results),
|
|
509
|
+
)
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
return SampleStep(response=response, history=current_messages)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
async def sample_impl(
|
|
516
|
+
context: Context,
|
|
517
|
+
messages: str | Sequence[str | SamplingMessage],
|
|
518
|
+
*,
|
|
519
|
+
system_prompt: str | None = None,
|
|
520
|
+
temperature: float | None = None,
|
|
521
|
+
max_tokens: int | None = None,
|
|
522
|
+
model_preferences: ModelPreferences | str | list[str] | None = None,
|
|
523
|
+
tools: Sequence[SamplingTool | Callable[..., Any]] | None = None,
|
|
524
|
+
result_type: type[ResultT] | None = None,
|
|
525
|
+
mask_error_details: bool | None = None,
|
|
526
|
+
) -> SamplingResult[ResultT]:
|
|
527
|
+
"""Implementation of Context.sample().
|
|
528
|
+
|
|
529
|
+
Send a sampling request to the client and await the response. This method
|
|
530
|
+
runs to completion automatically, executing a tool loop until the LLM
|
|
531
|
+
provides a final text response.
|
|
532
|
+
"""
|
|
533
|
+
# Safety limit to prevent infinite loops
|
|
534
|
+
max_iterations = 100
|
|
535
|
+
|
|
536
|
+
# Convert tools to SamplingTools
|
|
537
|
+
sampling_tools = prepare_tools(tools)
|
|
538
|
+
|
|
539
|
+
# Handle structured output with result_type
|
|
540
|
+
tool_choice: str | None = None
|
|
541
|
+
if result_type is not None and result_type is not str:
|
|
542
|
+
final_response_tool = create_final_response_tool(result_type)
|
|
543
|
+
sampling_tools = list(sampling_tools) if sampling_tools else []
|
|
544
|
+
sampling_tools.append(final_response_tool)
|
|
545
|
+
|
|
546
|
+
# Always require tool calls when result_type is set - the LLM must
|
|
547
|
+
# eventually call final_response (text responses are not accepted)
|
|
548
|
+
tool_choice = "required"
|
|
549
|
+
|
|
550
|
+
# Convert messages for the loop
|
|
551
|
+
current_messages: str | Sequence[str | SamplingMessage] = messages
|
|
552
|
+
|
|
553
|
+
for _iteration in range(max_iterations):
|
|
554
|
+
step = await sample_step_impl(
|
|
555
|
+
context,
|
|
556
|
+
messages=current_messages,
|
|
557
|
+
system_prompt=system_prompt,
|
|
558
|
+
temperature=temperature,
|
|
559
|
+
max_tokens=max_tokens,
|
|
560
|
+
model_preferences=model_preferences,
|
|
561
|
+
tools=sampling_tools,
|
|
562
|
+
tool_choice=tool_choice,
|
|
563
|
+
mask_error_details=mask_error_details,
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
# Check for final_response tool call for structured output
|
|
567
|
+
if result_type is not None and result_type is not str and step.is_tool_use:
|
|
568
|
+
for tool_call in step.tool_calls:
|
|
569
|
+
if tool_call.name == "final_response":
|
|
570
|
+
# Validate and return the structured result
|
|
571
|
+
type_adapter = get_cached_typeadapter(result_type)
|
|
572
|
+
|
|
573
|
+
# Unwrap if we wrapped primitives (non-object schemas)
|
|
574
|
+
input_data = tool_call.input
|
|
575
|
+
original_schema = compress_schema(
|
|
576
|
+
type_adapter.json_schema(), prune_titles=True
|
|
577
|
+
)
|
|
578
|
+
if (
|
|
579
|
+
original_schema.get("type") != "object"
|
|
580
|
+
and isinstance(input_data, dict)
|
|
581
|
+
and "value" in input_data
|
|
582
|
+
):
|
|
583
|
+
input_data = input_data["value"]
|
|
584
|
+
|
|
585
|
+
try:
|
|
586
|
+
validated_result = type_adapter.validate_python(input_data)
|
|
587
|
+
text = json.dumps(
|
|
588
|
+
type_adapter.dump_python(validated_result, mode="json")
|
|
589
|
+
)
|
|
590
|
+
return SamplingResult(
|
|
591
|
+
text=text,
|
|
592
|
+
result=validated_result,
|
|
593
|
+
history=step.history,
|
|
594
|
+
)
|
|
595
|
+
except ValidationError as e:
|
|
596
|
+
# Validation failed - add error as tool result
|
|
597
|
+
step.history.append(
|
|
598
|
+
SamplingMessage(
|
|
599
|
+
role="user",
|
|
600
|
+
content=[
|
|
601
|
+
ToolResultContent(
|
|
602
|
+
type="tool_result",
|
|
603
|
+
toolUseId=tool_call.id,
|
|
604
|
+
content=[
|
|
605
|
+
TextContent(
|
|
606
|
+
type="text",
|
|
607
|
+
text=(
|
|
608
|
+
f"Validation error: {e}. "
|
|
609
|
+
"Please try again with valid data."
|
|
610
|
+
),
|
|
611
|
+
)
|
|
612
|
+
],
|
|
613
|
+
isError=True,
|
|
614
|
+
)
|
|
615
|
+
],
|
|
616
|
+
)
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
# If not a tool use response, we're done
|
|
620
|
+
if not step.is_tool_use:
|
|
621
|
+
# For structured output, the LLM must use the final_response tool
|
|
622
|
+
if result_type is not None and result_type is not str:
|
|
623
|
+
raise RuntimeError(
|
|
624
|
+
f"Expected structured output of type {result_type.__name__}, "
|
|
625
|
+
"but the LLM returned a text response instead of calling "
|
|
626
|
+
"the final_response tool."
|
|
627
|
+
)
|
|
628
|
+
return SamplingResult(
|
|
629
|
+
text=step.text,
|
|
630
|
+
result=cast(ResultT, step.text if step.text else ""),
|
|
631
|
+
history=step.history,
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
# Continue with the updated history
|
|
635
|
+
current_messages = step.history
|
|
636
|
+
|
|
637
|
+
# After first iteration, reset tool_choice to auto
|
|
638
|
+
tool_choice = None
|
|
639
|
+
|
|
640
|
+
raise RuntimeError(f"Sampling exceeded maximum iterations ({max_iterations})")
|
|
@@ -7,12 +7,13 @@ from collections.abc import Callable
|
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
9
|
from mcp.types import Tool as SDKTool
|
|
10
|
-
from pydantic import
|
|
10
|
+
from pydantic import ConfigDict
|
|
11
11
|
|
|
12
|
-
from fastmcp.tools.
|
|
12
|
+
from fastmcp.tools.function_parsing import ParsedFunction
|
|
13
|
+
from fastmcp.utilities.types import FastMCPBaseModel
|
|
13
14
|
|
|
14
15
|
|
|
15
|
-
class SamplingTool(
|
|
16
|
+
class SamplingTool(FastMCPBaseModel):
|
|
16
17
|
"""A tool that can be used during LLM sampling.
|
|
17
18
|
|
|
18
19
|
SamplingTools bundle a tool's schema (name, description, parameters) with
|