The Upstreet Agents SDK is now in public beta πŸŽ‰Β Get started β†’

Action Agent (Personal Assistant)

This section describes how to build your own personal assistant Agent with Upstreet and the Google Calendar API, using custom React Hooks.

In this guide, we build an Action Agent capable of scheduling events on our Google Calendar for us, using the Google Calendar API. We use custom React Hooks in this example - We want to use nice, clean coding practices.

We define an Action Agent as an Agent which can take actions on behalf of you.

The source code for this example is available on GitHub.

Guide

Step 1: Setup usdk

Follow Setup the SDK to set up NodeJS and usdk.

Step 2: Initialize your agent

Create a new agent:

usdk create <your-agent-directory> -y

This will directly scaffold an agent for you in <your-agent-directory>. Learn more

Your agent directory now contains the Node application and git repository for your agent, should you choose to use git.

The -y flag means to skip the Agent Interview process, which we don't need here. You can also omit the agent directory. In that case, a directory will be created for you.

Step 3: Create a PersonalAssistant Component

Why manage our calendar manually when an AI agent can handle the task for us? We can easily build an Upstreet Agent to handle Calendar management, reducing scheduling conflicts and saving time.

This example, however, will be very simple. We want our Agent to be able to schedule a Google Calendar Event for us.

agent.tsx
const PersonalAssistant = () => {
  // We'll add functions, useState, useEffect here
 
  return <>{/* We can add components here to compose our Agent  */}</>
}

The PersonalAssistant component is just an empty wrapper component for now - it will later utilize a GoogleCalenderManager class to interact with the Google Calendar API, allowing users to create Calendar events programmatically.

Step 4: Using custom Hooks and better practices

In agent-renderer.tsx file, inside the AgentRenderer class, we can make a custom Hook called useCalendarKeysJson:

/packages/upstreet-agent/packages/react-agents/classes/agent-renderer.tsx
const useEnvironment = () => {
  return (env as any).WORKER_ENV as string
}
// place here below useEnvironment function
const useCalendarKeysJson = () => { 
  const CalenderKeysJsonString = (env as any).CALENDER_KEYS_JSON as string
  const CalenderKeysJson = JSON.parse(CalenderKeysJsonString) 
  return CalenderKeysJson 
} 

In the same file, there is AppContextValue mention. Make the below modification in your code.

this.appContextValue = new AppContextValue({
  subtleAi,
  agentJson: useAgentJson(),
  CalenderKeysJson: useCalendarKeysJson(), 
  environment: useEnvironment(),
  wallets: useWallets(),
  authToken: useAuthToken(),
  supabase: useSupabase(),
  conversationManager: useConversationManager(),
  chatsSpecification: useChatsSpecification(),
  codecs: useCodecs(),
  registry: useRegistry()
})

Now make some changes in app-value-context.tsx file's AppContextValue class:

/packages/upstreet-agent/packages/react-agents/classes/app-context-value.ts
export class AppContextValue {
  subtleAi: SubtleAi
  agentJson: object
  calendarKeysJson: object
  // other code remain same
 
  constructor({
    subtleAi,
    agentJson,
    CalenderKeysJson
    // other code remain same
  }: {
    subtleAi: SubtleAi
    agentJson: object
    CalenderKeysJson: object
    // other code remain same
  }) {
    this.subtleAi = subtleAi
    this.agentJson = agentJson
    this.CalenderKeysJson = CalenderKeysJson 
    // other code remain same
  }
}

In the same file, add theuseCalendarKeysJson custom hooks:

 
  useAgentJson() {
    return this.agentJson;
  }
 
  useCalendarKeysJson() { 
    return this.CalenderKeysJson; 
  } 
 
  // other code remain same

Now add useCalendarKeysJson in hooks.ts file.

/packages/upstreet-agent/packages/react-agents/hooks.ts
export const useAgent = () => {
  const agentContextValue = useContext(AgentContext)
  return agentContextValue
}
 
export const useCalendarKeysJson = () => { 
  const agentContextValue = useContext(AgentContext) 
  return agentContextValue.appContextValue.useCalendarKeysJson() 
} 
 
// other code remain same

You can now use useCalendarKeysJson as a Hook in your PersonalAssistant Component.

Step 5: Integrating the Google Calendar API

Let's build our GoogleCalendarManager, which will leverage a service account for authentication and handling token generation, event creation, and error handling.


First, you'll need some Google Calendar API credentials:

  • Calendar ID
  • API Key
  • Service Account Email
  • Private Key

πŸ”‘ Need help getting these? Check out the Google Calendar API docs.

Add them to your wrangler.toml:

wrangler.toml
...
CALENDER_KEYS_JSON = "{\"GOOGLE_API_KEY\":\"\",\"GOOGLE_SERVICE_ACCOUNT_EMAIL\":\"\",\"GOOGLE_PRIVATE_KEY\":\"",\"GOOGLE_CALENDAR_ID\":\"\"}"
...

Let's get back to the code.


This code provides an integration with the Google Calendar API by implementing a class called GoogleCalenderManager.

agent.tsx
 
