Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
efe8afa
fix: handle negative strings in _safe_int() and use continue in compa…
arpan404 Feb 28, 2026
69e9a78
fix: safe env var parsing in LLMSettings.from_env()
arpan404 Feb 28, 2026
cb44cbb
fix: resolve RequestCoalescer deadlock by releasing lock before await
arpan404 Feb 28, 2026
89ba686
fix: add eviction to InMemoryLLMCache to prevent unbounded growth
arpan404 Feb 28, 2026
e60732d
fix: handle race condition in hedged requests when winner errors
arpan404 Feb 28, 2026
1389bfa
test: add comprehensive LLM test suite
arpan404 Feb 28, 2026
3805717
test: add memory subsystem tests
arpan404 Feb 28, 2026
0f0988a
test: add agent subsystem tests
arpan404 Feb 28, 2026
9e6c8c4
test: add core streaming and telemetry tests
arpan404 Feb 28, 2026
3ddba02
test: add evals module tests
arpan404 Feb 28, 2026
9436983
test: add MCP store utilities tests
arpan404 Feb 28, 2026
c258bf4
test: add queue subsystem tests
arpan404 Feb 28, 2026
d9387db
test: add observability subsystem tests
arpan404 Feb 28, 2026
b409110
test: add tool subsystem tests
arpan404 Feb 28, 2026
f194693
test: add cross-module integration tests
arpan404 Feb 28, 2026
064b1bc
fix: enforce max_chars limit in sanitize_text output
arpan404 Feb 28, 2026
cfce9b9
fix: record circuit breaker failures in embed() method
arpan404 Feb 28, 2026
4fd653b
fix: use asyncio.to_thread directly instead of wrapping in partial
arpan404 Feb 28, 2026
82b723e
fix: use time.monotonic() in InMemoryLLMCache for clock-safe expiration
arpan404 Feb 28, 2026
568844d
examples: add beginner-level example projects (02-08)
arpan404 Feb 28, 2026
9caf0f0
examples: add intermediate-level example projects (09-16)
arpan404 Feb 28, 2026
4ed7c0f
examples: add News Digest streaming example (17)
arpan404 Feb 28, 2026
30468e1
examples: add Email Classifier structured output example (18)
arpan404 Feb 28, 2026
8a205c8
examples: add Meeting Notes dynamic InstructionProvider example (19)
arpan404 Feb 28, 2026
398f6ee
examples: add Customer Support Router SubagentRouter example (20)
arpan404 Feb 28, 2026
0e6d767
examples: add Habit Tracker SQLiteMemoryStore example (21)
arpan404 Feb 28, 2026
82b5521
examples: add Fitness Coach prehook/posthook example (22)
arpan404 Feb 28, 2026
986f6c4
examples: add Markdown Converter middleware example (23)
arpan404 Feb 28, 2026
cc35cb0
examples: add Data Pipeline DelegationPlan DAG example (24)
arpan404 Feb 28, 2026
7d09ee8
examples: add Content Moderator PolicyEngine example (25)
arpan404 Feb 28, 2026
d946621
examples: add Shopping Assistant context inheritance example (26)
arpan404 Feb 28, 2026
6e3495e
examples: add Report Generator deferred tools example (27)
arpan404 Feb 28, 2026
a2ebbac
examples: add Math Tutor reasoning/extended thinking example (28)
arpan404 Feb 28, 2026
0a72dee
examples: add System Monitor telemetry example (29)
arpan404 Feb 28, 2026
fb02807
examples: add Agent Test Harness eval system example (30)
arpan404 Feb 28, 2026
640d69a
examples: add Travel Planner delegation JoinPolicy example (31)
arpan404 Feb 28, 2026
009232e
examples: add API Health Monitor LLM resilience policies example (32)
arpan404 Feb 28, 2026
af527fa
examples: add Personal Wiki long-term memory example (33)
arpan404 Feb 28, 2026
30a73bf
examples: add Document Approval interaction config example (34)
arpan404 Feb 28, 2026
f3a62a1
examples: add Smart Home ToolPolicy access control example (35)
arpan404 Feb 28, 2026
66ed4d7
examples: add Hiring Pipeline parallel subagents example (36)
arpan404 Feb 28, 2026
af3cfcb
examples: add Grading System eval scorer example (37)
arpan404 Feb 28, 2026
ea7219f
examples: add Chat History Manager thread lifecycle example (38)
arpan404 Feb 28, 2026
4768362
examples: add Secure Agent sandboxing example (39)
arpan404 Feb 28, 2026
439e955
examples: add Production Agent capstone example (40)
arpan404 Feb 28, 2026
d577270
examples: refine Meeting Notes with Jinja2 prompt templates (19)
arpan404 Feb 28, 2026
d80e28a
examples: refine Customer Support Router with InstructionRole callbac…
arpan404 Feb 28, 2026
48420fa
examples: refine Content Moderator with PolicyRole callbacks (25)
arpan404 Feb 28, 2026
334f666
examples: refine System Monitor with Debugger facade (29)
arpan404 Feb 28, 2026
9fc8977
examples: refine Document Approval with AgentRunHandle lifecycle (34)
arpan404 Feb 28, 2026
402980d
examples: refine Chat History Manager with resume and env factory (38)
arpan404 Feb 28, 2026
7ebf77f
examples: split Data Pipeline (24) into multi-file project
arpan404 Feb 28, 2026
01cac27
examples: split Hiring Pipeline (36) into multi-file project
arpan404 Feb 28, 2026
3fcc672
examples: add Skills, MCP, InstructionRoles, env memory to Production…
arpan404 Feb 28, 2026
bc3cc92
chore: move agent skills from agent-skill/ to skills/
arpan404 Feb 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions examples/projects/02_Weather_Agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

