Observed since 21 Oct, 11:02 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
aiide is a framework to build LLM copilots.
Features:
Let's start by installing the package.
pip install aiide
This also installs LiteLLM and Pandas by default. If you would like to use other LLM providers such as Anthropic or Google AI, please install the respective SDK as well.
The whole tutorial uses OpenAI models but it should work with all the popular LLM providers.
Now that aiide is installed, let's create a simple chatbot similar to ChatGPT free tier.
from aiide import Aiide
class Chatbot(Aiide):
def __init__(self):
self.setup(system_message="You are a helpful assistant.", model="gpt-4o-mini-2024-07-18")
agent = Chatbot()
while True:
user_input = input("\nSend a message: ")
if user_input == "exit":
break
for delta in agent.chat(user_message=user_input):
if delta["type"] == "text":
print(delta["delta"],end="")
Let's break down the code:
setupandchathas a lot of optional parameters that you can find while hovering over the method in your IDE. Some notable parameters forsetuparemodel,temperature,api_key, and any supported LiteLLM completion parameters. Similarly,chathas parameters such astools,stop_words,tool_choiceetc.
user_message can take a couple of types of inputs. It can be a string as you've just seen, it can be an image object(PIL.Image) it can be an array of strings and images.
Different ways to use user_message:
user_message = "What's the weather like in SF"
image = Image.open("image.jpg")
user_message = image
user_message = [image, "Annotate the attached image"]
A natural question for the above snippet is how do we track the chat history?
aiide has first-class support for memory. I found that handling OpenAI JSON based schema is cumbersome and error-prone. So, I had abstracted the chat history into a Pandas DataFrame.
messages is a pandas DataFrame that stores all the messages, tool calls and responses of a chat session.
You can access the memory of the chatbot by calling agent.messages in the above example.
The schema of the messages DataFrame is as follows:
| role | content | arguments | response |
|---|---|---|---|
| system | You are a helpful assistant | None | None |
| user | What's the weather like in SF | None | None |
| tool | {'name':'get_weather','id':'abc'} | {'location':'SF'} | {'temperature':'72'} |
| assistant | The weather is 72 degress right now. | None | None |
You can use the memory DataFrame to analyze and manipulate the chat history and the tool calls and responses.
Currently the LLM can respond with text in any format. Sometimes it thinks first, sometimes it will answer in code right away. What if we want to structure the output in a specific way?
Currently to my knowledge, OpenAI and Google AI supports structured outputs. aiide has a common interface for structured outputs.
Let's see how we can use structured outputs in aiide.
from aiide import Aiide
from aiide.schema import structured_outputs_gen, Str
class Chatbot(Aiide):
def __init__(self):
self.setup(system_message="You are a helpful assistant.", model="gpt-4o-mini-2024-07-18")
def structured_outputs(self):
return structured_outputs_gen(
name="chain_of_thought",
properties=[
Str(name="thinking", description="Use this field to think out loud. Breakdown the user's query, plan your response, etc."),
Str(name="response"),
],
required=["thinking", "response"],
)
agent = Chatbot()
while True:
user_input = input("\nSend a message: ")
if user_input == "exit":
break
for delta in agent.chat(user_message=user_input,json_mode=True):
if delta["type"] == "text":
print(delta["delta"],end="")
Test out the infamous question How many R letters are there in the word "strawberry"?
Now, let's break down the changes code:
structured_outputs_gen and Str from aiide.schema. Both of these will aide(heh) in defining the structured outputs json schema.I did not love pydantic for defining json schemas. I have observed that a lot of developers are omitting fields such as descriptions and enums while defining schemas with pydantic. It also does not have an easier way to dynamically change the schema. So, I have created a simple interface to define structured outputs that is as flexible as possible and also helps the developer understand what they can do for each type by the help of intellisense. Please checkout the aiide's JSON Schema for more information.
structured_outputs in Aiide that returns the structured output generator. The beauty of this is that you can define multiple structured outputs return the appropriate one based on the context.json_mode=True to the chat method. This will enable the structured outputs.Tools are the heart of aiide. They are the actions that the LLM can take. They can be as simple as a function call or as complex as a hierarchical LLM agents.
You define tools as classes and pass instances to the Aiide instance. The lifecycle of handling the tool calls, it's execution and feeding the response back into the LLM is all handled by aiide. You however, can still control the execution of the tool based on the values of deltas.
Let's see how we can define a tool in aiide.
import random
import json
from aiide import Aiide, Tool
from aiide.schema import tool_def_gen, Str
class WeatherTool(Tool):
def __init__(self, parent):
self.error = False
def tool_def(self):
return tool_def_gen(
name="get_current_weather",
description="Get the current weather in a given location",
properties=[
Str(
name="location",
description="The city and state, e.g. San Francisco, CA",
),
Str(name="unit", enums=["celsius", "fahrenheit"]),
],
)
def main(self, location, unit="default"): # type: ignore
if self.error:
return json.dumps({"error": 404})
else:
return json.dumps({"location": location, "temperature": random.randint(0, 100), "unit": unit})
class Agent(Aiide):
def __init__(self):
# passing the chatbot instance to the tool for bi-directional communication
self.weatherTool = WeatherTool(self)
self.setup(
system_message="You are a helpful assistant.",
)
agent = Agent()
for delta in agent.chat(
user_message="What's the weather like in San Francisco, Tokyo, and Paris?",
tools=[agent.weatherTool],
):
# printing response based on the type of delta
if delta["type"] == "text":
print(delta["delta"], end="")
if delta["type"] == "tool_call":
print("Tool called:", delta["name"], "with arguments:", delta["arguments"])
if delta["type"] == "tool_response":
print("Tool response for tool:", delta["name"], " with arguments:", delta["arguments"], "is:", delta["response"])
# changing the execution of the tool based on the context of the conversation
if delta["type"] == "tool_call" and "tokyo" in delta["arguments"].lower():
agent.weatherTool.error = True
else:
agent.weatherTool.error = False
Let's break down the code:
Tool from aiide and tool_def_gen and Str from aiide.schema. Tool is the base class for all tools in aiide. tool_def_gen is a tool definition generator that helps in defining the tool schema. Str is a string type that can have description, enums.WeatherTool that inherits from Tool. We define the __init__ method to initialize the tool. We define the tool_def method to define the tool schema. We finally define the main method to define the tool logic.chat method through the tools array parameter.text, tool_call, and tool_response. When type of delta is tool, the delta will have name and arguments keys for the tool called. After the tool is executed, the delta will have a type of tool_response and the response key will have the response of the tool.error in the tool instance based on the location. This way, you can control the execution of a tool based on the context of the conversation and the tool's state. A good example would be taking user's consent before executing code.The delta schema is as follows:
| Type | Keys |
|---|---|
| text | - delta: The text content of the delta |
| - content: The full content of the delta, including the text and any additional data | |
| tool_call | - name: The name of the tool being called |
| - arguments: The arguments passed to the tool | |
| tool_response | - name: The name of the tool that generated the response |
| - arguments: The arguments passed to the tool | |
| - response: The response generated by the tool |
As mentioned earlier, aiide has a simple interface to define JSON schemas. This is useful for defining structured outputs and tools that might change based on the context of the conversation.
Since aiide uses LiteLLM under the hood, all the schema definitions follow OpenAI's specification.
Defining OpenAI function definitions in python currently has two ways
JSON SCHEMA
Defining schemas in python is very verbose and has no intellisense.
for example:
{
"type": "function",
"function": {
"name": "add_or_modify_form_values",
"parameters": {
"type": "object",
"properties": {
"field_name": {"type": "string"},
"value": {"type": "string"},
},
},
"description": "tool to add or modify field values",
},
"required":["field_name","value"]
}
Pydantic
Since Pydantic is meant mostly for defining and validating types, the class based structure makes it just as verbose. It gets more challenging when defining nested structures since each level requires a new class and also changing parts of the schema such as enums, required fields etc. on the fly is not as easy.
Our solution
aiide has the following types defined
from aiide.schema import tool_def_gen, structured_outputs_gen, Num, Float, Str, Bool, Array, Object, AnyOf, Nullable
Here is the implementation of the above tool definition with aiide.schema
The above tool has two input attributes
tool_def_gen(
name = "add_or_modify_form_values",
description = "tool to add or modify field values",
tool_def_gen(
name = "add_or_modify_form_values",
description = "tool to add or modify field values",
properties = [
Str(
name = "field_name",
enums=self.parent.df["Field"].values.tolist()
),
Str(
name = "value"
)
]
)
There are a couple of advantages to using aiide's schema definitions
There is currently one disadvantage to using aiide's schema definitions which is the lack of validation of the generated schema. Right now, we are trusting the API provider to provide the correct schema but we will be adding that feature in the future.
Complex example
{
"type": "function",
"function": {
"name": "add_or_modify",
"description": "tool to add or modify field values",
"parameters": {
"type": "object",
"properties": {
"add_or_update": {
"type": "array",
"description": "Add values for one or more placeholders at a time",
"items": {
"type": "object",
"name":"nest",
"properties": {
"field": {
"type": "string",
"description": "Field name you are adding or modifying a value",
},
"value": {
"type": "string",
"description": "Value of the field",
},
},
},
}
},
"required": ["add_or_update"],
},
},
}
tool_def_gen(
name = "add_or_modify",
description = "tool to add or modify field values",
Now let's define the properties which takes in a list of tool objects. Here we only have one object which itself is a Array(I know, confusing but it's true)
tool_def_gen(
name = "add_or_modify",
description = "tool to add or modify field values",
properties=[Array(name="add_or_update",description="Add values for one or more placeholders at a time")]
The Array also takes an item argument which can be any tool object. In our case it's a Object with two keys
tool_def_gen(
name="add_or_modify",
description="tool to add or modify field values",
properties=[
Array(
name="add_or_update",
description="Add values for one or more placeholders at a time",
item=Object(
name="nest",
properties=[
Str(
name="field",
description="Field name you are adding or modifying a value",
),
Str(name="value", description="Value of the field"),
],
),
)
],
)
If you are relatively new to JSON Schema, you might not have come across AnyOf and Nullable. Here is a quick explanation:
Tracking tokens and API costs can be a pain. aiide has a simple interface to track the tokens and the costs of the API calls.
from aiide import Aiide
class Chatbot(Aiide):
def __init__(self):
self.setup(system_message="You are a helpful assistant.", model="gpt-4o-mini")
agent = Chatbot()
while True:
user_input = input("\nSend a message: ")
if user_input == "exit":
break
for delta in agent.chat(user_message=user_input):
if delta["type"] == "text":
print(delta["delta"],end="")
print("Usage",agent.usage)
agent.usage will return a dictionary with the following keys:
| Name | Description |
|-------------------|--------------------------------------------------|
| prompt_tokens | Number of tokens in the prompt |
| completion_tokens | Number of tokens in the completion |
| usd | Cost of the API call in USD |
It works with Images, Tools and Structured Outputs.
Since all of the documentation is in the README of the aiide repository, you can pass this file to an LLM as context to help you write aiide copilots with ease.
Here is an example of how it works: Link to Gist
aiide has taken inspiration and code from the following repositories:
pypistats · 42 observations shown. Missing days are unknown. Source category: overall: without mirrors.
| Date / source period | Value | Observed at |
|---|---|---|
| 2026-10-20 – 2026-10-20 | 1 | 2026-10-21T12:20:50.0654167Z |
| 2026-10-18 – 2026-10-18 | 1 | 2026-10-21T12:20:50.0654167Z |
| 2026-10-17 – 2026-10-17 | 3 | 2026-10-21T12:20:50.0654167Z |
| 2026-10-13 – 2026-10-13 | 1 | 2026-10-21T12:20:50.0654167Z |
| 2026-10-10 – 2026-10-10 | 14 | 2026-10-21T12:20:50.0654167Z |
| 2026-10-09 – 2026-10-09 | 1 | 2026-10-21T12:20:50.0654167Z |
| 2026-10-08 – 2026-10-08 | 2 | 2026-10-21T12:20:50.0654167Z |
| 2026-10-06 – 2026-10-06 | 1 | 2026-10-21T12:20:50.0654167Z |
| 2026-10-05 – 2026-10-05 | 1 | 2026-10-21T12:20:50.0654167Z |
| 2026-10-04 – 2026-10-04 | 1 | 2026-10-21T12:20:50.0654167Z |
| 2026-10-02 – 2026-10-02 | 2 | 2026-10-21T12:20:50.0654167Z |
| 2026-09-30 – 2026-09-30 | 2 | 2026-10-21T12:20:50.0654167Z |
| 2026-09-28 – 2026-09-28 | 3 | 2026-10-21T12:20:50.0654167Z |
| 2026-09-27 – 2026-09-27 | 1 | 2026-10-21T12:20:50.0654167Z |
| 2026-09-26 – 2026-09-26 | 1 | 2026-10-21T12:20:50.0654167Z |
| 2026-09-24 – 2026-09-24 | 1 | 2026-10-21T12:20:50.0654167Z |
| 2026-09-23 – 2026-09-23 | 1 | 2026-10-21T12:20:50.0654167Z |
| 2026-09-22 – 2026-09-22 | 7 | 2026-10-21T12:20:50.0654167Z |
| 2026-09-12 – 2026-09-12 | 1 | 2026-10-21T12:20:50.0654167Z |
| 2026-09-08 – 2026-09-08 | 15 | 2026-10-21T12:20:50.0654167Z |
| 2026-09-07 – 2026-09-07 | 3 | 2026-10-21T12:20:50.0654167Z |
| 2026-09-06 – 2026-09-06 | 1 | 2026-10-21T12:20:50.0654167Z |
| 2026-09-01 – 2026-09-01 | 3 | 2026-10-21T12:20:50.0654167Z |
| 2026-08-31 – 2026-08-31 | 1 | 2026-10-21T12:20:50.0654167Z |
| 2026-08-28 – 2026-08-28 | 1 | 2026-10-21T12:20:50.0654167Z |
| 2026-08-27 – 2026-08-27 | 1 | 2026-10-21T12:20:50.0654167Z |
| 2026-08-26 – 2026-08-26 | 1 | 2026-10-21T12:20:50.0654167Z |
| 2026-08-25 – 2026-08-25 | 1 | 2026-10-21T12:20:50.0654167Z |
| 2026-08-24 – 2026-08-24 | 6 | 2026-10-21T12:20:50.0654167Z |
| 2026-08-23 – 2026-08-23 | 3 | 2026-10-21T12:20:50.0654167Z |
| 2026-08-21 – 2026-08-21 | 1 | 2026-10-21T12:20:50.0654167Z |
| 2026-08-20 – 2026-08-20 | 3 | 2026-10-21T12:20:50.0654167Z |
| 2026-08-17 – 2026-08-17 | 2 | 2026-10-21T12:20:50.0654167Z |
| 2026-08-15 – 2026-08-15 | 2 | 2026-10-21T12:20:50.0654167Z |
| 2026-08-14 – 2026-08-14 | 3 | 2026-10-21T12:20:50.0654167Z |
| 2026-08-09 – 2026-08-09 | 10 | 2026-10-21T12:20:50.0654167Z |
| 2026-08-02 – 2026-08-02 | 1 | 2026-10-21T12:20:50.0654167Z |
| 2026-08-01 – 2026-08-01 | 2 | 2026-10-21T12:20:50.0654167Z |
| 2026-07-31 – 2026-07-31 | 1 | 2026-10-21T12:20:50.0654167Z |
| 2026-07-27 – 2026-07-27 | 1 | 2026-10-21T12:20:50.0654167Z |
| 2026-07-25 – 2026-07-25 | 4 | 2026-10-21T12:20:50.0654167Z |
| 2026-07-23 – 2026-07-23 | 5 | 2026-10-21T12:20:50.0654167Z |
| Version | Published | Direct dependencies | Open version |
|---|---|---|---|
| 0.0.8 | 7 Jan 2025 | 4 |
This describes our collected data; it is not a security assessment.
Observed since 21 Oct, 11:02 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package:293766454632a44cbea069334f02c1c613fad338696a7c922c268086fe01c331