// Import all the required modules
import { 
  Action, 
  Agent, 
  PendingActionEvent, 
  useCalendarKeysJson 
} from 'react-agents'
import { z } from 'zod'
 
// integrating the Google Calendar API
interface CalenderEvent {
  summary: string
  description: string
  start: { dateTime: string } 
  end: { dateTime: string } 
} 
 
class GoogleCalenderManager {
  private readonly GOOGLE_Calender_ID: string
  private readonly GOOGLE_API_KEY: string
  private readonly GOOGLE_SERVICE_ACCOUNT_EMAIL: string
  private readonly GOOGLE_PRIVATE_KEY: string
  constructor({ 
    GOOGLE_Calender_ID, 
    GOOGLE_API_KEY, 
    GOOGLE_SERVICE_ACCOUNT_EMAIL, 
    GOOGLE_PRIVATE_KEY
  }: {
    GOOGLE_Calender_ID: string
    GOOGLE_API_KEY: string
    GOOGLE_SERVICE_ACCOUNT_EMAIL: string
    GOOGLE_PRIVATE_KEY: string
  }) {
    this.GOOGLE_Calender_ID = GOOGLE_Calender_ID 
    this.GOOGLE_API_KEY = GOOGLE_API_KEY
    this.GOOGLE_SERVICE_ACCOUNT_EMAIL = GOOGLE_SERVICE_ACCOUNT_EMAIL
    this.GOOGLE_PRIVATE_KEY = GOOGLE_PRIVATE_KEY
  } 
 
  private async getAccessToken(): Promise<string> {
    const now = Math.floor(Date.now() / 1000) 
    const expiry = now + 3600 // Token valid for 1 hour //
    const jwtHeader = btoa(JSON.stringify({ alg: 'RS256', typ: 'JWT' })) 
    const jwtClaimSet = btoa(
      JSON.stringify({
        iss: this.GOOGLE_SERVICE_ACCOUNT_EMAIL, 
        scope: 'https://www.googleapis.com/auth/Calendar', 
        aud: 'https://oauth2.googleapis.com/token', 
        exp: expiry, 
        iat: now 
      }) 
    ) 
 
    const signatureInput = `${jwtHeader}.${jwtClaimSet}`
    const signature = await this.signJwt(signatureInput) 
    const jwt = `${signatureInput}.${signature}`
    const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
      method: 'POST', 
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 
      body: `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${jwt}`
    }) 
    const tokenData = await tokenResponse.json() 
    return tokenData.access_token 
  } 
 
  private async signJwt(input: string): Promise<string> {
    const pemHeader = '-----BEGIN PRIVATE KEY-----'
    const pemFooter = '-----END PRIVATE KEY-----'
    const pemContents = this.GOOGLE_PRIVATE_KEY.substring(
      this.GOOGLE_PRIVATE_KEY.indexOf(pemHeader) + pemHeader.length, 
      this.GOOGLE_PRIVATE_KEY.indexOf(pemFooter) 
    ).replace(/\s/g, '') 
    const binaryDer = this.base64StringToArrayBuffer(pemContents) 
    const cryptoKey = await crypto.subtle.importKey(
      'pkcs8', 
      binaryDer, 
      {
        name: 'RSASSA-PKCS1-v1_5', 
        hash: 'SHA-256'
      }, 
      false, 
      ['sign'] 
    ) 
 
    const encoder = new TextEncoder() 
    const signatureBuffer = await crypto.subtle.sign(  
      'RSASSA-PKCS1-v1_5', 
      cryptoKey, 
      encoder.encode(input) 
    ) 
    const signatureArray = new Uint8Array(signatureBuffer) 
    return btoa(String.fromCharCode.apply(null, signatureArray)) 
      .replace(/=/g, '') 
      .replace(/\+/g, '-') 
      .replace(/\//g, '_') 
  } 
 
  private base64StringToArrayBuffer(base64: string): ArrayBuffer {
    const binaryString = atob(base64) 
    const bytes = new Uint8Array(binaryString.length) 
    for (let i = 0; i < binaryString.length; i++) { 
      bytes[i] = binaryString.charCodeAt(i) 
    } 
    return bytes.buffer 
  } 
  async setCalenderEvent(event: CalenderEvent): Promise<string> { 
    console.log('Creating event:', event) 
    const accessToken = await this.getAccessToken() 
    const response = await fetch( 
      `https://www.googleapis.com/Calendar/v3/Calenders/${this.GOOGLE_Calender_ID}/events?key=${this.GOOGLE_API_KEY}`, 
      { 
        method: 'POST', 
        headers: {
          Authorization: `Bearer ${accessToken}`, 
          'Content-Type': 'application/json'
        }, 
        body: JSON.stringify(event) 
      } 
    ) 
    console.log(response) 
    if (!response.ok) {  
      const errorText = await response.text() 
      throw new Error(`Failed to create event: ${errorText}`) 
    } 
    const result = await response.json() 
    console.log('Event created:', result) 
    return `Event created: ${result.htmlLink}`
  } 
} 

