From e6e058c6881ef17243e1f4dc6d1859ae2388ef11 Mon Sep 17 00:00:00 2001 From: Seth Erickson Date: Thu, 21 May 2026 21:22:56 +0000 Subject: [PATCH 01/17] draft agent guide --- content/blog/2026-05-18-agent-guide/index.md | 308 ++++++++++++++++++ .../tool_call_simple.mmd | 11 + .../tool_call_simple.png | Bin 0 -> 24788 bytes 3 files changed, 319 insertions(+) create mode 100644 content/blog/2026-05-18-agent-guide/index.md create mode 100644 content/blog/2026-05-18-agent-guide/tool_call_simple.mmd create mode 100644 content/blog/2026-05-18-agent-guide/tool_call_simple.png diff --git a/content/blog/2026-05-18-agent-guide/index.md b/content/blog/2026-05-18-agent-guide/index.md new file mode 100644 index 0000000..0e6630c --- /dev/null +++ b/content/blog/2026-05-18-agent-guide/index.md @@ -0,0 +1,308 @@ +--- +title: What is an AI Agent? +--- + +Simon Willison, a prolific software developer and writer on AI-assisted coding, +describes [AI agents](https://simonwillison.net/tags/ai-agents/) as "LLMs +calling tools in a loop to achieve a goal." It's great definition, particularly +if you are already familiar with the technical sense of its terms. If you +aren't, you might be wondering: What is a "tool", and what does it mean for an +LLM to "call" one? In this post, want to add some technical specificity to +Willison's definition by showing how to build a very simple agent in which an +LLM calls tools in a loop. + +Real-world agents (for example, coding agents) can be quite complicated pieces +of software. However, the core features of an agent are surprisingly simple to +implement. You might be surprised at how little code it takes to build an agent +that can do useful work using plain Python! Because our goal is pedagogical, not +practical, we'll use plain Python as much as possible. If you're goal is to +build a sophisticated agent with little effort, you should probably use one the +many SDKs for that purpose -- or ask your coding agent to! + +To follow this guide, you need an environment to run Python scripts and API +access to a large language model (LLM) provider. We will be using the DREAM +Lab’s AI gateway as our LLM provider, but other model providers should also +work. + +## Using LLMs through APIs + +Most computer programs, like agents, that *use* LLMs do so through web-based +APIs. Instead of running models directly, the program makes HTTP “requests” to +an LLM model provider over the web. Agents talk to model providers the same way +your web browser talks to web servers: using HTTP. An advantage of this approach +is it makes the software easier to write and run. We don’t need specialized +hardware for running models, and we don’t need to complex machine learning +frameworks (like PyTorch). Instead, we just need an HTTP client, like Python’s +[requests](https://pypi.org/project/requests/) library. + +Model providers (like Open AI, Anthropic, Google, AWS, etc.) expect programs to +use specific APIs to interact with their LLMs. Open AI’s [Chat Completion +API](https://developers.openai.com/api/reference/resources/chat), is one of the +oldest and most widely supported APIs for interacting with LLMs–and it’s the API +we’ll use here. + +To make HTTP requests to an LLM provider using the Chat Completion API, you need +three things: the API’s base URL, the name of the model you want use, and an API +access key. + +Here’s an example Python function that uses the `requests` library to make HTTP +requests to a model provider using the Chat Completion API. + +```python +import requests +import os + +def call_llm(prompt, api_base_url, api_model, api_key): + # full url for the chat completion request + request_url = f"{api_base_url}/v1/chat/completions" + + # http headers are used to authenticate our request + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + } + + # the request includes the model name and the prompt. + data = {"model": api_model, "messages": [{"role": "user", "content": prompt}]} + + # call the API + resp = requests.post(request_url, headers=headers, json=data, timeout=60) + + # raise server error if we got one + try: + resp.raise_for_status() + except requests.exceptions.HTTPError as e: + raise e + + # The Chat Completion json response can have multiple completions + # ("choices") for the same prompt, if requested. Here we only request a + # single completion, so return the first 'message' in 'choices' + return resp.json()["choices"][0]["message"] +``` + +The function takes four arguments: the prompt to include in the request (e.g., +"What is the weather in Paris?"), the base URL for the model providers API, the name +of the LLM we want to use, and a personal key for authenticating the request. + +To use this function with the DREAM Lab's AI Gateway, we could write a script like +the following. + +```python +import os + +prompt = "What is the weather in Paris?" +api_base_url = "https://litellm.dreamlab.ucsb.edu" +api_model = "gemini-3-flash-preview" +api_key = os.getenv("LLM_API_KEY") # key stored as environment variable + +msg = call_llm(prompt, api_base_url, api_model, api_key) + +# the LLM's generated text is included as 'content' +print(msg["content"]) +``` + +When I ran this script, I received the response: + +```md +As of right now in Paris, France: + +* **Temperature:** 13°C (55°F) +* **Conditions:** Clear skies and sunny. +* **Wind:** 11 km/h (7 mph) +* **Humidity:** 61% + +**Forecast for the rest of today:** +It is expected to stay clear and cool throughout the evening, with temperatures dropping to a low of about 7°C (45°F) overnight. + +**Tomorrow's Outlook:** +Similar weather is expected tomorrow, with mostly sunny skies and a high of 14°C (57°F). +``` + +The response you get back will likely be inaccurate (it was for me). In fact, +running the script multiple times will likely result in completely different +descriptions! That's because the model doesn't actually know what the current +weather in Paris is, so it makes up. It "hallucinates" a plausible description. + +## Use 'Tools' to Avoid Hallucinations + +One way to avoid hallucinations in LLM API responses is by providing the model with +"tools" that it can use. Tools provide LLMs with ways to access high quality +information or perform tasks, and to avoid having to fill-in missing details +with statistically likely text. To illustrate, we first need to modify our +`call_llm` function to include tools in our request. + +```python +# update call_llm to include optional 'tool' definitions +def call_llm(prompt, api_base_url, api_model, api_key, tools = None): + # full url for the chat completion request + request_url = f"{api_base_url}/v1/chat/completions" + + # http headers are used to authenticate our request + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + } + + # the request includes the model name and the prompt. + data = {"model": api_model, "messages": [{"role": "user", "content": prompt}]} + + # include tool definition if included + if tools: + data["tools"] = tools + + # call the API + resp = requests.post(request_url, headers=headers, json=data, timeout=60) + + # raise server error if we got one + try: + resp.raise_for_status() + except requests.exceptions.HTTPError as e: + raise e + + # return the first 'message' in 'choices' + return resp.json()["choices"][0]["message"] +``` + +Now let's define a `get_weather` tool, and include it with our request. How does +the LLM API response change? + +```python +# get_weather_schema is metadata describing the `get_weather` tool +# to include in the llm requests. It describes what the function +# does and its required parameters. This structure conforms with the +# Chat Completion API (`ChatCompletionTool`). +get_weather_schema = { + "type": "function", + "function": { + "name": "get_weather", + "description": "Get the current weather in a given location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "A place name (e.g., Paris)", + } + }, + "required": ["location"], + }, + }, +} + +# same prompt, api_base_url, api_model, and api_key as before +msg = call_llm(prompt, api_base_url, api_model, api_key, tools = [get_weather_schema]) +print(msg["content"]) # "None" +print(msg["too_calls"][0][function]) # {'arguments': '{"location": "Paris"}', 'name': 'get_weather'} +``` + +The response doesn't include text in the `content` key like before; instead, +what we get is a list of `tool_calls`, each with a `function` value like this: + +```json +{"arguments": '{"location": "Paris"}', "name": 'get_weather'}` +``` + +What is happening here? Instead of generating direct response to the prompt, the +LLM API has responded with a `tool_call`. As the name suggests, "tool calls" are +how the API calls (or invokes) the tools we included in the request. Our request +included the `get_weather` tool definition, and the response includes a tool +call to run the `get_weather` function with arguments `{"location": "Paris"}`. +The expectation is that we will run `get_weather()` and provide the LLM with the output +so that it can provided a response grounded in facts. + +To achieve this, we need to create a `get_weather()` function that we can call. +We'll use https://wttr.in as it provides a free, simple API that is sufficient +for our purposes: + +```py +# get_weather is our implementation of the function described in +# get_weather_schema. It gets the current weather for a given location +# using a weather API (wttr.in) +def get_weather(location: str) -> str: + url = f"https://wttr.in/{location}?format=3" + try: + response = requests.get(url, timeout=10) + response.raise_for_status() + return response.text.strip() + except Exception as e: + return f"Could not get weather for {location}: {e}" + +get_weather("Paris") # "paris: ☁️ +56°F" +``` + +To provide the LLM with the result of the tool call (`"paris: ☁️ +56°F"`), we +need make a new request that includes all the previous messages *plus* a new +message with the tool call output. (That's three messages in total: (1) our +initial prompt, (2) the LLM API's response with the tool call, and (3) the tool +call output). The current version of `call_llm()` is limited because it only +allows us to send the initial prompt. We need to modify it to send a complete +message sequence. + + +```python +# update call_llm to take list of messages instead of a single prompt +def call_llm(api_base_url, api_model, api_key, messages=None, tools=None): + messages = messages or [] + request_url = f"{api_base_url}/v1/chat/completions" + headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} + data = { + "model": model_name, + "messages": messages, + } + if tools: + data["tools"] = tools + + response = requests.post(request_url, headers=headers, json=data, timeout=60) + + try: + response.raise_for_status() + except requests.exceptions.HTTPError as e: + print(f"Server Error: {response.text}") + raise e + + resp = response.json() + return resp["choices"][0]["message"] + +``` + +Here's how the complete sequence with the LLM API: + + +```python +prompt = "What is the weather in Paris?" +messages = [{"role": "user", "content": prompt}] +tools = [get_weather_schema] # previously defined + +# initial request +msg = call_llm(messages=messages, tools=tools) +messages.append(msg) + +# handle tool calls +if "tool_calls" not in msg: + print("expected a tool call, got content:", msg.get("content")) + raise ValueError("No tool calls found in the response") +for call in msg["tool_calls"]: + args = json.loads(call["function"]["arguments"]) + name = call["function"]["name"] + if name != "get_weather": + raise ValueError(f"function name is not 'get_weather', got {name}") + result = get_weather(**args) + new_msg = {"role": "tool", "tool_call_id": call["id"], "content": str(result)} + messages.append(new_msg) + +# final request +msg = call_llm(messages=messages, tools=tools) +print(msg["content"]) # The weather in Paris is currently 56°F and cloudy. +``` + +The sequence of the communication between the user (us) and the various APIs is as follows: + +1. `call_llm()`: initial prompt message + tool definitions (`get_weather(location)`) +2. LLM API responds with tool call: `get_weather("location" = "Paris")` +3. `get_weather()`: request to Weather API: `{"location": "Paris"}` +4. Weather API response: "The weather in Paris is sunny." +5. `call_llm()`: tool call output: "The weather in Paris is sunny." +6. LLM API's final response to the prompt: "The weather in Paris is sunny." + +![tool call flow](tool_call_simple.png) + diff --git a/content/blog/2026-05-18-agent-guide/tool_call_simple.mmd b/content/blog/2026-05-18-agent-guide/tool_call_simple.mmd new file mode 100644 index 0000000..0705632 --- /dev/null +++ b/content/blog/2026-05-18-agent-guide/tool_call_simple.mmd @@ -0,0 +1,11 @@ +sequenceDiagram + participant Weather API + participant User + participant LLM API + + User->>LLM API: (1) Prompt + Tool: get_weather(location) + LLM API->>User: (2) Tool Call: get_weather(location="Paris") + User ->> Weather API: (3) {"location": "Paris"} + Weather API ->> User: (4) "The weather in Paris is sunny." + User->>LLM API: (5) Tool Call Result: "Weather in Paris is sunny." + LLM API->>User: (6) Final Response: "The weather in Paris is sunny." diff --git a/content/blog/2026-05-18-agent-guide/tool_call_simple.png b/content/blog/2026-05-18-agent-guide/tool_call_simple.png new file mode 100644 index 0000000000000000000000000000000000000000..3703249f0b64e001a1e4164fc4ff9396a18526a1 GIT binary patch literal 24788 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 Date: Thu, 21 May 2026 23:19:40 +0000 Subject: [PATCH 02/17] wip --- content/blog/2026-05-18-agent-guide/index.md | 200 ++++++++----------- 1 file changed, 87 insertions(+), 113 deletions(-) diff --git a/content/blog/2026-05-18-agent-guide/index.md b/content/blog/2026-05-18-agent-guide/index.md index 0e6630c..84e568f 100644 --- a/content/blog/2026-05-18-agent-guide/index.md +++ b/content/blog/2026-05-18-agent-guide/index.md @@ -4,20 +4,20 @@ title: What is an AI Agent? Simon Willison, a prolific software developer and writer on AI-assisted coding, describes [AI agents](https://simonwillison.net/tags/ai-agents/) as "LLMs -calling tools in a loop to achieve a goal." It's great definition, particularly -if you are already familiar with the technical sense of its terms. If you -aren't, you might be wondering: What is a "tool", and what does it mean for an -LLM to "call" one? In this post, want to add some technical specificity to -Willison's definition by showing how to build a very simple agent in which an -LLM calls tools in a loop. +calling tools in a loop to achieve a goal". It's a nice definition, particularly +if you are already familiar with the technical sense of the terms. If you +aren't, you might be wondering: What is a "tool", and how does a language model +"call" one? In this post, I want to add some technical specificity to Willison's +definition by showing how to build a very simple agent: we'll build a program in +which an LLM calls tools in a loop to achieve a goal. Real-world agents (for example, coding agents) can be quite complicated pieces of software. However, the core features of an agent are surprisingly simple to implement. You might be surprised at how little code it takes to build an agent -that can do useful work using plain Python! Because our goal is pedagogical, not -practical, we'll use plain Python as much as possible. If you're goal is to -build a sophisticated agent with little effort, you should probably use one the -many SDKs for that purpose -- or ask your coding agent to! +that can do useful work! Because our goal is pedagogical, not practical, we'll +use plain Python as much as possible. If you're goal is to build a sophisticated +agent with little effort, you should probably use one of the many agent SDKs +designed for the purpose -- or just ask your coding agent to! To follow this guide, you need an environment to run Python scripts and API access to a large language model (LLM) provider. We will be using the DREAM @@ -30,7 +30,7 @@ Most computer programs, like agents, that *use* LLMs do so through web-based APIs. Instead of running models directly, the program makes HTTP “requests” to an LLM model provider over the web. Agents talk to model providers the same way your web browser talks to web servers: using HTTP. An advantage of this approach -is it makes the software easier to write and run. We don’t need specialized +is that it makes the software easier to write and run. We don’t need specialized hardware for running models, and we don’t need to complex machine learning frameworks (like PyTorch). Instead, we just need an HTTP client, like Python’s [requests](https://pypi.org/project/requests/) library. @@ -38,64 +38,98 @@ frameworks (like PyTorch). Instead, we just need an HTTP client, like Python’s Model providers (like Open AI, Anthropic, Google, AWS, etc.) expect programs to use specific APIs to interact with their LLMs. Open AI’s [Chat Completion API](https://developers.openai.com/api/reference/resources/chat), is one of the -oldest and most widely supported APIs for interacting with LLMs–and it’s the API +oldest and most widely supported APIs for interacting with LLMs--and it’s the API we’ll use here. To make HTTP requests to an LLM provider using the Chat Completion API, you need three things: the API’s base URL, the name of the model you want use, and an API access key. -Here’s an example Python function that uses the `requests` library to make HTTP -requests to a model provider using the Chat Completion API. +The core of our agent will be Python function that uses the `requests` library +to make HTTP requests to an LLM model provider using the Chat Completion API. ```python import requests import os -def call_llm(prompt, api_base_url, api_model, api_key): - # full url for the chat completion request - request_url = f"{api_base_url}/v1/chat/completions" - - # http headers are used to authenticate our request - headers = { - "Authorization": f"Bearer {api_key}", - "Content-Type": "application/json", +# call_llm makes a request using the Chat Completion API. +def call_llm(messages, api_base_url, api_model, api_key, tools=None): + # http request url and headers + request_url = f"{api_base_url}/v1/chat/completions" + headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} + + # http request body: the data submitted to the API + data = { + "model": api_model, + "messages": messages, } - # the request includes the model name and the prompt. - data = {"model": api_model, "messages": [{"role": "user", "content": prompt}]} - - # call the API - resp = requests.post(request_url, headers=headers, json=data, timeout=60) + # include "tools" only if defined + if tools: + data["tools"] = tools - # raise server error if we got one + # call the API and print server error if we get one + response = requests.post(request_url, headers=headers, json=data, timeout=60) try: - resp.raise_for_status() + response.raise_for_status() except requests.exceptions.HTTPError as e: + print(f"Server Error: {response.text}") raise e - # The Chat Completion json response can have multiple completions - # ("choices") for the same prompt, if requested. Here we only request a - # single completion, so return the first 'message' in 'choices' - return resp.json()["choices"][0]["message"] + # The Chat Completion API supports multiple "choices". + # We only expect one: return the first message in 'choices' + resp = response.json() + return resp["choices"][0]["message"] ``` -The function takes four arguments: the prompt to include in the request (e.g., -"What is the weather in Paris?"), the base URL for the model providers API, the name -of the LLM we want to use, and a personal key for authenticating the request. +The `call_llm()` function takes as input a "messages" list and API parameters: + +- `messages`: a list of chat completion message objects (more on this next). +- `api_base_url`: the URL of our API endpoint (`https://litellm.dreamlab.ucsb.edu`) +- `api_model`: the name of the model we want to use (`gemini-3-flash-preview`) +- `api-key`: a personal API key to authorize our requests. +- `tools`: an *optional* list of tool definition (we'll come back to this). + +The functions output is a new "message" object with the LLM's response. + +## Chat Completion Message Structure + +The Chat Completion API expects a list of message objects representing the conversation history. This structured format allows the model to understand the context of a multi-turn dialogue. Every time you make a request, you send the entire conversation history (the `messages` list) back to the provider so the model remembers what was previously said. -To use this function with the DREAM Lab's AI Gateway, we could write a script like -the following. +Each message in the list is a JSON-like dictionary that must contain specific keys. The most common keys are: + +- **`role`**: Specifies who is sending the message. This can be one of four main roles: + - `system`: Used to set instructions, constraints, or the persona for the assistant (e.g., `"You are a helpful coding assistant."`). System messages are usually placed at the very beginning of the list. + - `user`: Represents messages or queries sent by the human user. + - `assistant`: Represents responses generated by the language model itself. Keeping track of assistant messages is crucial for maintaining a coherent conversation history. + - `tool`: Represents the output returned from a local function or tool call (which we will look at in the next section). +- **`content`**: The actual text content of the message. For most user and system messages, this is a string. For some assistant responses (such as those initiating tool calls) or tool responses, this may be `None` or contain structured data. + +An example of a single user message is: +```python +{"role": "user", "content": "What is the weather in Paris?"} +``` + +By passing a sequence of these objects, you build up a complete transcript of the conversation for the model to reference and build upon. + + +To use this function with the DREAM Lab's AI Gateway, we could write a script +like the following. ```python import os -prompt = "What is the weather in Paris?" api_base_url = "https://litellm.dreamlab.ucsb.edu" api_model = "gemini-3-flash-preview" api_key = os.getenv("LLM_API_KEY") # key stored as environment variable -msg = call_llm(prompt, api_base_url, api_model, api_key) +# messages with initial user prompt +messages = [{ + "role": "user", + "content": "What is the weather in Paris" +}] + +msg = call_llm(messages, api_base_url, api_model, api_key) # the LLM's generated text is included as 'content' print(msg["content"]) @@ -125,46 +159,12 @@ weather in Paris is, so it makes up. It "hallucinates" a plausible description. ## Use 'Tools' to Avoid Hallucinations -One way to avoid hallucinations in LLM API responses is by providing the model with -"tools" that it can use. Tools provide LLMs with ways to access high quality -information or perform tasks, and to avoid having to fill-in missing details -with statistically likely text. To illustrate, we first need to modify our -`call_llm` function to include tools in our request. - -```python -# update call_llm to include optional 'tool' definitions -def call_llm(prompt, api_base_url, api_model, api_key, tools = None): - # full url for the chat completion request - request_url = f"{api_base_url}/v1/chat/completions" - - # http headers are used to authenticate our request - headers = { - "Authorization": f"Bearer {api_key}", - "Content-Type": "application/json", - } - - # the request includes the model name and the prompt. - data = {"model": api_model, "messages": [{"role": "user", "content": prompt}]} - - # include tool definition if included - if tools: - data["tools"] = tools - - # call the API - resp = requests.post(request_url, headers=headers, json=data, timeout=60) - - # raise server error if we got one - try: - resp.raise_for_status() - except requests.exceptions.HTTPError as e: - raise e - - # return the first 'message' in 'choices' - return resp.json()["choices"][0]["message"] -``` - -Now let's define a `get_weather` tool, and include it with our request. How does -the LLM API response change? +One way to avoid hallucinations in LLM API responses is by providing the model +with "tools" that it can use. Tools provide LLMs with ways to access high +quality information or perform tasks, and to avoid having to fill-in missing +details with statistically likely text. To illustrate, we first need to define a +`get_weather` tool, and include it with our request. How does the LLM API +response change? ```python # get_weather_schema is metadata describing the `get_weather` tool @@ -190,7 +190,10 @@ get_weather_schema = { } # same prompt, api_base_url, api_model, and api_key as before -msg = call_llm(prompt, api_base_url, api_model, api_key, tools = [get_weather_schema]) +msg = call_llm(api_base_url, api_model, api_key, + messages = messages, + tools = [get_weather_schema] +) print(msg["content"]) # "None" print(msg["too_calls"][0][function]) # {'arguments': '{"location": "Paris"}', 'name': 'get_weather'} ``` @@ -207,8 +210,8 @@ LLM API has responded with a `tool_call`. As the name suggests, "tool calls" are how the API calls (or invokes) the tools we included in the request. Our request included the `get_weather` tool definition, and the response includes a tool call to run the `get_weather` function with arguments `{"location": "Paris"}`. -The expectation is that we will run `get_weather()` and provide the LLM with the output -so that it can provided a response grounded in facts. +The expectation is that we will run `get_weather()` and provide the LLM with the +output so that it can provided a response grounded in facts. To achieve this, we need to create a `get_weather()` function that we can call. We'll use https://wttr.in as it provides a free, simple API that is sufficient @@ -234,36 +237,7 @@ To provide the LLM with the result of the tool call (`"paris: ☁️ +56°F"`), need make a new request that includes all the previous messages *plus* a new message with the tool call output. (That's three messages in total: (1) our initial prompt, (2) the LLM API's response with the tool call, and (3) the tool -call output). The current version of `call_llm()` is limited because it only -allows us to send the initial prompt. We need to modify it to send a complete -message sequence. - - -```python -# update call_llm to take list of messages instead of a single prompt -def call_llm(api_base_url, api_model, api_key, messages=None, tools=None): - messages = messages or [] - request_url = f"{api_base_url}/v1/chat/completions" - headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} - data = { - "model": model_name, - "messages": messages, - } - if tools: - data["tools"] = tools - - response = requests.post(request_url, headers=headers, json=data, timeout=60) - - try: - response.raise_for_status() - except requests.exceptions.HTTPError as e: - print(f"Server Error: {response.text}") - raise e - - resp = response.json() - return resp["choices"][0]["message"] - -``` +call output). Here's how the complete sequence with the LLM API: From dd927b56f294797bce593106e7d40584fb9628fc Mon Sep 17 00:00:00 2001 From: Seth Erickson Date: Fri, 22 May 2026 19:09:47 +0000 Subject: [PATCH 03/17] wip --- content/blog/2026-05-18-agent-guide/index.md | 138 ++++++++++++------- 1 file changed, 85 insertions(+), 53 deletions(-) diff --git a/content/blog/2026-05-18-agent-guide/index.md b/content/blog/2026-05-18-agent-guide/index.md index 84e568f..5fd6d4b 100644 --- a/content/blog/2026-05-18-agent-guide/index.md +++ b/content/blog/2026-05-18-agent-guide/index.md @@ -45,18 +45,34 @@ To make HTTP requests to an LLM provider using the Chat Completion API, you need three things: the API’s base URL, the name of the model you want use, and an API access key. -The core of our agent will be Python function that uses the `requests` library -to make HTTP requests to an LLM model provider using the Chat Completion API. +The core of our agent is a Python function, `call_llm()`, that uses the +`requests` library to make HTTP requests to an LLM model provider using the Chat +Completion API. ```python import requests import os -# call_llm makes a request using the Chat Completion API. def call_llm(messages, api_base_url, api_model, api_key, tools=None): + """Makes a request using the Chat Completion API. + + Args: + messages (list): A list of "message" objects, described in more detail below. + api_base_url (str): The URL of our API endpoint (ex: `https://litellm.dreamlab.ucsb.edu`). + api_model (str): Name of the model to use (ex: `gemini-3-flash-preview`). + api_key (str): An API key to authorize the request. + tools (list, optional): An optional list of tool definitions. + + Returns: + dict: The first message object in the response choices. + """ + # http request url and headers request_url = f"{api_base_url}/v1/chat/completions" - headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" + } # http request body: the data submitted to the API data = { @@ -69,73 +85,91 @@ def call_llm(messages, api_base_url, api_model, api_key, tools=None): data["tools"] = tools # call the API and print server error if we get one - response = requests.post(request_url, headers=headers, json=data, timeout=60) - try: - response.raise_for_status() - except requests.exceptions.HTTPError as e: - print(f"Server Error: {response.text}") - raise e - + response = requests.post(request_url, headers=headers, json=data) + response.raise_for_status() # raise an error if http response status != 200 + # The Chat Completion API supports multiple "choices". # We only expect one: return the first message in 'choices' resp = response.json() return resp["choices"][0]["message"] ``` -The `call_llm()` function takes as input a "messages" list and API parameters: - -- `messages`: a list of chat completion message objects (more on this next). -- `api_base_url`: the URL of our API endpoint (`https://litellm.dreamlab.ucsb.edu`) -- `api_model`: the name of the model we want to use (`gemini-3-flash-preview`) -- `api-key`: a personal API key to authorize our requests. -- `tools`: an *optional* list of tool definition (we'll come back to this). - -The functions output is a new "message" object with the LLM's response. +The `call_llm()` takes several arguments but the primary input for the LLM is +the list of `messages`; the function also returns a new message object with the +output from the LLM. Let's take a closer look at what these "message" objects +consist of. ## Chat Completion Message Structure -The Chat Completion API expects a list of message objects representing the conversation history. This structured format allows the model to understand the context of a multi-turn dialogue. Every time you make a request, you send the entire conversation history (the `messages` list) back to the provider so the model remembers what was previously said. +The Chat Completion API expects a list of message objects representing the +conversation history. The message list represents the full context of a +multi-turn dialogue, typically between a "user" and the LLM "assistant". The +entire conversation history (the `messages` list) must be included with each +request, otherwise the LLM won't "remember" the full context of the +conversation. -Each message in the list is a JSON-like dictionary that must contain specific keys. The most common keys are: +Messages are json objects (Python dicts) with `role` and `content` keys: -- **`role`**: Specifies who is sending the message. This can be one of four main roles: - - `system`: Used to set instructions, constraints, or the persona for the assistant (e.g., `"You are a helpful coding assistant."`). System messages are usually placed at the very beginning of the list. - - `user`: Represents messages or queries sent by the human user. - - `assistant`: Represents responses generated by the language model itself. Keeping track of assistant messages is crucial for maintaining a coherent conversation history. - - `tool`: Represents the output returned from a local function or tool call (which we will look at in the next section). -- **`content`**: The actual text content of the message. For most user and system messages, this is a string. For some assistant responses (such as those initiating tool calls) or tool responses, this may be `None` or contain structured data. - -An example of a single user message is: -```python -{"role": "user", "content": "What is the weather in Paris?"} -``` - -By passing a sequence of these objects, you build up a complete transcript of the conversation for the model to reference and build upon. +- **`role`**: Specifies who is sending the message. This can be one of four main + roles: `user`, `assistant`, `tool`, or `system`. +- **`content`**: The actual text content of the message. +- Messages may include additional keys for tool calling. We'll come back to this. - -To use this function with the DREAM Lab's AI Gateway, we could write a script -like the following. +We'll talk about the "tool" role a little later (and we're mostly ignoring the +"system" role in this guide). In a simple, chat-based exchange (without tool +calls), the "messages" lists consists of alternating "user" and "assistant" +messages. To illustrate, let's use `call_llm()`, with a single prompt: "What is +the weather in Paris?" ```python import os +# messages with initial prompt (user role) +prompt = "What is the weather in Paris" +messages = [{"role": "user", "content": prompt}] + +# api config api_base_url = "https://litellm.dreamlab.ucsb.edu" api_model = "gemini-3-flash-preview" api_key = os.getenv("LLM_API_KEY") # key stored as environment variable -# messages with initial user prompt -messages = [{ - "role": "user", - "content": "What is the weather in Paris" -}] - msg = call_llm(messages, api_base_url, api_model, api_key) -# the LLM's generated text is included as 'content' -print(msg["content"]) +# msg has assistant role with API response +print(msg) # {"role": "assistant", "content": "The weather in Paris is ..."} ``` -When I ran this script, I received the response: +To continue the conversation, we would append the assistant response (`msg`) to +the `messages` list and then add an additional user message: + +```python +# messages = [{"role": "user", "content": "What is the weather in Paris?"}] +# msg = call_llm(messages, api_base_url, api_model, api_key) + +# append assistant response message list +messages.append(msg) + +# append new user prompt to message list +messages.append({"role": "user", "content": "temperature in C and F please!"}) + +# second assistant response +msg = call_llm(messages, api_base_url, api_model, api_key) +messages.append(msg) +``` + +The final `messages` list would include the following: + +| `role` | `content` | +| :---------- | :---------------------------------------- | +| `user` | `"What is the weather in Paris?"` | +| `assistant` | `"The weather in Paris is ..."` | +| `user` | `"temperature in C and F please!"` | +| `assistant` | `"It is 13°C (55°F) with clear skies..."` | + + +## Use 'Tools' to Avoid Hallucinations + +When I ran this script above, with the prompt "What is the weather in Paris?", I received the response: ```md As of right now in Paris, France: @@ -152,12 +186,10 @@ It is expected to stay clear and cool throughout the evening, with temperatures Similar weather is expected tomorrow, with mostly sunny skies and a high of 14°C (57°F). ``` -The response you get back will likely be inaccurate (it was for me). In fact, -running the script multiple times will likely result in completely different -descriptions! That's because the model doesn't actually know what the current -weather in Paris is, so it makes up. It "hallucinates" a plausible description. - -## Use 'Tools' to Avoid Hallucinations +At the time, this descriptions was not accurate. In fact, running the script +multiple times returned completely different weather conditions! That's because +the model doesn't actually know what the weather in Paris is, so it makes up. It +"hallucinates" a plausible description. One way to avoid hallucinations in LLM API responses is by providing the model with "tools" that it can use. Tools provide LLMs with ways to access high From 99ee2ce5ef401b6ee98626e49943b64150c665f7 Mon Sep 17 00:00:00 2001 From: Seth Erickson Date: Tue, 26 May 2026 18:04:52 +0000 Subject: [PATCH 04/17] wip --- content/blog/2026-05-18-agent-guide/index.md | 147 ++++++++++--------- 1 file changed, 81 insertions(+), 66 deletions(-) diff --git a/content/blog/2026-05-18-agent-guide/index.md b/content/blog/2026-05-18-agent-guide/index.md index 5fd6d4b..41868fb 100644 --- a/content/blog/2026-05-18-agent-guide/index.md +++ b/content/blog/2026-05-18-agent-guide/index.md @@ -161,7 +161,7 @@ The final `messages` list would include the following: | `role` | `content` | | :---------- | :---------------------------------------- | -| `user` | `"What is the weather in Paris?"` | +| `user` | `"What is the weather in Paris?"` | | `assistant` | `"The weather in Paris is ..."` | | `user` | `"temperature in C and F please!"` | | `assistant` | `"It is 13°C (55°F) with clear skies..."` | @@ -188,21 +188,26 @@ Similar weather is expected tomorrow, with mostly sunny skies and a high of 14° At the time, this descriptions was not accurate. In fact, running the script multiple times returned completely different weather conditions! That's because -the model doesn't actually know what the weather in Paris is, so it makes up. It -"hallucinates" a plausible description. - -One way to avoid hallucinations in LLM API responses is by providing the model -with "tools" that it can use. Tools provide LLMs with ways to access high -quality information or perform tasks, and to avoid having to fill-in missing -details with statistically likely text. To illustrate, we first need to define a -`get_weather` tool, and include it with our request. How does the LLM API -response change? +the model doesn't actually know what the weather in Paris is, so it makes up the +answer. It "hallucinates" a plausible description of the weather. One way to +avoid hallucinations in LLM API responses is to provide the model with "tools" +that it can use. Tools provide LLMs with ways to access current information, +perform tasks, and avoid having to fill-in missing details with statistically +likely text. + +To illustrate how tools work, we'll create a tool called `get_weather` that +returns current weather conditions for a given location. For now, we're not +concerned with *implementing* the tool. First, we just want to change our +request so that the LLM API is aware of the tool. + +The optional `tools` argument of our `call_llm()` function is used to provide +the LLM API with structured descriptions of tools it can call. In this context, +you can think of "tools" as metadata describing a function in terms of inputs +and outputs. Here's how we would describe our `get_weather` tool using the Chat +Completion API: ```python -# get_weather_schema is metadata describing the `get_weather` tool -# to include in the llm requests. It describes what the function -# does and its required parameters. This structure conforms with the -# Chat Completion API (`ChatCompletionTool`). +# get_weather_schema describes the `get_weather` tool. get_weather_schema = { "type": "function", "function": { @@ -220,39 +225,47 @@ get_weather_schema = { }, }, } +``` + +Now let's see how our response changes when we include this tool (`get_weather_schema`). + +```python +prompt = "What is the weather in Paris" +messages = [{"role": "user", "content": prompt}] # same prompt, api_base_url, api_model, and api_key as before -msg = call_llm(api_base_url, api_model, api_key, - messages = messages, - tools = [get_weather_schema] -) -print(msg["content"]) # "None" +msg = call_llm(messages, api_base_url, api_model, api_key, tools = [get_weather_schema]) + +# print response details +print(msg["role"]) # "assistant" +print(msg["content"]) # None print(msg["too_calls"][0][function]) # {'arguments': '{"location": "Paris"}', 'name': 'get_weather'} ``` -The response doesn't include text in the `content` key like before; instead, -what we get is a list of `tool_calls`, each with a `function` value like this: +The response has changed in a few ways. First, doesn't include any `content` +(the `content` key is still present in the response message, but its values is +`None`). Second, there is a new key, `tool_calls`, which is a list of objects +like this: ```json {"arguments": '{"location": "Paris"}', "name": 'get_weather'}` ``` -What is happening here? Instead of generating direct response to the prompt, the -LLM API has responded with a `tool_call`. As the name suggests, "tool calls" are -how the API calls (or invokes) the tools we included in the request. Our request -included the `get_weather` tool definition, and the response includes a tool -call to run the `get_weather` function with arguments `{"location": "Paris"}`. -The expectation is that we will run `get_weather()` and provide the LLM with the -output so that it can provided a response grounded in facts. +As the name suggests, "tool calls" are how the API calls (or invokes) the tools +we included in the request. Our request included the `get_weather` tool +definition, and the response includes a tool call to run the `get_weather` +function with arguments `{"location": "Paris"}`. The expectation is that we will +run `get_weather()` and make an additional request with the output +from the tool call. -To achieve this, we need to create a `get_weather()` function that we can call. -We'll use https://wttr.in as it provides a free, simple API that is sufficient -for our purposes: +It's time to implement the `get_weather()` function so that we can call it from +our python code. We'll use https://wttr.in as it provides a free, simple API +that is sufficient for our purposes: ```py -# get_weather is our implementation of the function described in -# get_weather_schema. It gets the current weather for a given location -# using a weather API (wttr.in) +# get_weather is our python implemention of the `get_weather` tool. +# It gets the current weather for a given location using a weather +# API (wttr.in) def get_weather(location: str) -> str: url = f"https://wttr.in/{location}?format=3" try: @@ -262,53 +275,55 @@ def get_weather(location: str) -> str: except Exception as e: return f"Could not get weather for {location}: {e}" +# example get_weather("Paris") # "paris: ☁️ +56°F" ``` -To provide the LLM with the result of the tool call (`"paris: ☁️ +56°F"`), we -need make a new request that includes all the previous messages *plus* a new -message with the tool call output. (That's three messages in total: (1) our -initial prompt, (2) the LLM API's response with the tool call, and (3) the tool -call output). - -Here's how the complete sequence with the LLM API: +Now that we have implemented `get_weather`, we can call the Python function +using the arguments in the tool call, and then send the output back to the LLM. +The tool call output is included in the message of a second request, using the +`"tool"` role: ```python -prompt = "What is the weather in Paris?" -messages = [{"role": "user", "content": prompt}] -tools = [get_weather_schema] # previously defined +# continuing from above ... +# msg = call_llm(messages, api_base_url, api_model, api_key, tools = [get_weather_schema]) -# initial request -msg = call_llm(messages=messages, tools=tools) -messages.append(msg) - -# handle tool calls -if "tool_calls" not in msg: - print("expected a tool call, got content:", msg.get("content")) - raise ValueError("No tool calls found in the response") +# run tool call in response: for call in msg["tool_calls"]: - args = json.loads(call["function"]["arguments"]) + + # parse tool call function name and arguments name = call["function"]["name"] if name != "get_weather": - raise ValueError(f"function name is not 'get_weather', got {name}") + raise ValueError(f"unexpected function name: {name}") + args = json.loads(call["function"]["arguments"]) result = get_weather(**args) - new_msg = {"role": "tool", "tool_call_id": call["id"], "content": str(result)} + + # message with tool cal output + new_msg = { + "role": "tool", + "tool_call_id": call["id"], + "content": str(result), + } messages.append(new_msg) -# final request -msg = call_llm(messages=messages, tools=tools) +# final request with tool call results +msg = call_llm(messages, api_base_url, api_model, api_key, tools = [get_weather_schema]) +messages.append(msg) print(msg["content"]) # The weather in Paris is currently 56°F and cloudy. ``` -The sequence of the communication between the user (us) and the various APIs is as follows: +The final message sequence to/from the LLM API and the `get_weather()` tool call +are represented in the table below. Note that the tool output is sent to the LLM +API using a message with `"tool"` role, not the typical `"user"` role. + +| `role` | `content` | `tool_calls` | +| :--------------------------------- | :-------------------------------------------------- | :-------------------------------------------------------------- | +| `user` | "What is the weather in Paris?" | | +| `assistant` | *None* | `{"arguments": '{"location": "Paris"}', "name": 'get_weather'}` | +| *run tool:* `get_weather("Paris")` | | | +| `tool` | "Paris: ☁️ +56°F" | | +| `assistant` | "The weather in Paris is currently 56°F and cloudy" | | + -1. `call_llm()`: initial prompt message + tool definitions (`get_weather(location)`) -2. LLM API responds with tool call: `get_weather("location" = "Paris")` -3. `get_weather()`: request to Weather API: `{"location": "Paris"}` -4. Weather API response: "The weather in Paris is sunny." -5. `call_llm()`: tool call output: "The weather in Paris is sunny." -6. LLM API's final response to the prompt: "The weather in Paris is sunny." - -![tool call flow](tool_call_simple.png) From cc0196e646426c116470eb0854fae3d7334c32aa Mon Sep 17 00:00:00 2001 From: Seth Erickson Date: Tue, 26 May 2026 19:40:44 +0000 Subject: [PATCH 05/17] wip --- content/blog/2026-05-18-agent-guide/index.md | 39 +++++++++++------- .../tool_call_simple.mmd | 11 ----- .../tool_call_simple.png | Bin 24788 -> 0 bytes 3 files changed, 24 insertions(+), 26 deletions(-) delete mode 100644 content/blog/2026-05-18-agent-guide/tool_call_simple.mmd delete mode 100644 content/blog/2026-05-18-agent-guide/tool_call_simple.png diff --git a/content/blog/2026-05-18-agent-guide/index.md b/content/blog/2026-05-18-agent-guide/index.md index 41868fb..65ffeb1 100644 --- a/content/blog/2026-05-18-agent-guide/index.md +++ b/content/blog/2026-05-18-agent-guide/index.md @@ -2,14 +2,14 @@ title: What is an AI Agent? --- -Simon Willison, a prolific software developer and writer on AI-assisted coding, -describes [AI agents](https://simonwillison.net/tags/ai-agents/) as "LLMs -calling tools in a loop to achieve a goal". It's a nice definition, particularly -if you are already familiar with the technical sense of the terms. If you -aren't, you might be wondering: What is a "tool", and how does a language model -"call" one? In this post, I want to add some technical specificity to Willison's -definition by showing how to build a very simple agent: we'll build a program in -which an LLM calls tools in a loop to achieve a goal. +The prolific software developer and writer on AI-assisted coding, Simon Willison +describes [AI agents](https://simonwillison.net/tags/ai-agents/) as "**LLMs +calling tools in a loop to achieve a goal**". It's a good definition, +particularly if you are already familiar with the technical sense of the terms. +If you aren't, you might be wondering: What is a "tool", and how does a language +model "call" one? In this post, I want to add some technical specificity to +Willison's definition by showing how to build a very simple agent: we'll build a +program in which an LLM calls tools in a loop to achieve a goal. Real-world agents (for example, coding agents) can be quite complicated pieces of software. However, the core features of an agent are surprisingly simple to @@ -35,8 +35,8 @@ hardware for running models, and we don’t need to complex machine learning frameworks (like PyTorch). Instead, we just need an HTTP client, like Python’s [requests](https://pypi.org/project/requests/) library. -Model providers (like Open AI, Anthropic, Google, AWS, etc.) expect programs to -use specific APIs to interact with their LLMs. Open AI’s [Chat Completion +Model providers (like OpenAI, Anthropic, Google, AWS, etc.) expect programs to +use specific APIs to interact with their LLMs. OpenAI’s [Chat Completion API](https://developers.openai.com/api/reference/resources/chat), is one of the oldest and most widely supported APIs for interacting with LLMs--and it’s the API we’ll use here. @@ -248,7 +248,7 @@ The response has changed in a few ways. First, doesn't include any `content` like this: ```json -{"arguments": '{"location": "Paris"}', "name": 'get_weather'}` +{"arguments": "{'location': 'Paris'}", "name": "get_weather"}` ``` As the name suggests, "tool calls" are how the API calls (or invokes) the tools @@ -313,17 +313,26 @@ messages.append(msg) print(msg["content"]) # The weather in Paris is currently 56°F and cloudy. ``` -The final message sequence to/from the LLM API and the `get_weather()` tool call -are represented in the table below. Note that the tool output is sent to the LLM -API using a message with `"tool"` role, not the typical `"user"` role. +The final message sequence to/from the LLM API is represented in the table +below. Note that the tool output is sent to the LLM API using a message with +`"tool"` role, not the typical `"user"` role. | `role` | `content` | `tool_calls` | | :--------------------------------- | :-------------------------------------------------- | :-------------------------------------------------------------- | | `user` | "What is the weather in Paris?" | | | `assistant` | *None* | `{"arguments": '{"location": "Paris"}', "name": 'get_weather'}` | -| *run tool:* `get_weather("Paris")` | | | | `tool` | "Paris: ☁️ +56°F" | | | `assistant` | "The weather in Paris is currently 56°F and cloudy" | | +## Calling Tools in a Loop +Let's revisit Willison's definition of AI agents: they are "**LLMs calling tools +in a loop to achieve a goal**. At this point, we have a better understanding of +how LLMs call tools: we include descriptions of available tools in our LLM API +requests; the response messages may include `tool_calls`, which we process +locally; we then send tool call output back to the LLM API as messages with the +`"tool"` role. In the code above, we only processed a single response message +from the LLM API. In fact, that's not how AI agents tend to work. They call +tools "in a loop" -- sometimes called the "agent loop". +... diff --git a/content/blog/2026-05-18-agent-guide/tool_call_simple.mmd b/content/blog/2026-05-18-agent-guide/tool_call_simple.mmd deleted file mode 100644 index 0705632..0000000 --- a/content/blog/2026-05-18-agent-guide/tool_call_simple.mmd +++ /dev/null @@ -1,11 +0,0 @@ -sequenceDiagram - participant Weather API - participant User - participant LLM API - - User->>LLM API: (1) Prompt + Tool: get_weather(location) - LLM API->>User: (2) Tool Call: get_weather(location="Paris") - User ->> Weather API: (3) {"location": "Paris"} - Weather API ->> User: (4) "The weather in Paris is sunny." - User->>LLM API: (5) Tool Call Result: "Weather in Paris is sunny." - LLM API->>User: (6) Final Response: "The weather in Paris is sunny." diff --git a/content/blog/2026-05-18-agent-guide/tool_call_simple.png b/content/blog/2026-05-18-agent-guide/tool_call_simple.png deleted file mode 100644 index 3703249f0b64e001a1e4164fc4ff9396a18526a1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24788 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 Date: Tue, 26 May 2026 22:23:57 +0000 Subject: [PATCH 06/17] agent loop diagram --- content/blog/2026-05-18-agent-guide/agent-loop.mmd | 5 +++++ content/blog/2026-05-18-agent-guide/agent-loop.svg | 1 + content/blog/2026-05-18-agent-guide/index.md | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 content/blog/2026-05-18-agent-guide/agent-loop.mmd create mode 100644 content/blog/2026-05-18-agent-guide/agent-loop.svg diff --git a/content/blog/2026-05-18-agent-guide/agent-loop.mmd b/content/blog/2026-05-18-agent-guide/agent-loop.mmd new file mode 100644 index 0000000..948d459 --- /dev/null +++ b/content/blog/2026-05-18-agent-guide/agent-loop.mmd @@ -0,0 +1,5 @@ +graph TD + User["User
(role: user)"] -->|"prompt"| LLM["LLM
(role: assistant)"] + LLM -->|"result"| User + LLM -->|"tool_calls: [...]"| Agent["Agent Tools
(role: tool)"] + Agent --> LLM diff --git a/content/blog/2026-05-18-agent-guide/agent-loop.svg b/content/blog/2026-05-18-agent-guide/agent-loop.svg new file mode 100644 index 0000000..7e422ea --- /dev/null +++ b/content/blog/2026-05-18-agent-guide/agent-loop.svg @@ -0,0 +1 @@ +

prompt

result

tool_calls: [...]

User
(role: user)

LLM
(role: assistant)

Agent Tools
(role: tool)

\ No newline at end of file diff --git a/content/blog/2026-05-18-agent-guide/index.md b/content/blog/2026-05-18-agent-guide/index.md index 65ffeb1..2735662 100644 --- a/content/blog/2026-05-18-agent-guide/index.md +++ b/content/blog/2026-05-18-agent-guide/index.md @@ -335,4 +335,4 @@ locally; we then send tool call output back to the LLM API as messages with the from the LLM API. In fact, that's not how AI agents tend to work. They call tools "in a loop" -- sometimes called the "agent loop". -... +![Agent Loop](agent-loop.svg) From 34c70621ec127eddcb998a2d630c8d89615caf77 Mon Sep 17 00:00:00 2001 From: Seth Erickson Date: Tue, 26 May 2026 23:33:51 +0000 Subject: [PATCH 07/17] wip --- .../2026-05-18-agent-guide/agent-loop.mmd | 4 +- .../2026-05-18-agent-guide/agent-loop.svg | 2 +- content/blog/2026-05-18-agent-guide/index.md | 43 +++++++++++-------- 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/content/blog/2026-05-18-agent-guide/agent-loop.mmd b/content/blog/2026-05-18-agent-guide/agent-loop.mmd index 948d459..f3a8af0 100644 --- a/content/blog/2026-05-18-agent-guide/agent-loop.mmd +++ b/content/blog/2026-05-18-agent-guide/agent-loop.mmd @@ -1,5 +1,5 @@ -graph TD +graph LR User["User
(role: user)"] -->|"prompt"| LLM["LLM
(role: assistant)"] LLM -->|"result"| User LLM -->|"tool_calls: [...]"| Agent["Agent Tools
(role: tool)"] - Agent --> LLM + Agent -->|"tool result"| LLM diff --git a/content/blog/2026-05-18-agent-guide/agent-loop.svg b/content/blog/2026-05-18-agent-guide/agent-loop.svg index 7e422ea..f8d68a9 100644 --- a/content/blog/2026-05-18-agent-guide/agent-loop.svg +++ b/content/blog/2026-05-18-agent-guide/agent-loop.svg @@ -1 +1 @@ -

prompt

result

tool_calls: [...]

User
(role: user)

LLM
(role: assistant)

Agent Tools
(role: tool)

\ No newline at end of file +

prompt

result

tool_calls: [...]

tool result

User
(role: user)

LLM
(role: assistant)

Agent Tools
(role: tool)

\ No newline at end of file diff --git a/content/blog/2026-05-18-agent-guide/index.md b/content/blog/2026-05-18-agent-guide/index.md index 2735662..d03f565 100644 --- a/content/blog/2026-05-18-agent-guide/index.md +++ b/content/blog/2026-05-18-agent-guide/index.md @@ -239,7 +239,7 @@ msg = call_llm(messages, api_base_url, api_model, api_key, tools = [get_weather_ # print response details print(msg["role"]) # "assistant" print(msg["content"]) # None -print(msg["too_calls"][0][function]) # {'arguments': '{"location": "Paris"}', 'name': 'get_weather'} +print(msg["too_calls"][0][function]) # {"arguments": "{'location': 'Paris'}", "name": "get_weather"}` ``` The response has changed in a few ways. First, doesn't include any `content` @@ -282,7 +282,7 @@ get_weather("Paris") # "paris: ☁️ +56°F" Now that we have implemented `get_weather`, we can call the Python function using the arguments in the tool call, and then send the output back to the LLM. The tool call output is included in the message of a second request, using the -`"tool"` role: +`"tool"` role. ```python @@ -317,22 +317,31 @@ The final message sequence to/from the LLM API is represented in the table below. Note that the tool output is sent to the LLM API using a message with `"tool"` role, not the typical `"user"` role. -| `role` | `content` | `tool_calls` | -| :--------------------------------- | :-------------------------------------------------- | :-------------------------------------------------------------- | -| `user` | "What is the weather in Paris?" | | -| `assistant` | *None* | `{"arguments": '{"location": "Paris"}', "name": 'get_weather'}` | -| `tool` | "Paris: ☁️ +56°F" | | -| `assistant` | "The weather in Paris is currently 56°F and cloudy" | | +| Role | Content / Tool Call | +| :---------- | :--------------------------------------------------------------------------- | +| `user` | "What is the weather in Paris?" | +| `assistant` | tool call: `{"arguments": "{'location': 'Paris'}", "name": "get_weather"}` | +| `tool` | "Paris: ☁️ +56°F" | +| `assistant` | "The weather in Paris is currently 56°F and cloudy" | ## Calling Tools in a Loop -Let's revisit Willison's definition of AI agents: they are "**LLMs calling tools -in a loop to achieve a goal**. At this point, we have a better understanding of -how LLMs call tools: we include descriptions of available tools in our LLM API -requests; the response messages may include `tool_calls`, which we process -locally; we then send tool call output back to the LLM API as messages with the -`"tool"` role. In the code above, we only processed a single response message -from the LLM API. In fact, that's not how AI agents tend to work. They call -tools "in a loop" -- sometimes called the "agent loop". +Let's revisit Willison's definition. AI agents are "**LLMs calling tools in a +loop to achieve a goal**. At this point, we have a better understanding of how +LLMs call tools: we include descriptions of available tools in our requests; the +response may include `tool_calls`; we process tool calls locally and send the +output back using the `"tool"` role. In the code above, we only processed the +tool calls for a single response message. If the LLM API responded to the first +tool call with a second (for example, if the first didn't work as expected), the +second tool call would be ignored. We can address this by continuing to process +tool calls, and making new requests to the LLM API with tool output, until we +stop receiving responses with tool calls. + +This is the idea of "calling tools in a loop": as long as the LLM API continues +to respond with tool calls, the agent continues to handle the calls and make new +requests with the results. The "agent loop" (represented on the right-hand side +of the figure below) is only broken when the LLM API stop responding with tool +calls. + +![The Agent Loop](agent-loop.svg) -![Agent Loop](agent-loop.svg) From b3adefa55ed4d04652519b53c49a382e8eab464a Mon Sep 17 00:00:00 2001 From: Seth Erickson Date: Fri, 29 May 2026 16:51:12 +0000 Subject: [PATCH 08/17] wip --- content/blog/2026-05-18-agent-guide/index.md | 146 ++++++++++++++++++- 1 file changed, 145 insertions(+), 1 deletion(-) diff --git a/content/blog/2026-05-18-agent-guide/index.md b/content/blog/2026-05-18-agent-guide/index.md index d03f565..76c792b 100644 --- a/content/blog/2026-05-18-agent-guide/index.md +++ b/content/blog/2026-05-18-agent-guide/index.md @@ -276,7 +276,7 @@ def get_weather(location: str) -> str: return f"Could not get weather for {location}: {e}" # example -get_weather("Paris") # "paris: ☁️ +56°F" +get_weather("Paris") # "Paris: ☁️ +56°F" ``` Now that we have implemented `get_weather`, we can call the Python function @@ -345,3 +345,147 @@ calls. ![The Agent Loop](agent-loop.svg) +To implement this "agent loop" in Python, we can create a function that +repeatedly calls the LLM, checks if the response contains any `tool_calls`, +executes them, and appends the results back to our conversation history. The +loop breaks when the LLM's response no longer contains tool calls. + +Notice that the `tools` argument expected by `agent_loop()` differs from the `tools` argument of `call_llm()`. While `call_llm()` only expects tool schemas (metadata) to pass to the API, `agent_loop()` expects a list of tuples containing both the schema *and* the executable Python function, so it can actually run the requested tools. + +```python +import json + +def agent_loop(prompt, api_base_url, api_model, api_key, tools=None): + """ + Run the main agent loop, interacting with the LLM and executing any requested tools. + Returns list of messages from the agent's interaction. + """ + tools = tools or [] + + # Separate schemas for the API and build a dictionary of implementations + tool_schemas = [schema for schema, func in tools] + tool_funcs = {schema["function"]["name"]: func for schema, func in tools} + + # messages is our full context. Initially, just the user prompt + messages = [{"role": "user", "content": prompt}] + + while True: + # Call the LLM with the current conversation history and available tool schemas + msg = call_llm(messages, api_base_url, api_model, api_key, tools=tool_schemas) + messages.append(msg) + + # break the loop when the response is not a tool call + if not msg.get("tool_calls"): + break + + # run all tool calls in the message and append tool call results to messages + for call in msg.get("tool_calls", []): + args = json.loads(call["function"]["arguments"]) + name = call["function"]["name"] + + if name in tool_funcs: + func = tool_funcs[name] + try: + result = func(**args) + except Exception as e: + result = f"Error executing {name}: {e}" + else: + result = f"Error: Tool {name} not found." + + messages.append( + {"role": "tool", "tool_call_id": call["id"], "content": str(result)} + ) + + return messages +``` + +## Adding Multiple Tools + +The real power of an agent loop becomes apparent when we provide the LLM with +multiple tools. The model can then orchestrate calling these tools in sequence +to achieve a multi-step goal. Let's add a second tool, `send_message`, which +simulates sending a message to a specific recipient. + +First, we define the tool definition for `send_message`: + +```python +# send_message_schema describes the `send_message` tool +send_message_schema = { + "type": "function", + "function": { + "name": "send_message", + "description": "send a message to someone", + "parameters": { + "type": "object", + "properties": { + "to": { + "type": "string", + "description": "the person to send the message to", + }, + "message": { + "type": "string", + "description": "the body of the messsage", + }, + }, + "required": ["to", "message"], + }, + }, +} +``` + +Next, we provide a Python implementation of `send_messages`. For demonstration +purposes, we will simply store the messages in an dictionary acting as an inbox +for multiple users. + +```python +# A fake email inbox to store messages +inboxes = {} + +def send_message(to: str, message: str): + to_key = to.lower() + if to_key not in inboxes: + inboxes[to_key] = [] + inboxes[to_key].append(message) + return f"Message sent to {to}" +``` + +Now, we can give our agent a more complex prompt: *"Send a message to Tom about +the weather in Paris."* For the agent loop, `tools` includes both the tool +definitions and the tool implementations as tuples. + +```python +prompt = "Send a message to Tom about the weather in Paris." +tools = [ + (get_weather_schema, get_weather), + (send_message_schema, send_message) +] + +messages = agent_loop(prompt, api_base_url, api_model, api_key, tools=tools) +``` + +Behind the scenes, the LLM recognizes that it needs the current weather in Paris +first. It issues a tool call to `get_weather`. Once our loop provides the +weather data back to the model, it realizes it has the information needed to +fulfill the second part of the user's request and issues another tool call to +`send_message`. + +If we print out the conversation transcript as the agent executes, it looks like this: + +| Role | Content / Tool Call | +| :---------- | :--------------------------------------------------------------------------- | +| `user` | "Send a message to Tom about the weather in Paris." | +| `assistant` | tool call: `{"arguments": "{'location': 'Paris'}", "name": "get_weather"}` | +| `tool` | "Paris: ☁️ 🌡️+59°F 🌬️↘9mph" | +| `assistant` | tool call: `{"arguments": "{'to': 'Tom', 'message': 'The current weather in Paris is ☁️ 59°F with a 9mph wind.'}", "name": "send_message"}` | +| `tool` | "Message sent to Tom" | +| `assistant` | "OK. I've sent that message to Tom." | + +Let's confirm that the message was actually sent to Tom: + +```python +print(inboxes["tom"]) +# ['The current weather in Paris is ☁️ 59°F with a 9mph wind.'] +``` + +We gave the LLM a goal, we gave it relevant *tools*, and it used those those +tools to achieve a goal! From 771215e92aed025d6ebb35ee3f10e24b624ef1b3 Mon Sep 17 00:00:00 2001 From: Seth Erickson Date: Fri, 29 May 2026 17:01:25 +0000 Subject: [PATCH 09/17] typos --- content/blog/2026-05-18-agent-guide/index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/content/blog/2026-05-18-agent-guide/index.md b/content/blog/2026-05-18-agent-guide/index.md index 76c792b..a27b60d 100644 --- a/content/blog/2026-05-18-agent-guide/index.md +++ b/content/blog/2026-05-18-agent-guide/index.md @@ -42,8 +42,8 @@ oldest and most widely supported APIs for interacting with LLMs--and it’s the we’ll use here. To make HTTP requests to an LLM provider using the Chat Completion API, you need -three things: the API’s base URL, the name of the model you want use, and an API -access key. +four things: a prompt (or "message"), the API’s base URL, the name of the model +you want use, and an API access key. The core of our agent is a Python function, `call_llm()`, that uses the `requests` library to make HTTP requests to an LLM model provider using the Chat @@ -94,7 +94,7 @@ def call_llm(messages, api_base_url, api_model, api_key, tools=None): return resp["choices"][0]["message"] ``` -The `call_llm()` takes several arguments but the primary input for the LLM is +The `call_llm()` function takes several arguments but the primary input for the LLM is the list of `messages`; the function also returns a new message object with the output from the LLM. Let's take a closer look at what these "message" objects consist of. From 4bcecd4cdcc9315a64d7bb7ec6886dca74e6ae37 Mon Sep 17 00:00:00 2001 From: Seth Erickson Date: Fri, 29 May 2026 17:04:34 +0000 Subject: [PATCH 10/17] more copy edits --- content/blog/2026-05-18-agent-guide/index.md | 36 ++++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/content/blog/2026-05-18-agent-guide/index.md b/content/blog/2026-05-18-agent-guide/index.md index a27b60d..1450108 100644 --- a/content/blog/2026-05-18-agent-guide/index.md +++ b/content/blog/2026-05-18-agent-guide/index.md @@ -2,7 +2,7 @@ title: What is an AI Agent? --- -The prolific software developer and writer on AI-assisted coding, Simon Willison +The prolific software developer and writer on AI-assisted coding, Simon Willison, describes [AI agents](https://simonwillison.net/tags/ai-agents/) as "**LLMs calling tools in a loop to achieve a goal**". It's a good definition, particularly if you are already familiar with the technical sense of the terms. @@ -15,7 +15,7 @@ Real-world agents (for example, coding agents) can be quite complicated pieces of software. However, the core features of an agent are surprisingly simple to implement. You might be surprised at how little code it takes to build an agent that can do useful work! Because our goal is pedagogical, not practical, we'll -use plain Python as much as possible. If you're goal is to build a sophisticated +use plain Python as much as possible. If your goal is to build a sophisticated agent with little effort, you should probably use one of the many agent SDKs designed for the purpose -- or just ask your coding agent to! @@ -31,7 +31,7 @@ APIs. Instead of running models directly, the program makes HTTP “requests” an LLM model provider over the web. Agents talk to model providers the same way your web browser talks to web servers: using HTTP. An advantage of this approach is that it makes the software easier to write and run. We don’t need specialized -hardware for running models, and we don’t need to complex machine learning +hardware for running models, and we don’t need complex machine learning frameworks (like PyTorch). Instead, we just need an HTTP client, like Python’s [requests](https://pypi.org/project/requests/) library. @@ -94,10 +94,10 @@ def call_llm(messages, api_base_url, api_model, api_key, tools=None): return resp["choices"][0]["message"] ``` -The `call_llm()` function takes several arguments but the primary input for the LLM is -the list of `messages`; the function also returns a new message object with the -output from the LLM. Let's take a closer look at what these "message" objects -consist of. +The `call_llm()` function takes several arguments, but the primary input for the +LLM is the list of `messages`; the function also returns a new message object +with the output from the LLM. Let's take a closer look at what these "message" +objects consist of. ## Chat Completion Message Structure @@ -117,7 +117,7 @@ Messages are json objects (Python dicts) with `role` and `content` keys: We'll talk about the "tool" role a little later (and we're mostly ignoring the "system" role in this guide). In a simple, chat-based exchange (without tool -calls), the "messages" lists consists of alternating "user" and "assistant" +calls), the "messages" list consists of alternating "user" and "assistant" messages. To illustrate, let's use `call_llm()`, with a single prompt: "What is the weather in Paris?" @@ -186,7 +186,7 @@ It is expected to stay clear and cool throughout the evening, with temperatures Similar weather is expected tomorrow, with mostly sunny skies and a high of 14°C (57°F). ``` -At the time, this descriptions was not accurate. In fact, running the script +At the time, this description was not accurate. In fact, running the script multiple times returned completely different weather conditions! That's because the model doesn't actually know what the weather in Paris is, so it makes up the answer. It "hallucinates" a plausible description of the weather. One way to @@ -242,8 +242,8 @@ print(msg["content"]) # None print(msg["too_calls"][0][function]) # {"arguments": "{'location': 'Paris'}", "name": "get_weather"}` ``` -The response has changed in a few ways. First, doesn't include any `content` -(the `content` key is still present in the response message, but its values is +The response has changed in a few ways. First, it doesn't include any `content` +(the `content` key is still present in the response message, but its value is `None`). Second, there is a new key, `tool_calls`, which is a list of objects like this: @@ -259,11 +259,11 @@ run `get_weather()` and make an additional request with the output from the tool call. It's time to implement the `get_weather()` function so that we can call it from -our python code. We'll use https://wttr.in as it provides a free, simple API +our Python code. We'll use https://wttr.in as it provides a free, simple API that is sufficient for our purposes: ```py -# get_weather is our python implemention of the `get_weather` tool. +# get_weather is our Python implementation of the `get_weather` tool. # It gets the current weather for a given location using a weather # API (wttr.in) def get_weather(location: str) -> str: @@ -299,7 +299,7 @@ for call in msg["tool_calls"]: args = json.loads(call["function"]["arguments"]) result = get_weather(**args) - # message with tool cal output + # message with tool call output new_msg = { "role": "tool", "tool_call_id": call["id"], @@ -424,7 +424,7 @@ send_message_schema = { }, "message": { "type": "string", - "description": "the body of the messsage", + "description": "the body of the message", }, }, "required": ["to", "message"], @@ -433,8 +433,8 @@ send_message_schema = { } ``` -Next, we provide a Python implementation of `send_messages`. For demonstration -purposes, we will simply store the messages in an dictionary acting as an inbox +Next, we provide a Python implementation of `send_message`. For demonstration +purposes, we will simply store the messages in a dictionary acting as an inbox for multiple users. ```python @@ -487,5 +487,5 @@ print(inboxes["tom"]) # ['The current weather in Paris is ☁️ 59°F with a 9mph wind.'] ``` -We gave the LLM a goal, we gave it relevant *tools*, and it used those those +We gave the LLM a goal, we gave it relevant *tools*, and it used those tools to achieve a goal! From a41ee7711bb2fc5a53f85bf4d20a2980d7cdf85f Mon Sep 17 00:00:00 2001 From: Seth Erickson Date: Fri, 29 May 2026 17:42:10 +0000 Subject: [PATCH 11/17] rework last part --- content/blog/2026-05-18-agent-guide/index.md | 30 +++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/content/blog/2026-05-18-agent-guide/index.md b/content/blog/2026-05-18-agent-guide/index.md index 1450108..e621361 100644 --- a/content/blog/2026-05-18-agent-guide/index.md +++ b/content/blog/2026-05-18-agent-guide/index.md @@ -345,12 +345,14 @@ calls. ![The Agent Loop](agent-loop.svg) -To implement this "agent loop" in Python, we can create a function that -repeatedly calls the LLM, checks if the response contains any `tool_calls`, -executes them, and appends the results back to our conversation history. The -loop breaks when the LLM's response no longer contains tool calls. - -Notice that the `tools` argument expected by `agent_loop()` differs from the `tools` argument of `call_llm()`. While `call_llm()` only expects tool schemas (metadata) to pass to the API, `agent_loop()` expects a list of tuples containing both the schema *and* the executable Python function, so it can actually run the requested tools. +To implement an agent in Python, we will create a function called `agent_loop()` +that makes a request, runs tools, and makes additional requests until the LLM +response no longer contains `tool_calls`. Notice that the `tools` argument +expected by `agent_loop()` differs from the `tools` argument of `call_llm()`. +While `call_llm()` only expects tool schemas (metadata) to pass to the API, +`agent_loop()` expects a list of tuples containing both the schema *and* the +executable Python function. The agent loop needs both because it making API +requests and also processing tools. ```python import json @@ -463,13 +465,7 @@ tools = [ messages = agent_loop(prompt, api_base_url, api_model, api_key, tools=tools) ``` -Behind the scenes, the LLM recognizes that it needs the current weather in Paris -first. It issues a tool call to `get_weather`. Once our loop provides the -weather data back to the model, it realizes it has the information needed to -fulfill the second part of the user's request and issues another tool call to -`send_message`. - -If we print out the conversation transcript as the agent executes, it looks like this: +The complete list of messages returned from `agent_loop()` looks like this: | Role | Content / Tool Call | | :---------- | :--------------------------------------------------------------------------- | @@ -480,7 +476,13 @@ If we print out the conversation transcript as the agent executes, it looks like | `tool` | "Message sent to Tom" | | `assistant` | "OK. I've sent that message to Tom." | -Let's confirm that the message was actually sent to Tom: +Reading the message list, we can see that the LLM responded to the initial +prompt with two tool calls in a row: the first to get the weather in Paris, and +the second to send the message to Tom. The final message from the LLM +"assistant" confirms that that message was sent. The agent loop ends because +this message doesn't include additional tool calls. + +We can also confirm that Tom received a message: ```python print(inboxes["tom"]) From a032c6626fe564379c661cfe66404e3bd028b0e4 Mon Sep 17 00:00:00 2001 From: Seth Erickson Date: Fri, 29 May 2026 17:59:55 +0000 Subject: [PATCH 12/17] suggestions from Jon --- content/blog/2026-05-18-agent-guide/index.md | 56 ++++++++++---------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/content/blog/2026-05-18-agent-guide/index.md b/content/blog/2026-05-18-agent-guide/index.md index e621361..dba01b8 100644 --- a/content/blog/2026-05-18-agent-guide/index.md +++ b/content/blog/2026-05-18-agent-guide/index.md @@ -2,14 +2,15 @@ title: What is an AI Agent? --- -The prolific software developer and writer on AI-assisted coding, Simon Willison, -describes [AI agents](https://simonwillison.net/tags/ai-agents/) as "**LLMs -calling tools in a loop to achieve a goal**". It's a good definition, -particularly if you are already familiar with the technical sense of the terms. -If you aren't, you might be wondering: What is a "tool", and how does a language -model "call" one? In this post, I want to add some technical specificity to -Willison's definition by showing how to build a very simple agent: we'll build a -program in which an LLM calls tools in a loop to achieve a goal. +The prolific software developer and writer on AI-assisted coding, Simon +Willison, describes [AI agents](https://simonwillison.net/tags/ai-agents/) as +**Large Language Models (LLMs) calling tools in a loop to achieve a goal**. It's +a good definition, particularly if you are already familiar with the technical +sense of the terms. If you aren't, you might be wondering: What is a "tool", and +how does a language model "call" one? In this post, I want to add some technical +specificity to Willison's definition by showing how to build a very simple +agent: we'll build a program in which an LLM calls tools in a loop to achieve a +goal. Real-world agents (for example, coding agents) can be quite complicated pieces of software. However, the core features of an agent are surprisingly simple to @@ -19,17 +20,16 @@ use plain Python as much as possible. If your goal is to build a sophisticated agent with little effort, you should probably use one of the many agent SDKs designed for the purpose -- or just ask your coding agent to! -To follow this guide, you need an environment to run Python scripts and API -access to a large language model (LLM) provider. We will be using the DREAM -Lab’s AI gateway as our LLM provider, but other model providers should also -work. +To follow this guide, you will need to know how to run Python scripts and have +API access to an LLM provider. We will be using the DREAM Lab’s AI gateway as +our LLM provider, but other model providers should also work. ## Using LLMs through APIs Most computer programs, like agents, that *use* LLMs do so through web-based -APIs. Instead of running models directly, the program makes HTTP “requests” to -an LLM model provider over the web. Agents talk to model providers the same way -your web browser talks to web servers: using HTTP. An advantage of this approach +APIs. Agents talk to model providers the same way your web browser talks to web +servers: using HTTP. Instead of running models directly, the program makes HTTP +“requests” to an LLM model provider over the web. An advantage of this approach is that it makes the software easier to write and run. We don’t need specialized hardware for running models, and we don’t need complex machine learning frameworks (like PyTorch). Instead, we just need an HTTP client, like Python’s @@ -42,8 +42,12 @@ oldest and most widely supported APIs for interacting with LLMs--and it’s the we’ll use here. To make HTTP requests to an LLM provider using the Chat Completion API, you need -four things: a prompt (or "message"), the API’s base URL, the name of the model -you want use, and an API access key. +four things: + +1. A list of "messages" with the user prompt as the last message (described in detail below) +2. The API’s base URL (e.g, `https://litellm.dreamlab.ucsb.edu`) +3. The name of the model to use (e.g., `gemini-3-flash-preview`) +4. An API access key to authenticate requests The core of our agent is a Python function, `call_llm()`, that uses the `requests` library to make HTTP requests to an LLM model provider using the Chat @@ -57,10 +61,10 @@ def call_llm(messages, api_base_url, api_model, api_key, tools=None): """Makes a request using the Chat Completion API. Args: - messages (list): A list of "message" objects, described in more detail below. - api_base_url (str): The URL of our API endpoint (ex: `https://litellm.dreamlab.ucsb.edu`). - api_model (str): Name of the model to use (ex: `gemini-3-flash-preview`). - api_key (str): An API key to authorize the request. + messages (list): A list of "message" objects, with prompt + api_base_url (str): The URL of our API endpoint. + api_model (str): Name of the model to use. + api_key (str): An API key. tools (list, optional): An optional list of tool definitions. Returns: @@ -94,10 +98,9 @@ def call_llm(messages, api_base_url, api_model, api_key, tools=None): return resp["choices"][0]["message"] ``` -The `call_llm()` function takes several arguments, but the primary input for the -LLM is the list of `messages`; the function also returns a new message object -with the output from the LLM. Let's take a closer look at what these "message" -objects consist of. +The primary input for `call_llm()` is the list of `messages`; its output is +a new message with the response from the LLM API. Let's take a closer +look at what these "message" objects consist of. ## Chat Completion Message Structure @@ -105,8 +108,7 @@ The Chat Completion API expects a list of message objects representing the conversation history. The message list represents the full context of a multi-turn dialogue, typically between a "user" and the LLM "assistant". The entire conversation history (the `messages` list) must be included with each -request, otherwise the LLM won't "remember" the full context of the -conversation. +request. This is how the LLM "remembers" the full context of the conversation. Messages are json objects (Python dicts) with `role` and `content` keys: From 9f45f19b659364ddfdf5f3e93896a6d37c6d4624 Mon Sep 17 00:00:00 2001 From: Seth Erickson Date: Fri, 29 May 2026 18:00:56 +0000 Subject: [PATCH 13/17] word wrap --- content/blog/2026-05-18-agent-guide/index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/content/blog/2026-05-18-agent-guide/index.md b/content/blog/2026-05-18-agent-guide/index.md index dba01b8..540c09d 100644 --- a/content/blog/2026-05-18-agent-guide/index.md +++ b/content/blog/2026-05-18-agent-guide/index.md @@ -98,9 +98,9 @@ def call_llm(messages, api_base_url, api_model, api_key, tools=None): return resp["choices"][0]["message"] ``` -The primary input for `call_llm()` is the list of `messages`; its output is -a new message with the response from the LLM API. Let's take a closer -look at what these "message" objects consist of. +The primary input for `call_llm()` is the list of `messages`; its output is a +new message with the response from the LLM API. Let's take a closer look at what +these "message" objects consist of. ## Chat Completion Message Structure From 44681af7d4469eee396713eb35787977c654bd44 Mon Sep 17 00:00:00 2001 From: Seth Erickson Date: Fri, 29 May 2026 21:59:50 +0000 Subject: [PATCH 14/17] final section + copy edits --- content/blog/2026-05-18-agent-guide/index.md | 48 +++++++++++++++----- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/content/blog/2026-05-18-agent-guide/index.md b/content/blog/2026-05-18-agent-guide/index.md index 540c09d..d10beb4 100644 --- a/content/blog/2026-05-18-agent-guide/index.md +++ b/content/blog/2026-05-18-agent-guide/index.md @@ -38,8 +38,9 @@ frameworks (like PyTorch). Instead, we just need an HTTP client, like Python’s Model providers (like OpenAI, Anthropic, Google, AWS, etc.) expect programs to use specific APIs to interact with their LLMs. OpenAI’s [Chat Completion API](https://developers.openai.com/api/reference/resources/chat), is one of the -oldest and most widely supported APIs for interacting with LLMs--and it’s the API -we’ll use here. +oldest and most widely supported APIs for interacting with LLMs--and it’s the +API we’ll use here. There are other APIs with similar affordances, such as +Anthropic's Messages API, but the basic concepts are the same. To make HTTP requests to an LLM provider using the Chat Completion API, you need four things: @@ -169,7 +170,7 @@ The final `messages` list would include the following: | `assistant` | `"It is 13°C (55°F) with clear skies..."` | -## Use 'Tools' to Avoid Hallucinations +## Using 'Tools' to Avoid Hallucinations When I ran this script above, with the prompt "What is the weather in Paris?", I received the response: @@ -264,7 +265,7 @@ It's time to implement the `get_weather()` function so that we can call it from our Python code. We'll use https://wttr.in as it provides a free, simple API that is sufficient for our purposes: -```py +```python # get_weather is our Python implementation of the `get_weather` tool. # It gets the current weather for a given location using a weather # API (wttr.in) @@ -288,10 +289,7 @@ The tool call output is included in the message of a second request, using the ```python -# continuing from above ... -# msg = call_llm(messages, api_base_url, api_model, api_key, tools = [get_weather_schema]) - -# run tool call in response: +# process tool calls from response message for call in msg["tool_calls"]: # parse tool call function name and arguments @@ -353,7 +351,7 @@ response no longer contains `tool_calls`. Notice that the `tools` argument expected by `agent_loop()` differs from the `tools` argument of `call_llm()`. While `call_llm()` only expects tool schemas (metadata) to pass to the API, `agent_loop()` expects a list of tuples containing both the schema *and* the -executable Python function. The agent loop needs both because it making API +executable Python function. The agent loop needs both because it is making API requests and also processing tools. ```python @@ -403,7 +401,7 @@ def agent_loop(prompt, api_base_url, api_model, api_key, tools=None): return messages ``` -## Adding Multiple Tools +## Using Multiple Tools The real power of an agent loop becomes apparent when we provide the LLM with multiple tools. The model can then orchestrate calling these tools in sequence @@ -481,7 +479,7 @@ The complete list of messages returned from `agent_loop()` looks like this: Reading the message list, we can see that the LLM responded to the initial prompt with two tool calls in a row: the first to get the weather in Paris, and the second to send the message to Tom. The final message from the LLM -"assistant" confirms that that message was sent. The agent loop ends because +"assistant" confirms that message was sent. The agent loop ends because this message doesn't include additional tool calls. We can also confirm that Tom received a message: @@ -493,3 +491,31 @@ print(inboxes["tom"]) We gave the LLM a goal, we gave it relevant *tools*, and it used those tools to achieve a goal! + +## Where to go from here + +Agents are able do (hopefully) useful work through the integration of LLMs and +agent tools via an API. Modern LLMs are specifically trained to respond to use +tools through techniques like [reinforcement +learning](https://en.wikipedia.org/wiki/Reinforcement_learning). The Chat +Completion API and similar APIs, like Anthropic's Messages API, allow us to +include tool definitions in our requests *to the LLM*, receive tool calls *from +the LLM*, and feed results *back to the LLM*. Ultimately, the agent is +responsible for carrying-out the action by processing tool calls. + +Agents are largely defined by the tools they make available to the LLM. The most +salient difference between our weather-checking agent and a sophisticated coding +agent is the tool set. Coding agents include tools for reading and writing text +and running shell commands. Real-world agents are significantly more complex +because they have to handle a wide variety of edge cases that our simple loop +ignores. They need to manage context limits (compaction or summarization when +the conversation gets too long), handle interruptions from the user, deal with +API rate limits, retry failed tool calls intelligently, and prevent the agent +from getting stuck in infinite loops. + +If you are interested in exploring agent development further, you should +probably check out an agent framework and library, like +[smolagents](https://github.com/huggingface/smolagents), +[AutoGen](https://github.com/microsoft/autogen), +[LangGraph](https://github.com/langchain-ai/langgraph), or +[LlamaIndex](https://www.llamaindex.ai/). \ No newline at end of file From fbd52dc64ab6457e85c8321d270dada4ddd5c51f Mon Sep 17 00:00:00 2001 From: Seth Erickson Date: Fri, 29 May 2026 22:02:43 +0000 Subject: [PATCH 15/17] front matter + fixes --- content/blog/2026-05-18-agent-guide/index.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/content/blog/2026-05-18-agent-guide/index.md b/content/blog/2026-05-18-agent-guide/index.md index d10beb4..90fcde9 100644 --- a/content/blog/2026-05-18-agent-guide/index.md +++ b/content/blog/2026-05-18-agent-guide/index.md @@ -1,5 +1,10 @@ --- -title: What is an AI Agent? +title: What is an AI Agent? +author: Seth Erickson +tags: + - guide + - ai + - python --- The prolific software developer and writer on AI-assisted coding, Simon @@ -46,7 +51,7 @@ To make HTTP requests to an LLM provider using the Chat Completion API, you need four things: 1. A list of "messages" with the user prompt as the last message (described in detail below) -2. The API’s base URL (e.g, `https://litellm.dreamlab.ucsb.edu`) +2. The API’s base URL (e.g., `https://litellm.dreamlab.ucsb.edu`) 3. The name of the model to use (e.g., `gemini-3-flash-preview`) 4. An API access key to authenticate requests @@ -62,7 +67,7 @@ def call_llm(messages, api_base_url, api_model, api_key, tools=None): """Makes a request using the Chat Completion API. Args: - messages (list): A list of "message" objects, with prompt + messages (list): A list of "message" objects, with prompt. api_base_url (str): The URL of our API endpoint. api_model (str): Name of the model to use. api_key (str): An API key. @@ -479,7 +484,7 @@ The complete list of messages returned from `agent_loop()` looks like this: Reading the message list, we can see that the LLM responded to the initial prompt with two tool calls in a row: the first to get the weather in Paris, and the second to send the message to Tom. The final message from the LLM -"assistant" confirms that message was sent. The agent loop ends because +"assistant" confirms that the message was sent. The agent loop ends because this message doesn't include additional tool calls. We can also confirm that Tom received a message: @@ -494,8 +499,8 @@ tools to achieve a goal! ## Where to go from here -Agents are able do (hopefully) useful work through the integration of LLMs and -agent tools via an API. Modern LLMs are specifically trained to respond to use +Agents are able to do (hopefully) useful work through the integration of LLMs and +agent tools via an API. Modern LLMs are specifically trained to use tools through techniques like [reinforcement learning](https://en.wikipedia.org/wiki/Reinforcement_learning). The Chat Completion API and similar APIs, like Anthropic's Messages API, allow us to From 9359c25c9ed1ed3a4fd97dd5f95b630c229c97ba Mon Sep 17 00:00:00 2001 From: Seth Erickson Date: Fri, 29 May 2026 22:24:43 +0000 Subject: [PATCH 16/17] add link to dreamlab ai gateway docs --- content/blog/2026-05-18-agent-guide/index.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/content/blog/2026-05-18-agent-guide/index.md b/content/blog/2026-05-18-agent-guide/index.md index 90fcde9..7fb34dc 100644 --- a/content/blog/2026-05-18-agent-guide/index.md +++ b/content/blog/2026-05-18-agent-guide/index.md @@ -26,8 +26,9 @@ agent with little effort, you should probably use one of the many agent SDKs designed for the purpose -- or just ask your coding agent to! To follow this guide, you will need to know how to run Python scripts and have -API access to an LLM provider. We will be using the DREAM Lab’s AI gateway as -our LLM provider, but other model providers should also work. +API access to an LLM provider. We will be using [DREAM Lab’s AI +gateway](https://dreamlab.ucsb.edu/guides/ai-gateway.html) as our LLM provider, +but other model providers should also work. ## Using LLMs through APIs From 9301f189c8f31aa3686bc1ea1168e397b372b54a Mon Sep 17 00:00:00 2001 From: Seth Erickson Date: Fri, 29 May 2026 23:00:07 +0000 Subject: [PATCH 17/17] fix #41 --- content/blog/2026-05-18-agent-guide/index.md | 49 +++++++++++--------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/content/blog/2026-05-18-agent-guide/index.md b/content/blog/2026-05-18-agent-guide/index.md index 7fb34dc..4fe7fc4 100644 --- a/content/blog/2026-05-18-agent-guide/index.md +++ b/content/blog/2026-05-18-agent-guide/index.md @@ -23,7 +23,7 @@ implement. You might be surprised at how little code it takes to build an agent that can do useful work! Because our goal is pedagogical, not practical, we'll use plain Python as much as possible. If your goal is to build a sophisticated agent with little effort, you should probably use one of the many agent SDKs -designed for the purpose -- or just ask your coding agent to! +designed for the purpose -- or ask your coding agent to! To follow this guide, you will need to know how to run Python scripts and have API access to an LLM provider. We will be using [DREAM Lab’s AI @@ -57,8 +57,8 @@ four things: 4. An API access key to authenticate requests The core of our agent is a Python function, `call_llm()`, that uses the -`requests` library to make HTTP requests to an LLM model provider using the Chat -Completion API. +`requests` library to make a single HTTP requests to an LLM model provider using +the Chat Completion API. ```python import requests @@ -105,17 +105,19 @@ def call_llm(messages, api_base_url, api_model, api_key, tools=None): return resp["choices"][0]["message"] ``` -The primary input for `call_llm()` is the list of `messages`; its output is a -new message with the response from the LLM API. Let's take a closer look at what -these "message" objects consist of. +This might look complicated, but the gist of `call_llm()` is that it sends +"messages" to a web server that understands the Chat Completion API and returns +a new message with the LLM's response. A list of messages go in, a single +message comes back. Now let's take a closer look at what these "message" consist +of. ## Chat Completion Message Structure -The Chat Completion API expects a list of message objects representing the -conversation history. The message list represents the full context of a -multi-turn dialogue, typically between a "user" and the LLM "assistant". The -entire conversation history (the `messages` list) must be included with each -request. This is how the LLM "remembers" the full context of the conversation. +The Chat Completion API expects a list of messages representing the conversation +history. The message list represents the full context of a multi-turn dialogue, +typically between a "user" and the LLM "assistant". The entire conversation +history (the `messages` list) must be included with each request. This is how +the LLM "remembers" the full context of the conversation. Messages are json objects (Python dicts) with `role` and `content` keys: @@ -134,7 +136,7 @@ the weather in Paris?" import os # messages with initial prompt (user role) -prompt = "What is the weather in Paris" +prompt = "What is the weather in Paris?" messages = [{"role": "user", "content": prompt}] # api config @@ -210,10 +212,12 @@ concerned with *implementing* the tool. First, we just want to change our request so that the LLM API is aware of the tool. The optional `tools` argument of our `call_llm()` function is used to provide -the LLM API with structured descriptions of tools it can call. In this context, -you can think of "tools" as metadata describing a function in terms of inputs -and outputs. Here's how we would describe our `get_weather` tool using the Chat -Completion API: +the LLM API with structured descriptions of tools it can call. The structure is +defined by the [Chat Completion +API](https://developers.openai.com/api/reference/resources/chat). In this +context, you can think of "tools" as metadata describing a function in terms of +inputs and outputs. Here's how we would describe our `get_weather` tool using +the Chat Completion API: ```python # get_weather_schema describes the `get_weather` tool. @@ -268,8 +272,8 @@ run `get_weather()` and make an additional request with the output from the tool call. It's time to implement the `get_weather()` function so that we can call it from -our Python code. We'll use https://wttr.in as it provides a free, simple API -that is sufficient for our purposes: +our Python code. We'll use https://wttr.in, a site that provides a simple (free) +API for fetching current weather. ```python # get_weather is our Python implementation of the `get_weather` tool. @@ -506,13 +510,13 @@ tools through techniques like [reinforcement learning](https://en.wikipedia.org/wiki/Reinforcement_learning). The Chat Completion API and similar APIs, like Anthropic's Messages API, allow us to include tool definitions in our requests *to the LLM*, receive tool calls *from -the LLM*, and feed results *back to the LLM*. Ultimately, the agent is +the LLM*, and feed results *back to the LLM*. Ultimately, the agent (our code) is responsible for carrying-out the action by processing tool calls. Agents are largely defined by the tools they make available to the LLM. The most salient difference between our weather-checking agent and a sophisticated coding agent is the tool set. Coding agents include tools for reading and writing text -and running shell commands. Real-world agents are significantly more complex +and running shell commands. Also, real-world agents are significantly more complex because they have to handle a wide variety of edge cases that our simple loop ignores. They need to manage context limits (compaction or summarization when the conversation gets too long), handle interruptions from the user, deal with @@ -522,6 +526,5 @@ from getting stuck in infinite loops. If you are interested in exploring agent development further, you should probably check out an agent framework and library, like [smolagents](https://github.com/huggingface/smolagents), -[AutoGen](https://github.com/microsoft/autogen), -[LangGraph](https://github.com/langchain-ai/langgraph), or -[LlamaIndex](https://www.llamaindex.ai/). \ No newline at end of file +[Pydantic AI](https://github.com/pydantic/pydantic-ai), or +[LangGraph](https://github.com/langchain-ai/langgraph).