# Weather Agent

An example agent that checks the weather for any city using a custom tool. This example introduces **tools** - how to give an agent the ability to call functions.

Prerequisites
- Run this from the repository root.
- Ensure scripts/setup_example.sh is executable: chmod +x scripts/setup_example.sh

Usage
- Run (relative):
./scripts/setup_example.sh --project-dir=examples/projects/02_Weather_Agent

- Run (absolute):
./scripts/setup_example.sh --project-dir=/Users/username/pathtoafk/examples/projects/02_Weather_Agent

Tip: build the absolute path dynamically from the repo root:
./scripts/setup_example.sh --project-dir=$(pwd)/examples/projects/02_Weather_Agent

Expected interaction
User: What's the weather like in Tokyo?
Agent: The weather in Tokyo is 68F and clear - a beautiful day! Is there another city you'd like me to check?

The agent uses the get_weather tool to look up weather data and responds conversationally.

137 changes: 98 additions & 39 deletions examples/projects/02_Weather_Agent/main.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,100 @@
from afk.core import Runner
from afk.agents import Agent
from afk.tools import ToolRegistry, tool

from afk.agents import Agent
from afk.tools import tool
from afk.core import Runner

from pydantic import BaseModel
import asyncio
class WeatherArgs(BaseModel):
city: str

@tool(args_model=WeatherArgs, name="get_weather", description="Get current weather for a city.")
def get_weather(args: WeatherArgs) -> dict:
return {"city": args.city, "temp_f": 72, "condition": "sunny"}

