Skip to content

Latest commit

 

History

History
950 lines (742 loc) · 23.6 KB

File metadata and controls

950 lines (742 loc) · 23.6 KB

Getting Started with Harmony Protocol

This guide will walk you through everything you need to know to start using the Harmony Protocol Node.js library effectively.

Table of Contents

  1. Installation & Setup
  2. Core Concepts
  3. Your First Harmony Conversation
  4. Understanding Channels
  5. Working with Tools
  6. Streaming Responses
  7. Rendering and Output Formatting
  8. Integration with AI Providers
  9. Real-World Examples
  10. Best Practices
  11. Common Pitfalls
  12. Troubleshooting

Installation & Setup

Prerequisites

  • Node.js ≥ 18.0.0
  • TypeScript ≥ 5.0.0 (for TypeScript projects)
  • OpenAI-compatible model that supports Harmony formatting

Installation

npm install harmony-protocol-js

Quick Verification

Create a simple test file to verify the installation:

// test-harmony.ts
import { loadHarmonyEncoding, Message, Role } from 'harmony-protocol-js';

const encoding = loadHarmonyEncoding();
const message = Message.user('Hello, Harmony!');

console.log('✅ Harmony Protocol is working!');
console.log('Message:', message.getTextContent());
console.log('Tokens:', encoding.encode(message.getTextContent()).length);

Run it:

npx tsx test-harmony.ts

Core Concepts

1. Messages and Roles

Harmony Protocol supports five distinct roles:

enum Role {
  SYSTEM = 'system',      // Instructions and configuration
  DEVELOPER = 'developer', // Development-specific instructions
  USER = 'user',          // End-user messages
  ASSISTANT = 'assistant', // AI model responses
  TOOL = 'tool'           // Tool execution results
}

2. Channels

Channels organize assistant responses into different types:

enum Channel {
  ANALYSIS = 'analysis',     // Internal reasoning
  COMMENTARY = 'commentary', // Meta-commentary and explanations
  FINAL = 'final'           // User-facing final response
}

3. Special Tokens

Harmony uses special tokens to structure conversations:

Token ID Purpose
`< start >`
`< message >`
`< end >`
`< channel >`

Your First Harmony Conversation

Let's build a complete example step by step:

Step 1: Create Messages

import { Message, Role, createSystemContent } from 'harmony-protocol-js';

// System message with configuration
const systemContent = createSystemContent()
  .withIdentity('Helpful AI Assistant')
  .withRequiredChannels(['analysis', 'final'])
  .withReasoningEffort('medium')
  .build();

const systemMessage = Message.system(systemContent);
const userMessage = Message.user('What is the capital of France?');

Step 2: Build Conversation

import { Conversation } from 'harmony-protocol-js';

const conversation = Conversation.fromMessages([
  systemMessage,
  userMessage
]);

// Validate the conversation
const validation = conversation.validate();
if (!validation.isValid) {
  console.error('Validation errors:', validation.errors);
}

Step 3: Render for Model

import { loadHarmonyEncoding } from 'harmony-protocol-js';

const encoding = loadHarmonyEncoding();

// Get tokens ready for your OpenAI model
const tokens = encoding.renderConversationForCompletion(conversation, Role.ASSISTANT);

console.log(`Ready to send ${tokens.length} tokens to model`);

Step 4: Handle Model Response

// After getting response from your model
function parseModelResponse(responseTokens: number[]) {
  const messages = encoding.parseMessagesFromCompletionTokens(
    responseTokens,
    Role.ASSISTANT
  );

  // Extract different channels
  const analysis = messages.find(m => m.channel === Channel.ANALYSIS);
  const final = messages.find(m => m.channel === Channel.FINAL);

  return {
    reasoning: analysis?.getTextContent(),
    answer: final?.getTextContent()
  };
}

Understanding Channels

Channels allow the AI model to provide structured, multi-layered responses:

Analysis Channel

Internal reasoning and problem breakdown:

// Model might output:
// <|start|>assistant<|channel|>analysis<|message|>
// The user is asking about France's capital. This is a straightforward
// geographical question. The capital of France is Paris.
// <|end|>

Commentary Channel

Meta-information and context:

// <|start|>assistant<|channel|>commentary<|message|>
// This is a basic geography question that most people learn in school.
// France has had Paris as its capital since 987 AD.
// <|end|>

Final Channel

