chore: import upstream snapshot with attribution
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Has been cancelled
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
# Structured Output
|
||||
|
||||
A tool that returns a plain `str` produces the result twice: as text in `content`, and as `{"result": "..."}` in `structured_content`.
|
||||
|
||||
This page is about that second channel: where it comes from, every shape it can take, and how the SDK keeps it honest.
|
||||
|
||||
The short version: **the return type annotation is the output schema**. You already wrote it.
|
||||
|
||||
## The output schema
|
||||
|
||||
```python title="server.py" hl_lines="9"
|
||||
--8<-- "docs_src/structured_output/tutorial001.py"
|
||||
```
|
||||
|
||||
The line that matters is the signature: `-> int`.
|
||||
|
||||
Because of it, the tool the SDK sends during `tools/list` carries an `output_schema` next to the input schema it builds from your parameters (**[Tools](tools.md)** covers that one):
|
||||
|
||||
```json
|
||||
{
|
||||
"properties": {
|
||||
"result": {"title": "Result", "type": "integer"}
|
||||
},
|
||||
"required": ["result"],
|
||||
"title": "get_temperatureOutput",
|
||||
"type": "object"
|
||||
}
|
||||
```
|
||||
|
||||
A bare `int` isn't a JSON object, so the SDK **wraps** it in `{"result": ...}`. Call the tool and both channels are filled:
|
||||
|
||||
```python
|
||||
result.content # [TextContent(text="17")]
|
||||
result.structured_content # {"result": 17}
|
||||
```
|
||||
|
||||
Every scalar gets the same wrapper: `str`, `int`, `float`, `bool`, `bytes`, `None`.
|
||||
|
||||
## Two channels
|
||||
|
||||
Why send the same value twice?
|
||||
|
||||
* `content` is for the **model**. A language model reads text; this is the only part of the result it sees.
|
||||
* `structured_content` is for the **application** the model runs inside: code that wants `17`, not a sentence containing "17".
|
||||
* `output_schema` is the contract between them, published before the tool is ever called.
|
||||
|
||||
You return one Python value. The SDK fills in all three.
|
||||
|
||||
## Return a model
|
||||
|
||||
Declare the shape as a Pydantic `BaseModel` and return an instance:
|
||||
|
||||
```python title="server.py" hl_lines="8-11 15"
|
||||
--8<-- "docs_src/structured_output/tutorial002.py"
|
||||
```
|
||||
|
||||
`WeatherData` **is** the schema now. No wrapper, no `result` key:
|
||||
|
||||
```json
|
||||
{
|
||||
"properties": {
|
||||
"temperature": {"description": "Degrees Celsius.", "title": "Temperature", "type": "number"},
|
||||
"humidity": {"description": "Relative humidity, 0 to 1.", "title": "Humidity", "type": "number"},
|
||||
"conditions": {"title": "Conditions", "type": "string"}
|
||||
},
|
||||
"required": ["temperature", "humidity", "conditions"],
|
||||
"title": "WeatherData",
|
||||
"type": "object"
|
||||
}
|
||||
```
|
||||
|
||||
`structured_content` is the object, field for field:
|
||||
|
||||
```python
|
||||
result.structured_content # {"temperature": 16.2, "humidity": 0.83, "conditions": "Overcast"}
|
||||
```
|
||||
|
||||
And the model is not left out. The SDK serializes the same object to JSON text for `content`:
|
||||
|
||||
```json
|
||||
{
|
||||
"temperature": 16.2,
|
||||
"humidity": 0.83,
|
||||
"conditions": "Overcast"
|
||||
}
|
||||
```
|
||||
|
||||
Notice the `Field(description=...)` on `temperature` and `humidity` landed in the schema. The same `Field` that described your **inputs** describes your outputs.
|
||||
|
||||
!!! info
|
||||
If you've used FastAPI's `response_model`, you already know this: a Pydantic model as the declared
|
||||
response, serialized and documented for you. The only difference is that here the return annotation
|
||||
is the whole declaration.
|
||||
|
||||
## A `TypedDict`
|
||||
|
||||
Not every shape deserves a class. A `TypedDict` produces the same schema:
|
||||
|
||||
```python title="server.py" hl_lines="8"
|
||||
--8<-- "docs_src/structured_output/tutorial003.py"
|
||||
```
|
||||
|
||||
A `TypedDict` is a plain `dict` at runtime, so that is what you build and return. The schema, the validation, and `structured_content` are identical to the `BaseModel` version (minus the descriptions, which `TypedDict` has no place for).
|
||||
|
||||
## A dataclass
|
||||
|
||||
Dataclasses work too, and so does any ordinary class whose attributes have type hints. The SDK builds a Pydantic model out of the annotations behind the scenes.
|
||||
|
||||
```python title="server.py" hl_lines="8-9"
|
||||
--8<-- "docs_src/structured_output/tutorial004.py"
|
||||
```
|
||||
|
||||
Three spellings, one schema. Use whichever your codebase already has.
|
||||
|
||||
## Lists
|
||||
|
||||
A `list[...]` isn't a JSON object either, so it gets the `{"result": ...}` wrapper, with your item type as a `$defs` reference inside it:
|
||||
|
||||
```python title="server.py" hl_lines="15"
|
||||
--8<-- "docs_src/structured_output/tutorial005.py"
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"$defs": {
|
||||
"WeatherData": {
|
||||
"properties": {
|
||||
"temperature": {"title": "Temperature", "type": "number"},
|
||||
"humidity": {"title": "Humidity", "type": "number"},
|
||||
"conditions": {"title": "Conditions", "type": "string"}
|
||||
},
|
||||
"required": ["temperature", "humidity", "conditions"],
|
||||
"title": "WeatherData",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"result": {"items": {"$ref": "#/$defs/WeatherData"}, "title": "Result", "type": "array"}
|
||||
},
|
||||
"required": ["result"],
|
||||
"title": "get_forecastOutput",
|
||||
"type": "object"
|
||||
}
|
||||
```
|
||||
|
||||
Ask for a two-day forecast and `structured_content` is `{"result": [{...}, {...}]}`. `content` becomes **two** `TextContent` blocks, one per item: a list is flattened for the model rather than dumped as one string.
|
||||
|
||||
`tuple[...]`, unions, and `Optional[...]` are wrapped the same way.
|
||||
|
||||
## Dictionaries
|
||||
|
||||
`dict[str, ...]` is the one generic that already *is* a JSON object, so it isn't wrapped:
|
||||
|
||||
```python title="server.py" hl_lines="9"
|
||||
--8<-- "docs_src/structured_output/tutorial006.py"
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"additionalProperties": {"type": "number"},
|
||||
"title": "get_temperaturesDictOutput",
|
||||
"type": "object"
|
||||
}
|
||||
```
|
||||
|
||||
```python
|
||||
result.structured_content # {"London": 16.2, "Reykjavik": 4.4}
|
||||
```
|
||||
|
||||
The keys must be `str`. A `dict[int, float]` can't be a JSON object, so it falls back to the `{"result": ...}` wrapper.
|
||||
|
||||
## Validation
|
||||
|
||||
`output_schema` is not documentation. Whatever your function returns is **validated against it** before it leaves the server.
|
||||
|
||||
You don't notice while you build the value by hand: Pydantic already made sure your `WeatherData` was a `WeatherData`. You notice the day the data comes from somewhere you don't control:
|
||||
|
||||
```python title="server.py" hl_lines="9 21"
|
||||
--8<-- "docs_src/structured_output/tutorial007.py"
|
||||
```
|
||||
|
||||
The annotation promises `WeatherData`. The upstream response stopped sending `humidity`.
|
||||
|
||||
!!! check
|
||||
Call `get_weather` and it does not quietly hand the client a half-empty object. The call fails,
|
||||
and the first lines of the error name the field:
|
||||
|
||||
```text
|
||||
Error executing tool get_weather: 1 validation error for WeatherData
|
||||
humidity
|
||||
Field required [type=missing, input_value={'temperature': 16.2, 'conditions': 'Overcast'}, input_type=dict]
|
||||
```
|
||||
|
||||
That text comes back as the tool result with `is_error=True`, so the model knows the call failed
|
||||
instead of confidently reading weather that isn't there.
|
||||
|
||||
Returning a plain `dict` from a `-> WeatherData` tool is fine, by the way. That's exactly what `json.loads` produced. Validation is on the value, not on the Python type.
|
||||
|
||||
## Opting out
|
||||
|
||||
Sometimes the return annotation is for your type checker, not for the protocol. Pass `structured_output=False` and the tool is text-only:
|
||||
|
||||
```python title="server.py" hl_lines="6"
|
||||
--8<-- "docs_src/structured_output/tutorial008.py"
|
||||
```
|
||||
|
||||
No `output_schema`, no wrapping, no validation. `structured_content` is `None` and `content` is the string you returned.
|
||||
|
||||
The opposite, `structured_output=True`, turns the automatic detection into a requirement: a tool whose return type can't produce a schema raises at import time instead of falling back to text.
|
||||
|
||||
## A class without type hints
|
||||
|
||||
There is one way to end up unstructured without asking for it: return a class that has **no annotations on its body**.
|
||||
|
||||
```python title="server.py" hl_lines="6-9"
|
||||
--8<-- "docs_src/structured_output/tutorial009.py"
|
||||
```
|
||||
|
||||
`Station` sets `name` and `online` inside `__init__`, but the *class* declares nothing. The SDK reads class annotations, finds none, and gives up.
|
||||
|
||||
!!! warning
|
||||
It gives up **silently**. `output_schema` is `None`, `structured_content` is `None`, and the text
|
||||
the model reads is the object's `repr`:
|
||||
|
||||
```text
|
||||
"<server.Station object at 0x7f539d75b230>"
|
||||
```
|
||||
|
||||
No error, no warning, a useless tool. Move the annotations onto the class body, or pass
|
||||
`structured_output=True`, which turns this into a hard error the moment the module imports:
|
||||
`Function get_station: return type <class 'server.Station'> is not serializable for structured output`.
|
||||
|
||||
!!! tip
|
||||
Need full control (building the `CallToolResult` yourself, or attaching `_meta` that the
|
||||
application can see but the model can't)? That's **[The low-level Server](../advanced/low-level-server.md)**.
|
||||
|
||||
## Recap
|
||||
|
||||
* The **return type annotation** is the output schema. It's published in `tools/list` as `output_schema`.
|
||||
* Scalars, lists, tuples and unions are wrapped in `{"result": ...}`. Models, `TypedDict`s, dataclasses, annotated classes and `dict[str, ...]` are objects already and stay as they are.
|
||||
* Every result carries `content` (text, for the model) **and** `structured_content` (data, for the application).
|
||||
* What you return is validated against the schema. A mismatch is a tool error, not a corrupt result.
|
||||
* `structured_output=False` opts a tool out. A class without type hints opts out silently; watch for it.
|
||||
|
||||
You now own everything a tool can say back. Next, the second primitive: **[Resources](resources.md)**.
|
||||
Reference in New Issue
Block a user