agent = Agent(
name="weather-bot",
model="ollama_chat/gpt-oss:20b",
instructions="Answer weather questions using your tools.",
tools=[get_weather], # ← Attach tools here
reasoning_effort="high",
reasoning_enabled=True,
"""
---
name: Weather Agent
description: An agent that checks weather for any city using a custom tool.
tags: [agent, runner, tools]
---
---
This example introduces **tools** - how to give an agent the ability to call functions. The agent can check the weather for any city using a custom tool defined with the `@tool` decorator and a Pydantic `args_model`.
---
"""

from pydantic import BaseModel, Field # <- Pydantic is used to define the schema (shape) of the arguments your tool accepts. The LLM uses this schema to know what parameters to pass when calling the tool.

from afk.core import Runner # <- Runner is responsible for executing agents and managing their state. Tl;dr: it's what you use to run your agents after you create them.
from afk.agents import Agent # <- Agent is the main class for creating agents in AFK. It encapsulates the model, instructions, and other configurations that define the agent's behavior. Tl;dr: you create an Agent to define what your agent is and how it should behave, and then you use the Runner to execute it.
from afk.tools import tool # <- The `tool` decorator turns a plain function into an AFK Tool that agents can call. You define the args schema with a Pydantic model and the decorator handles the rest.


# --- Step 1: Define the tool's argument schema ---

class GetWeatherArgs(BaseModel):
"""Schema for the get_weather tool. The LLM reads this schema (including field descriptions) to know how to call the tool."""
city: str = Field(description="The city to get weather for") # <- Each field becomes a parameter the LLM can fill in. The `description` helps the LLM understand what value to provide.


# --- Step 2: Create the tool using the @tool decorator ---

@tool(args_model=GetWeatherArgs, name="get_weather", description="Get the current weather for a city") # <- `args_model` tells AFK the shape of the arguments. `name` and `description` are what the LLM sees when deciding which tool to call.
def get_weather(args: GetWeatherArgs) -> str:
"""Simulate a weather lookup. In a real app, you'd call a weather API here."""
weather_data = {
"new york": "72\u00b0F, Partly Cloudy",
"london": "58\u00b0F, Rainy",
"tokyo": "68\u00b0F, Clear",
"paris": "64\u00b0F, Overcast",
"sydney": "80\u00b0F, Sunny",
} # <- Hardcoded weather data for demonstration. Replace this with a real API call in production.

city_lower = args.city.lower()
if city_lower in weather_data:
return f"Weather in {args.city}: {weather_data[city_lower]}"
return f"Weather in {args.city}: 70\u00b0F, Clear (simulated default)" # <- Fallback for cities not in our mock data.


# --- Step 3: Create the agent and give it the tool ---

weather_agent = Agent(
name="weather_agent", # <- What you want to call your agent.
model="ollama_chat/gpt-oss:20b", # <- The llm model the agent will use. See https://afk.arpan.sh/library/agents for more details.
instructions="""
You are a helpful weather assistant. When the user asks about the weather in a city,
use the get_weather tool to look it up and share the result in a friendly, conversational way.
If the user doesn't ask about weather, just chat normally and let them know you can check
the weather for any city if they'd like.

# example conversations:

# example 1: user asks about weather
user: "What's the weather like in Tokyo?"
agent: *calls get_weather with city="Tokyo"*
agent: "The weather in Tokyo is 68F and clear - a beautiful day! Is there another city you'd like me to check?"

# example 2: user doesn't ask about weather
user: "Hello!"
agent: "Hey there! I'm your weather assistant. Want me to check the weather for a city? Just name a place!"

**NOTE**: Always use the get_weather tool when the user asks about weather. Don't make up weather data yourself.

""", # <- Instructions that guide the agent's behavior. Notice we tell the agent to use the tool rather than guessing weather data.
tools=[get_weather], # <- This is the key part! Pass your tools as a list to the agent. The agent will see the tool's name, description, and parameter schema, and the LLM will decide when to call it based on the user's input.
)
runner = Runner()

if __name__ == "__main__":
user_input = input(
"[] > "
) # <- Take user input from the console to interact with the agent.

response = runner.run_sync(
weather_agent, user_message=user_input
) # <- Run the agent synchronously using the Runner. The Runner handles the tool-calling loop: if the LLM decides to call a tool, the Runner executes it and feeds the result back to the LLM automatically.

print(
f"[weather_agent] > {response.final_text}"
) # <- Print the agent's response to the console. By the time we get final_text, any tool calls have already been resolved and the LLM has composed its final answer.



async def main():
runner = Runner()
result = await runner.run_stream(agent, user_message="What's the weather in Austin?")

async for r in result:
if r.type== "tool_started":
print(f"Agent is calling tool: {r.tool_name} with args: {r.data}")
elif r.type == "tool_completed":
print(f"Agent got tool response: {r.tool_output}")
elif r.type == "text_delta":
print(r.text_delta, end="", flush=True)
else:
print(f"Received unknown result type: {r.type}")

asyncio.run(main())
"""
---
Tl;dr: This example creates a weather agent that uses a custom tool to check the weather for any city. The key concepts are: (1) defining a tool with the `@tool` decorator and a Pydantic `args_model` that describes the tool's parameters, (2) passing the tool to an Agent via `tools=[...]`, and (3) letting the LLM decide when to call the tool based on the user's input. The Runner handles the entire tool-calling loop automatically.
---
---
What's next?
- Try adding more tools to the agent! For example, you could add a "get_forecast" tool that returns a 5-day forecast, and the agent will learn to use both tools as needed.
- Experiment with the tool's description and parameter descriptions to see how they affect when and how the LLM calls the tool.
- Try removing the tool from the agent's tools list and see how the agent responds differently - it will no longer be able to look up weather data.
- Check out the next example to learn about multi-turn conversations and how agents can maintain context across multiple interactions!
---
"""
6 changes: 2 additions & 4 deletions examples/projects/02_Weather_Agent/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
[project]
name = "02-weather-agent"
version = "0.1.0"
description = "Add your description here"
description = "An agent that checks weather using tools"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"pydantic>=2.12.5",
]
dependencies = []
1 change: 1 addition & 0 deletions examples/projects/03_Calculator_Agent/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13
28 changes: 28 additions & 0 deletions examples/projects/03_Calculator_Agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

