LLM Message is removed from chat #1

Closed
opened 2026-02-16 08:17:39 -05:00 by yindo · 1 comment
Owner

Originally created by @michal-artur-marciniak on GitHub (Mar 14, 2025).

Hello!

I've started playing with your gen ui, but have some problem. I've created new agent, that has HIT. From console.log outputs I see that my agent is correctly gathering data and asking good questions, but in UI I can only see message until LLM is not done with writing (also it should only shows nextPrompt value, while he shows also whole JSON object), after it message is removed from chat and I cannot see it in state.messages. Is it problem with you GenUI or maybe I didn't understand how to append messages?

Below is my agent code:

index.ts

import {
  END,
  START,
  StateGraph,
  interrupt,
} from "@langchain/langgraph";
import {
  BuyPropertyAgentAnnotation,
  BuyPropertyAgentState,
  BuyPropertyAgentUpdate,
} from "./types";
import { gatherContactDetails } from "./nodes/gather-contact-details";

async function askHuman(
  state: BuyPropertyAgentState,
): Promise<BuyPropertyAgentUpdate> {
  const answer: string = interrupt(state.contactDetails?.nextPrompt);
  return { messages: { role: "human", content: answer } };
}

function routeAfterGatherContactDetails(
  state: BuyPropertyAgentState,
): typeof END | "askHuman" {
  if (!state.contactDetails?.exit) {
    return "askHuman";
  }
  return END;
}

const graph = new StateGraph(BuyPropertyAgentAnnotation)
  .addNode("gatherContactDetails", gatherContactDetails)
  .addNode("askHuman", askHuman)
  .addEdge(START, "gatherContactDetails")
  .addConditionalEdges("gatherContactDetails", routeAfterGatherContactDetails, [
    END,
    "askHuman",
  ])
  .addEdge("askHuman", "gatherContactDetails");

export const agent = graph.compile();
agent.name = "Buy Property Assistant Agent";

types.ts

import { Annotation } from "@langchain/langgraph";
import { GenerativeUIAnnotation } from "../types";
import { HumanResponse } from "@langchain/langgraph/prebuilt";

export type ContactDetails = {
  name: string;
  surname: string;
  email: string;
  phoneNumber: string;
  permissionToCommunicate: boolean;
  exit: boolean;
  nextPrompt: string;
};

export const BuyPropertyAgentAnnotation = Annotation.Root({
  messages: GenerativeUIAnnotation.spec.messages,
  contactDetails: Annotation<ContactDetails | undefined>(),
  humanResponse: Annotation<HumanResponse | undefined>(),
});

export type BuyPropertyAgentState = typeof BuyPropertyAgentAnnotation.State;
export type BuyPropertyAgentUpdate = typeof BuyPropertyAgentAnnotation.Update;

gatherContactDetails.ts

import { z } from "zod";
import { BuyPropertyAgentState, BuyPropertyAgentUpdate } from "../types";
import { ChatOpenAI } from "@langchain/openai";
import { formatMessages } from "@/agent/utils/format-messages";
import { AIMessage } from "@langchain/core/messages";

const GATHER_CONTACT_DETAILS = `
You are an AI agent for a real estate company tasked with gathering contact 
details from a potential home buyer through a conversational chat. 
Your goal is to collect required information.

Required Fields (Must Have):
1. Name
2. Surname 
3. Phone Number (10 digits, validated) 
4. Email (valid format with "@" and domain)
5. Permission to communicate

Communication Style: 
- Paraphrase user input naturally, preserving 100% of its meaning (except for name, surname, or permission—repeat verbatim).
- For email/phone (initial or corrected):
  1. Repeat input verbatim (e.g., "You provided 555-1234" or "You provided john@exa"). 
  2. Validate:
    - Phone: 10 digits US standard (strip non-digits).

    - Email: Contains "@" and valid domain.

  3. If invalid, say: "This appears invalid because [reason]. Please provide it again:" and treat the new input as a fresh response (restart from step 1).

  4. Always ask: "Please confirm this is correct: [exact input]?" after successful validation!

  5. Await explicit "yes" before validating.

 
Rules:
- After any email/phone input (initial or corrected): 
  1. Repeat it exactly as provided (e.g., "You provided 555-1234"). 

  2. Validate format:
    - Phone: 10 digits US standard (strip non-digits first). 

    - Email: Contains "@" with valid domain.

  3. If invalid, say: "This appears invalid because [reason]. Please provide it again:" and restart the process from step 1. 

  4. Always ask: "Please confirm this is correct: [exact input]?" after successful validation 

  5. Wait for explicit "yes."

  6. Repeat until input is both confirmed ("yes") and valid.

- Ask ONE question at a time.  
- Tailor prompts to flow naturally.  
- Gather all "Must Have" info —politely re-ask if refused, explaining it’s required
- If user asks off-topic questions, say: "I’ll address that after we finish gathering your preferences."  
- Do not greet user,
- Do not use words like great, etc.

<conversation>
{CONVERSATION}
</conversation>

<current_details>
{DETAILS}
</current_details>
`;