User-facing response:

// <|start|>assistant<|channel|>final<|message|>
// The capital of France is Paris.
// <|end|>

Channel Configuration

// Require specific channels
const systemContent = createSystemContent()
  .withRequiredChannels(['analysis', 'final'])  // Analysis + Final only
  .build();

// Or allow all channels
const systemContent = createSystemContent()
  .withRequiredChannels(['analysis', 'commentary', 'final'])
  .build();

Working with Tools

Tools extend the model's capabilities with external functions:

Built-in Tool Namespaces

import {
  createSystemContent,
  createBrowserToolNamespace,
  createPythonToolNamespace
} from 'harmony-protocol-js';

const systemContent = createSystemContent()
  .withBrowserTool()     // Web browsing and search
  .withPythonTool()      // Python code execution
  .withCommonFunctionTools()  // Math, date/time, file operations
  .build();

Custom Tools

import { createToolDescription, createToolNamespace } from 'harmony-protocol-js';

// Define a custom tool
const weatherTool = createToolDescription(
  'get_weather',
  'Get current weather for a location',
  {
    type: 'object',
    properties: {
      location: {
        type: 'string',
        description: 'City name or coordinates'
      },
      units: {
        type: 'string',
        enum: ['celsius', 'fahrenheit'],
        default: 'celsius'
      }
    },
    required: ['location']
  }
);

// Create namespace
const weatherNamespace = createToolNamespace(
  'weather',
  'Weather information tools',
  [weatherTool]
);

// Add to system
const systemContent = createSystemContent()
  .withTools(weatherNamespace)
  .build();

Streaming Responses

For real-time user interfaces, use the streaming parser:

Basic Streaming Setup

import { StreamableParser, loadHarmonyEncoding, Role } from 'harmony-protocol-js';

const encoding = loadHarmonyEncoding();
const parser = new StreamableParser(encoding, Role.ASSISTANT);

Processing Streaming Data

// From OpenAI streaming response
async function handleStreamingResponse(stream: ReadableStream) {
  const reader = stream.getReader();

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    // Process each chunk
    parser.processText(new TextDecoder().decode(value));

    // Get real-time content delta
    const delta = parser.getLastContentDelta();
    if (delta) {
      // Update UI immediately
      appendToChat(delta);
    }

    // Check for complete messages
    const stats = parser.getStats();
    if (stats.totalMessages > 0) {
      console.log(`Received ${stats.totalMessages} complete messages`);
    }
  }

  // Get final structured messages
  const messages = parser.intoMessages();
  finalizeChat(messages);
}

Error Handling in Streaming

// Robust streaming with error recovery
function processStreamChunk(chunk: string) {
  try {
    parser.processText(chunk);

    const delta = parser.getLastContentDelta();
    if (delta) {
      updateUI(delta);
    }
  } catch (error) {
    console.warn('Stream parsing error:', error);
    // Parser continues working despite errors
  }
}

Rendering and Output Formatting

Harmony Protocol provides comprehensive rendering helpers for different output formats and use cases:

Basic Rendering Functions

import {
  renderToText,
  renderToMarkdown,
  renderToHTML,
  renderFinalOnly,
  renderSummary
} from 'harmony-protocol-js';

// Simple text output for logs or debugging
const text = renderToText(conversation, {
  showChannels: true,
  showRoles: true
});

// Markdown for documentation
const markdown = renderToMarkdown(conversation);

// HTML for web interfaces
const html = renderToHTML(conversation);

// Only final responses for clean chat UI
const chatOutput = renderFinalOnly(conversation);

Advanced Custom Rendering

import { HarmonyRenderer } from 'harmony-protocol-js';

const renderer = new HarmonyRenderer({
  showChannels: true,
  showRoles: true,
  channelLabels: {
    analysis: '🤔 Analysis',
    commentary: '💭 Notes',
    final: '💬 Response'
  },
  roleLabels: {
    user: 'You',
    assistant: 'AI Assistant'
  },
  maxContentLength: 200,  // Truncate long messages
  includeTimestamps: true
});

const output = renderer.renderConversation(conversation);
console.log(output.text);      // Plain text
console.log(output.markdown);  // Markdown format
console.log(output.html);      // HTML format
console.log(output.structured); // Structured data for analysis

Channel-Specific Rendering