# Calculator Agent

An example agent built on afk that demonstrates multiple tools. The agent is a math calculator that can add, subtract, multiply, and divide using the appropriate tool based on the user's request.

Prerequisites
- Run this from the repository root.
- Ensure scripts/setup_example.sh is executable: chmod +x scripts/setup_example.sh

Usage
- Run (relative):
./scripts/setup_example.sh --project-dir=examples/projects/03_Calculator_Agent

- Run (absolute):
./scripts/setup_example.sh --project-dir=/Users/username/pathtoafk/examples/projects/03_Calculator_Agent

Tip: build the absolute path dynamically from the repo root:
./scripts/setup_example.sh --project-dir=$(pwd)/examples/projects/03_Calculator_Agent

Expected interaction
User: What is 12 times 5?
Agent: 12 * 5 = 60

User: Divide 100 by 4
Agent: 100 / 4 = 25.0

The agent will select the correct tool (add, subtract, multiply, or divide) based on your request.

94 changes: 94 additions & 0 deletions examples/projects/03_Calculator_Agent/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""
---
name: Calculator Agent
description: A calculator agent with multiple math tools that can add, subtract, multiply, and divide.
tags: [agent, runner, tools]
---
---
This example demonstrates how to give an agent multiple tools so it can perform different operations. The agent is a calculator that can add, subtract, multiply, and divide. It shows how the LLM selects the right tool based on the user's intent, and how to use Pydantic models for structured tool arguments with multiple fields.
---
"""

from pydantic import BaseModel, Field # <- Pydantic is used to define structured argument models for tools. This lets you specify exactly what inputs each tool expects, with types, descriptions, and validation built in.
from afk.core import Runner # <- Runner is responsible for executing agents and managing their state. Tl;dr: it's what you use to run your agents after you create them.
from afk.agents import Agent # <- Agent is the main class for creating agents in AFK. It encapsulates the model, instructions, and other configurations that define the agent's behavior. Tl;dr: you create an Agent to define what your agent is and how it should behave, and then you use the Runner to execute it.
from afk.tools import tool # <- The @tool decorator turns a plain Python function into a tool that an agent can call. You give it a name, description, and an args_model so the LLM knows when and how to use it.


# --- Tool argument schema ---

class TwoNumberArgs(BaseModel): # <- A Pydantic model that defines the arguments for our math tools. Both tools share the same schema: two numbers (a and b). Using a shared model avoids duplication and keeps things consistent.
a: float = Field(description="The first number") # <- Field lets you attach metadata like descriptions so the LLM understands what each argument means.
b: float = Field(description="The second number")


# --- Tool definitions ---

@tool(args_model=TwoNumberArgs, name="add", description="Add two numbers together") # <- Each @tool call registers a tool the agent can use. The name and description help the LLM decide which tool to call based on the user's message.
def add(args: TwoNumberArgs) -> str:
result = args.a + args.b
return f"{args.a} + {args.b} = {result}"


@tool(args_model=TwoNumberArgs, name="subtract", description="Subtract second number from first")
def subtract(args: TwoNumberArgs) -> str:
result = args.a - args.b
return f"{args.a} - {args.b} = {result}"


@tool(args_model=TwoNumberArgs, name="multiply", description="Multiply two numbers")
def multiply(args: TwoNumberArgs) -> str:
result = args.a * args.b
return f"{args.a} * {args.b} = {result}"


@tool(args_model=TwoNumberArgs, name="divide", description="Divide first number by second")
def divide(args: TwoNumberArgs) -> str:
if args.b == 0:
return "Error: Cannot divide by zero!" # <- Always handle edge cases in your tools. The LLM will relay this message back to the user.
result = args.a / args.b
return f"{args.a} / {args.b} = {result}"


# --- Agent setup ---

calculator = Agent(
name="calculator", # <- What you want to call your agent.
model="ollama_chat/gpt-oss:20b", # <- The llm model the agent will use. See https://afk.arpan.sh/library/agents for more details.
instructions="""
You are a friendly math calculator assistant. When a user asks you to perform a calculation, you should use the appropriate tool (add, subtract, multiply, or divide) to compute the result. Always show your work by using the tool and then presenting the result clearly.

