picoflow.io

Lesson 8: Compare Step

This step can compare two or more hotels on these attributes: price,room type,amenities,distance.


Screen shots

The screenshots of the comparisons are as follows: compare price


compare amenities


compare room types


compare distance


Prompt Explanation

  1. This prompt use a technique to ask LLM to maintain its mini states within the conversation. This will simplify tracking these states at code level. However, the trade-off is LLM may not 100% reliably observing all the rules.
  2. We have to prompt variables that are obtain from previous steps and use it as additional context: {{ChosenHotels}} and {{AvailableHotels}}
  3. States 2 and 3 are house keeping.
  4. State 4 imposes some logics into LLM for this step.

Full step prompt

## State Machine Prompt for Hotel Selection and Comparison
### Variable
  - ** `ChosenHotels` = {{ChosenHotels}}
  - ** `AvailableHotels` = {{AvailableHotels}}

### Instructions
  - ** If user wants to compare hotels, go to `State 1`
  - ** If user chooses a feature, hotels are selected in `ChosenHotels`, to to `State 2`
  
### State 1: Present List of Hotels and Collect Selection
- **Examine Variable `ChosenHotels`**
  - if already set, go to `State 2`
- **Responsibility:**
  - Display a list of available hotels show with number bullets from `AvailableHotels`
  - Request the user to select one or more hotels from the list for comparison.
  - Collect and confirm the user's selection(s).
  - Action Example: `Here are some hotels available for you, please choose one or more hotels by typing the name(s) or number(s) from the list`:
    1. Hotel A
    2. Hotel B
    3. Hotel C
    4. Hotel D

### State 2: Collect Hotel Feature Selection
- **Responsibility:**
  - Once a hotel has been selected, prompt the user to provide ONLY one feature they want to compare for the selected hotels.
  - Provide a list of features for the user to choose from:
  - Action Example: 
    - `Great choice! Now, let's get more details. Please select one of the following features to compare for your hotel`:
      1. `price`
      2. `room type`
      3. `amenities`
      4. `distance`
  - Feature Mapping, below is a map of what will be set the  tool `generate_comparison` property `feature`
      1. `price` ==> `price`
      2. `room type` ==> `roomType`
      3. `amenities` ==> `amenities`
      4. `distance` ==> `distance`
  
### State 3: Confirm Selections and Offer Re-selection
  - **Responsibility:** 
    - Confirm that both the hotel(s) and the selected feature have been gathered successfully.
    - Provide the user with an option 
      - to  proceed with the comparison 
      - or re-select a different set hotels for comparison
      - or re-select a different feature for comparison
      - or resume to hotel booking
    - Action Example:
      - Here is what you’ve selected:
      - Hotel(s): [Selected Hotels]
      - Feature: [Selected Feature]

      - Would you like to proceed with this comparison or:
        1. Re-select a different hotel set
        2. Re-select a different feature
        3. Proceed with the comparison
        4. Exit comparison, resume hotel booking

### State 4: Generate Comparison
  - **Responsibility:**
    - If the user chooses to proceed to compare, call the tool `generate_comparison`, set property `hotels` to selected hotel and property `feature` to selected feature.
    - If user chooses to re-select hotels,  return to `State 1`.
    - If user chooses to re-select a feature, go to `State 2`.
    - If the user chose to exit comparison, resume or go back to the hotel booking, call the `resume_booking` tool

Code Explanation

  1. We provides two tools one to capture the comparison selection and generate the comparison chart, another to resume to the PresentStep to finish the booking.
  2. in generate_comparison, a markdown formatted char is generated base on the comparison criteria.
  3. We also read and save the internal step state.
    const hotelAvailable = this.flow.getStepState(
      PresentStep,
      'hotelFound',
    ) as object[];
    ---
    this.saveState({ chosen_hotels: finalHotels }, true);
  1. Last, we output the markdown result directly to the user without invoking a second LLM call by doing this:
  const table = GenChart.getChart(finalHotels);
  const msg = new AiMessageEx(this, table, { direct: true });
  return { step: CompareStep, message: msg };

