Skip to content

Instantly share code, notes, and snippets.

@kleneway
Created September 12, 2024 22:07
Show Gist options
  • Save kleneway/923a04ebcceba35f897f813e3b1009b3 to your computer and use it in GitHub Desktop.
Save kleneway/923a04ebcceba35f897f813e3b1009b3 to your computer and use it in GitHub Desktop.
/**
* This is a version of a tool-calling behavior that implements a "phased scenario" style prompt
* for restaurant selection and reservation. The conversational agent guides the user through
* selecting a restaurant and making a reservation based on their preferences in phases.
*/
import { defineTool } from "@pioneersquarelabs/language-barrier";
import { defineBehaviorModule } from "@pioneersquarelabs/vdot";
import { z } from "zod";
/**
* This object keeps track of the preferences we learn about the user for restaurant selection.
*/
export type RestaurantPreferences = {
cuisine?: string;
location?: string;
priceRange?: string;
partySize?: number;
dateTime?: string;
};
/**
* This prompt sets the overall tone for the agent interaction.
*/
function basePrompt(): string {
return "You are a helpful assistant designed to help users select a restaurant and make a reservation. Your goal is to understand the user's preferences and assist them through the process. Pay close attention to the instructions section.";
}
/**
* This prompt refines the objective of the agent depending on what we know so far.
*/
function phaseSpecificInstructions(prefs: Readonly<RestaurantPreferences>): string {
if (!prefs.cuisine && !prefs.location && !prefs.priceRange && !prefs.partySize && !prefs.dateTime) {
return "# Instructions\nYour current goal is to learn the user's preferred cuisine, location, price range, party size, and desired date/time for the reservation. Determine these preferences through natural conversation without being too direct.";
}
if (!prefs.cuisine) {
return `# Current knowledge\n${formatKnowledge(prefs)}\n# Instructions\nFocus on learning the user's preferred cuisine. Ask questions that help uncover this preference naturally.`;
}
if (!prefs.location) {
return `# Current knowledge\n${formatKnowledge(prefs)}\n# Instructions\nFocus on learning the user's preferred location for dining. Ask questions that help uncover this preference naturally.`;
}
if (!prefs.priceRange) {
return `# Current knowledge\n${formatKnowledge(prefs)}\n# Instructions\nFocus on understanding the user's preferred price range for dining out. Ask questions that help uncover this preference naturally.`;
}
if (!prefs.partySize) {
return `# Current knowledge\n${formatKnowledge(prefs)}\n# Instructions\nFocus on determining the number of people in the user's party. Ask questions that help uncover this information naturally.`;
}
if (!prefs.dateTime) {
return `# Current knowledge\n${formatKnowledge(prefs)}\n# Instructions\nFocus on determining the user's preferred date and time for the reservation. Ask questions that help uncover this preference naturally.`;
}
return `# Current knowledge\n${formatKnowledge(prefs)}\n# Instructions\nYou have gathered all necessary preferences. Provide restaurant recommendations based on the user's preferences and offer to make a reservation.`;
}
function formatKnowledge(prefs: Readonly<RestaurantPreferences>): string {
return Object.entries(prefs)
.filter(([_, value]) => value !== undefined)
.map(([key, value]) => `${key}: ${value}`)
.join('\n');
}
/**
* This tool allows the agent to assert user preferences for restaurant selection.
*/
export const assertRestaurantPreferences = defineTool({
name: "assertRestaurantPreferences",
description: "Assert preferences that you have learned about the user for restaurant selection",
parameters: {
cuisine: z.string().describe("Preferred cuisine of the user").optional(),
location: z.string().describe("Preferred dining location of the user").optional(),
priceRange: z.string().describe("The preferred price range for dining").optional(),
partySize: z.number().describe("Number of people in the dining party").optional(),
dateTime: z.string().describe("Preferred date and time for the reservation").optional(),
},
});
/**
* This tool allows the agent to make a restaurant reservation.
*/
export const makeReservation = defineTool({
name: "makeReservation",
description: "Make a reservation at the selected restaurant",
parameters: {
restaurantName: z.string().describe("Name of the selected restaurant"),
dateTime: z.string().describe("Date and time of the reservation"),
partySize: z.number().describe("Number of people in the party"),
specialRequests: z.string().describe("Any special requests for the reservation").optional(),
},
});
/**
* This behavior module manages the conversational flow for restaurant selection and reservation.
*/
export const restaurantReservationBehavior = defineBehaviorModule({
toolbox: [assertRestaurantPreferences, makeReservation],
initialize: () => {
const initialPreferences: RestaurantPreferences = {};
return {
systemPrompt: [
basePrompt(),
phaseSpecificInstructions(initialPreferences),
].join("\n"),
toolChoice: "auto",
userData: initialPreferences,
options: {
model: "claude-3-5-sonnet-20240620",
tokenConstraints: { maxOutput: 1024 },
temperature: 0.3,
},
};
},
onToolResult: async (state, toolCall) => {
if (toolCall.name === assertRestaurantPreferences.name) {
let updated = false;
for (const [key, value] of Object.entries(toolCall.args)) {
if (value !== undefined) {
(state.userData as any)[key] = value;
updated = true;
}
}
if (updated) {
state.systemPrompt = [
basePrompt(),
phaseSpecificInstructions(state.userData),
].join("\n");
}
} else if (toolCall.name === makeReservation.name) {
// Here you would typically integrate with an actual reservation system
// For this example, we'll just update the system prompt to confirm the reservation
state.systemPrompt = `${basePrompt()}\n# Instructions\nA reservation has been made at ${toolCall.args.restaurantName} for ${toolCall.args.partySize} people on ${toolCall.args.dateTime}. Confirm the reservation details with the user and ask if they need anything else.`;
}
},
});
@kleneway
Copy link
Author