Breakdown summary of the GoogleCalenderManager Class and its functions

  1. Constructor: Initializes the GoogleCalenderManager with Google API credentials (GOOGLE_Calender_ID, GOOGLE_API_KEY, GOOGLE_SERVICE_ACCOUNT_EMAIL, GOOGLE_PRIVATE_KEY).
  2. getAccessToken: Generates an OAuth2 access token using a JWT for authorizing Google Calendar API requests.
  3. signJwt: Signs a JSON Web Token (JWT) using the private key for secure authorization.
  4. base64StringToArrayBuffer: Converts a base64-encoded string into an ArrayBuffer, which is used for cryptographic operations.
  5. setCalenderEvent: Posts a new event to the specified Google Calendar using the access token and provided event details.

Step 6: Initialize the GoogleCalenderManager instance

Now let's modify the PersonalAssistant component.

In the code snippet, the credentials are being fetched using useCalendarKeysJson() and are used to initialize the GoogleCalenderManager instance.

agent.tsx
const PersonalAssistant = () => {

  // get credentials from wrangler.toml
  const CalenderKeysJson = useCalendarKeysJson() 
 
  const googleCalenderManager = new GoogleCalenderManager({ 
    GOOGLE_Calender_ID: CalenderKeysJson.GOOGLE_Calender_ID, 
    GOOGLE_API_KEY: CalenderKeysJson.GOOGLE_API_KEY, 
    GOOGLE_SERVICE_ACCOUNT_EMAIL: CalenderKeysJson.GOOGLE_SERVICE_ACCOUNT_EMAIL, 
    GOOGLE_PRIVATE_KEY: CalenderKeysJson.GOOGLE_PRIVATE_KEY
  }) 
 
  return <>{/* We can add components here to compose our Agent  */}</>
}

Now we'll use the <Action> tag to define how the Agent should respond to the default text perception.

return (
  <>
    <Action
      name="setCalenderEvent"
      description="Sets a new event in the user's Google Calendar."
      schema={ 

        // update according to https://developers.google.com/Calendar/api/v3/reference/events
        z.object({ 

          summary: z.string(), 
          startDateTime: z.string(), 
          endDateTime: z.string(), 
          description: z.string() 
        })
      } 
      examples={[   
        {
          summary: 'Meeting with John Doe', 
          startDateTime: '2023-06-15T10:00:00-07:00', 
          endDateTime: '2023-06-15T11:00:00-07:00', 
          description: 'Discuss the project timeline and requirements.'
        } 
      ]} 
      handler={async (e: PendingActionEvent) => { 
        const { summary, description, startDateTime, endDateTime } = e.data 
          .message.args as { 
          summary: string
          description: string
          startDateTime: string
          endDateTime: string
        } 
        const event = {
          summary, 
          description, 
          start: { dateTime: startDateTime }, 
          end: { dateTime: endDateTime } 
        } 
        await googleCalenderManager.setCalenderEvent(event) 
      }} 
    />
  </>
)

Breakdown summary of this <Action> Component

  1. Purpose of the <Action> Component

<Action> components define specific actions that your agent can perform in response to user inputs. Learn more

This component is used to define a specific action that can be triggered, in this case, setting an event in Google Calendar.

  1. Defining Action Properties

    Each <Action> is structured with the following properties:

    • name: A unique identifier for the action. Example: 'setCalenderEvent'.
    • description: Explains what the action does. In this case, it sets a new event in the user's Google Calendar.
    • schema: Specifies the input structure for the action, defined using a zod schema. The schema expects the event's summary (summary), start date and time (startDateTime), end date and time (endDateTime), and a description (description), all of which must be strings.
    • examples: Provides sample inputs to guide the agent’s behavior. Example: { summary: 'Meeting with John Doe', startDateTime: '2023-06-15T10:00:00-07:00', endDateTime: '2023-06-15T11:00:00-07:00', description: 'Discuss project timeline and requirements.' }.
  2. handler: The Action's Core Logic

    The handler function is the core of the action. This function contains the logic that will be executed when the action is triggered. In this case, the action is to create a new event in the user's Google Calendar. Here's a breakdown:

    • PendingActionEvent: The handler receives an event object of type PendingActionEvent, which contains all the data and context for the action being triggered. This event object has a data field, which holds the message.args. The args will contain the arguments passed when the action was triggered.
    • Destructuring: Inside the handler, the event data (e.data.message.args) is destructured into the specific fields: summary, description, startDateTime, and endDateTime. These correspond to the values passed when the action was triggered.
    • Event Creation: Once the necessary data is extracted, an event object is created.

    This object is structured according to the Google Calendar API's expected format:

    • Calling googleCalenderManager.setCalenderEvent: The googleCalenderManager.setCalenderEvent(event) method is then called to create the event in Google Calendar. This method is asynchronous, so the await keyword is used to ensure that the event is created before proceeding.

Step 7: Test out your PersonalAssistant Agent!

You can run usdk chat to test it out. Learn more

You can ask it questions like:

Schedule my meeting with Steve on 15 November at 10 PM.

The source code for this example is available on GitHub.

Share its response in our Discord community; we'd love to know what it responds to you.