Skip to main content
Tier 3 is the Qlaw chat (“God’s Eye View”) plus its shared tool set. Both the chat agent and the Enrich lens run on dspy.ReActV2 — DSPy 3.3.0’s native tool-calling agent: parallel tool calls, a reserved submit tool, multi-turn replay of prior calls, and prompt-cache reuse.

Design

  • Stateful dspy.History. QlawChat retains the live GraphSession, the ReActV2 agent, and its dspy.History across calls. The optional client history list is accepted only to bootstrap a fresh instance for HTTP compatibility. Later turns use the instance-owned history directly.
  • Persistent tools. Tools are built once as closures over the chat’s GraphSession (a mutable holder for the current GraphState). dspy.Tool infers name, description, and schema from the function, and ReActV2 executes plain callables directly, so the agent never has to serialize the whole graph into a tool argument.
  • The invariant holds. The LM still never mutates the graph. run_lens and add_node orchestrate, but every wiring change goes through GraphState.apply() (deterministic expansion). A tool error is captured by ReActV2 as a tool result, so the agent can recover.
  • One dspy.ReActV2 per chat session. reset() creates a new agent and clears history. Calls are serialized because the session and tool-bound graph are mutable. Zero-shot is the supported mode.

Tools

make_chat_tools(session, lenses) in qlaw/chat.py: ChatNode (in qlaw/graph.py) is the chat’s own child DTO. Unlike the lens output contracts (SubNode, Concept, …), its type spans the full NodeType taxonomy. It participates in NodeBatch.children, so apply() needs no special-casing. qlaw/tools.py keeps only graph-agnostic tools. search remains a NotImplementedError stub — inject a real implementation via DI: GroundingEnricher(search_tool=...).

Mapping from the current app

Session storage on the server

ChatSessionStore is a bounded, per-session_id, independently locked map of QlawChat instances. The browser stores one opaque id per tab in sessionStorage; the server maps it to a live agent. Missing or expired entries start a fresh conversation rather than sharing another client’s history.

Streaming to the client

dspy.streamify(chat, status_message_provider=ChatStatusProvider()) wraps the agent and returns an async generator of events, ending with the final dspy.Prediction. ReActV2 emits its final answer as submit tool-call arguments, so token-level streaming of answer does not apply. ChatStatusProvider surfaces tool activity as status events, and the complete answer arrives in the done frame. The done frame carries the final graph. Chat tools expand the graph during the agent run, so the client must adopt done.graph — the request graph may be stale by the time the answer arrives. See API and streaming for the SSE envelope.
Last modified on August 9, 2026