Anatomy of an agent
Create a more complex agent with dynamic prompts, tools, structured data, and dependency injection.
Goal
Our goal for this agent is to demonstrate how to automate customer support for a bank (albeit within a limited context). The agent interacts with customers, analyzes their queries, and generates meaningful responses. It showcases how an agent can integrate with a database to retrieve customer-specific information, such as their name and account balance. It also demonstrates how an agent can return a structured response.
Define the agent
The first step is to define the agent. In AXAR, an agent is a subclass of Axar's built-in Agent
class.
When defining an agent, we need to specify:
The LLM the agent should use: This is done using the
@model
decorator.A high-level prompt or instruction for the agent: This is specified using the class-level
@systemPrompt
decorator.Its input and output format: These are defined using the
@input
and@output
annotations (along with generics provided in the class declaration).
We also want to provide the agent with access to the customerId
and a database connection. This is achieved using simple dependency injection through the agent's constructor.
With that our SupportAgent
now look like this:
// Specify the AI model used by the agent (e.g., OpenAI GPT-4 mini version).
@model('openai:gpt-4o-mini')
// Provide a system-level prompt to guide the agent's behavior and tone.
@systemPrompt(`
You are a support agent in our bank.
Give the customer support and judge the risk level of their query.
Reply using the customer's name.
`)
// Define the expected output format of the agent.
@output(SupportResponse)
export class SupportAgent extends Agent<string, SupportResponse> {
// Initialize with a customer ID and a DB connection to fetch any customer-specific data.
constructor(
private customerId: number,
private db: DatabaseConn,
) {
super();
}
// More to be implemented...
}
If an agent only works with plain strings as input or output, we don’t need to use the @input
or @output
annotations respectively. For example, in the code above, there is no @input
annotation because the input is a simple string.
Dynamic prompts
Now that the agent has access to the customerId
and the database, we want it to dynamically insert customer-specific context at runtime. To achieve this, we use the @systemPrompt
decorator with an instance method inside the agent class.
The purpose of this method is to retrieve the customer's name from the database and include it as part of the agent's context. This way, whenever the agent processes a customer query, it already has the relevant context available.
Here’s how it looks:
// ...
export class SupportAgent extends Agent<string, SupportResponse> {
// ...
// Provide additional context for the agent about the customer's details.
@systemPrompt()
async getCustomerContext(): Promise<string> {
// Fetch the customer's name from the database and provide it as context.
const name = await this.db.customerName(this.customerId);
return `The customer's name is '${name}'`;
}
}
Define schema
As you may have noticed, we have yet to define the agent's response schemaSupportResponse
. For this support agent, we want the response to include:
A support advice: A string containing human-readable advice for the customer.
Risk level: A number between 0 and 1 representing the risk level of the customer query.
Card blocking: A boolean indicating whether blocking the customer’s card is necessary.
Customer’s emotional state: An optional field using an enum (
Happy
,Sad
, orNeutral
) to represent the customer’s emotional state.
Since this schema is for an LLM, we want to provide not just the structure of the response but also additional metadata such as descriptions and validation rules. In AXAR, we use annotations to define schemas with this metadata.
Here’s how we define the SupportResponse
schema:
@schema()
export class SupportResponse {
@property('Human-readable advice to give to the customer.')
support_advice!: string;
@property("Whether to block customer's card.")
block_card!: boolean;
@property('Risk level of query')
@min(0)
@max(1)
risk!: number;
@property("Customer's emotional state")
@optional()
status?: 'Happy' | 'Sad' | 'Neutral';
}
Define tool
What if an agent needs to perform an action or gather additional data based on a request? This is where tools become valuable. Tools allow an agent to access specific functionality, which they can invoke only when needed. In AXAR, tools are defined as instance methods and annotated with @tool
.
For this agent, we define a tool that takes a parameter of type ToolParams
(containing the customer's name and whether to include pending transactions) and returns the customer's bank balance.
Here’s how it looks:
@schema()
class ToolParams {
@property("Customer's name")
customerName!: string;
@property('Whether to include pending transactions')
@optional()
includePending?: boolean;
}
// ...
export class SupportAgent extends Agent<string, SupportResponse> {
// ...
// Define a tool (function) accessible to the agent for retrieving the customer's balance.
@tool("Get customer's current balance")
async customerBalance(params: ToolParams): Promise<number> {
// Fetch the customer's balance, optionally including pending transactions.
return this.db.customerBalance(
this.customerId,
params.customerName,
params.includePending ?? true,
);
}
}
Wrapping up
This guide covered how to build a functional, context-aware agent using AXAR AI. With dynamic prompts, schema definitions, and tools, you can create agents tailored to specific tasks and workflows. From here, you can explore more advanced use cases or refine the approach as needed for your applications.
The full example code is available in the examples directory on our GitHub.
Last updated