AXAR AI
Github
  • Welcome
  • Getting Started
    • Quickstart
    • Anatomy of an agent
  • Basics
    • Agent
    • Tool
    • Schema
    • Model
  • Advanced
    • Configuration
    • Telemetry
  • Examples
    • A real world agent
  • Resourcers
    • API reference
    • Github
    • Discord
    • LinkedIn
Powered by GitBook
On this page
  • Goal
  • Define the agent
  • Dynamic prompts
  • Define schema
  • Define tool
  • Wrapping up
  1. Getting Started

Anatomy of an agent

Create a more complex agent with dynamic prompts, tools, structured data, and dependency injection.

PreviousQuickstartNextAgent

Last updated 4 months ago

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).

Adding @inputand @output decorators might seem redundant, but it is necessary because TypeScript does not retain generics information at runtime.

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.

We have omitted the implementation of DatabaseConn for brevity.

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, or Neutral) 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,
    );
  }
}

As you can see, we have also defined the schema for ToolParams using the @schema annotation. This provides the agent with a clear understanding of the structure of the parameters required by the tool.

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 directory on our GitHub.

examples
Bank agent flow