If the user asks something that isn't a math question, politely let them know you're a calculator and can only help with math.

Be friendly and encouraging!
""", # <- Instructions that guide the agent's behavior. The agent will choose the right tool based on these instructions and the user's message.
tools=[add, subtract, multiply, divide], # <- Pass all four tools to the agent. The LLM will automatically pick the right one based on what the user asks. This is the key concept: one agent, multiple tools.
)
runner = Runner()

if __name__ == "__main__":
user_input = input(
"[] > "
) # <- Take user input from the console to interact with the agent.

response = runner.run_sync(
calculator, user_message=user_input
) # <- Run the agent synchronously using the Runner. We pass the user's input as a message to the agent.

print(
f"[calculator] > {response.final_text}"
) # <- Print the agent's response to the console. Note: the response is an object that contains various information about the agent's execution, but we are only interested in the final text output for this example.



"""
---
Tl;dr: This example creates a calculator agent with four math tools (add, subtract, multiply, divide). It demonstrates how to define multiple tools using the @tool decorator with a shared Pydantic args model, and how to pass them all to a single agent. The LLM automatically selects the correct tool based on the user's intent. The user interacts through the console, and the agent's response is printed back.
---
---
What's next?
- Try adding more tools like power, square root, or modulo to see how the agent handles a larger toolset.
- Experiment with chaining operations: ask the agent to "add 5 and 3, then multiply the result by 2" and see how it handles multi-step calculations.
- Check out the other examples in the library to see how to create multi-agent systems where agents can delegate tasks to each other!
---
"""
7 changes: 7 additions & 0 deletions examples/projects/03_Calculator_Agent/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[project]
name = "03-calculator-agent"
version = "0.1.0"
description = "A calculator agent with multiple math tools"
readme = "README.md"
requires-python = ">=3.13"
dependencies = []
1 change: 1 addition & 0 deletions examples/projects/04_Todo_Manager/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13
38 changes: 38 additions & 0 deletions examples/projects/04_Todo_Manager/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@

# Todo Manager

A stateful todo manager agent built on afk. The agent can add, list, complete, and remove tasks using tools that maintain shared state across calls.

Prerequisites
- Run this from the repository root.
- Ensure scripts/setup_example.sh is executable: chmod +x scripts/setup_example.sh

Usage
- Run (relative):
./scripts/setup_example.sh --project-dir=examples/projects/04_Todo_Manager

- Run (absolute):
./scripts/setup_example.sh --project-dir=/Users/username/pathtoafk/examples/projects/04_Todo_Manager

Tip: build the absolute path dynamically from the repo root:
./scripts/setup_example.sh --project-dir=$(pwd)/examples/projects/04_Todo_Manager

Expected interaction
Agent: Hi! I'm your todo manager. I can add, list, complete, and remove tasks for you.
User: Add buy groceries
Agent: Added todo #1: "buy groceries"
User: Add finish homework
Agent: Added todo #2: "finish homework"
User: Show my tasks
Agent: Here are your todos:
1. [ ] buy groceries
2. [ ] finish homework
User: Complete 1
Agent: Marked todo #1 "buy groceries" as done!
User: Show my tasks
Agent: Here are your todos:
1. [x] buy groceries
2. [ ] finish homework

The agent maintains state across the conversation, so todos persist until you quit.

Loading
Loading