Messages¶
Every interaction in Ravi is a typed message. Messages form the conversation history that the LLM sees, the tool calls it makes, and the results it receives back.
Message type hierarchy¶
graph LR
BASE["BaseClientMessage\n(role, content, to_dict, from_dict)"]
BASE --> SYS["SystemMessage\nrole=system\ncontent: str"]
BASE --> USR["UserMessage\nrole=user\ncontent: List[MediaType]"]
BASE --> ASS["AssistantMessage\nrole=assistant\ncontent: Optional[List[MediaType]]\n+ tool_calls, reasoning, usage"]
BASE --> TC["ToolCallMessage\nrole=tool_call\nname, arguments: Dict"]
BASE --> TER["ToolExecutionResultMessage\nrole=tool_response\ntool_call_id, name, is_error"]
style BASE fill:#0d2b2b,stroke:#2dd4bf,color:#e2ecff
style SYS fill:#1a1a1a,stroke:#94a3b8,color:#e2ecff
style USR fill:#1a1a2e,stroke:#818cf8,color:#e2ecff
style ASS fill:#2b1a0d,stroke:#fb923c,color:#e2ecff
style TC fill:#3b1a1a,stroke:#f87171,color:#e2ecff
style TER fill:#1a2b1a,stroke:#4ade80,color:#e2ecff A conversation turn¶
sequenceDiagram
autonumber
participant U as User
participant A as Agent
participant L as LLM
participant T as Tool
U->>A: "Search for the latest LLM papers"
Note over A: UserMessage(content=["Search..."])
A->>L: [SystemMessage, UserMessage]
L-->>A: AssistantMessage(tool_calls=[web_search])
Note over A: ToolCallMessage {name, arguments}
A->>T: web_search(query="latest LLM papers")
T-->>A: ToolExecutionResultMessage
A->>L: [SystemMessage, UserMessage, AssistantMessage, ToolResult]
L-->>A: AssistantMessage(content="Here are the papers...")
A-->>U: Final text answer Message types in code¶
SystemMessage¶
from ravi.core.messages import SystemMessage
msg = SystemMessage("You are a precise research assistant.")
# content is a plain string
UserMessage — text and multi-modal¶
from ravi.core.messages import UserMessage, ImageContent, AudioContent
# Text only
msg = UserMessage(content=["What year is it?"])
# Text + image URL
msg = UserMessage(content=[
"What is this chart showing?",
ImageContent(url="https://example.com/chart.png", detail="high"),
])
# Image from bytes (no URL needed)
msg = UserMessage(content=[
ImageContent(data=open("photo.jpg", "rb").read(), media_type="image/jpeg"),
])
# Image from Files API
msg = UserMessage(content=[ImageContent(file_id="file-abc123")])
AssistantMessage — text and tool calls¶
# Text-only reply (no tool calls)
# content = list of text parts, reasoning = CoT text if model supports it
# Tool-call-only reply (content may be None)
for tc in assistant_msg.tool_calls:
print(tc.name) # ✅ correct
print(tc.arguments) # ✅ dict
# NOT tc.function['name'] ❌
ToolCallMessage + ToolExecutionResultMessage¶
from ravi.core.messages import ToolCallMessage, ToolExecutionResultMessage
# Reading a tool call
call = ToolCallMessage(id="call-123", name="web_search", arguments={"query": "LLMs"})
print(call.name) # "web_search"
print(call.arguments) # {"query": "LLMs"}
# Building a result to add back to history
result = ToolExecutionResultMessage(
tool_call_id="call-123",
name="web_search",
content=[{"type": "text", "text": "Here are 10 results..."}],
is_error=False,
)
Multi-modal content types¶
UserMessage.content accepts a List[MediaType] — any mix of these:
graph LR
MT["MediaType\n(Union)"] --> STR["str\nplain text"]
MT --> IC["ImageContent\n{url | file_id | data}\ndetail: low/high/auto"]
MT --> AC["AudioContent\n{data: bytes}\nformat: mp3/wav"]
MT --> VC["VideoContent\n{data: bytes}\nformat: mp4"]
MT --> PIL["PIL.Image.Image\nauto-converted"]
style MT fill:#0d2b2b,stroke:#2dd4bf,color:#e2ecff
style IC fill:#1a1a2e,stroke:#818cf8,color:#e2ecff
style AC fill:#2b1a0d,stroke:#fb923c,color:#e2ecff ImageContent detail levels¶
| Value | Tokens used | Use when |
|---|---|---|
"low" | ~85 | Thumbnails, icons |
"high" | ~1000 | Charts, diagrams, documents |
"original" | Actual size | Maximum detail |
"auto" | Model decides | Default — best balance |
Serialisation¶
All messages support to_dict() / from_dict() for storage and transport.
# Serialise
data = msg.to_dict() # {"role": "user", "content": [...]}
# Deserialise
from ravi.core.messages import UserMessage
msg = UserMessage.from_dict(data)
Stream chunks¶
When an agent streams (run_stream()), it yields these chunk types:
| Chunk | type field | Key attribute |
|---|---|---|
TextDeltaChunk | "text_delta" | .text — incremental token |
ReasoningDeltaChunk | "reasoning_delta" | .text — CoT reasoning token |
CompletionChunk | "completion" | .message — final AssistantMessage |
StructuredOutputChunk | "structured_output" | .result.parsed — validated Pydantic model |
from ravi.core.messages import (
TextDeltaChunk, ReasoningDeltaChunk,
CompletionChunk, StructuredOutputChunk,
)
async for chunk in agent.run_stream("..."):
match chunk.type:
case "text_delta":
print(chunk.text, end="")
case "reasoning_delta":
pass # internal CoT — usually hidden
case "completion":
final = chunk.message
case "structured_output":
result = chunk.result.parsed # Pydantic model instance if valid
Source¶
| File | What it owns |
|---|---|
core/messages/_types.py | ImageContent, AudioContent, VideoContent, MediaType, all StreamChunk subclasses |
core/messages/client_messages.py | SystemMessage, UserMessage, AssistantMessage, ToolCallMessage, ToolExecutionResultMessage |