const contactStateSchema = z.object({
  name: z.string(),
  surname: z.string(),
  phoneNumber: z.string(),
  email: z.string(),
  permissionToCommunicate: z.boolean(),
  exit: z.boolean().describe("Set to true, when you have all required data"),
  nextPrompt: z.string().describe("Your prompt to ask for missing info"),
});

export async function gatherContactDetails(
  state: BuyPropertyAgentState,
): Promise<BuyPropertyAgentUpdate> {
  const model = new ChatOpenAI({
    model: "gpt-4o-mini",
    temperature: 0,
  }).withStructuredOutput(contactStateSchema);

  const prompt = GATHER_CONTACT_DETAILS.replace(
    "{CONVERSATION}",
    formatMessages(state.messages),
  ).replace("{DETAILS}", JSON.stringify(state.contactDetails));

  const response = await model.invoke([{ role: "user", content: prompt }]);

  console.log(`Response: ${JSON.stringify(response)}`);
  console.log(`NextPrompt: ${JSON.stringify(response.nextPrompt)}`);
  console.log("---------");
  console.log(`Messages: ${JSON.stringify(state.messages, null, 4)}`);
  console.log("---------");
  const nextPrompt = new AIMessage({ content: response.nextPrompt });
  return {
    contactDetails: response,
    messages: [nextPrompt],
  };
}
Originally created by @michal-artur-marciniak on GitHub (Mar 14, 2025). Hello! I've started playing with your gen ui, but have some problem. I've created new agent, that has HIT. From console.log outputs I see that my agent is correctly gathering data and asking good questions, but in UI I can only see message until LLM is not done with writing (also it should only shows nextPrompt value, while he shows also whole JSON object), after it message is removed from chat and I cannot see it in state.messages. Is it problem with you GenUI or maybe I didn't understand how to append messages? Below is my agent code: **index.ts** ```typescript import { END, START, StateGraph, interrupt, } from "@langchain/langgraph"; import { BuyPropertyAgentAnnotation, BuyPropertyAgentState, BuyPropertyAgentUpdate, } from "./types"; import { gatherContactDetails } from "./nodes/gather-contact-details"; async function askHuman( state: BuyPropertyAgentState, ): Promise<BuyPropertyAgentUpdate> { const answer: string = interrupt(state.contactDetails?.nextPrompt); return { messages: { role: "human", content: answer } }; } function routeAfterGatherContactDetails( state: BuyPropertyAgentState, ): typeof END | "askHuman" { if (!state.contactDetails?.exit) { return "askHuman"; } return END; } const graph = new StateGraph(BuyPropertyAgentAnnotation) .addNode("gatherContactDetails", gatherContactDetails) .addNode("askHuman", askHuman) .addEdge(START, "gatherContactDetails") .addConditionalEdges("gatherContactDetails", routeAfterGatherContactDetails, [ END, "askHuman", ]) .addEdge("askHuman", "gatherContactDetails"); export const agent = graph.compile(); agent.name = "Buy Property Assistant Agent"; ``` **types.ts** ```typescript import { Annotation } from "@langchain/langgraph"; import { GenerativeUIAnnotation } from "../types"; import { HumanResponse } from "@langchain/langgraph/prebuilt"; export type ContactDetails = { name: string; surname: string; email: string; phoneNumber: string; permissionToCommunicate: boolean; exit: boolean; nextPrompt: string; }; export const BuyPropertyAgentAnnotation = Annotation.Root({ messages: GenerativeUIAnnotation.spec.messages, contactDetails: Annotation<ContactDetails | undefined>(), humanResponse: Annotation<HumanResponse | undefined>(), }); export type BuyPropertyAgentState = typeof BuyPropertyAgentAnnotation.State; export type BuyPropertyAgentUpdate = typeof BuyPropertyAgentAnnotation.Update; ``` **gatherContactDetails.ts** ```typescript import { z } from "zod"; import { BuyPropertyAgentState, BuyPropertyAgentUpdate } from "../types"; import { ChatOpenAI } from "@langchain/openai"; import { formatMessages } from "@/agent/utils/format-messages"; import { AIMessage } from "@langchain/core/messages"; const GATHER_CONTACT_DETAILS = ` You are an AI agent for a real estate company tasked with gathering contact details from a potential home buyer through a conversational chat. Your goal is to collect required information. Required Fields (Must Have): 1. Name 2. Surname 3. Phone Number (10 digits, validated) 4. Email (valid format with "@" and domain) 5. Permission to communicate Communication Style: - Paraphrase user input naturally, preserving 100% of its meaning (except for name, surname, or permission—repeat verbatim). - For email/phone (initial or corrected): 1. Repeat input verbatim (e.g., "You provided 555-1234" or "You provided john@exa"). 2. Validate: - Phone: 10 digits US standard (strip non-digits). - Email: Contains "@" and valid domain. 3. If invalid, say: "This appears invalid because [reason]. Please provide it again:" and treat the new input as a fresh response (restart from step 1). 4. Always ask: "Please confirm this is correct: [exact input]?" after successful validation! 5. Await explicit "yes" before validating. Rules: - After any email/phone input (initial or corrected): 1. Repeat it exactly as provided (e.g., "You provided 555-1234"). 2. Validate format: - Phone: 10 digits US standard (strip non-digits first). - Email: Contains "@" with valid domain. 3. If invalid, say: "This appears invalid because [reason]. Please provide it again:" and restart the process from step 1. 4. Always ask: "Please confirm this is correct: [exact input]?" after successful validation 5. Wait for explicit "yes." 6. Repeat until input is both confirmed ("yes") and valid. - Ask ONE question at a time. - Tailor prompts to flow naturally. - Gather all "Must Have" info —politely re-ask if refused, explaining it’s required - If user asks off-topic questions, say: "I’ll address that after we finish gathering your preferences." - Do not greet user, - Do not use words like great, etc. <conversation> {CONVERSATION} </conversation> <current_details> {DETAILS} </current_details> `; const contactStateSchema = z.object({ name: z.string(), surname: z.string(), phoneNumber: z.string(), email: z.string(), permissionToCommunicate: z.boolean(), exit: z.boolean().describe("Set to true, when you have all required data"), nextPrompt: z.string().describe("Your prompt to ask for missing info"), }); export async function gatherContactDetails( state: BuyPropertyAgentState, ): Promise<BuyPropertyAgentUpdate> { const model = new ChatOpenAI({ model: "gpt-4o-mini", temperature: 0, }).withStructuredOutput(contactStateSchema); const prompt = GATHER_CONTACT_DETAILS.replace( "{CONVERSATION}", formatMessages(state.messages), ).replace("{DETAILS}", JSON.stringify(state.contactDetails)); const response = await model.invoke([{ role: "user", content: prompt }]); console.log(`Response: ${JSON.stringify(response)}`); console.log(`NextPrompt: ${JSON.stringify(response.nextPrompt)}`); console.log("---------"); console.log(`Messages: ${JSON.stringify(state.messages, null, 4)}`); console.log("---------"); const nextPrompt = new AIMessage({ content: response.nextPrompt }); return { contactDetails: response, messages: [nextPrompt], }; } ```
yindo closed this issue 2026-02-16 08:17:39 -05:00
Author
Owner

@michal-artur-marciniak commented on GitHub (Mar 14, 2025):

I thought that this is problem with UI, but not - interrupt removes AI message.

@michal-artur-marciniak commented on GitHub (Mar 14, 2025): I thought that this is problem with UI, but not - interrupt removes AI message.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraphjs-gen-ui-examples#1