Skip to main content
Every conversation in infy is a list of messages, and every provider speaks the same ChatModel protocol. Four message dataclasses carry the content, and one protocol defines the six methods every model implements. This page covers both.

The message types

infy has four message types, defined as plain dataclasses in infy/messages.py. There is no deep class hierarchy: each type is a small dataclass with a type property and a text property. The union Message = HumanMessage | AIMessage | SystemMessage | ToolMessage is what every model method accepts as a list.

HumanMessage

HumanMessage holds user input. Its content is either a string or a list of content blocks (TextBlock, ImageBlock, ToolCallBlock, ToolResultBlock). The name and id fields are optional.
The text property always returns a plain string. If content is a list of blocks, it concatenates the text of each block, so you can read .text regardless of how the message was built.

AIMessage

AIMessage is what a model returns. It carries the generated content, plus tool calls, usage metadata, and response metadata.
has_tool_calls is a convenience property: it returns True when tool_calls is non-empty. Use it to decide whether to run tools before continuing the loop.
Each entry in tool_calls is a ToolCall with name, args (a dict), and id. Usage is a UsageMetadata with input_tokens, output_tokens, and total_tokens (the total is computed automatically when not supplied).

SystemMessage

SystemMessage holds a system instruction. It has content and an optional name, plus the same text property.

ToolMessage

ToolMessage reports the result of running a tool back to the model. Unlike the other three, its content is always a string, and it must reference the tool_call_id it answers.
The optional artifact field lets you attach a richer object (for example a dataframe) alongside the string content, and status records whether the tool succeeded.

The ChatModel protocol

ChatModel in infy/models.py is a runtime_checkable Protocol. It is the entire contract for a chat model: one model_name attribute and six methods. Every provider (OpenAI, Anthropic, Gemini/Vertex, Ollama) implements it, so models are interchangeable in chains, agents, and graphs.

generate and agenerate

generate runs one completion and returns an AIMessage. agenerate is the async equivalent, awaitable and identical in signature.
Both accept the same keyword arguments: tools, tool_choice, temperature, max_tokens, and any extra provider kwargs.

stream and astream

stream yields AIMessageChunk objects as tokens arrive. astream is the async generator form, iterated with async for.
AIMessageChunk is additive: you can fold a stream into a single chunk with +, then call materialize() to get a complete AIMessage (with parsed tool calls and summed usage).
astream is declared as a plain def that returns an AsyncIterator, not as an async def. You iterate its return value with async for; you do not await the call itself.

bind_tools

bind_tools returns a BoundChatModel with tools pre-attached, so you do not pass tools= on every call. Tools are ToolSchema objects (name, description, parameters).
BoundChatModel exposes the same generate, agenerate, stream, and astream methods, forwarding the bound tools on each call. You can call bind_tools again to replace them, or with_structured_output on top.

with_structured_output

with_structured_output returns a StructuredOutputModel that instructs the model to reply as JSON and parses the reply for you. It accepts two kinds of schema:
  • A pydantic model class: the reply is validated with model_validate_json, and the validated instance is returned. This requires pydantic (pip install infy[pydantic]).
  • A plain dict (JSON schema): the reply is parsed by the Rust JsonParser and a dict is returned, with no validation.
The schema instruction is built once and cached, so it is not re-serialized on every call. StructuredOutputModel also supports agenerate, and its stream/astream yield partial dicts as JSON arrives (pydantic validation applies on generate/agenerate, not mid-stream).
StructuredOutputModel composes. Call bind_tools on it, or chain it with the | operator into a runnable, and it still parses the final output.

Interchangeable by design

Because every provider satisfies the same protocol, swapping models is a one-line change. The messages you build, the tools you bind, and the structured schemas you attach all stay the same.

Providers

The OpenAI, Anthropic, Gemini, and Ollama classes and how to configure them.

Runnables and chains

Compose models, parsers, and functions with the | operator.

Agents

Build tool-calling agents with create_agent.

Governance

Add deny-by-default policy, approval, and audit around tool calls.