Here's the previous version:

/**
 * This is a version of a tool-calling behavior that implements a "phased scenario" style prompt.
 * In the scenario prompt, we're trying to fulfill some objective that looks like "filling out a form".
 * The _phased_ version of the scenario prompt updates the instructions in the system prompt as we go
 * to guide the LLM toward more specific sub-goals.
 *
 * See tools.ts for a simplified version of this.
 */
import { defineTool } from "@pioneersquarelabs/language-barrier";
import { defineBehaviorModule } from "@pioneersquarelabs/vdot";
import { z } from "zod";

/**
 * This is our object where we keep track of the details we learn about the user that we're talking
 * with.  The LLM is effectively "filling this out".
 */
export type UserDetails = {
  name?: string;
  age?: string;
};

/**
 * This is the prompt that sets the overall tone for the LLM interaction.
 */
function basePrompt(): string {
  return "You are a friendly conversational agent whose goal is to learn about a user.  Pay close attention to the instructions section.";
}

/**
 * This prompt refines the objective of the LLM detpending on what we know so far.
 */
function phaseSpecificInstructions(details: Readonly<UserDetails>): string {
  if (details.age == null && details.name == null) {
    return "# Instructions\nYour current goal is to learn the name and age of the user.  Figure it out but without being overbearing or asking questions that are too direct.";
  }
  if (details.age == null) {
    const knowledge = `# Current knowledge\n User name: + ${details.name}`;
    return `${knowledge}\n# Instructions\nFocus on learning the age of the user.  Ask questions that aren't too direct to learn it.`;
  }
  if (details.name == null) {
    const knowledge = `# Current knowledge\nUser age: ${details.age}`;
    return `${knowledge}\n# Instructions\nFocus on learning the age of the user.  Ask questions that aren't too direct to learn it.`;
  }
  return `# Current knowledge\nUser age: ${details.age}\nUser name: ${details.name}\n# Instructions\nYou have finished learning about the user.  Now you should just converse with them - maybe find out about their day!`;
}

/**
 * This is the tool that the LLM can call to assert user information.  Note that this tool doesn't
 * have any knowledge of the current phase - it always stays the same.
 */
export const assertUserDetails = defineTool({
  name: "assertUserDetails",
  description: "Assert details that you have learned about the user",
  parameters: {
    name: z.string().describe("name of the user").optional(),
    age: z
      .string()
      .describe(
        "The age of the user in whatever units they have provided you with",
      )
      .optional(),
  },
});

/**
 * This is the behavior module where we bring it all together.
 */
export const toolUsingBehavior = defineBehaviorModule({
  // Note that here we have the tool definition.  This will always be here.
  toolbox: [assertUserDetails],
  initialize: () => {
    const initialUserData: UserDetails = {};
    return {
      systemPrompt: [
        basePrompt(),
        phaseSpecificInstructions(initialUserData),
      ].join("\n"),
      toolChoice: "auto",
      // The as cast is necessary here because we're doing everything inline.
      userData: initialUserData,
      options: {
        model: "claude-3-5-sonnet-20240620",
        tokenConstraints: { maxOutput: 1024 },
        temperature: 0.2,
      },
    };
  },
  /**
   * This is where we process the results when our tool gets called.  Note that here, our tool call
   * has already been parsed and has the correct type.  As we go, we update the system prompt so that
   * the model is appropriately focused.
   */
  onToolResult: async (state, toolCall) => {
    /**
     * Now we're dynamically changing the prompt based on what the current state of the world is.
     */
    if (toolCall.name === assertUserDetails.name) {
      if (toolCall.args.age) {
        state.userData.age = toolCall.args.age;
        state.systemPrompt = [
          basePrompt(),
          phaseSpecificInstructions(state.userData),
        ].join("\n");
      }
      if (toolCall.args.name) {
        state.userData.name = toolCall.args.name;
        state.systemPrompt = [
          basePrompt(),
          phaseSpecificInstructions(state.userData),
        ].join("\n");
      }
    }
  },
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment