fastmcp/examples/sampling/structured_output.py
2025-12-09 19:29:02 -05:00

163 lines
5.3 KiB
Python

# /// script
# dependencies = ["anthropic", "fastmcp", "rich"]
# ///
"""
Structured Output Example
Use `result_type` to get validated Pydantic models from an LLM.
MCP Flow:
1. Client calls server tool
2. Server makes sampling request with result_type=YourModel
3. LLM response is parsed and validated against the Pydantic schema
4. Server returns structured data to client
Run:
python examples/sampling/structured_output.py
"""
import asyncio
from enum import Enum
from pydantic import BaseModel, Field
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from fastmcp import Client, Context, FastMCP
from fastmcp.server.sampling.anthropic import AnthropicSamplingHandler
console = Console()
# ============================================================================
# PYDANTIC MODELS - Define the schema for LLM responses
# ============================================================================
class Sentiment(str, Enum):
POSITIVE = "positive"
NEGATIVE = "negative"
NEUTRAL = "neutral"
MIXED = "mixed"
class SentimentAnalysis(BaseModel):
sentiment: Sentiment = Field(description="Overall sentiment")
confidence: float = Field(ge=0.0, le=1.0, description="Confidence 0.0-1.0")
reasoning: str = Field(description="Brief explanation")
key_phrases: list[str] = Field(description="Influential phrases")
# ============================================================================
# SAMPLING HANDLER WITH LOGGING
# ============================================================================
class LoggingAnthropicHandler(AnthropicSamplingHandler):
async def __call__(self, messages, params, context):
console.print(
" [bold blue]⚡ SAMPLING[/] Calling Claude API...", highlight=False
)
result = await super().__call__(messages, params, context)
console.print(
" [bold blue]⚡ SAMPLING[/] Response received", highlight=False
)
return result
# ============================================================================
# SERVER
# ============================================================================
mcp = FastMCP("Sentiment Analyzer")
@mcp.tool
async def analyze_sentiment(text: str, ctx: Context) -> dict:
"""Analyze sentiment and return structured results."""
console.print(" [bold yellow]📦 SERVER[/] Tool 'analyze_sentiment' called")
console.print(
" [bold yellow]📦 SERVER[/] Sampling with result_type=SentimentAnalysis"
)
result = await ctx.sample(
messages=f"Analyze the sentiment of this text:\n\n{text}",
system_prompt="You are a sentiment analysis expert.",
result_type=SentimentAnalysis,
)
console.print(
f" [bold yellow]📦 SERVER[/] Got validated {type(result.result).__name__}"
)
return result.result.model_dump() # type: ignore[union-attr]
# ============================================================================
# CLIENT
# ============================================================================
async def main():
console.print()
console.print(
Panel.fit(
"[bold]Structured Output Example[/]\n\n"
"The [cyan]result_type[/] parameter ensures LLM responses\n"
"match your Pydantic schema.",
border_style="bright_black",
)
)
console.print()
handler = LoggingAnthropicHandler(default_model="claude-sonnet-4-5-20250929")
test_texts = [
("I love this! Best purchase ever!", "😊"),
("Terrible. Would not recommend.", "😞"),
("It's okay. Nothing special.", "😐"),
]
async with Client(mcp, sampling_handler=handler) as client:
for text, _ in test_texts:
console.rule(style="dim")
console.print(f"[bold green]🖥️ CLIENT[/] Analyzing: [italic]{text}[/]")
console.print()
result = await client.call_tool("analyze_sentiment", {"text": text})
data = result.data
# Display result in a nice table
console.print()
console.print("[bold green]🖥️ CLIENT[/] Received structured result:")
emoji = {"positive": "😊", "negative": "😞", "neutral": "😐", "mixed": "🤔"}
table = Table(show_header=False, box=None, padding=(0, 2))
table.add_column(style="bold")
table.add_column()
table.add_row(
"Sentiment", f"{emoji.get(data['sentiment'], '')} {data['sentiment']}"
)
table.add_row(
"Confidence",
f"[green]{'' * int(data['confidence'] * 10)}[/][dim]{'' * (10 - int(data['confidence'] * 10))}[/] {data['confidence']:.0%}",
)
table.add_row("Reasoning", data["reasoning"])
table.add_row("Key phrases", ", ".join(data["key_phrases"]))
console.print(Panel(table, border_style="green"))
console.print()
console.print(
Panel(
"[bold]Key concept:[/] The result_type parameter enforces a Pydantic schema.\n"
"Invalid LLM responses are automatically rejected and retried.",
border_style="bright_black",
)
)
console.print()
if __name__ == "__main__":
asyncio.run(main())