// Get content from specific channels
const analysisOnly = renderChannel(conversation, Channel.ANALYSIS);
const finalOnly = renderChannel(conversation, Channel.FINAL);

// Render by channel for detailed processing
const channelOutputs = renderer.renderByChannel(conversation);
Object.entries(channelOutputs).forEach(([channel, output]) => {
  console.log(`${channel}: ${output.messageCount} messages`);
  console.log(output.combinedContent);
});

Export Formats

// JSON export for data persistence
const jsonExport = {
  conversation: conversation.toJSON(),
  rendered: renderer.renderConversation(conversation),
  exportedAt: new Date().toISOString()
};

// CSV export for analytics
const structured = renderer.renderConversation(conversation).structured;
const csvData = structured.messages.map(msg =>
  `"${msg.role}","${msg.channel || 'none'}","${msg.content}",${msg.originalLength}`
);

Integration with AI Providers

OpenAI Integration

import OpenAI from 'openai';

const openai = new OpenAI();

async function callHarmonyModel(conversation: Conversation) {
  const encoding = loadHarmonyEncoding();
  const tokens = encoding.renderConversationForCompletion(conversation, Role.ASSISTANT);

  // Convert tokens to text for OpenAI
  const harmonyText = encoding.decode(tokens);

  const response = await openai.completions.create({
    model: 'your-harmony-compatible-model',
    prompt: harmonyText,
    max_tokens: 2000,
    stream: false
  });

  const responseText = response.choices[0].text || '';
  const responseTokens = encoding.encode(responseText);

  return encoding.parseMessagesFromCompletionTokens(responseTokens, Role.ASSISTANT);
}

Local Model Integration (Ollama)

async function callOllamaHarmony(conversation: Conversation) {
  const encoding = loadHarmonyEncoding();
  const harmonyText = encoding.decode(
    encoding.renderConversationForCompletion(conversation, Role.ASSISTANT)
  );

  const response = await fetch('http://localhost:11434/api/generate', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      model: 'your-harmony-model',
      prompt: harmonyText,
      stream: false
    })
  });

  const result = await response.json();
  const responseTokens = encoding.encode(result.response);

  return encoding.parseMessagesFromCompletionTokens(responseTokens, Role.ASSISTANT);
}

Real-World Examples

Here are practical patterns you'll commonly use:

Building a Chat Application

import {
  Conversation,
  Message,
  Role,
  Channel,
  createSystemContent,
  HarmonyRenderer,
  loadHarmonyEncoding
} from 'harmony-protocol-js';

class HarmonyChatApp {
  private conversation: Conversation;
  private encoding = loadHarmonyEncoding();
  private renderer = new HarmonyRenderer({
    showChannels: false,  // Clean chat UI
    showRoles: true,
    roleLabels: {
      user: 'You',
      assistant: 'Assistant'
    }
  });

  constructor() {
    // Initialize with system message
    const systemContent = createSystemContent()
      .withIdentity('Helpful Chat Assistant')
      .withRequiredChannels(['final'])  // Only need final responses
      .withReasoningEffort('medium')
      .build();

    this.conversation = Conversation.fromMessages([
      Message.system(systemContent)
    ]);
  }

  addUserMessage(text: string) {
    this.conversation.addMessage(Message.user(text));
  }

  async getResponse(): Promise<string> {
    // Call your AI model here
    const response = await this.callAIModel();

    // Add response to conversation
    this.conversation.addMessage(response);

    // Return only the final response for UI
    return response.getTextContent();
  }

  getChatHistory(): string {
    // Filter to show only user and final assistant messages
    const chatMessages = this.conversation.messages.filter(msg =>
      msg.role === Role.USER ||
      (msg.role === Role.ASSISTANT && msg.channel === Channel.FINAL)
    );

    return this.renderer.renderConversation(
      new Conversation(chatMessages)
    ).text;
  }

  private async callAIModel(): Promise<Message> {
    // Implementation depends on your AI provider
    // This is a simplified example
    const tokens = this.encoding.renderConversationForCompletion(
      this.conversation,
      Role.ASSISTANT
    );

    // Call your model and get response tokens
    const responseTokens = await yourModelCall(tokens);

    // Parse the response
    const messages = this.encoding.parseMessagesFromCompletionTokens(
      responseTokens,
      Role.ASSISTANT
    );

    return messages.find(m => m.channel === Channel.FINAL) || messages[0];
  }
}

Document Analysis with Reasoning

