In May 2025 I made my first open source contribution.
Context
For a class I had to work on a group project. Working with a team means compromise. I strongly dislike using frameworks for LLM projects. At first I couldn’t put my finger on what bothered me until I read Anthropic’s post on building effective agents. They put words on my frustration: frameworks add abstraction layers that make the underlying prompts and responses harder to debug.
Since everyone insisted on using a framework, I looked at the options beyond Langchain and found agno. Their approach seemed close to how I like to do things, so we went with it.
The problem
We wanted our agent to have access to a to-do list. Todoist seemed perfect, and agno already had a toolkit for it. I imported it, gave it to the agent, and it kept failing with no visibility into why.
I decided to test the toolkit on its own to figure out whether it was the agent or the toolkit. I got an error. Usually my first instinct is that I did something wrong, not that there’s a bug in a popular Python library. This time I just ctrl+clicked into the source on VSCode.
After some print statements and checking the documentation, I found a few small bugs. Todoist’s API returns a nested list and the toolkit wasn’t handling it correctly. Easy fix. I opened a GitHub issue and submitted a PR that was accepted the same day.
But while debugging, I found something that stuck with me.
What I learned
I was curious how passing a Toolkit object to an Agent results in a valid OpenAI function-calling schema. Reading the code, I found they extract it directly from each method’s docstring and type hints using Python’s inspect module.
docstring = inspect.getdoc(method) or ""
description = docstring.split('\n')[0]
That was it. No manually written JSON dictionaries. I took the same approach in my own mini framework. Here is what the Toolkit base class ended up looking like:
import inspect
from typing import get_type_hints
class Toolkit:
def _get_tools(self) -> list[dict]:
tools = []
for name, method in inspect.getmembers(self, predicate=inspect.ismethod):
if name.startswith('_'):
continue
sig = inspect.signature(method)
hints = get_type_hints(method)
docstring = inspect.getdoc(method) or ""
properties = {}
required = []
for param_name, param in sig.parameters.items():
param_type = hints.get(param_name, str)
properties[param_name] = {
"type": "string" if param_type == str else "number"
}
if param.default is inspect.Parameter.empty:
required.append(param_name)
tools.append({
"type": "function",
"function": {
"name": name,
"description": docstring.split('\n')[0],
"parameters": {
"type": "object",
"properties": properties,
"required": required
}
}
})
return tools
Creating a toolkit is then just inheriting from it and writing normal methods with type hints and docstrings:
class TodoistToolkit(Toolkit):
def __init__(self, api_token: str):
self.client = TodoistAPI(api_token)
def get_tasks(self) -> str:
"""Get all active tasks from Todoist."""
tasks = self.client.get_tasks()
return "\n".join(f"- {t.content}" for t in tasks)
def add_task(self, content: str, due_string: str = None) -> str:
"""Add a new task to Todoist."""
task = self.client.add_task(content=content, due_string=due_string)
return f"Task created: {task.content}"
def close_task(self, task_id: str) -> str:
"""Mark a task as complete by its ID."""
self.client.close_task(task_id=task_id)
return f"Task {task_id} marked as complete."
Initializing an agent with one or more toolkits is then just:
agent = Agent(
model="anthropic/claude-sonnet-4-6",
system_prompt="You are a helpful personal assistant.",
toolkits=[TodoistToolkit(api_token="...")]
)
reply = agent.run_chat("What do I have left to do today?")
The Agent class collects the schemas from all toolkits and passes them to the API. When the model returns a tool call, it resolves the toolkit and method by name, executes it, appends the result, and loops until the model stops calling tools:
class Agent:
def __init__(self, model: str, system_prompt: str = None, toolkits: list[Toolkit] = None):
self.client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key="...")
self.model = model
self.toolkits = toolkits or []
self.tools = self._get_all_tools(self.toolkits)
self.messages = [{"role": "system", "content": system_prompt}] if system_prompt else []
def run_chat(self, message: str) -> str:
self.messages.append({"role": "user", "content": message})
response = self._call()
while response.choices[0].message.tool_calls:
self._process_tool_calls(response.choices[0].message.tool_calls)
response = self._call()
reply = response.choices[0].message.content
self.messages.append({"role": "assistant", "content": reply})
return reply
def _call(self):
return self.client.chat.completions.create(
model=self.model,
messages=self.messages,
tools=self.tools or None
)
def _get_all_tools(self, toolkits: list[Toolkit]) -> list[dict]:
tools = []
for toolkit in toolkits:
for tool in toolkit._get_tools():
tool["function"]["name"] = f"{toolkit.__class__.__name__}.{tool['function']['name']}"
tools.append(tool)
return tools
def _process_tool_calls(self, tool_calls):
for call in tool_calls:
class_name, method_name = call.function.name.split(".", 1)
for toolkit in self.toolkits:
if toolkit.__class__.__name__ == class_name:
result = toolkit._execute_function(method_name, json.loads(call.function.arguments))
self.messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})
Reading their code changed how I write mine.
Read open source code
This experience reminded me of something I think gets underrated as advice: read open source code. Not to copy it, but because the best engineers in the world wrote it. It’s one of the few places where you can see how clean, elegant, and efficient code actually looks in practice, not in a tutorial but in production, in a library used by tens of thousands of people. You learn more from one good codebase than from most courses.