| # Messages & Types | |
| **Part 1: Foundation - Lesson 2** | |
| > Structuring conversation data for AI agents | |
| ## Overview | |
| In Lesson 1, you learned about Runnables - the foundation of composability. Now we'll explore how to structure the *data* that flows through these Runnables, specifically for conversational AI systems. | |
| Messages are the lingua franca of AI agents. They provide a standardized way to represent conversations, tool calls, system instructions, and more. By the end of this lesson, you'll understand why proper message typing is crucial for building reliable agents. | |
| You will see tools in use. Everything about tools will be explained in depth in the 3. main part of this tutorial. | |
| ## Why Does This Matter? | |
| ### The Problem: Unstructured Conversations | |
| Imagine building a chatbot without message types: | |
| ```javascript | |
| // Bad: Everything is just strings | |
| const conversation = [ | |
| "You are a helpful assistant", | |
| "What's the weather?", | |
| "The weather is sunny", | |
| "Thanks!" | |
| ]; | |
| // Questions: | |
| // - Which messages are from the user? | |
| // - Which are from the AI? | |
| // - Which is the system instruction? | |
| // - How do we handle tool calls? | |
| // - What about metadata like timestamps? | |
| ``` | |
| This quickly becomes unmaintainable. You can't tell who said what, when, or why. | |
| ### The Solution: Typed Messages | |
| ```javascript | |
| // Good: Structured message types | |
| const conversation = [ | |
| new SystemMessage("You are a helpful assistant"), | |
| new HumanMessage("What's the weather?"), | |
| new AIMessage("The weather is sunny"), | |
| new HumanMessage("Thanks!") | |
| ]; | |
| // Now we can: | |
| // - Filter by message type | |
| // - Format differently for display | |
| // - Track metadata automatically | |
| // - Handle tool calls properly | |
| // - Validate conversation structure | |
| ``` | |
| ## Learning Objectives | |
| By the end of this lesson, you will: | |
| - β Understand the four core message types | |
| - β Implement a robust message class hierarchy | |
| - β Add metadata and timestamps automatically | |
| - β Format messages for LLM consumption | |
| - β Build a conversation history manager | |
| - β Handle special message types (tool calls, function results) | |
| ## Core Message Types | |
| Every conversational AI system needs these four fundamental message types: | |
| ### 1. SystemMessage | |
| **Purpose**: Instructions that shape the AI's behavior | |
| **Characteristics**: | |
| - Always at the start of conversations | |
| - Not visible to end users | |
| - Defines the AI's role, personality, constraints | |
| - Typically set by developers, not users | |
| **Example**: | |
| ```javascript | |
| new SystemMessage( | |
| `You are a helpful Python programming tutor. | |
| Explain concepts clearly with code examples. | |
| Always encourage learning.` | |
| ) | |
| ``` | |
| **When to use**: | |
| - Setting AI personality | |
| - Defining response format | |
| - Adding constraints or rules | |
| - Providing context | |
| ### 2. HumanMessage | |
| **Purpose**: Input from the user/human | |
| **Characteristics**: | |
| - User questions, commands, or statements | |
| - What the AI should respond to | |
| - Can contain multiple paragraphs | |
| - May include context or files | |
| **Example**: | |
| ```javascript | |
| new HumanMessage( | |
| "How do I reverse a string in Python?" | |
| ) | |
| ``` | |
| **When to use**: | |
| - User input in chatbots | |
| - Queries to agents | |
| - Commands to execute | |
| ### 3. AIMessage | |
| **Purpose**: Responses from the AI/assistant | |
| **Characteristics**: | |
| - The AI's text responses | |
| - Can include reasoning, answers, questions | |
| - May contain tool calls (function requests) | |
| - Generated by the LLM | |
| **Example**: | |
| ```javascript | |
| new AIMessage( | |
| "Here's how to reverse a string in Python:\n\n" + | |
| "```python\ntext = 'hello'\nreversed_text = text[::-1]\n```" | |
| ) | |
| ``` | |
| **When to use**: | |
| - LLM responses | |
| - Agent outputs | |
| - Generated text | |
| ### 4. ToolMessage | |
| **Purpose**: Results from tool/function execution | |
| **Characteristics**: | |
| - Returns data from external functions | |
| - Links back to the AI's tool call | |
| - Often structured data (JSON) | |
| - Input for AI's next response | |
| **Example**: | |
| ```javascript | |
| new ToolMessage( | |
| JSON.stringify({ temperature: 72, condition: "sunny" }), | |
| "get_weather" // tool name | |
| ) | |
| ``` | |
| **When to use**: | |
| - Returning function results to the AI | |
| - Providing external data | |
| - Completing tool calls | |
| ## Message Flow in Conversations | |
| Here's how messages typically flow in an agent conversation: | |
| ``` | |
| 1. System β "You are a helpful assistant with access to a calculator" | |
| 2. Human β "What's 123 * 456?" | |
| 3. AI β [Calls calculator tool with 123, 456] | |
| 4. Tool β [Returns 56088] | |
| 5. AI β "The result of 123 * 456 is 56,088" | |
| 6. Human β "Thanks!" | |
| 7. AI β "You're welcome!" | |
| ``` | |
| ## Implementation | |
| Let's build our message system from the ground up. | |
| ### Step 1: Base Message Class | |
| Every message type will inherit from this base: | |
| **Location:** `src/core/message.js` | |
| ```javascript | |
| /** | |
| * BaseMessage - Foundation for all message types | |
| * | |
| * Contains common functionality: | |
| * - Content storage | |
| * - Metadata tracking | |
| * - Timestamps | |
| * - Serialization | |
| */ | |
| export class BaseMessage { | |
| constructor(content, additionalKwargs = {}) { | |
| this.content = content; | |
| this.additionalKwargs = additionalKwargs; | |
| this.timestamp = Date.now(); | |
| this.id = this.generateId(); | |
| } | |
| /** | |
| * Generate unique ID for this message | |
| */ | |
| generateId() { | |
| return `msg_${this.timestamp}_${Math.random().toString(36).substr(2, 9)}`; | |
| } | |
| /** | |
| * Get the message type (overridden in subclasses) | |
| */ | |
| get type() { | |
| throw new Error('Subclass must implement type getter'); | |
| } | |
| /** | |
| * Convert to JSON for storage/transmission | |
| */ | |
| toJSON() { | |
| return { | |
| id: this.id, | |
| type: this.type, | |
| content: this.content, | |
| timestamp: this.timestamp, | |
| ...this.additionalKwargs | |
| }; | |
| } | |
| /** | |
| * Create message from JSON | |
| */ | |
| static fromJSON(json) { | |
| const MessageClass = MESSAGE_TYPES[json.type]; | |
| if (!MessageClass) { | |
| throw new Error(`Unknown message type: ${json.type}`); | |
| } | |
| const message = new MessageClass(json.content, json.additionalKwargs); | |
| message.id = json.id; | |
| message.timestamp = json.timestamp; | |
| return message; | |
| } | |
| /** | |
| * Format for display | |
| */ | |
| toString() { | |
| const date = new Date(this.timestamp).toLocaleTimeString(); | |
| return `[${date}] ${this.type}: ${this.content}`; | |
| } | |
| } | |
| ``` | |
| **Key design decisions**: | |
| - β `content` is always a string (or can be converted to one) | |
| - β `additionalKwargs` allows extension without changing the API | |
| - β `timestamp` is added automatically for tracking | |
| - β `id` ensures we can reference specific messages | |
| - β `toJSON()` / `fromJSON()` enable persistence | |
| ### Step 2: System Message | |
| **Location:** `src/core/message.js` | |
| ```javascript | |
| /** | |
| * SystemMessage - Instructions for the AI | |
| * | |
| * Sets the context, role, and constraints for the assistant. | |
| * Typically appears at the start of conversations. | |
| */ | |
| export class SystemMessage extends BaseMessage { | |
| constructor(content, additionalKwargs = {}) { | |
| super(content, additionalKwargs); | |
| } | |
| get type() { | |
| return 'system'; | |
| } | |
| /** | |
| * System messages often need special formatting | |
| */ | |
| toPromptFormat() { | |
| return { | |
| role: 'system', | |
| content: this.content | |
| }; | |
| } | |
| } | |
| ``` | |
| **Usage**: | |
| ```javascript | |
| const systemMsg = new SystemMessage( | |
| "You are a expert Python programmer. Be concise." | |
| ); | |
| console.log(systemMsg.type); // "system" | |
| console.log(systemMsg.content); // "You are a expert..." | |
| ``` | |
| ### Step 3: Human Message | |
| **Location:** `src/core/message.js` | |
| ```javascript | |
| /** | |
| * HumanMessage - User input | |
| * | |
| * Represents messages from the human/user. | |
| * The primary input the AI responds to. | |
| */ | |
| export class HumanMessage extends BaseMessage { | |
| constructor(content, additionalKwargs = {}) { | |
| super(content, additionalKwargs); | |
| } | |
| get type() { | |
| return 'human'; | |
| } | |
| toPromptFormat() { | |
| return { | |
| role: 'user', | |
| content: this.content | |
| }; | |
| } | |
| } | |
| ``` | |
| **Usage**: | |
| ```javascript | |
| const humanMsg = new HumanMessage("What's the capital of France?"); | |
| console.log(humanMsg.type); // "human" | |
| ``` | |
| ### Step 4: AI Message (with Tool Calls) | |
| **Location:** `src/core/message.js` | |
| ```javascript | |
| /** | |
| * AIMessage - Assistant responses | |
| * | |
| * Represents messages from the AI assistant. | |
| * Can include tool calls for function execution. | |
| */ | |
| export class AIMessage extends BaseMessage { | |
| constructor(content, additionalKwargs = {}) { | |
| super(content, additionalKwargs); | |
| // Tool calls are requests to execute functions | |
| this.toolCalls = additionalKwargs.toolCalls || []; | |
| } | |
| get type() { | |
| return 'ai'; | |
| } | |
| /** | |
| * Check if this message requests tool execution | |
| */ | |
| hasToolCalls() { | |
| return this.toolCalls.length > 0; | |
| } | |
| /** | |
| * Get specific tool call by index | |
| */ | |
| getToolCall(index = 0) { | |
| return this.toolCalls[index]; | |
| } | |
| toPromptFormat() { | |
| const formatted = { | |
| role: 'assistant', | |
| content: this.content | |
| }; | |
| if (this.hasToolCalls()) { | |
| formatted.tool_calls = this.toolCalls; | |
| } | |
| return formatted; | |
| } | |
| } | |
| ``` | |
| **Usage**: | |
| ```javascript | |
| // Simple response | |
| const aiMsg1 = new AIMessage("The capital of France is Paris."); | |
| // Response with tool call | |
| const aiMsg2 = new AIMessage("Let me calculate that for you.", { | |
| toolCalls: [{ | |
| id: 'call_123', | |
| type: 'function', | |
| function: { | |
| name: 'calculator', | |
| arguments: JSON.stringify({ operation: 'multiply', a: 5, b: 3 }) | |
| } | |
| }] | |
| }); | |
| console.log(aiMsg2.hasToolCalls()); // true | |
| ``` | |
| ### Step 5: Tool Message | |
| **Location:** `src/core/message.js` | |
| ```javascript | |
| /** | |
| * ToolMessage - Tool execution results | |
| * | |
| * Contains the output from executing a tool/function. | |
| * Sent back to the AI to inform its next response. | |
| */ | |
| export class ToolMessage extends BaseMessage { | |
| constructor(content, toolCallId, additionalKwargs = {}) { | |
| super(content, additionalKwargs); | |
| this.toolCallId = toolCallId; | |
| } | |
| get type() { | |
| return 'tool'; | |
| } | |
| toPromptFormat() { | |
| return { | |
| role: 'tool', | |
| content: this.content, | |
| tool_call_id: this.toolCallId | |
| }; | |
| } | |
| } | |
| ``` | |
| **Usage**: | |
| ```javascript | |
| const toolMsg = new ToolMessage( | |
| JSON.stringify({ result: 15 }), | |
| 'call_123' // Links back to the AI's tool call | |
| ); | |
| console.log(toolMsg.type); // "tool" | |
| console.log(toolMsg.toolCallId); // "call_123" | |
| ``` | |
| ### Step 6: Message Type Registry | |
| To support `fromJSON()`, we need a registry: | |
| ```javascript | |
| /** | |
| * Registry mapping type strings to message classes | |
| */ | |
| export const MESSAGE_TYPES = { | |
| 'system': SystemMessage, | |
| 'human': HumanMessage, | |
| 'ai': AIMessage, | |
| 'tool': ToolMessage | |
| }; | |
| ``` | |
| ## Complete Implementation | |
| Here's everything together: | |
| ```javascript | |
| /** | |
| * Message System - Typed conversation data structures | |
| * | |
| * @module core/message | |
| */ | |
| /** | |
| * BaseMessage - Foundation for all message types | |
| */ | |
| export class BaseMessage { | |
| constructor(content, additionalKwargs = {}) { | |
| if (content === undefined || content === null) { | |
| throw new Error('Message content cannot be undefined or null'); | |
| } | |
| this.content = String(content); // Ensure string | |
| this.additionalKwargs = additionalKwargs; | |
| this.timestamp = Date.now(); | |
| this.id = this.generateId(); | |
| } | |
| generateId() { | |
| return `msg_${this.timestamp}_${Math.random().toString(36).substr(2, 9)}`; | |
| } | |
| get type() { | |
| throw new Error('Subclass must implement type getter'); | |
| } | |
| toJSON() { | |
| return { | |
| id: this.id, | |
| type: this.type, | |
| content: this.content, | |
| timestamp: this.timestamp, | |
| ...this.additionalKwargs | |
| }; | |
| } | |
| static fromJSON(json) { | |
| const MessageClass = MESSAGE_TYPES[json.type]; | |
| if (!MessageClass) { | |
| throw new Error(`Unknown message type: ${json.type}`); | |
| } | |
| const message = new MessageClass(json.content, json.additionalKwargs); | |
| message.id = json.id; | |
| message.timestamp = json.timestamp; | |
| return message; | |
| } | |
| toString() { | |
| const date = new Date(this.timestamp).toLocaleTimeString(); | |
| return `[${date}] ${this.type.toUpperCase()}: ${this.content}`; | |
| } | |
| /** | |
| * Format for LLM consumption | |
| */ | |
| toPromptFormat() { | |
| throw new Error('Subclass must implement toPromptFormat()'); | |
| } | |
| } | |
| /** | |
| * SystemMessage - AI instructions and context | |
| */ | |
| export class SystemMessage extends BaseMessage { | |
| get type() { | |
| return 'system'; | |
| } | |
| toPromptFormat() { | |
| return { | |
| role: 'system', | |
| content: this.content | |
| }; | |
| } | |
| } | |
| /** | |
| * HumanMessage - User input | |
| */ | |
| export class HumanMessage extends BaseMessage { | |
| get type() { | |
| return 'human'; | |
| } | |
| toPromptFormat() { | |
| return { | |
| role: 'user', | |
| content: this.content | |
| }; | |
| } | |
| } | |
| /** | |
| * AIMessage - Assistant responses | |
| */ | |
| export class AIMessage extends BaseMessage { | |
| constructor(content, additionalKwargs = {}) { | |
| super(content, additionalKwargs); | |
| this.toolCalls = additionalKwargs.toolCalls || []; | |
| } | |
| get type() { | |
| return 'ai'; | |
| } | |
| hasToolCalls() { | |
| return this.toolCalls.length > 0; | |
| } | |
| getToolCall(index = 0) { | |
| return this.toolCalls[index]; | |
| } | |
| toPromptFormat() { | |
| const formatted = { | |
| role: 'assistant', | |
| content: this.content | |
| }; | |
| if (this.hasToolCalls()) { | |
| formatted.tool_calls = this.toolCalls; | |
| } | |
| return formatted; | |
| } | |
| } | |
| /** | |
| * ToolMessage - Function execution results | |
| */ | |
| export class ToolMessage extends BaseMessage { | |
| constructor(content, toolCallId, additionalKwargs = {}) { | |
| super(content, additionalKwargs); | |
| this.toolCallId = toolCallId; | |
| } | |
| get type() { | |
| return 'tool'; | |
| } | |
| toPromptFormat() { | |
| return { | |
| role: 'tool', | |
| content: this.content, | |
| tool_call_id: this.toolCallId | |
| }; | |
| } | |
| } | |
| /** | |
| * Message type registry | |
| */ | |
| export const MESSAGE_TYPES = { | |
| 'system': SystemMessage, | |
| 'human': HumanMessage, | |
| 'ai': AIMessage, | |
| 'tool': ToolMessage | |
| }; | |
| export default { | |
| BaseMessage, | |
| SystemMessage, | |
| HumanMessage, | |
| AIMessage, | |
| ToolMessage, | |
| MESSAGE_TYPES | |
| }; | |
| ``` | |
| ## Real-World Examples | |
| ### Example 1: Simple Conversation | |
| ```javascript | |
| const conversation = [ | |
| new SystemMessage("You are a helpful math tutor."), | |
| new HumanMessage("What's 5 + 3?"), | |
| new AIMessage("5 + 3 equals 8."), | |
| new HumanMessage("Thanks!"), | |
| new AIMessage("You're welcome!") | |
| ]; | |
| // Display conversation | |
| conversation.forEach(msg => { | |
| console.log(msg.toString()); | |
| }); | |
| // Output: | |
| // [10:30:45] SYSTEM: You are a helpful math tutor. | |
| // [10:30:46] HUMAN: What's 5 + 3? | |
| // [10:30:47] AI: 5 + 3 equals 8. | |
| // [10:30:48] HUMAN: Thanks! | |
| // [10:30:49] AI: You're welcome! | |
| ``` | |
| ### Example 2: Tool Call Flow | |
| ```javascript | |
| // User asks a question requiring calculation | |
| const messages = [ | |
| new SystemMessage("You are an assistant with access to a calculator."), | |
| new HumanMessage("What's 1234 * 5678?") | |
| ]; | |
| // AI decides to use calculator | |
| const aiWithToolCall = new AIMessage( | |
| "I'll calculate that for you.", | |
| { | |
| toolCalls: [{ | |
| id: 'call_abc123', | |
| type: 'function', | |
| function: { | |
| name: 'calculator', | |
| arguments: JSON.stringify({ | |
| operation: 'multiply', | |
| a: 1234, | |
| b: 5678 | |
| }) | |
| } | |
| }] | |
| } | |
| ); | |
| messages.push(aiWithToolCall); | |
| // Tool executes and returns result | |
| const toolResult = new ToolMessage( | |
| JSON.stringify({ result: 7006652 }), | |
| 'call_abc123' | |
| ); | |
| messages.push(toolResult); | |
| // AI incorporates result in final response | |
| const finalResponse = new AIMessage( | |
| "The result of 1234 Γ 5678 is 7,006,652." | |
| ); | |
| messages.push(finalResponse); | |
| console.log('Has tool calls?', aiWithToolCall.hasToolCalls()); // true | |
| console.log('Tool call:', aiWithToolCall.getToolCall(0)); | |
| ``` | |
| ### Example 3: Conversation Persistence | |
| ```javascript | |
| // Save conversation to JSON | |
| const conversation = [ | |
| new SystemMessage("You are helpful."), | |
| new HumanMessage("Hello!"), | |
| new AIMessage("Hi there!") | |
| ]; | |
| const json = conversation.map(msg => msg.toJSON()); | |
| const saved = JSON.stringify(json, null, 2); | |
| console.log('Saved:', saved); | |
| // Later: Load conversation from JSON | |
| const loaded = JSON.parse(saved); | |
| const restored = loaded.map(msgData => BaseMessage.fromJSON(msgData)); | |
| console.log('Restored:', restored.length, 'messages'); | |
| restored.forEach(msg => console.log(msg.toString())); | |
| ``` | |
| ### Example 4: Filtering Messages | |
| ```javascript | |
| const history = [ | |
| new SystemMessage("You are helpful."), | |
| new HumanMessage("Hi"), | |
| new AIMessage("Hello!"), | |
| new HumanMessage("How are you?"), | |
| new AIMessage("I'm doing well!") | |
| ]; | |
| // Get only human messages | |
| const humanMessages = history.filter(msg => msg.type === 'human'); | |
| console.log('Human said:', humanMessages.map(m => m.content)); | |
| // Get only AI messages | |
| const aiMessages = history.filter(msg => msg.type === 'ai'); | |
| console.log('AI said:', aiMessages.map(m => m.content)); | |
| // Get last N messages (sliding window) | |
| const lastThree = history.slice(-3); | |
| console.log('Recent:', lastThree.map(m => m.toString())); | |
| ``` | |
| ### Example 5: Custom Metadata | |
| ```javascript | |
| // Add custom metadata to messages | |
| const userMsg = new HumanMessage("Hello!", { | |
| userId: 'user_123', | |
| sessionId: 'sess_456', | |
| language: 'en' | |
| }); | |
| const aiMsg = new AIMessage("Hi there!", { | |
| model: 'llama-3.1', | |
| temperature: 0.7, | |
| tokens: 150 | |
| }); | |
| console.log('User metadata:', userMsg.additionalKwargs); | |
| console.log('AI metadata:', aiMsg.additionalKwargs); | |
| // Metadata is preserved in JSON | |
| const json = userMsg.toJSON(); | |
| console.log(json.userId); // 'user_123' | |
| ``` | |
| ## Advanced Patterns | |
| ### Pattern 1: Message Builder | |
| For complex message construction: | |
| ```javascript | |
| class MessageBuilder { | |
| constructor() { | |
| this.messages = []; | |
| } | |
| system(content) { | |
| this.messages.push(new SystemMessage(content)); | |
| return this; // Chainable | |
| } | |
| human(content, metadata = {}) { | |
| this.messages.push(new HumanMessage(content, metadata)); | |
| return this; | |
| } | |
| ai(content, metadata = {}) { | |
| this.messages.push(new AIMessage(content, metadata)); | |
| return this; | |
| } | |
| build() { | |
| return this.messages; | |
| } | |
| } | |
| // Usage | |
| const conversation = new MessageBuilder() | |
| .system("You are helpful.") | |
| .human("Hello!") | |
| .ai("Hi there!") | |
| .human("How are you?") | |
| .ai("I'm great!") | |
| .build(); | |
| ``` | |
| ### Pattern 2: Conversation History Manager | |
| ```javascript | |
| class ConversationHistory { | |
| constructor(maxMessages = 100) { | |
| this.messages = []; | |
| this.maxMessages = maxMessages; | |
| } | |
| add(message) { | |
| this.messages.push(message); | |
| // Keep only last N messages (sliding window) | |
| if (this.messages.length > this.maxMessages) { | |
| // Always keep system message if it exists | |
| const systemMsg = this.messages.find(m => m.type === 'system'); | |
| const recentMessages = this.messages.slice(-this.maxMessages + 1); | |
| this.messages = systemMsg | |
| ? [systemMsg, ...recentMessages.filter(m => m.type !== 'system')] | |
| : recentMessages; | |
| } | |
| } | |
| getAll() { | |
| return [...this.messages]; // Return copy | |
| } | |
| getLast(n = 1) { | |
| return this.messages.slice(-n); | |
| } | |
| getByType(type) { | |
| return this.messages.filter(msg => msg.type === type); | |
| } | |
| clear() { | |
| // Keep system message | |
| const systemMsg = this.messages.find(m => m.type === 'system'); | |
| this.messages = systemMsg ? [systemMsg] : []; | |
| } | |
| toPromptFormat() { | |
| return this.messages.map(msg => msg.toPromptFormat()); | |
| } | |
| save() { | |
| return JSON.stringify(this.messages.map(m => m.toJSON())); | |
| } | |
| static load(json) { | |
| const data = JSON.parse(json); | |
| const history = new ConversationHistory(); | |
| history.messages = data.map(msgData => BaseMessage.fromJSON(msgData)); | |
| return history; | |
| } | |
| } | |
| // Usage | |
| const history = new ConversationHistory(maxMessages: 50); | |
| history.add(new SystemMessage("You are helpful.")); | |
| history.add(new HumanMessage("Hi")); | |
| history.add(new AIMessage("Hello!")); | |
| console.log('Total messages:', history.getAll().length); | |
| console.log('Last message:', history.getLast()[0].content); | |
| // Format for LLM | |
| const formatted = history.toPromptFormat(); | |
| // [{ role: 'system', content: '...' }, { role: 'user', content: '...' }, ...] | |
| ``` | |
| ### Pattern 3: Message Validation | |
| ```javascript | |
| class MessageValidator { | |
| static validate(message) { | |
| const errors = []; | |
| // Check content | |
| if (!message.content || message.content.trim().length === 0) { | |
| errors.push('Message content cannot be empty'); | |
| } | |
| // Check type | |
| if (!MESSAGE_TYPES[message.type]) { | |
| errors.push(`Invalid message type: ${message.type}`); | |
| } | |
| // Check tool messages have tool call ID | |
| if (message.type === 'tool' && !message.toolCallId) { | |
| errors.push('Tool messages must have a toolCallId'); | |
| } | |
| return { | |
| valid: errors.length === 0, | |
| errors | |
| }; | |
| } | |
| static validateConversation(messages) { | |
| const errors = []; | |
| // First message should be system message (recommended) | |
| if (messages.length > 0 && messages[0].type !== 'system') { | |
| errors.push('Conversation should start with a system message'); | |
| } | |
| // Tool messages should follow AI messages with tool calls | |
| for (let i = 1; i < messages.length; i++) { | |
| const prev = messages[i - 1]; | |
| const curr = messages[i]; | |
| if (curr.type === 'tool') { | |
| if (prev.type !== 'ai' || !prev.hasToolCalls()) { | |
| errors.push( | |
| `Tool message at index ${i} should follow AI message with tool calls` | |
| ); | |
| } | |
| } | |
| } | |
| return { | |
| valid: errors.length === 0, | |
| errors | |
| }; | |
| } | |
| } | |
| // Usage | |
| const msg = new HumanMessage("Hello!"); | |
| const result = MessageValidator.validate(msg); | |
| console.log('Valid?', result.valid); | |
| console.log('Errors:', result.errors); | |
| ``` | |
| ### Pattern 4: Message Formatting Utilities | |
| ```javascript | |
| class MessageFormatter { | |
| /** | |
| * Format messages for display in UI | |
| */ | |
| static toDisplayFormat(messages) { | |
| return messages.map(msg => { | |
| const time = new Date(msg.timestamp).toLocaleTimeString(); | |
| const icon = this.getIcon(msg.type); | |
| return { | |
| id: msg.id, | |
| icon, | |
| time, | |
| type: msg.type, | |
| content: msg.content, | |
| sender: this.getSenderName(msg.type) | |
| }; | |
| }); | |
| } | |
| static getIcon(type) { | |
| const icons = { | |
| 'system': 'βοΈ', | |
| 'human': 'π€', | |
| 'ai': 'π€', | |
| 'tool': 'π οΈ' | |
| }; | |
| return icons[type] || 'π¬'; | |
| } | |
| static getSenderName(type) { | |
| const names = { | |
| 'system': 'System', | |
| 'human': 'You', | |
| 'ai': 'Assistant', | |
| 'tool': 'Tool' | |
| }; | |
| return names[type] || 'Unknown'; | |
| } | |
| /** | |
| * Format messages for LLM (OpenAI-style) | |
| */ | |
| static toOpenAIFormat(messages) { | |
| return messages.map(msg => { | |
| const formatted = msg.toPromptFormat(); | |
| // Map our types to OpenAI's expected roles | |
| const roleMap = { | |
| 'human': 'user', | |
| 'ai': 'assistant', | |
| 'system': 'system', | |
| 'tool': 'tool' | |
| }; | |
| formatted.role = roleMap[msg.type] || msg.type; | |
| return formatted; | |
| }); | |
| } | |
| /** | |
| * Create markdown representation | |
| */ | |
| static toMarkdown(messages) { | |
| return messages.map(msg => { | |
| const sender = this.getSenderName(msg.type); | |
| const time = new Date(msg.timestamp).toLocaleString(); | |
| return `**${sender}** (${time})\n\n${msg.content}\n\n---\n`; | |
| }).join('\n'); | |
| } | |
| } | |
| // Usage | |
| const messages = [ | |
| new SystemMessage("You are helpful."), | |
| new HumanMessage("Hi"), | |
| new AIMessage("Hello!") | |
| ]; | |
| console.log('Display format:', MessageFormatter.toDisplayFormat(messages)); | |
| console.log('OpenAI format:', MessageFormatter.toOpenAIFormat(messages)); | |
| console.log('Markdown:', MessageFormatter.toMarkdown(messages)); | |
| ``` | |
| ## Integration with LLMs | |
| Messages need to be formatted for the LLM. Here's how different models expect them: | |
| ### OpenAI-Style Format | |
| ```javascript | |
| function formatForOpenAI(messages) { | |
| return messages.map(msg => ({ | |
| role: msg.type === 'human' ? 'user' : msg.type, | |
| content: msg.content | |
| })); | |
| } | |
| ``` | |
| ### Llama-Style Format (with chat template) | |
| ```javascript | |
| function formatForLlama(messages) { | |
| // Llama uses special tokens | |
| let formatted = ''; | |
| for (const msg of messages) { | |
| if (msg.type === 'system') { | |
| formatted += `<<SYS>>\n${msg.content}\n<</SYS>>\n\n`; | |
| } else if (msg.type === 'human') { | |
| formatted += `[INST] ${msg.content} [/INST]`; | |
| } else if (msg.type === 'ai') { | |
| formatted += `${msg.content}`; | |
| } | |
| } | |
| return formatted; | |
| } | |
| ``` | |
| ### Our Flexible Approach | |
| ```javascript | |
| class MessageFormatter { | |
| static format(messages, style = 'openai') { | |
| const formatters = { | |
| 'openai': this.formatOpenAI, | |
| 'llama': this.formatLlama, | |
| 'raw': this.formatRaw | |
| }; | |
| const formatter = formatters[style]; | |
| if (!formatter) { | |
| throw new Error(`Unknown format style: ${style}`); | |
| } | |
| return formatter(messages); | |
| } | |
| } | |
| ``` | |
| ## Debugging Tips | |
| ### Tip 1: Pretty Print Conversations | |
| ```javascript | |
| function printConversation(messages) { | |
| console.log('\n=== Conversation ===\n'); | |
| messages.forEach((msg, idx) => { | |
| console.log(`${idx + 1}. ${msg.toString()}`); | |
| }); | |
| console.log('\n===================\n'); | |
| } | |
| ``` | |
| ### Tip 2: Visualize Message Flow | |
| ```javascript | |
| function visualizeFlow(messages) { | |
| const flow = messages.map(msg => { | |
| const icon = msg.type === 'human' ? 'β' : 'β'; | |
| return `${icon} ${msg.type}: ${msg.content.substring(0, 50)}...`; | |
| }); | |
| console.log('\nMessage Flow:'); | |
| flow.forEach(line => console.log(line)); | |
| } | |
| ``` | |
| ### Tip 3: Inspect Metadata | |
| ```javascript | |
| function inspectMetadata(message) { | |
| console.log('Message Details:'); | |
| console.log('- ID:', message.id); | |
| console.log('- Type:', message.type); | |
| console.log('- Timestamp:', new Date(message.timestamp).toISOString()); | |
| console.log('- Content length:', message.content.length); | |
| console.log('- Metadata:', message.additionalKwargs); | |
| if (message.type === 'ai' && message.hasToolCalls()) { | |
| console.log('- Tool calls:', message.toolCalls.length); | |
| } | |
| } | |
| ``` | |
| ## Common Mistakes | |
| ### β Mistake 1: Wrong Message Order | |
| ```javascript | |
| // Bad: AI message before human input | |
| const bad = [ | |
| new AIMessage("Hello!"), | |
| new HumanMessage("Hi") | |
| ]; | |
| ``` | |
| **Fix**: Always respond TO something | |
| ```javascript | |
| const good = [ | |
| new HumanMessage("Hi"), | |
| new AIMessage("Hello!") | |
| ]; | |
| ``` | |
| ### β Mistake 2: Forgetting System Message | |
| ```javascript | |
| // Bad: No context for the AI | |
| const bad = [ | |
| new HumanMessage("Write code") | |
| ]; | |
| ``` | |
| **Fix**: Always set context | |
| ```javascript | |
| const good = [ | |
| new SystemMessage("You are a coding assistant."), | |
| new HumanMessage("Write code") | |
| ]; | |
| ``` | |
| ### β Mistake 3: Not Linking Tool Messages | |
| ```javascript | |
| // Bad: Tool message without proper ID | |
| const bad = new ToolMessage("result", undefined); | |
| ``` | |
| **Fix**: Always link to the tool call | |
| ```javascript | |
| const toolCallId = aiMessage.getToolCall(0).id; | |
| const good = new ToolMessage("result", toolCallId); | |
| ``` | |
| ### β Mistake 4: Modifying Message Content | |
| ```javascript | |
| // Bad: Changing message after creation | |
| const msg = new HumanMessage("Hello"); | |
| msg.content = "Hi"; // Don't do this! | |
| ``` | |
| **Fix**: Create a new message | |
| ```javascript | |
| const newMsg = new HumanMessage("Hi"); | |
| ``` | |
| ## Mental Model | |
| Think of messages as a timeline: | |
| ``` | |
| Time β | |
| ββββββββββββββββββββββββββββββββββββββββ | |
| βοΈ System: "You are helpful" | |
| β | |
| ββ Human: "What's 2+2?" | |
| β | |
| ββ AI: "Let me calculate..." | |
| β ββ [Tool Call: calculator(2, 2)] | |
| β | |
| ββ Tool: "4" | |
| β | |
| ββ AI: "2+2 equals 4" | |
| ``` | |
| Each message is immutable - once created, it represents a moment in time. | |
| ## Exercises | |
| Practice what you've learned! | |
| ### Exercise 5: Build a Message Formatter | |
| Create a function that formats messages for console display with colors and icons. | |
| **Requirements**: | |
| - Different colors for each message type | |
| - Icons for visual distinction | |
| - Timestamp display | |
| - Content truncation for long messages | |
| **Starter code**: `exercises/02-message-formatter.js` | |
| ### Exercise 6: Implement Conversation Validation | |
| Build a validator that checks conversation structure. | |
| **Rules to check**: | |
| - System message should be first (if present) | |
| - Tool messages must follow AI messages with tool calls | |
| - No empty messages | |
| - Alternating human/AI after system message | |
| **Starter code**: `exercises/02-conversation-validator.js` | |
| ### Exercise 7: Create a Chat History Manager | |
| Build a class that manages conversation history with: | |
| - Add messages | |
| - Get last N messages | |
| - Filter by type | |
| - Save/load from JSON | |
| - Sliding window (max messages) | |
| **Starter code**: `exercises/02-chat-history.js` | |
| ### Exercise 8: Tool Call Flow | |
| Simulate a complete tool call flow: | |
| 1. Human asks a question requiring a tool | |
| 2. AI responds with a tool call | |
| 3. Tool executes and returns result | |
| 4. AI incorporates result in response | |
| **Starter code**: `exercises/02-tool-flow.js` | |
| ## Summary | |
| Congratulations! You now understand message types and why they're crucial for AI agents. | |
| ### Key Takeaways | |
| 1. **Four core types**: System, Human, AI, Tool | |
| 2. **Structured data**: Messages have types, timestamps, IDs, metadata | |
| 3. **Immutability**: Messages represent moments in time | |
| 4. **Tool calls**: AI messages can request function execution | |
| 5. **Serialization**: Messages can be saved/loaded as JSON | |
| ### Why This Matters | |
| - β **Clarity**: Know who said what, when | |
| - β **Debugging**: Track conversation flow easily | |
| - β **Persistence**: Save and restore conversations | |
| - β **Validation**: Ensure proper structure | |
| - β **Formatting**: Adapt to different LLM formats | |
| ### Building Blocks | |
| Messages are the data structure that flows through Runnables: | |
| ```javascript | |
| // Messages flow through Runnables | |
| const conversation = [ | |
| new SystemMessage("You are helpful"), | |
| new HumanMessage("Hello") | |
| ]; | |
| // Runnables process messages | |
| const response = await chatModel.invoke(conversation); | |
| // Returns: AIMessage("Hi there!") | |
| ``` | |
| ## Next Steps | |
| In the next lesson, we'll wrap **node-llama-cpp** as a Runnable that works with our message types! | |
| **Preview**: You'll learn: | |
| - Loading local LLMs | |
| - Converting messages to prompts | |
| - Streaming responses | |
| - Managing model context | |
| β‘οΈ [Continue to Lesson 3: The LLM Wrapper](03-llm-wrapper.md) | |
| ## Additional Resources | |
| - [OpenAI Chat Format Documentation](https://platform.openai.com/docs/guides/chat) | |
| - [Anthropic Messages API](https://docs.anthropic.com/claude/reference/messages_post) | |
| - [JSON Schema for Validation](https://json-schema.org/) | |
| ## Questions & Discussion | |
| **Q: Why not just use strings?** | |
| A: Strings don't capture metadata, can't distinguish between types, and make debugging harder. Typed messages provide structure and context. | |
| **Q: Can I add custom message types?** | |
| A: Yes! Extend `BaseMessage` and register in `MESSAGE_TYPES`: | |
| ```javascript | |
| class CustomMessage extends BaseMessage { | |
| get type() { return 'custom'; } | |
| } | |
| MESSAGE_TYPES['custom'] = CustomMessage; | |
| ``` | |
| **Q: How do I handle multi-modal content (images)?** | |
| A: Store in `additionalKwargs`: | |
| ```javascript | |
| new HumanMessage("What's in this image?", { | |
| images: ['data:image/png;base64,...'] | |
| }) | |
| ``` | |
| **Q: Should I validate every message?** | |
| A: For production, yes. For development, optional but helpful for catching bugs early. | |
| --- | |
| **Built with β€οΈ for learners who want to understand AI agents deeply** | |
| [β Previous: Runnable](01-runnable.md) | [Tutorial Index](../README.md) | [Next: LLM Wrapper β](03-llm-wrapper.md) |