async function analyzeDocument(document: string): Promise<{
  analysis: string;
  summary: string;
  recommendations: string;
}> {
  const systemContent = createSystemContent()
    .withIdentity('Document Analysis Expert')
    .withRequiredChannels(['analysis', 'commentary', 'final'])
    .withReasoningEffort('high')
    .build();

  const conversation = Conversation.fromMessages([
    Message.system(systemContent),
    Message.user(`Please analyze this document and provide recommendations:\n\n${document}`)
  ]);

  // Get AI response
  const responseMessages = await callAIModel(conversation);

  // Extract different types of content
  const analysis = responseMessages
    .find(m => m.channel === Channel.ANALYSIS)
    ?.getTextContent() || '';

  const commentary = responseMessages
    .find(m => m.channel === Channel.COMMENTARY)
    ?.getTextContent() || '';

  const summary = responseMessages
    .find(m => m.channel === Channel.FINAL)
    ?.getTextContent() || '';

  return {
    analysis,
    summary,
    recommendations: commentary
  };
}

Streaming Chat Interface

import { StreamingRenderer } from 'harmony-protocol-js';

class StreamingChatUI {
  private parser = new StreamableParser(loadHarmonyEncoding(), Role.ASSISTANT);
  private streamRenderer = new StreamingRenderer({
    showChannels: false,
    showRoles: false
  });

  async handleStreamingResponse(stream: ReadableStream, onUpdate: (text: string) => void) {
    const reader = stream.getReader();
    let accumulated = '';

    try {
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        // Process the chunk
        const chunk = new TextDecoder().decode(value);
        this.parser.processText(chunk);

        // Get incremental content
        const delta = this.parser.getLastContentDelta();
        if (delta) {
          accumulated += delta;
          // Update UI immediately for responsive feel
          onUpdate(accumulated);
        }
      }

      // Finalize with complete messages
      const messages = this.parser.intoMessages();
      const finalMessage = messages.find(m => m.channel === Channel.FINAL);
      if (finalMessage) {
        onUpdate(finalMessage.getTextContent());
      }
    } catch (error) {
      console.error('Streaming error:', error);
      onUpdate('Sorry, there was an error processing the response.');
    }
  }
}

Best Practices

1. Message Construction

// ✅ Good: Use builder pattern for system content
const systemContent = createSystemContent()
  .withIdentity('Expert Assistant')
  .withRequiredChannels(['analysis', 'final'])
  .build();

// ❌ Avoid: Manual construction
const systemContent = {
  identity: 'Expert Assistant',
  required_channels: ['analysis', 'final']
};

2. Error Handling

// ✅ Good: Validate before processing
const validation = conversation.validate();
if (!validation.isValid) {
  throw new Error(`Invalid conversation: ${validation.errors.join(', ')}`);
}

// ✅ Good: Handle encoding errors
try {
  const tokens = encoding.renderConversation(conversation);
} catch (error) {
  if (error instanceof RenderError) {
    console.error('Rendering failed:', error.message);
  }
}

3. Performance Optimization

// ✅ Good: Reuse encoding instances
const encoding = loadHarmonyEncoding();  // Create once
// Use encoding multiple times

// ✅ Good: Use streaming for real-time UIs
const parser = new StreamableParser(encoding, Role.ASSISTANT);

// ❌ Avoid: Creating new instances repeatedly
// const encoding = loadHarmonyEncoding();  // Don't do this in loops

4. Channel Management

// ✅ Good: Filter channels based on use case
const finalOnly = conversation.filter(msg =>
  msg.role !== Role.ASSISTANT || msg.channel === Channel.FINAL
);

// ✅ Good: Drop analysis when not needed
const tokens = encoding.renderConversation(conversation, {
  drop_analysis_when_final_present: true
});

Common Pitfalls

1. Token Array Handling

// ❌ Wrong: Assuming tokens are strings
const tokens = encoding.encode(text);
console.log(tokens.join(''));  // This won't work

// ✅ Correct: Tokens are numbers, decode to get text
const text = encoding.decode(tokens);
console.log(text);

2. Channel Requirements

// ❌ Wrong: Forgetting to specify channels in system message
const system = Message.system('You are a helpful assistant.');

// ✅ Correct: Always specify required channels
const system = Message.system(
  createSystemContent()
    .withRequiredChannels(['final'])
    .build()
);

