183 lines
5.1 KiB
Python
183 lines
5.1 KiB
Python
"""TypedDict spec for the JSON-safe wire form of Part, Message, and Response.
|
|
|
|
These are the exact shapes returned by ``Part.to_dict()``,
|
|
``Message.to_dict()``, and ``Response.to_dict()`` — and accepted by the
|
|
matching ``from_dict`` classmethods. They are the canonical wire format;
|
|
use them to annotate any code that reads or writes serialized llm data.
|
|
|
|
Example::
|
|
|
|
from llm.serialization import MessageDict
|
|
|
|
def save_messages(conn, messages: list[MessageDict]) -> None:
|
|
for m in messages:
|
|
conn.execute(
|
|
"INSERT INTO messages(role, parts_json) VALUES (?, ?)",
|
|
(m["role"], json.dumps(m["parts"])),
|
|
)
|
|
|
|
Or pair with Pydantic's TypeAdapter for runtime validation::
|
|
|
|
from pydantic import TypeAdapter
|
|
from llm.serialization import MessageDict
|
|
|
|
msg = TypeAdapter(MessageDict).validate_python(incoming_dict)
|
|
|
|
Or export JSON Schema for cross-language consumers::
|
|
|
|
schema = TypeAdapter(MessageDict).json_schema()
|
|
|
|
The TypedDicts are erased at runtime — zero overhead. ``NotRequired``
|
|
keys may be absent from a serialized payload; required keys must
|
|
always be present.
|
|
"""
|
|
|
|
from typing import Any, Dict, List, Literal, Union
|
|
|
|
# NotRequired moved to typing in 3.11; use typing_extensions for 3.10
|
|
# support. typing_extensions is a transitive dep via pydantic.
|
|
from typing_extensions import NotRequired, TypedDict
|
|
|
|
__all__ = [
|
|
"AttachmentDict",
|
|
"AttachmentPartDict",
|
|
"MessageDict",
|
|
"PartDict",
|
|
"PromptDict",
|
|
"ReasoningPartDict",
|
|
"ResponseDict",
|
|
"TextPartDict",
|
|
"ToolCallPartDict",
|
|
"ToolResultPartDict",
|
|
"UsageDict",
|
|
]
|
|
|
|
|
|
# ---- Attachment payload (nested inside AttachmentPartDict + tool results) ----
|
|
|
|
|
|
class AttachmentDict(TypedDict, total=False):
|
|
"""Nested attachment payload. All fields optional — an Attachment
|
|
may carry a type, a url, a path, and/or base64-encoded content.
|
|
"""
|
|
|
|
type: str
|
|
url: str
|
|
path: str
|
|
# base64-encoded bytes when the attachment was constructed with raw
|
|
# content= bytes.
|
|
content: str
|
|
|
|
|
|
# ---- Per-Part TypedDicts (discriminated by the `type` field) -----------------
|
|
|
|
|
|
class TextPartDict(TypedDict):
|
|
type: Literal["text"]
|
|
text: str
|
|
provider_metadata: NotRequired[Dict[str, Any]]
|
|
|
|
|
|
class ReasoningPartDict(TypedDict):
|
|
type: Literal["reasoning"]
|
|
text: str
|
|
# `redacted=True` with `text=""` is the marker for opaque
|
|
# reasoning (OpenAI GPT-5, Gemini without thoughts). The token
|
|
# total lives on response usage, not on the Part.
|
|
redacted: NotRequired[bool]
|
|
provider_metadata: NotRequired[Dict[str, Any]]
|
|
|
|
|
|
class ToolCallPartDict(TypedDict):
|
|
type: Literal["tool_call"]
|
|
name: str
|
|
arguments: Dict[str, Any]
|
|
tool_call_id: NotRequired[str]
|
|
# True for provider-executed calls (Anthropic web search, Gemini code
|
|
# execution). Adapters use this to restore provider-side blocks on
|
|
# the next turn.
|
|
server_executed: NotRequired[bool]
|
|
provider_metadata: NotRequired[Dict[str, Any]]
|
|
|
|
|
|
class ToolResultPartDict(TypedDict):
|
|
type: Literal["tool_result"]
|
|
name: str
|
|
output: str
|
|
tool_call_id: NotRequired[str]
|
|
server_executed: NotRequired[bool]
|
|
exception: NotRequired[str]
|
|
attachments: NotRequired[List[AttachmentDict]]
|
|
provider_metadata: NotRequired[Dict[str, Any]]
|
|
|
|
|
|
class AttachmentPartDict(TypedDict):
|
|
type: Literal["attachment"]
|
|
attachment: NotRequired[AttachmentDict]
|
|
provider_metadata: NotRequired[Dict[str, Any]]
|
|
|
|
|
|
PartDict = Union[
|
|
TextPartDict,
|
|
ReasoningPartDict,
|
|
ToolCallPartDict,
|
|
ToolResultPartDict,
|
|
AttachmentPartDict,
|
|
]
|
|
"""Discriminated union of Part dict shapes. Use with
|
|
``pydantic.TypeAdapter(PartDict)`` to validate / dispatch by ``type``.
|
|
"""
|
|
|
|
|
|
# ---- Message ----------------------------------------------------------------
|
|
|
|
|
|
class MessageDict(TypedDict):
|
|
"""JSON-safe form of ``llm.Message``.
|
|
|
|
``role`` is one of "user", "assistant", "system", "tool" in practice
|
|
— typed as ``str`` here to leave room for provider-specific values.
|
|
"""
|
|
|
|
role: str
|
|
parts: List[PartDict]
|
|
provider_metadata: NotRequired[Dict[str, Any]]
|
|
|
|
|
|
# ---- Response + nested shapes -----------------------------------------------
|
|
|
|
|
|
class PromptDict(TypedDict):
|
|
"""The ``prompt`` sub-dict of ``Response.to_dict()`` — captures the
|
|
full input chain that was sent for this turn plus any options that
|
|
apply."""
|
|
|
|
messages: List[MessageDict]
|
|
options: NotRequired[Dict[str, Any]]
|
|
system: NotRequired[str]
|
|
|
|
|
|
class UsageDict(TypedDict, total=False):
|
|
"""Optional usage block on ``ResponseDict``. All fields optional;
|
|
providers vary in which they report."""
|
|
|
|
input: int
|
|
output: int
|
|
details: Dict[str, Any]
|
|
|
|
|
|
class ResponseDict(TypedDict):
|
|
"""JSON-safe form of ``llm.Response`` — everything needed for
|
|
``Response.from_dict`` to rehydrate and ``response.reply()`` to
|
|
continue a conversation across a process boundary.
|
|
"""
|
|
|
|
model: str
|
|
prompt: PromptDict
|
|
messages: List[MessageDict]
|
|
# Audit fields — present on a freshly-serialized response, optional
|
|
# on hand-constructed ones.
|
|
id: NotRequired[str]
|
|
usage: NotRequired[UsageDict]
|
|
datetime_utc: NotRequired[str]
|