Overview

3 Working with LLMs

The chapter expands the agent framework by adding the LLM side of the system, complementing the tools introduced earlier. It explains why a common abstraction is needed to hide provider-specific differences and presents a base class that standardizes the main ways agents interact with models: simple text completion, chat-based conversation, continuing a chat after tool execution, and structured output generation. To support these interactions, it introduces lightweight data models for completion results, chat messages, and message roles, using Pydantic and an async-first design to keep the interface consistent and flexible.

A major focus is the design of structured communication with models. Completion is treated as a straightforward prompt-and-response flow, while chat is modeled as a sequence of role-tagged messages that can also carry tool call requests. The chapter shows how tool results are converted back into chat messages so the model can synthesize a final answer, making the tool-calling loop explicit and reusable. It also motivates structured output as a safer alternative to free-form responses, since a model can be guided to return data in a fixed schema that downstream code can validate and consume reliably.

The second half of the chapter turns the abstraction into a working implementation by building an Ollama-based LLM class. This subclass connects to a running Ollama service and uses its async client to support completion, chat, tool-result continuation, and schema-based structured output, while translating between the framework’s own data types and Ollama’s message and tool formats. The chapter closes with an end-to-end demonstration using the Hailstone tool: the model requests the tool, the tool is executed, the result is sent back, and the model produces a final natural-language answer. This completes the tool-calling workflow and prepares the way for building full agents.

Having added tools to our LLM agent framework, the focus of this chapter is to add the other main component of LLM agents—their backbone LLM—to our framework. We’ll specify the interface that all future LLMs must conform to through the BaseLLM class and implement the OllamaLLM subclass to enable the use of any of the open-source LLMs supported by Ollama.
Our LLM agent with its backbone LLM and equipped tools. The backbone LLM is the Qwen3-7b model from the OllamaLLM class that we’ll implement later in this chapter.
Supporting text completion with complete().
Supporting chat interactions with LLMs via chat().
The UML class diagrams for BaseLLM and new data structures.
An example of a CompleteResult object that is returned from an invocation of complete().
Example ChatMessage and ChatRole objects within a chat() invocation.
The familiar tool-calling process, now shown within a chat() invocation.
The structured output LLM interaction. An instruction prompt and the desired output format, mdl, are passed as the inputs to structured_output(). The LLM generates a response that can then be used to create an instance of mdl, a subclass of pydantic.BaseModel.
Integrating with the Ollama LLM inference framework. The OllamaLLM connects to a running Ollama server, often running locally on your machine, to work with any of the supported open-source LLMs.
UML class diagram for OllamaLLM.
Integrating with Ollama’s text completion interface. A complete() invocation of OllamaLLM invokes the generate() method of the Ollama library and returns an Ollama data type from which we derive the final return type, CompleteResult.
Three utility functions for converting between llm-agents-from-scratch data types and corresponding Ollama equivalent data types.
Integrating with Ollama’s structured output interface, which is facilitated by their chat interface. A structured_output() invocation of OllamaLLM invokes the ollama.chat() but not before creating the Ollama message data object. The returned result is an ollama.ChatResponse from which we extract the JSON payload corresponding to mdl that we then use to validate and create the final structured output object.
Integrating with Ollama’s chat interface to implement chat(). The user’s input and chat history are converted to Ollama messages, and tools are converted to Ollama tools. Next, ollama.chat() is invoked and its response is then converted back to a ChatMessage. This ChatMessage is returned along with another one derived from the user’s input.
Integrating with Ollama’s chat interface to implement continue_chat_with_tool_results(). The tool-call results and chat history are converted to Ollama messages, and tools are converted to Ollama tools. Next, ollama.chat() is invoked and its response is then converted back to a ChatMessage. This ChatMessage is returned along with the list of ChatMessage objects derived from the tool-call results.

Summary

  • APIs provided by LLM providers typically support two modes of interaction with LLMs: one for text completion and another for conversational dialogue.
  • Our BaseLLM class supports text completion via the complete() method, and the conversational mode of interaction through our chat() method.
  • One important use case for LLMs is structured output, where we instruct an LLM to produce an output that conforms to a specified data format, often described in JSON.
  • Structured outputs with our BaseLLM class, can be performed via the structured_output() method.
  • Integrations into LLM providers and their companion Python SDKs can be added to our framework by subclassing BaseLLM.
  • The OllamaLLM integration implemented in this chapter allows for the use of any open-source LLMs supported by Ollama in our framework.

FAQ

What is the purpose of the BaseLLM class in this chapter?The BaseLLM class defines a standard, flexible interface for working with LLMs in the framework so different providers and libraries can be accessed through a common API.
Which LLM interaction modes does BaseLLM support?BaseLLM supports text completion through complete(), conversational chat through chat(), continuing a chat with tool results through continue_chat_with_tool_results(), and structured output generation through structured_output().
Why are CompleteResult and ChatMessage needed?These data structures standardize LLM interactions. CompleteResult stores a prompt and its generated response, while ChatMessage represents chat-based messages with roles and optional tool calls.
What is ChatRole and why is it implemented as an enum?ChatRole is an enum that limits message senders to a finite set of valid roles: SYSTEM, USER, ASSISTANT, and TOOL. Using an enum helps enforce consistent message roles across the framework.
How does tool calling work in a chat interaction?The LLM returns an ASSISTANT message containing one or more ToolCall objects. The framework executes those tools, collects the ToolCallResult objects, and sends them back to the LLM for synthesis.
What does continue_chat_with_tool_results() do?It is a convenience method that converts ToolCallResult objects into ChatMessage objects with the TOOL role, then continues the chat so the LLM can respond to the tool outputs.
Why is structured_output() useful?It lets the LLM return responses in a predefined schema, usually JSON, which makes downstream parsing and validation much more reliable than working with free-form text.
How is structured_output() typed in this framework?It uses a generic type bound to pydantic.BaseModel, so callers provide a model class and receive an instance of that model as the output.
What is OllamaLLM and what does it add to BaseLLM?OllamaLLM is a BaseLLM subclass that integrates with Ollama’s async client. It adds a model selection attribute and a client connection to a running Ollama service, enabling completion, chat, tool calls, and structured outputs with open-source models.
How does the chapter demonstrate a full tool-calling workflow?It uses the Hailstone tool with OllamaLLM to first request a tool call, then executes the tool, sends the result back using continue_chat_with_tool_results(), and finally receives the LLM’s synthesized answer.

pro $24.99 per month

  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose one free eBook per month to keep
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime

lite $19.99 per month

  • access to all Manning books, including MEAPs!

team

5, 10 or 20 seats+ for your team - learn more


choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Build a Multi-Agent System (from Scratch) ebook for free
choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Build a Multi-Agent System (from Scratch) ebook for free