3. Streaming Parser State

// ❌ Wrong: Not handling parser state
parser.processText(chunk);
// Continue without checking state

// ✅ Correct: Check parsing state
parser.processText(chunk);
const stats = parser.getStats();
if (stats.totalMessages > previousMessageCount) {
  handleNewMessages(parser.getMessages());
}

4. Tool Validation

// ❌ Wrong: Creating tools without validation
const tool = { name: 'bad-name!', description: '' };

// ✅ Correct: Use validation
const validation = validateToolDescription(tool);
if (!validation.isValid) {
  console.error('Tool validation failed:', validation.errors);
}

Troubleshooting

Common Issues and Solutions

Token Array vs String Confusion

Problem: Getting TypeError: tokens.join is not a function

// ❌ This will fail
const tokens = encoding.encode('Hello');
const text = tokens.join('');  // tokens is number[], not string[]

Solution: Use the decode method

// ✅ Correct approach
const tokens = encoding.encode('Hello');
const text = encoding.decode(tokens);  // Properly decode tokens to text

Missing Channel in System Content

Problem: Model doesn't output expected channels

// ❌ Generic system message
const system = Message.system('You are helpful.');

Solution: Always specify required channels

// ✅ Explicit channel requirements
const system = Message.system(
  createSystemContent()
    .withRequiredChannels(['analysis', 'final'])
    .build()
);

Streaming Parser Not Working

Problem: No content appears during streaming

// ❌ Not checking for deltas
parser.processText(chunk);
// Missing delta extraction

Solution: Always check for content deltas

// ✅ Proper streaming handling
parser.processText(chunk);
const delta = parser.getLastContentDelta();
if (delta) {
  updateUI(delta);
}

Module Import Errors

Problem: Cannot resolve module 'harmony-protocol-js'

  • Check that the package is installed: npm list harmony-protocol-js
  • Verify Node.js version ≥ 18.0.0
  • For TypeScript projects, ensure moduleResolution: "node" in tsconfig.json

Performance Issues with Large Conversations

Problem: Slow rendering with many messages

// ❌ Re-creating encoding repeatedly
function processMessage() {
  const encoding = loadHarmonyEncoding();  // Expensive!
  // ... processing
}

Solution: Reuse encoding instances

// ✅ Create once, use many times
const globalEncoding = loadHarmonyEncoding();

function processMessage() {
  // Use globalEncoding
}

Invalid Tool Definitions

Problem: ValidationError: Tool name contains invalid characters

// ❌ Invalid tool name
const tool = createToolDescription('get-weather!', ...);

Solution: Use valid tool names (alphanumeric + underscores only)

// ✅ Valid tool name
const tool = createToolDescription('get_weather', ...);

Debugging Tips

Enable Verbose Logging

// Add this to see detailed parsing information
process.env.HARMONY_DEBUG = 'true';

const encoding = loadHarmonyEncoding();
// Will now show detailed token information

Inspect Conversation Structure

// Check conversation validity
const validation = conversation.validate();
console.log('Validation:', validation);

// Examine message structure
conversation.messages.forEach((msg, i) => {
  console.log(`Message ${i}:`, {
    role: msg.role,
    channel: msg.channel,
    contentLength: msg.getTextContent().length
  });
});

Test Token Roundtrip

// Verify encoding/decoding works correctly
const originalText = 'Test message';
const tokens = encoding.encode(originalText);
const decodedText = encoding.decode(tokens);
console.log('Roundtrip successful:', originalText === decodedText);

Getting Help

If you encounter issues not covered here:

  1. Check Examples: Look at the working examples in the /examples directory
  2. Enable Debug Mode: Set HARMONY_DEBUG=true for detailed logging
  3. Validate Inputs: Always validate conversations and tools before processing
  4. Read Error Messages: Harmony Protocol provides detailed error messages with suggestions
  5. Report Issues: Open a GitHub issue with minimal reproduction code

Next Steps

Now that you understand the basics, explore:

  1. Advanced Examples - See complex usage patterns
  2. API Reference - Complete method documentation
  3. Integration Guides - Provider-specific setup
  4. Migration Guide - Coming from Rust implementation

Need Help?

  • 📖 Documentation: Browse the complete docs
  • 💬 Issues: Report bugs on GitHub
  • 🔍 Examples: Check the examples directory
  • 📧 Contact: Reach out to the maintainers