the extra payload direct: true is specifying the last message to be directly displayed to a user.


Full step code:

const ComparePrompt = Prompt.file('prompt/compare.md');
eexport class CompareStep extends Step {
  constructor(flow: Flow, isActive?: boolean) {
    super(CompareStep, flow, isActive);
  }

  public async run(message: string): Promise<MessageContent> {
    return await super.run(message);
  }

  protected onActivate(flag: boolean) {
    if (flag) {
      this.eraseMemory();
    }
  }

  public onCrossStep(_langMessage: MessageTypes): MessageTypes {
    return new HumanMessageExt(this, 'What hotels choice I have');
  }

  public getPrompt(): string {
    const chosen_hotels = (this.getState('chosen_hotels') as []) ?? [];
    const available_hotel = this.getState(`available_hotel`) ?? [];

    const hotels = chosen_hotels.map((entry) => {
      return { hotelName: entry['hotelName'] };
    });

    const prompt = Prompt.replace(ComparePrompt, {
      ChosenHotels: JSON.stringify(hotels),
      AvailableHotels: JSON.stringify(available_hotel),
    });

    return prompt;
  }

  public defineTool(): ToolType[] {
    return [
      {
        name: 'generate_comparison',
        description: 'Call to generate comparison',
        schema: z.object({
          hotels: z.array(z.string()).describe('Hotels name chosen'),
          feature: z.string().describe('chosen feature'),
        }),
      },
      {
        name: 'resume_booking',
        description: 'Resume to booking',
        schema: z.object({
          isResumed: z.boolean().describe('is resume booking'),
        }),
      },
    ];
  }
  public getTool(): string[] {
    return ['generate_comparison', 'resume_booking'];
  }

    protected async generate_comparison(
    tool: ToolCall,
  ): Promise<ToolResponseType> {
    //perform a hotel search
    let chosenHotels;
    try {
      chosenHotels = JSON.parse(tool.args?.hotels);
    } catch (_e) {
      chosenHotels = tool.args?.hotels;
    }

    const feature = tool.args?.feature;
    // console.log(`feature: ${feature}`);

    //find the full hotel doc from DB
    const fetchHotels = (await PricingEngine.fetchHotels(
      chosenHotels,
    )) as object[];

    //merge the price into the chosenHotels JSON
    const hotelAvailable = this.flow.getStepState(
      PresentStep,
      'hotelFound',
    ) as object[];

    let finalHotels = fetchHotels.map((doc) => {
      for (const aHotel of hotelAvailable) {
        if (aHotel['hotelName'] === doc['hotelName']) {
          const myFeatures = {};
          merge(myFeatures, { hotelName: doc['hotelName'] });
          if (feature === 'amenities') {
            merge(myFeatures, GenChart.flattenObject(doc['amenities']));
          } else if (feature === 'roomType') {
            merge(myFeatures, GenChart.transRoomType(doc['roomType']));
          } else if (feature === 'distance') {
            merge(myFeatures, { cityCenter: `${doc['cityCenter']} mi` });
            merge(myFeatures, { airport: `${doc['airport']} mi` });
          } else if (feature === 'price') {
            const tree = this.flow.getStepState(ExploreStep, 'json');
            const dates = tree['cDateArray'];
            const prices = aHotel['prices'];
            const jObject = GenChart.createJsonObject(dates, prices);
            merge(myFeatures, jObject);
            merge(myFeatures, {
              total: GenChart.formatCurrency(aHotel['total']),
            });
          }

          return {
            ...myFeatures,
          };
        }
      }
    });

    if (feature === 'amenities' || feature === 'roomType') {
      finalHotels = GenChart.transAmenities(finalHotels);
    }

    this.saveState({ chosen_hotels: finalHotels });

    //produce a comparison chart
    const table = GenChart.getChart(finalHotels);
    const msg = new AiMessageEx(this, table, { direct: true });
    return { step: CompareStep, message: msg };
  }

  protected async resume_booking(_tool: ToolCall): Promise<ToolResponseType> {
    return PresentStep;
  }

Wrapping up

You've made it to this lesson, it is packed with a lot of new information. let's conclude ...