diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..0c37387096c8c4bc03584f83037a053e3be53a9b --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +OPENAI_API_KEY=your_api_key_here \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..944000a19e7e4fcbeb910fc7468a0266ee54f5ad --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +models +node_modules +.idea +.env +internal +ui +*.txt +node-llama-docs + +frontend* +VIDEO_SCRIPT.md \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000000000000000000000000000000000..aefc1e9131908070976897e574ca918e25ee5718 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,118 @@ +# Contributing Guidelines + +Thank you for considering contributing to AI Agents from Scratch! + +## Project Philosophy + +This repository teaches AI agent fundamentals by building from scratch. Every contribution should support this learning mission. + +**Core Principles:** +- **Clarity over cleverness** - Code should be easy to understand +- **Fundamentals first** - No black boxes or magic +- **Progressive learning** - Each example builds on the previous +- **Local-first** - No API dependencies + +## Types of Contributions + +### Bug Reports +Found something broken? Open an issue with: +- Which example (`intro/`, `react-agent/`, etc.) +- What you expected vs. what happened +- Your environment (Node version, OS, model used) +- Steps to reproduce + +### Documentation Improvements +- Typos and grammar fixes +- Clearer explanations +- Better code comments +- Additional examples in documentation +- Diagrams and visualizations + +### New Examples +Want to add a new agent pattern? Great! Please: +1. **Open an issue first** - let's discuss if it fits +2. Follow the existing structure: +- `pattern-name/pattern-name.js` - Working code +- `pattern-name/CODE.md` - Detailed code walkthrough +- `pattern-name/CONCEPT.md` - Why it matters, use cases +3. Keep it simple and well-commented +4. Test thoroughly with at least one model + +### Code Improvements +- Performance optimizations (with benchmarks) +- Better error handling +- Clearer variable names +- More helpful console output + +## What We're Not Looking For + +- Framework integrations (LangChain, etc.) - this repo teaches what they do +- Cloud API examples - keep it local +- Production features (monitoring, scaling) - this is educational +- Complex abstractions - keep it beginner-friendly + +## Contribution Process + +1. **Fork** the repository +2. **Create a branch**: `git checkout -b fix/issue-description` +3. **Make changes** and test thoroughly +4. **Commit** with clear messages: `git commit -m "Fix: clarify ReAct loop explanation"` +5. **Push**: `git push origin fix/issue-description` +6. **Open a Pull Request** with: +- Clear title +- Description of what changed and why +- Which issue it addresses (if any) + +## Code Standards + +- Use clear, descriptive variable names +- Add comments explaining *why*, not just *what* +- Follow existing code style (no linter, just match the patterns) +- Keep examples self-contained (one file when possible) +- Test with Qwen or Llama models before submitting + +## Documentation Standards + +- Use clear, simple language +- Explain concepts before code +- Include diagrams where helpful (ASCII art is fine!) +- Provide real-world use cases +- Link to related examples + +## Example Structure +``` +new-pattern/ +├── new-pattern.js # The working code +├── CODE.md # Line-by-line walkthrough +└── CONCEPT.md # High-level explanation +``` + +**CODE.md should include:** +- Prerequisites +- Step-by-step code breakdown +- How to run it +- Expected output + +**CONCEPT.md should include:** +- What problem it solves +- Why this pattern matters +- Real-world applications +- Simple diagrams + +## Getting Help + +- Not sure if your idea fits? **Open an issue to discuss** +- Stuck on implementation? **Ask in the issue** +- Want to pair on something? **Reach out!** + +## License + +By contributing, you agree that your contributions will be licensed under the same license as the project (MIT). + +## Recognition + +All contributors will be recognized in the README. Thank you for helping others learn! + +--- + +**Questions?** Open an issue or reach out. Happy to help guide your contribution! \ No newline at end of file diff --git a/DOWNLOAD.md b/DOWNLOAD.md new file mode 100644 index 0000000000000000000000000000000000000000..e93fe6f2ba43f9cecd7a980b0eb0e886503e15ab --- /dev/null +++ b/DOWNLOAD.md @@ -0,0 +1,24 @@ +Download the models used in this repository + +You can adjust the quantization level to balance model precision and file size: +Use `:Q8_0` for higher precision and better output quality, but note that it requires more memory and storage. +Use `:Q6_K` for a good balance between size and accuracy (recommended default). +Use `:Q5_K_S` for a smaller model that loads faster and uses less memory, but with slightly lower precision. + +``` +npx --no node-llama-cpp pull --dir ./models hf:Qwen/Qwen3-1.7B-GGUF:Q8_0 --filename Qwen3-1.7B-Q8_0.gguf +``` + +``` +npx --no node-llama-cpp pull --dir ./models hf:giladgd/gpt-oss-20b-GGUF/gpt-oss-20b.MXFP4.gguf +``` + +``` +npx --no node-llama-cpp pull --dir ./models hf:unsloth/DeepSeek-R1-0528-Qwen3-8B-GGUF:Q6_K --filename DeepSeek-R1-0528-Qwen3-8B-Q6_K.gguf +``` + +``` +npx --no node-llama-cpp pull --dir ./models hf:giladgd/Apertus-8B-Instruct-2509-GGUF:Q6_K +``` + + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..04dbac5cd519de48b751dba1a0897a059810a855 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +FROM node:18-slim + +# Install dependencies for building node-llama-cpp +RUN apt-get update && apt-get install -y python3 make g++ curl + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install npm dependencies +RUN npm install + +# Copy source code +COPY . . + +# Create models directory +RUN mkdir -p models + +# Download the model during build (so it's baked into the image) +# Using direct download URL for speed if possible, or use node-llama-cpp pull +RUN npx --no node-llama-cpp pull --dir ./models hf:Qwen/Qwen3-1.7B-GGUF:Q8_0 --filename Qwen3-1.7B-Q8_0.gguf + +# Expose the port HF expects +EXPOSE 7860 + +# Start the server +CMD ["node", "server.js"] diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000000000000000000000000000000000000..f558a17b26b0b4dc75b0a6c24ecd305116d8dbc8 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 [Your Name] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/PROMPTING.md b/PROMPTING.md new file mode 100644 index 0000000000000000000000000000000000000000..a774f1cdbb5e645cc4ac6fc19af4b50bfa838d99 --- /dev/null +++ b/PROMPTING.md @@ -0,0 +1,160 @@ +# Prompt Engineering + +Prompt engineering offers the quickest and most straightforward method for shaping how an agent behaves—defining +its personality, function, and choices (such as when it should utilize tools). Agents operate using two prompt categories: +system-level and user-level prompts. + +User-level prompts consist of the messages individuals enter during conversation. These vary with each interaction +and remain outside the developer's control. + +System-level prompts contain instructions established by developers that remain constant throughout the dialogue. +These define the agent's tone, capabilities, limitations, and guidelines for tool usage. + +Look into the system prompts from Anthropic + +https://docs.claude.com/en/release-notes/system-prompts#september-29-2025 + +## Prompt Design + +When creating prompts for agents, you need to achieve two things: + +1. Make the agent solve problems well + +- Help it complete complex tasks correctly +- Enable clear, logical thinking +- Reduce mistakes + +2. Keep the agent's personality consistent + +- Define who the agent is and how it speaks +- Match your brand's voice +- Respond with appropriate emotion for each situation + +Both goals matter equally. An accurate answer delivered rudely hurts the user experience. A friendly answer that +doesn't actually help is useless. + +## Prompt Strategies + +### Agents Role + +Giving the LLM a specific role improves its responses - it naturally adopts that role's vocabulary and expertise. +Examples: + +"You are a pediatrician" → Uses medical terms, discusses child development, recommends age-appropriate treatments +"You are a chef" → Explains cooking techniques, suggests ingredient substitutions, discusses flavor profiles +"You are a high school math teacher" → Breaks down problems step-by-step, uses simple language, provides practice examples +"You are a startup founder" → Focuses on growth, uses business metrics, thinks about scalability + +Make roles specific: +Instead of: "You are a writer" +Better: "You are a tech blogger who simplifies complex AI concepts for beginners" + +Roles work best for specialized questions and should be set in system prompts. + +### Be Specific, Not Vague + +LLMs interpret instructions literally. Vague prompts produce random results. Specific prompts produce consistent outputs. +Vague vs Specific Examples: + +❌ Vague: "Write something about dogs" +✅ Specific: "Write a 3-paragraph guide on training a puppy to sit" + +❌ Vague: "Make it better" +✅ Specific: "Fix grammar errors and shorten to under 100 words" + +❌ Vague: "Be professional" +✅ Specific: "Use formal language, avoid contractions, address the reader as 'you'" + +❌ Vague: "Analyze this data" +✅ Specific: "Find the top 3 trends and explain what caused each one" + +Why it matters: The LLM has thousands of ways to interpret vague instructions. It will guess what you want—and often +guess wrong. Clear instructions eliminate guesswork and give you control over the output. + +Rule of thumb: If a human assistant would need to ask clarifying questions, your prompt is too vague. + +### Structuring LLM Inputs with JSON +Using JSON to structure your input helps LLMs understand tasks more clearly and makes integration easier. Instead of +sending a blob of text, break your request into labeled parts like task, input, constraints, and output_format. + +Benefits +- Clarity: JSON keys show the model what each part means. +- Reliability: Easier to parse and validate responses. +- Consistency: Reduces random or narrative answers. +- Integration: Works well with APIs and schemas. + +Best Practices +- Keep it simple and shallow — avoid deep nesting. +- Use descriptive keys ("task", "context", "constraints"). +- Tell the model the exact output format (e.g., “Respond with valid JSON only”). +- Optionally define a JSON Schema to enforce structure. +- Always validate the response in your code. + +Example +```` +{ + "task": "summarize", + "input_text": " - Article text here. - ", + "constraints": { + "max_words": 100, + "audience": "non-technical" + }, + "output_format": { + "type": "JSON", + "schema": { + "summary": "string", + "key_points": ["string"] + } + } +} +```` + +This structured format helps the model separate what to do, what data to use, and how to reply, resulting in +more consistent, machine-readable outputs. + +### Few-Shot Prompting + +Few-shot prompting means giving the LLM a few examples of what you want before asking it to do a new task. +It’s like showing a student two or three solved problems so they understand the pattern. + +Example +``` +Example 1: +Feedback: "The room was clean and quiet." +Category: Positive + +Example 2: +Feedback: "The staff were rude and unhelpful." +Category: Negative + +Example 3: +Feedback: "Breakfast was okay, but the coffee was cold." +Category: Neutral + +Now categorize this: +Feedback: "The view from the balcony was amazing!" +Category: +``` + +The model learns from the examples and continues in the same style — here, it would answer: +"Good morning" + +Few-shot prompts are useful when you want consistent tone, format, or logic without retraining the model. + +### Chain of Thought + +Chain of thought means asking the LLM to think step by step instead of jumping straight to the answer. +It helps the model reason better, especially for logic, math, or multi-step problems. + +Example + +Question: If 3 apples cost $6, how much do 5 apples cost? +Let's think step by step. + +Model reasoning: +3 apples → $6 → each apple costs $2. +5 apples × $2 = $10. + +Answer: $10 + +By encouraging step-by-step thinking, you help the model make fewer mistakes and explain its reasoning clearly. \ No newline at end of file diff --git a/README.md b/README.md index 99f50fab0507c5196591c614f0f39d3afbc07a1f..a6c5651746aefccf8bf9efeee1227a8cde69e491 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,504 @@ ---- -title: Email -emoji: 🦀 -colorFrom: yellow -colorTo: gray -sdk: docker -pinned: false ---- - -Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference +> **Read the full interactive version:** +> This repository is part of **AI Agents From Scratch** - a hands-on learning series where we build AI agents *step by step*, explain every design decision, and visualize what’s happening under the hood. +> +> 👉 **https://agentsfromscratch.com** +> +> If you prefer **long-form explanations, diagrams, and conceptual deep dives**, start there - then come back here to explore the code. + + +# AI Agents From Scratch + +Learn to build AI agents locally without frameworks. Understand what happens under the hood before using production frameworks. + +## Purpose + +This repository teaches you to build AI agents from first principles using **local LLMs** and **node-llama-cpp**. By working through these examples, you'll understand: + +- How LLMs work at a fundamental level +- What agents really are (LLM + tools + patterns) +- How different agent architectures function +- Why frameworks make certain design choices + +**Philosophy**: Learn by building. Understand deeply, then use frameworks wisely. + +## Related Projects + +### [AI Product from Scratch](https://github.com/pguso/ai-product-from-scratch) + +[![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?logo=typescript&logoColor=white)](https://www.typescriptlang.org/) +[![React](https://img.shields.io/badge/React-20232A?logo=react&logoColor=61DAFB)](https://reactjs.org/) +[![Node.js](https://img.shields.io/badge/Node.js-339933?logo=node.js&logoColor=white)](https://nodejs.org/) + +Learn AI product development fundamentals with local LLMs. Covers prompt engineering, structured output, multi-step reasoning, API design, and frontend integration through 10 comprehensive lessons with visual diagrams. + +### [AI Agents from Scratch in Python](https://github.com/pguso/agents-from-scratch) + +![Python](https://img.shields.io/badge/Python-3776AB?logo=python&logoColor=white) + +## Next Phase: Build LangChain & LangGraph Concepts From Scratch + +> After mastering the fundamentals, the next stage of this project walks you through **re-implementing the core parts of LangChain and LangGraph** in plain JavaScript using local models. +> This is **not** about building a new framework, it’s about understanding *how frameworks work*. + +## Phase 1: Agent Fundamentals - From LLMs to ReAct + +### Prerequisites +- Node.js 18+ +- At least 8GB RAM (16GB recommended) +- Download models and place in `./models/` folder, details in [DOWNLOAD.md](DOWNLOAD.md) + +### Installation +```bash +npm install +``` + +### Run Examples +```bash +node intro/intro.js +node simple-agent/simple-agent.js +node react-agent/react-agent.js +``` + +## Learning Path + +Follow these examples in order to build understanding progressively: + +### 1. **Introduction** - Basic LLM Interaction +`intro/` | [Code](examples/01_intro/intro.js) | [Code Explanation](examples/01_intro/CODE.md) | [Concepts](examples/01_intro/CONCEPT.md) + +**What you'll learn:** +- Loading and running a local LLM +- Basic prompt/response cycle + +**Key concepts**: Model loading, context, inference pipeline, token generation + +--- + +### 2. (Optional) **OpenAI Intro** - Using Proprietary Models +`openai-intro/` | [Code](examples/02_openai-intro/openai-intro.js) | [Code Explanation](examples/02_openai-intro/CODE.md) | [Concepts](examples/02_openai-intro/CONCEPT.md) + +**What you'll learn:** +- How to call hosted LLMs (like GPT-4) +- Temperature Control +- Token Usage + +**Key concepts**: Inference endpoints, network latency, cost vs control, data privacy, vendor dependence + +--- + +### 3. **Translation** - System Prompts & Specialization +`translation/` | [Code](examples/03_translation/translation.js) | [Code Explanation](examples/03_translation/CODE.md) | [Concepts](examples/03_translation/CONCEPT.md) + +**What you'll learn:** +- Using system prompts to specialize agents +- Output format control +- Role-based behavior +- Chat wrappers for different models + +**Key concepts**: System prompts, agent specialization, behavioral constraints, prompt engineering + +--- + +### 4. **Think** - Reasoning & Problem Solving +`think/` | [Code](examples/04_think/think.js) | [Code Explanation](examples/04_think/CODE.md) | [Concepts](examples/04_think/CONCEPT.md) + +**What you'll learn:** +- Configuring LLMs for logical reasoning +- Complex quantitative problems +- Limitations of pure LLM reasoning +- When to use external tools + +**Key concepts**: Reasoning agents, problem decomposition, cognitive tasks, reasoning limitations + +--- + +### 5. **Batch** - Parallel Processing +`batch/` | [Code](examples/05_batch/batch.js) | [Code Explanation](examples/05_batch/CODE.md) | [Concepts](examples/05_batch/CONCEPT.md) + +**What you'll learn:** +- Processing multiple requests concurrently +- Context sequences for parallelism +- GPU batch processing +- Performance optimization + +**Key concepts**: Parallel execution, sequences, batch size, throughput optimization + +--- + +### 6. **Coding** - Streaming & Response Control +`coding/` | [Code](examples/06_coding/coding.js) | [Code Explanation](examples/06_coding/CODE.md) | [Concepts](examples/06_coding/CONCEPT.md) + +**What you'll learn:** +- Real-time streaming responses +- Token limits and budget management +- Progressive output display +- User experience optimization + +**Key concepts**: Streaming, token-by-token generation, response control, real-time feedback + +--- + +### 7. **Simple Agent** - Function Calling (Tools) +`simple-agent/` | [Code](examples/07_simple-agent/simple-agent.js) | [Code Explanation](examples/07_simple-agent/CODE.md) | [Concepts](examples/07_simple-agent/CONCEPT.md) + +**What you'll learn:** +- Function calling / tool use fundamentals +- Defining tools the LLM can use +- JSON Schema for parameters +- How LLMs decide when to use tools + +**Key concepts**: Function calling, tool definitions, agent decision making, action-taking + +**This is where text generation becomes agency!** + +--- + +### 8. **Simple Agent with Memory** - Persistent State +`simple-agent-with-memory/` | [Code](examples/08_simple-agent-with-memory/simple-agent-with-memory.js) | [Code Explanation](examples/08_simple-agent-with-memory/CODE.md) | [Concepts](examples/08_simple-agent-with-memory/CONCEPT.md) + +**What you'll learn:** +- Persisting information across sessions +- Long-term memory management +- Facts and preferences storage +- Memory retrieval strategies + +**Key concepts**: Persistent memory, state management, memory systems, context augmentation + +--- + +### 9. **ReAct Agent** - Reasoning + Acting +`react-agent/` | [Code](examples/09_react-agent/react-agent.js) | [Code Explanation](examples/09_react-agent/CODE.md) | [Concepts](examples/09_react-agent/CONCEPT.md) + +**What you'll learn:** +- ReAct pattern (Reason → Act → Observe) +- Iterative problem solving +- Step-by-step tool use +- Self-correction loops + +**Key concepts**: ReAct pattern, iterative reasoning, observation-action cycles, multi-step agents + +**This is the foundation of modern agent frameworks!** + +--- + +### 10. **AoT Agent** - Atom of Thought Planning +`aot-agent/` | [Code](examples/10_aot-agent/aot-agent.js) | [Code Explanation](examples/10_aot-agent/CODE.md) | [Concepts](examples/10_aot-agent/CONCEPT.md) + +**What you'll learn:** +- Atom of Thought methodology +- Atomic planning for multi-step computations +- Dependency management between operations +- Structured JSON output for reasoning plans +- Deterministic execution of plans + +**Key concepts**: AoT planning, atomic operations, dependency resolution, plan validation, structured reasoning + +--- + +## Documentation Structure + +Each example folder contains: + +- **`.js`** - The working code example +- **`CODE.md`** - Step-by-step code explanation +- Line-by-line breakdowns +- What each part does +- How it works +- **`CONCEPT.md`** - High-level concepts +- Why it matters for agents +- Architectural patterns +- Real-world applications +- Simple diagrams + +## Core Concepts + +### What is an AI Agent? + +``` +AI Agent = LLM + System Prompt + Tools + Memory + Reasoning Pattern + ─┬─ ──────┬────── ──┬── ──┬─── ────────┬──────── + │ │ │ │ │ + Brain Identity Hands State Strategy +``` + +### Evolution of Capabilities + +``` +1. intro → Basic LLM usage +2. translation → Specialized behavior (system prompts) +3. think → Reasoning ability +4. batch → Parallel processing +5. coding → Streaming & control +6. simple-agent → Tool use (function calling) +7. memory-agent → Persistent state +8. react-agent → Strategic reasoning + tool use +``` + +### Architecture Patterns + +**Simple Agent (Steps 1-5)** +``` +User → LLM → Response +``` + +**Tool-Using Agent (Step 6)** +``` +User → LLM ⟷ Tools → Response +``` + +**Memory Agent (Step 7)** +``` +User → LLM ⟷ Tools → Response + ↕ + Memory +``` + +**ReAct Agent (Step 8)** +``` +User → LLM → Think → Act → Observe + ↑ ↓ ↓ ↓ + └──────┴──────┴──────┘ + Iterate until solved +``` + +## ️ Helper Utilities + +### PromptDebugger +`helper/prompt-debugger.js` + +Utility for debugging prompts sent to the LLM. Shows exactly what the model sees, including: +- System prompts +- Function definitions +- Conversation history +- Context state + +Usage example in `simple-agent/simple-agent.js` + +## ️ Project Structure - Fundamentals + +``` +ai-agents/ +├── README.md ← You are here +├─ examples/ +├── 01_intro/ +│ ├── intro.js +│ ├── CODE.md +│ └── CONCEPT.md +├── 02_openai-intro/ +│ ├── openai-intro.js +│ ├── CODE.md +│ └── CONCEPT.md +├── 03_translation/ +│ ├── translation.js +│ ├── CODE.md +│ └── CONCEPT.md +├── 04_think/ +│ ├── think.js +│ ├── CODE.md +│ └── CONCEPT.md +├── 05_batch/ +│ ├── batch.js +│ ├── CODE.md +│ └── CONCEPT.md +├── 06_coding/ +│ ├── coding.js +│ ├── CODE.md +│ └── CONCEPT.md +├── 07_simple-agent/ +│ ├── simple-agent.js +│ ├── CODE.md +│ └── CONCEPT.md +├── 08_simple-agent-with-memory/ +│ ├── simple-agent-with-memory.js +│ ├── memory-manager.js +│ ├── CODE.md +│ └── CONCEPT.md +├── 09_react-agent/ +│ ├── react-agent.js +│ ├── CODE.md +│ └── CONCEPT.md +├── helper/ +│ └── prompt-debugger.js +├── models/ ← Place your GGUF models here +└── logs/ ← Debug outputs +``` + +## Phase 2: Building a Production Framework (Tutorial) + +After mastering the fundamentals above, **Phase 2** takes you from scratch examples to production-grade framework design. You'll rebuild core concepts from **LangChain** and **LangGraph** to understand how real frameworks work internally. + +### What You'll Build + +A lightweight but complete agent framework with: +- **Runnable Interface**, The composability pattern that powers everything +- **Message System**, Typed conversation structures (Human, AI, System, Tool) +- **Chains**, Composing multiple operations into pipelines +- **Memory**, Persistent state across conversations +- **Tools**, Function calling and external integrations +- **Agents**, Decision-making loops (ReAct, Tool-calling) +- **Graphs**, State machines for complex workflows (LangGraph concepts) + +### Learning Approach + +**Tutorial-first**: Step-by-step lessons with exercises +**Implementation-driven**: Build each component yourself +**Framework-compatible**: Learn patterns used in LangChain.js + +### Structure Overview + +``` +tutorial/ +├── 01-foundation/ # 1. Core Abstractions +│ ├── 01-runnable/ +│ │ ├── lesson.md # Why Runnable matters +│ │ ├── exercises/ # Hands-on practice +│ │ └── solutions/ # Reference implementations +│ ├── 02-messages/ # Structuring conversations +│ ├── 03-llm-wrapper/ # Wrapping node-llama-cpp +│ └── 04-context/ # Configuration & callbacks +│ +├── 02-composition/ # 2. Building Chains +│ ├── 01-prompts/ # Template system +│ ├── 02-parsers/ # Structured outputs +│ ├── 03-llm-chain/ # Your first chain +│ ├── 04-piping/ # Composition patterns +│ └── 05-memory/ # Conversation state +│ +├── 03-agency/ # 3. Tools & Agents +│ ├── 01-tools/ # Function definitions +│ ├── 02-tool-executor/ # Safe execution +│ ├── 03-simple-agent/ # Basic agent loop +│ ├── 04-react-agent/ # Reasoning + Acting +│ └── 05-structured-agent/ # JSON mode +│ +└── 04-graphs/ # 4. State Machines + ├── 01-state-basics/ # Nodes & edges + ├── 02-channels/ # State management + ├── 03-conditional-edges/ # Dynamic routing + ├── 04-executor/ # Running workflows + ├── 05-checkpointing/ # Persistence + └── 06-agent-graph/ # Agents as graphs + +src/ +├── core/ # Runnable, Messages, Context +├── llm/ # LlamaCppLLM wrapper +├── prompts/ # Template system +├── chains/ # LLMChain, SequentialChain +├── tools/ # BaseTool, built-in tools +├── agents/ # AgentExecutor, ReActAgent +├── memory/ # BufferMemory, WindowMemory +└── graph/ # StateGraph, CompiledGraph +``` + +### Why This Matters + +**Understanding beats using**: When you know how frameworks work internally, you can: +- Debug issues faster +- Customize behavior confidently +- Make architectural decisions wisely +- Build your own extensions +- Read framework source code fluently + +**Learn once, use everywhere**: The patterns you'll learn (Runnable, composition, state machines) apply to: +- LangChain.js - You'll understand their abstractions +- LangGraph.js - You'll grasp state management +- Any agent framework - Same core concepts +- Your own projects - Build custom solutions + +### Getting Started with Phase 2 + +After completing the fundamentals (intro → react-agent), start the tutorial: + +[Overview](tutorial/README.md) + +```bash +# Start with the foundation +cd tutorial/01-foundation/01-runnable +lesson.md # Read the lesson +node exercises/01-*.js # Complete exercises +node solutions/01-*-solution.js # Check your work +``` + +Each lesson includes: +- **Conceptual explanation**, Why it matters +- **Code walkthrough**, How to build it +- **Exercises**, Practice implementing +- **Solutions**, Reference code +- **Real-world examples**, Practical usage + +**Time commitment**: ~8 weeks, 3-5 hours/week + +### What You'll Achieve + +By the end, you'll have: +1. Built a working agent framework from scratch +2. Understood how LangChain/LangGraph work internally +3. Mastered composability patterns +4. Created reusable components (tools, chains, agents) +5. Implemented state machines for complex workflows +6. Gained confidence to use or extend any framework + +**Then**: Use LangChain.js in production, knowing exactly what happens under the hood. + +--- + +## Key Takeaways + +### After Phase 1 (Fundamentals), you'll understand: + +1. **LLMs are stateless**: Context must be managed explicitly +2. **System prompts shape behavior**: Same model, different roles +3. **Function calling enables agency**: Tools transform text generators into agents +4. **Memory is essential**: Agents need to remember across sessions +5. **Reasoning patterns matter**: ReAct > simple prompting for complex tasks +6. **Performance matters**: Parallel processing, streaming, token limits +7. **Debugging is crucial**: See exactly what the model receives + +### After Phase 2 (Framework Tutorial), you'll master: + +1. **The Runnable pattern**: Why everything in frameworks uses one interface +2. **Composition over configuration**: Building complex systems from simple parts +3. **Message-driven architecture**: How frameworks structure conversations +4. **Chain abstraction**: Connecting prompts, LLMs, and parsers seamlessly +5. **Tool orchestration**: Safe execution with timeouts and error handling +6. **Agent execution loops**: The mechanics of decision-making agents +7. **State machines**: Managing complex workflows with graphs +8. **Production patterns**: Error handling, retries, streaming, and debugging + +### What frameworks give you: + +Now that you understand the fundamentals, frameworks like LangChain, CrewAI, or AutoGPT provide: +- Pre-built reasoning patterns and agent templates +- Extensive tool libraries and integrations +- Production-ready error handling and retries +- Multi-agent orchestration +- Observability and monitoring +- Community extensions and plugins + +**You'll use them better because you know what they're doing under the hood.** + +## Additional Resources + +- **node-llama-cpp**: [GitHub](https://github.com/withcatai/node-llama-cpp) +- **Model Hub**: [Hugging Face](https://huggingface.co/models?library=gguf) +- **GGUF Format**: Quantized models for local inference + +## Contributing + +This is a learning resource. Feel free to: +- Suggest improvements to documentation +- Add more example patterns +- Fix bugs or unclear explanations +- Share what you built! + +## License + +Educational resource - use and modify as needed for learning. + +--- + +**Built with ❤️ for people who want to truly understand AI agents** + +Start with `intro/` and work your way through. Each example builds on the previous one. Read both CODE.md and CONCEPT.md for full understanding. + +Happy learning! diff --git a/SUMMARY_COMPOSITION.md b/SUMMARY_COMPOSITION.md new file mode 100644 index 0000000000000000000000000000000000000000..b0c64927d7c3d8104bd91dc4844da99264d57170 --- /dev/null +++ b/SUMMARY_COMPOSITION.md @@ -0,0 +1,26 @@ +# Tổng hợp kiến thức: AI Agents from Scratch - Phần Composition + +Tài liệu này tổng hợp các khái niệm về cách kết hợp các thành phần (Composition) để tạo nên hệ thống AI mạnh mẽ hơn. + +## 1. Prompts (Mẫu câu lệnh) +Thay vì hardcode các chuỗi văn bản, chúng ta sử dụng các **Template** để quản lý đầu vào cho LLM. + +* **PromptTemplate**: Mẫu cơ bản với các biến giữ chỗ (placeholders). Giúp tách biệt logic code khỏi nội dung văn bản. +* **ChatPromptTemplate**: Mẫu chuyên dụng cho các model chat (như GPT-4, Llama 3). + * Cấu trúc hóa hội thoại thành danh sách tin nhắn: `System`, `Human`, `AI`. + * Hỗ trợ tiêm biến vào từng loại tin nhắn. + * Là tiêu chuẩn cho các ứng dụng AI hiện đại. +* **PipelinePromptTemplate**: Cho phép ghép nối nhiều template nhỏ thành một template lớn, giúp quản lý các prompt phức tạp. + +## 2. Output Parsers (Bộ phân tích đầu ra) +Chuyển đổi văn bản thô từ LLM thành cấu trúc dữ liệu mà ứng dụng có thể sử dụng (JSON, Object, Array). + +* **Vấn đề:** Output của LLM thường không nhất quán và khó parse bằng Regex. +* **StructuredOutputParser**: Công cụ mạnh mẽ nhất. + * **Schema Definition**: Định nghĩa rõ ràng các trường (fields), kiểu dữ liệu (type), mô tả (description) và giá trị cho phép (enum). + * **Format Instructions**: Parser tự động sinh ra hướng dẫn định dạng (ví dụ: "Respond in JSON format...") để chèn vào prompt. + * **Validation**: Tự động kiểm tra kết quả trả về có đúng schema hay không. +* **Lợi ích:** Đảm bảo tính ổn định (reliability) cho hệ thống, biến AI từ một "chatbot" thành một "công cụ xử lý dữ liệu". + +--- +*Tài liệu được tạo tự động bởi Antigravity IDE sau quá trình tự học và phân tích code.* diff --git a/SUMMARY_FOUNDATION.md b/SUMMARY_FOUNDATION.md new file mode 100644 index 0000000000000000000000000000000000000000..dde9e5521622aeab6e767a710dc6b12d046c7b8f --- /dev/null +++ b/SUMMARY_FOUNDATION.md @@ -0,0 +1,46 @@ +# Tổng hợp kiến thức: AI Agents from Scratch - Phần Foundation + +Tài liệu này tổng hợp các khái niệm cốt lõi đã học được từ 4 bài học đầu tiên trong series "AI Agents from Scratch". + +## 1. Runnable (Đơn vị thực thi) +**Runnable** là "viên gạch LEGO" của framework, chuẩn hóa giao diện cho mọi thành phần (LLM, Parser, Tool). + +* **Hợp đồng (Contract):** Mọi Runnable đều phải triển khai phương thức `_call(input, config)`. +* **3 Phương thức thực thi:** + 1. `invoke(input)`: Chạy đơn lẻ (1 input -> 1 output). + 2. `stream(input)`: Trả về kết quả dạng dòng (chunks) theo thời gian thực. + 3. `batch([inputs])`: Xử lý song song một danh sách input để tăng hiệu suất. +* **Lợi ích:** Cho phép nối các thành phần khác nhau thành một chuỗi (chain) dễ dàng bằng `.pipe()`. + +## 2. Messages (Tin nhắn & Cấu trúc dữ liệu) +Thay vì sử dụng chuỗi văn bản thuần túy, hội thoại được cấu trúc hóa thành các đối tượng để dễ quản lý và phân loại. + +* **Các loại tin nhắn:** + * `SystemMessage`: Chỉ thị hệ thống, thiết lập hành vi/nhân cách cho AI. + * `HumanMessage`: Tin nhắn từ người dùng. + * `AIMessage`: Phản hồi từ AI. + * `ToolMessage`: Kết quả trả về từ việc gọi công cụ (function calling). +* **Quản lý hội thoại:** Cần có cơ chế (như `ConversationHistory`) để lưu trữ, giới hạn độ dài (sliding window) và lọc tin nhắn theo loại. + +## 3. LLM Wrapper (Bọc mô hình ngôn ngữ) +**LLM Wrapper** biến đổi một thư viện LLM thô (như `node-llama-cpp`) thành một **Runnable**. + +* **Vai trò:** Đóng vai trò như một Adapter (bộ chuyển đổi). +* **Chức năng:** + * Chuyển đổi input (chuỗi hoặc danh sách Message) thành format mà model hiểu được. + * Xử lý việc gọi model (generate/stream). + * Trả về kết quả dưới dạng `AIMessage`. +* **Kết quả:** Giúp thay thế model dễ dàng mà không ảnh hưởng đến phần còn lại của hệ thống. + +## 4. Context & Configuration (Ngữ cảnh & Cấu hình) +**RunnableConfig** là cơ chế truyền thông tin xuyên suốt chuỗi xử lý mà không làm rối mã nguồn. + +* **Vấn đề giải quyết:** Tránh việc phải truyền tham số cấu hình (như `userId`, `debug flag`) qua từng hàm thủ công. +* **Thành phần của Config:** + * `callbacks`: Hệ thống hook để theo dõi (log, metrics) tại các điểm bắt đầu/kết thúc/lỗi. + * `metadata`: Dữ liệu ngữ cảnh (User ID, Session ID). + * `configurable`: Các tham số thay đổi lúc chạy (Runtime overrides), ví dụ: thay đổi `temperature` của LLM cho từng request cụ thể. +* **Ứng dụng:** Rất hữu ích cho A/B testing, logging tập trung và quản lý đa người dùng. + +--- +*Tài liệu được tạo tự động bởi Antigravity IDE sau quá trình tự học và phân tích code.* diff --git a/SUMMARY_FULL.md b/SUMMARY_FULL.md new file mode 100644 index 0000000000000000000000000000000000000000..50779bd8dbefa12e51071ccb66b73469dd104621 --- /dev/null +++ b/SUMMARY_FULL.md @@ -0,0 +1,56 @@ +# Tổng hợp kiến thức: AI Agents from Scratch + +Tài liệu này tổng hợp toàn bộ các khái niệm cốt lõi và mẫu thiết kế đã học được từ repository "AI Agents from Scratch". + +## PHẦN 1: FOUNDATION (NỀN TẢNG) + +### 1. Runnable (Đơn vị thực thi) +**Runnable** là "viên gạch LEGO" của framework, chuẩn hóa giao diện cho mọi thành phần. +* **Hợp đồng (Contract):** Triển khai phương thức `_call(input, config)`. +* **3 Chế độ:** `invoke` (đơn), `stream` (dòng), `batch` (song song). +* **Composition:** Dễ dàng nối chuỗi bằng `.pipe()`. + +### 2. Messages (Cấu trúc hội thoại) +Sử dụng các lớp đối tượng thay vì chuỗi trần. +* **SystemMessage**: Chỉ thị, nhân cách. +* **HumanMessage**: Input người dùng. +* **AIMessage**: Output mô hình. +* **ToolMessage**: Kết quả gọi hàm. + +### 3. LLM Wrapper +Đóng gói model thô (node-llama-cpp) thành một **Runnable** để đồng bộ hóa giao diện và dễ dàng thay thế. + +### 4. Context & Configuration +Truyền `RunnableConfig` xuyên suốt pipeline. +* `callbacks`: Logging, metrics, side-effects. +* `metadata`: Context người dùng/phiên. +* `configurable`: Runtime overrides (ví dụ: thay đổi temperature động). + +--- + +## PHẦN 2: COMPOSITION (KẾT HỢP) + +### 1. Prompts +Quản lý đầu vào LLM bằng Templates. +* **PromptTemplate**: Tách logic khỏi văn bản, hỗ trợ biến số. +* **ChatPromptTemplate**: Cấu trúc hóa hội thoại đa lượt (Multi-turn conversation). + +### 2. Output Parsers +Chuyển đổi văn bản thô từ LLM thành dữ liệu có cấu trúc. +* **StructuredOutputParser**: Định nghĩa Schema (JSON), tự động sinh hướng dẫn định dạng (`format_instructions`) và validate kết quả. Giải quyết vấn đề output không nhất quán của LLM. + +--- + +## PHẦN 3: PROJECT PATTERNS (MẪU THIẾT KẾ THỰC TẾ) + +Từ dự án **Smart Email Classifier**, rút ra mẫu kiến trúc tham khảo cho các tác vụ phân loại/xử lý văn bản: + +1. **Separation of Concerns (Phân tách mối quan tâm):** + * `ParserRunnable`: Chỉ lo việc làm sạch và chuẩn hóa dữ liệu đầu vào. + * `ClassifierRunnable`: Chỉ lo việc gọi LLM và xử lý logic phân loại. +2. **Pipeline:** Kết nối `Parser -> Classifier`. +3. **Side Effects via Callbacks:** Sử dụng Callback để ghi log lịch sử và tính toán thống kê (Statistics), giữ cho code chính sạch sẽ. +4. **Strict System Prompts:** Sử dụng System Prompt chi tiết để định nghĩa danh mục và ép kiểu JSON output. + +--- +*Tài liệu được tạo tự động bởi Antigravity IDE.* diff --git a/examples/01_intro/CODE.md b/examples/01_intro/CODE.md new file mode 100644 index 0000000000000000000000000000000000000000..09abc9f59bea8f0e4483fb9b784b85a3243d958b --- /dev/null +++ b/examples/01_intro/CODE.md @@ -0,0 +1,112 @@ +# Code Explanation: intro.js + +This file demonstrates the most basic interaction with a local LLM (Large Language Model) using node-llama-cpp. + +## Step-by-Step Code Breakdown + +### 1. Import Required Modules +```javascript +import { + getLlama, + LlamaChatSession, +} from "node-llama-cpp"; +import {fileURLToPath} from "url"; +import path from "path"; +``` +- **getLlama**: Main function to initialize the llama.cpp runtime +- **LlamaChatSession**: Class for managing chat conversations with the model +- **fileURLToPath** and **path**: Standard Node.js modules for handling file paths + +### 2. Set Up Directory Path +```javascript +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +``` +- Since ES modules don't have `__dirname` by default, we create it manually +- This gives us the directory path of the current file +- Needed to locate the model file relative to this script + +### 3. Initialize Llama Runtime +```javascript +const llama = await getLlama(); +``` +- Creates the main llama.cpp instance +- This initializes the underlying C++ runtime for model inference +- Must be done before loading any models + +### 4. Load the Model +```javascript +const model = await llama.loadModel({ + modelPath: path.join( + __dirname, + "../", + "models", + "Qwen3-1.7B-Q8_0.gguf" + ) +}); +``` +- Loads a quantized model file (GGUF format) +- **Qwen3-1.7B-Q8_0.gguf**: A 1.7 billion parameter model, quantized to 8-bit +- The model is stored in the `models` folder at the repository root +- Loading the model into memory takes a few seconds + +### 5. Create a Context +```javascript +const context = await model.createContext(); +``` +- A **context** represents the model's working memory +- It holds the conversation history and current state +- Has a fixed size limit (default: model's maximum context size) +- All prompts and responses are stored in this context + +### 6. Create a Chat Session +```javascript +const session = new LlamaChatSession({ + contextSequence: context.getSequence(), +}); +``` +- **LlamaChatSession**: High-level API for chat-style interactions +- Uses a sequence from the context to maintain conversation state +- Automatically handles prompt formatting and response parsing + +### 7. Define the Prompt +```javascript +const prompt = `do you know node-llama-cpp`; +``` +- Simple question to test if the model knows about the library we're using +- This will be sent to the model for processing + +### 8. Send Prompt and Get Response +```javascript +const a1 = await session.prompt(prompt); +console.log("AI: " + a1); +``` +- **session.prompt()**: Sends the prompt to the model and waits for completion +- The model generates a response based on its training +- We log the response to the console with "AI:" prefix + +### 9. Clean Up Resources +```javascript +session.dispose() +context.dispose() +model.dispose() +llama.dispose() +``` +- **Important**: Always dispose of resources when done +- Frees up memory and GPU resources +- Prevents memory leaks in long-running applications +- Must be done in this order (session → context → model → llama) + +## Key Concepts Demonstrated + +1. **Basic LLM initialization**: Loading a model and creating inference context +2. **Simple prompting**: Sending a question and receiving a response +3. **Resource management**: Proper cleanup of allocated resources + +## Expected Output + +When you run this script, you should see output like: +``` +AI: Yes, I'm familiar with node-llama-cpp. It's a Node.js binding for llama.cpp... +``` + +The exact response will vary based on the model's training data and generation parameters. diff --git a/examples/01_intro/CONCEPT.md b/examples/01_intro/CONCEPT.md new file mode 100644 index 0000000000000000000000000000000000000000..9816eb0ee42ba0b54d2b6d4f7d9d58ecfc67893c --- /dev/null +++ b/examples/01_intro/CONCEPT.md @@ -0,0 +1,175 @@ +# Concept: Basic LLM Interaction + +## Overview + +This example introduces the fundamental concepts of working with a Large Language Model (LLM) running locally on your machine. It demonstrates the simplest possible interaction: loading a model and asking it a question. + +## What is a Local LLM? + +A **Local LLM** is an AI language model that runs entirely on your own computer, without requiring internet connectivity or external API calls. Key benefits: + +- **Privacy**: Your data never leaves your machine +- **Cost**: No per-token API charges +- **Control**: Full control over model selection and parameters +- **Offline**: Works without internet connection + +## Core Components + +### 1. Model Files (GGUF Format) + +``` +┌─────────────────────────────┐ +│ Qwen3-1.7B-Q8_0.gguf │ +│ (Model Weights File) │ +│ │ +│ • Stores learned patterns │ +│ • Quantized for efficiency │ +│ • Loaded into RAM/VRAM │ +└─────────────────────────────┘ +``` + +- **GGUF**: File format optimized for llama.cpp +- **Quantization**: Reduces model size (e.g., 8-bit instead of 16-bit) +- **Trade-off**: Smaller size and faster speed vs. slight quality loss + +### 2. The Inference Pipeline + +``` +User Input → Model → Generation → Response + ↓ ↓ ↓ ↓ + "Hello" Context Sampling "Hi there!" +``` + +**Flow Diagram:** +``` +┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ +│ Prompt │ --> │ Context │ --> │ Model │ --> │ Response │ +│ │ │ (Memory) │ │(Weights) │ │ (Text) │ +└──────────┘ └──────────┘ └──────────┘ └──────────┘ +``` + +### 3. Context Window + +The **context** is the model's working memory: + +``` +┌─────────────────────────────────────────┐ +│ Context Window │ +│ ┌─────────────────────────────────┐ │ +│ │ System Prompt (if any) │ │ +│ ├─────────────────────────────────┤ │ +│ │ User: "do you know node-llama?" │ │ +│ ├─────────────────────────────────┤ │ +│ │ AI: "Yes, I'm familiar..." │ │ +│ ├─────────────────────────────────┤ │ +│ │ (Space for more conversation) │ │ +│ └─────────────────────────────────┘ │ +└─────────────────────────────────────────┘ +``` + +- Limited size (e.g., 2048, 4096, or 8192 tokens) +- When full, old messages must be removed +- All previous messages influence the next response + +## How LLMs Generate Responses + +### Token-by-Token Generation + +LLMs don't generate entire sentences at once. They predict one **token** (word piece) at a time: + +``` +Prompt: "What is AI?" + +Generation Process: +"What is AI?" → [Model] → "AI" +"What is AI? AI" → [Model] → "is" +"What is AI? AI is" → [Model] → "a" +"What is AI? AI is a" → [Model] → "field" +... continues until stop condition +``` + +**Visualization:** +``` +Input Prompt + ↓ +┌────────────┐ +│ Model │ → Token 1: "AI" +│ Processes │ → Token 2: "is" +│ & Predicts│ → Token 3: "a" +└────────────┘ → Token 4: "field" + → ... +``` + +## Key Concepts for AI Agents + +### 1. Stateless Processing +- Each prompt is independent unless you maintain context +- The model has no memory between different script runs +- To build an "agent", you need to: + - Keep the context alive between prompts + - Maintain conversation history + - Add tools/functions (covered in later examples) + +### 2. Prompt Engineering Basics +The way you phrase questions affects the response: + +``` +❌ Poor: "node-llama-cpp" +✅ Better: "do you know node-llama-cpp" +✅ Best: "Explain what node-llama-cpp is and how it works" +``` + +### 3. Resource Management +LLMs consume significant resources: + +``` +Model Loading + ↓ +┌─────────────────┐ +│ RAM/VRAM Usage │ ← Models need gigabytes +│ CPU/GPU Time │ ← Inference takes time +│ Memory Leaks? │ ← Must cleanup properly +└─────────────────┘ + ↓ +Proper Disposal +``` + +## Why This Matters for Agents + +This basic example establishes the foundation for AI agents: + +1. **Agents need LLMs to "think"**: The model processes information and generates responses +2. **Agents need context**: To maintain state across interactions +3. **Agents need structure**: Later examples add tools, memory, and reasoning loops + +## Next Steps + +After understanding basic prompting, explore: +- **System prompts**: Giving the model a specific role or behavior +- **Function calling**: Allowing the model to use tools +- **Memory**: Persisting information across sessions +- **Reasoning patterns**: Like ReAct (Reasoning + Acting) + +## Diagram: Complete Architecture + +``` +┌──────────────────────────────────────────────────┐ +│ Your Application │ +│ ┌────────────────────────────────────────────┐ │ +│ │ node-llama-cpp Library │ │ +│ │ ┌──────────────────────────────────────┐ │ │ +│ │ │ llama.cpp (C++ Runtime) │ │ │ +│ │ │ ┌────────────────────────────────┐ │ │ │ +│ │ │ │ Model File (GGUF) │ │ │ │ +│ │ │ │ • Qwen3-1.7B-Q8_0.gguf │ │ │ │ +│ │ │ └────────────────────────────────┘ │ │ │ +│ │ └──────────────────────────────────────┘ │ │ +│ └────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────┘ + ↕ + ┌──────────────┐ + │ CPU / GPU │ + └──────────────┘ +``` + +This layered architecture allows you to build sophisticated AI agents on top of basic LLM interactions. diff --git a/examples/01_intro/intro.js b/examples/01_intro/intro.js new file mode 100644 index 0000000000000000000000000000000000000000..3e7c4fa1fbde5ea51616ce22d446be0ba6ae84a9 --- /dev/null +++ b/examples/01_intro/intro.js @@ -0,0 +1,36 @@ +import { + getLlama, + LlamaChatSession, +} from "node-llama-cpp"; +import {fileURLToPath} from "url"; +import path from "path"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + + +const llama = await getLlama(); +const model = await llama.loadModel({ + modelPath: path.join( + __dirname, + '..', + '..', + 'models', + 'Qwen3-1.7B-Q8_0.gguf' + ) +}); + +const context = await model.createContext(); +const session = new LlamaChatSession({ + contextSequence: context.getSequence(), +}); + +const prompt = `do you know node-llama-cpp`; + +const a1 = await session.prompt(prompt); +console.log("AI: " + a1); + + +session.dispose() +context.dispose() +model.dispose() +llama.dispose() diff --git a/examples/02_openai-intro/CODE.md b/examples/02_openai-intro/CODE.md new file mode 100644 index 0000000000000000000000000000000000000000..5bf843cace71d965cc30ed34b4dd4f407f23fa73 --- /dev/null +++ b/examples/02_openai-intro/CODE.md @@ -0,0 +1,394 @@ +# Code Explanation: OpenAI Intro + +This guide walks through each example in `openai-intro.js`, explaining how to work with OpenAI's API from the ground up. + +## Requirements + +Before running this example, you’ll need an OpenAI account, an API key, and a valid billing method. + +### Get API Key + +https://platform.openai.com/api-keys + +### Add Billing Method + +https://platform.openai.com/settings/organization/billing/overview + +### Configure environment variables + +```bash + cp .env.example .env +``` +Then edit `.env` and add your actual API key. + +## Setup and Initialization + +```javascript +import OpenAI from 'openai'; +import 'dotenv/config'; + +const client = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); +``` + +**What's happening:** +- `import OpenAI from 'openai'` - Import the official OpenAI SDK for Node.js +- `import 'dotenv/config'` - Load environment variables from `.env` file +- `new OpenAI({...})` - Create a client instance that handles API authentication and requests +- `process.env.OPENAI_API_KEY` - Your API key from platform.openai.com (never hardcode this!) + +**Why it matters:** The client object is your interface to OpenAI's models. All API calls go through this client. + +--- + +## Example 1: Basic Chat Completion + +```javascript +const response = await client.chat.completions.create({ + model: 'gpt-4o', + messages: [ + { role: 'user', content: 'What is node-llama-cpp?' } + ], +}); + +console.log(response.choices[0].message.content); +``` + +**What's happening:** +- `chat.completions.create()` - The primary method for sending messages to ChatGPT models +- `model: 'gpt-4o'` - Specifies which model to use (gpt-4o is the latest, most capable model) +- `messages` array - Contains the conversation history +- `role: 'user'` - Indicates this message comes from the user (you) +- `response.choices[0]` - The API returns an array of possible responses; we take the first one +- `message.content` - The actual text response from the AI + +**Response structure:** +```javascript +{ + id: 'chatcmpl-...', + object: 'chat.completion', + created: 1234567890, + model: 'gpt-4o', + choices: [ + { + index: 0, + message: { + role: 'assistant', + content: 'node-llama-cpp is a...' + }, + finish_reason: 'stop' + } + ], + usage: { + prompt_tokens: 10, + completion_tokens: 50, + total_tokens: 60 + } +} +``` + +--- + +## Example 2: System Prompts + +```javascript +const response = await client.chat.completions.create({ + model: 'gpt-4o', + messages: [ + { role: 'system', content: 'You are a coding assistant that talks like a pirate.' }, + { role: 'user', content: 'Explain what async/await does in JavaScript.' } + ], +}); +``` + +**What's happening:** +- `role: 'system'` - Special message type that sets the AI's behavior and personality +- System messages are processed first and influence all subsequent responses +- The model will maintain this behavior throughout the conversation + +**Why it matters:** System prompts are how you specialize AI behavior. They're the foundation of creating focused agents with specific roles (translator, coder, analyst, etc.). + +**Key insight:** Same model + different system prompts = completely different agents! + +--- + +## Example 3: Temperature Control + +```javascript +// Focused response +const focusedResponse = await client.chat.completions.create({ + model: 'gpt-4o', + messages: [{ role: 'user', content: prompt }], + temperature: 0.2, +}); + +// Creative response +const creativeResponse = await client.chat.completions.create({ + model: 'gpt-4o', + messages: [{ role: 'user', content: prompt }], + temperature: 1.5, +}); +``` + +**What's happening:** +- `temperature` - Controls randomness in the output (range: 0.0 to 2.0) +- **Low temperature (0.0 - 0.3):** + - More focused and deterministic + - Same input → similar output + - Best for: factual answers, code generation, data extraction +- **Medium temperature (0.7 - 1.0):** + - Balanced creativity and coherence + - Default for most use cases +- **High temperature (1.2 - 2.0):** + - More creative and varied + - Same input → very different outputs + - Best for: creative writing, brainstorming, story generation + +**Real-world usage:** +- Code completion: temperature 0.2 +- Customer support: temperature 0.5 +- Creative content: temperature 1.2 + +--- + +## Example 4: Conversation Context + +```javascript +const messages = [ + { role: 'system', content: 'You are a helpful coding tutor.' }, + { role: 'user', content: 'What is a Promise in JavaScript?' }, +]; + +const response1 = await client.chat.completions.create({ + model: 'gpt-4o', + messages: messages, +}); + +// Add AI response to history +messages.push(response1.choices[0].message); + +// Add follow-up question +messages.push({ role: 'user', content: 'Can you show me a simple example?' }); + +// Second request with full context +const response2 = await client.chat.completions.create({ + model: 'gpt-4o', + messages: messages, +}); +``` + +**What's happening:** +- OpenAI models are **stateless** - they don't remember previous conversations +- We maintain context by sending the entire conversation history with each request +- Each request is independent; you must include all relevant messages + +**Message order in the array:** +1. System prompt (optional, but recommended first) +2. Previous user message +3. Previous assistant response +4. Current user message + +**Why it matters:** This is how chatbots remember context. The full conversation is sent every time. + +**Performance consideration:** +- More messages = more tokens = higher cost +- Longer conversations eventually hit token limits +- Real applications need conversation trimming or summarization strategies + +--- + +## Example 5: Streaming Responses + +```javascript +const stream = await client.chat.completions.create({ + model: 'gpt-4o', + messages: [ + { role: 'user', content: 'Write a haiku about programming.' } + ], + stream: true, // Enable streaming +}); + +for await (const chunk of stream) { + const content = chunk.choices[0]?.delta?.content || ''; + process.stdout.write(content); +} +``` + +**What's happening:** +- `stream: true` - Instead of waiting for the complete response, receive it token-by-token +- `for await...of` - Iterate over the stream as chunks arrive +- `delta.content` - Each chunk contains a small piece of text (often just a word or partial word) +- `process.stdout.write()` - Write without newline to display text progressively + +**Streaming vs. Non-streaming:** + +**Non-streaming (default):** +``` +[Request sent] +[Wait 5 seconds...] +[Full response arrives] +``` + +**Streaming:** +``` +[Request sent] +Once [chunk arrives: "Once"] +upon [chunk arrives: " upon"] +a [chunk arrives: " a"] +time [chunk arrives: " time"] +... +``` + +**Why it matters:** +- Better user experience (immediate feedback) +- Appears faster even though total time is similar +- Essential for real-time chat interfaces +- Allows early processing/display of partial results + +**When to use streaming:** +- Interactive chat applications +- Long-form content generation +- When user experience matters more than simplicity + +**When to NOT use streaming:** +- Simple scripts or automation +- When you need the complete response before processing +- Batch processing + +--- + +## Example 6: Token Usage + +```javascript +const response = await client.chat.completions.create({ + model: 'gpt-4o', + messages: [ + { role: 'user', content: 'Explain recursion in 3 sentences.' } + ], + max_tokens: 100, +}); + +console.log("Token usage:"); +console.log("- Prompt tokens: " + response.usage.prompt_tokens); +console.log("- Completion tokens: " + response.usage.completion_tokens); +console.log("- Total tokens: " + response.usage.total_tokens); +``` + +**What's happening:** +- `max_tokens` - Limits the length of the AI's response +- `response.usage` - Contains token consumption details +- **Prompt tokens:** Your input (messages you sent) +- **Completion tokens:** AI's output (the response) +- **Total tokens:** Sum of both (what you're billed for) + +**Understanding tokens:** +- Tokens ≠ words +- 1 token ≈ 0.75 words (in English) +- "hello" = 1 token +- "chatbot" = 2 tokens ("chat" + "bot") +- Punctuation and spaces count as tokens + +**Why it matters:** +1. **Cost control:** You pay per token +2. **Context limits:** Models have maximum token limits (e.g., gpt-4o: 128,000 tokens) +3. **Response control:** Use `max_tokens` to prevent overly long responses + +**Practical limits:** +```javascript +// Prevent runaway responses +max_tokens: 150, // ~100 words + +// Brief responses +max_tokens: 50, // ~35 words + +// Longer content +max_tokens: 1000, // ~750 words +``` + +**Cost estimation (approximate):** +- GPT-4o: $5 per 1M input tokens, $15 per 1M output tokens +- GPT-3.5-turbo: $0.50 per 1M input tokens, $1.50 per 1M output tokens + +--- + +## Example 7: Model Comparison + +```javascript +// GPT-4o - Most capable +const gpt4Response = await client.chat.completions.create({ + model: 'gpt-4o', + messages: [{ role: 'user', content: prompt }], +}); + +// GPT-3.5-turbo - Faster and cheaper +const gpt35Response = await client.chat.completions.create({ + model: 'gpt-3.5-turbo', + messages: [{ role: 'user', content: prompt }], +}); +``` + +**Available models:** + +| Model | Best For | Speed | Cost | Context Window | +|-------|----------|-------|------|----------------| +| `gpt-4o` | Complex tasks, reasoning, accuracy | Medium | $$$ | 128K tokens | +| `gpt-4o-mini` | Balanced performance/cost | Fast | $$ | 128K tokens | +| `gpt-3.5-turbo` | Simple tasks, high volume | Very Fast | $ | 16K tokens | + +**Choosing the right model:** +- **Use GPT-4o when:** + - Complex reasoning required + - High accuracy is critical + - Working with code or technical content + - Quality > speed/cost + +- **Use GPT-4o-mini when:** + - Need good performance at lower cost + - Most general-purpose tasks + +- **Use GPT-3.5-turbo when:** + - Simple classification or extraction + - High-volume, low-complexity tasks + - Speed is critical + - Budget constraints + +**Pro tip:** Start with gpt-4o for development, then evaluate if cheaper models work for your use case. + +--- + +## Error Handling + +```javascript +try { + await basicCompletion(); +} catch (error) { + console.error("Error:", error.message); + if (error.message.includes('API key')) { + console.error("\nMake sure to set your OPENAI_API_KEY in a .env file"); + } +} +``` + +**Common errors:** +- `401 Unauthorized` - Invalid or missing API key +- `429 Too Many Requests` - Rate limit exceeded +- `500 Internal Server Error` - OpenAI service issue +- `Context length exceeded` - Too many tokens in conversation + +**Best practices:** +- Always use try-catch with async calls +- Check error types and provide helpful messages +- Implement retry logic for transient failures +- Monitor token usage to avoid limit errors + +--- + +## Key Takeaways + +1. **Stateless Nature:** Models don't remember. You send full context each time. +2. **Message Roles:** `system` (behavior), `user` (input), `assistant` (AI response) +3. **Temperature:** Controls creativity (0 = focused, 2 = creative) +4. **Streaming:** Better UX for real-time applications +5. **Token Management:** Monitor usage for cost and limits +6. **Model Selection:** Choose based on task complexity and budget \ No newline at end of file diff --git a/examples/02_openai-intro/CONCEPT.md b/examples/02_openai-intro/CONCEPT.md new file mode 100644 index 0000000000000000000000000000000000000000..ad08a2337bd7fb8e1c5c19db8e8b4ad0c8188257 --- /dev/null +++ b/examples/02_openai-intro/CONCEPT.md @@ -0,0 +1,950 @@ +# Concepts: Understanding OpenAI APIs + +This guide explains the fundamental concepts behind working with OpenAI's language models, which form the foundation for building AI agents. + +## What is the OpenAI API? + +The OpenAI API provides programmatic access to powerful language models like GPT-4o and GPT-3.5-turbo. Instead of running models locally, you send requests to OpenAI's servers and receive responses. + +**Key characteristics:** +- **Cloud-based:** Models run on OpenAI's infrastructure +- **Pay-per-use:** Charged by token consumption +- **Production-ready:** Enterprise-grade reliability and performance +- **Latest models:** Immediate access to newest model releases + +**Comparison with Local LLMs (like node-llama-cpp):** + +| Aspect | OpenAI API | Local LLMs | +|--------|------------|------------| +| **Setup** | API key only | Download models, need GPU/RAM | +| **Cost** | Pay per token | Free after initial setup | +| **Performance** | Consistent, high-quality | Depends on your hardware | +| **Privacy** | Data sent to OpenAI | Completely local/private | +| **Scalability** | Unlimited (with payment) | Limited by your hardware | + +--- + +## The Chat Completions API + +### Request-Response Cycle + +``` +You (Client) OpenAI (Server) + | | + | POST /v1/chat/completions | + | { | + | model: "gpt-4o", | + | messages: [...] | + | } | + |------------------------------->| + | | + | [Processing...] | + | [Model inference] | + | [Generate response] | + | | + | Response | + | { | + | choices: [{ | + | message: { | + | content: "..." | + | } | + | }] | + | } | + |<-------------------------------| + | | +``` + +**Key point:** Each request is independent. The API doesn't store conversation history. + +--- + +## Message Roles: The Conversation Structure + +Every message has a `role` that determines its purpose: + +### 1. System Messages + +```javascript +{ role: 'system', content: 'You are a helpful Python tutor.' } +``` + +**Purpose:** Define the AI's behavior, personality, and capabilities + +**Think of it as:** +- The AI's "job description" +- Invisible to the end user +- Sets constraints and guidelines + +**Examples:** +```javascript +// Specialist agent +"You are an expert SQL database administrator." + +// Tone and style +"You are a friendly customer support agent. Be warm and empathetic." + +// Output format control +"You are a JSON API. Always respond with valid JSON, never plain text." + +// Behavioral constraints +"You are a code reviewer. Be constructive and focus on best practices." +``` + +**Best practices:** +- Keep it concise but specific +- Place at the beginning of the messages array +- Update it to change agent behavior +- Use for ethical guidelines and output formatting + +### 2. User Messages + +```javascript +{ role: 'user', content: 'How do I use async/await?' } +``` + +**Purpose:** Represent the human's input or questions + +**Think of it as:** +- What you're asking the AI +- The prompt or query +- The instruction to follow + +### 3. Assistant Messages + +```javascript +{ role: 'assistant', content: 'Async/await is a way to handle promises...' } +``` + +**Purpose:** Represent the AI's previous responses + +**Think of it as:** +- The AI's conversation history +- Context for follow-up questions +- What the AI has already said + +### Conversation Flow Example + +```javascript +[ + { role: 'system', content: 'You are a math tutor.' }, + + // First exchange + { role: 'user', content: 'What is 15 * 24?' }, + { role: 'assistant', content: '15 * 24 = 360' }, + + // Follow-up (knows context) + { role: 'user', content: 'What about dividing that by 3?' }, + { role: 'assistant', content: '360 ÷ 3 = 120' }, +] +``` + +**Why this matters:** The role structure enables: +1. **Context awareness:** AI understands conversation history +2. **Behavior control:** System prompts shape responses +3. **Multi-turn conversations:** Natural back-and-forth dialogue + +--- + +## Statelessness: A Critical Concept + +**Most important principle:** OpenAI's API is stateless. + +### What does stateless mean? + +Each API call is independent. The model doesn't remember previous requests. + +``` +Request 1: "My name is Alice" +Response 1: "Hello Alice!" + +Request 2: "What's my name?" +Response 2: "I don't know your name." ← No memory! +``` + +### How to maintain context + +**You must send the full conversation history:** + +```javascript +const messages = []; + +// First turn +messages.push({ role: 'user', content: 'My name is Alice' }); +const response1 = await client.chat.completions.create({ + model: 'gpt-4o', + messages: messages // ["My name is Alice"] +}); +messages.push(response1.choices[0].message); + +// Second turn - include full history +messages.push({ role: 'user', content: "What's my name?" }); +const response2 = await client.chat.completions.create({ + model: 'gpt-4o', + messages: messages // Full conversation! +}); +``` + +### Implications + +**Benefits:** +- ✅ Simple architecture (no server-side state) +- ✅ Easy to scale (any server can handle any request) +- ✅ Full control over context (you decide what to include) + +**Challenges:** +- ❌ You manage conversation history +- ❌ Token costs increase with conversation length +- ❌ Must implement your own memory/persistence +- ❌ Context window limits eventually hit + +**Real-world solutions:** +```javascript +// Trim old messages when too long +if (messages.length > 20) { + messages = [messages[0], ...messages.slice(-10)]; // Keep system + last 10 +} + +// Summarize old context +if (totalTokens > 10000) { + const summary = await summarizeConversation(messages); + messages = [systemMessage, summary, ...recentMessages]; +} +``` + +--- + +## Temperature: Controlling Randomness + +Temperature controls how "creative" or "random" the model's output is. + +### How it works technically + +When generating each token, the model assigns probabilities to possible next tokens: + +``` +Input: "The sky is" +Possible next tokens: + - "blue" → 70% probability + - "clear" → 15% probability + - "dark" → 10% probability + - "purple" → 5% probability +``` + +**Temperature modifies these probabilities:** + +**Temperature = 0.0 (Deterministic)** +``` +Always pick the highest probability token +"The sky is blue" ← Same output every time +``` + +**Temperature = 0.7 (Balanced)** +``` +Sample probabilistically with slight randomness +"The sky is blue" or "The sky is clear" +``` + +**Temperature = 1.5 (Creative)** +``` +Flatten probabilities, allow unlikely choices +"The sky is purple" or "The sky is dancing" ← More surprising! +``` + +### Practical Guidelines + +**Temperature 0.0 - 0.3: Focused Tasks** +- Code generation +- Data extraction +- Factual Q&A +- Classification +- Translation + +Example: +```javascript +// Extract JSON from text - needs consistency +temperature: 0.1 +``` + +**Temperature 0.5 - 0.9: Balanced Tasks** +- General conversation +- Customer support +- Content summarization +- Educational content + +Example: +```javascript +// Friendly chatbot +temperature: 0.7 +``` + +**Temperature 1.0 - 2.0: Creative Tasks** +- Story writing +- Brainstorming +- Poetry/creative content +- Generating variations + +Example: +```javascript +// Generate 10 different marketing taglines +temperature: 1.3 +``` + +--- + +## Streaming: Real-time Responses + +### Non-Streaming (Default) + +``` +User: "Tell me a story" +[Wait...] +[Wait...] +[Wait...] +Response: "Once upon a time, there was a..." (all at once) +``` + +**Pros:** +- Simple to implement +- Easy to handle errors +- Get complete response before processing + +**Cons:** +- Appears slow for long responses +- No feedback during generation +- Poor user experience for chat + +### Streaming + +``` +User: "Tell me a story" +"Once" +"Once upon" +"Once upon a" +"Once upon a time" +"Once upon a time there" +... +``` + +**Pros:** +- Immediate feedback +- Appears faster +- Better user experience +- Can process tokens as they arrive + +**Cons:** +- More complex code +- Harder error handling +- Can't see full response before displaying + +### When to Use Each + +**Use Non-Streaming:** +- Batch processing scripts +- When you need to analyze the full response +- Simple command-line tools +- API endpoints that return complete results + +**Use Streaming:** +- Chat interfaces +- Interactive applications +- Long-form content generation +- Any user-facing application where UX matters + +--- + +## Tokens: The Currency of LLMs + +### What are tokens? + +Tokens are the fundamental units that language models process. They're not exactly words, but pieces of text. + +**Tokenization examples:** +``` +"Hello world" → ["Hello", " world"] = 2 tokens +"coding" → ["coding"] = 1 token +"uncoded" → ["un", "coded"] = 2 tokens +``` + +### Why tokens matter + +**1. Cost** +You pay per token (input + output): +``` +Request: 100 tokens +Response: 150 tokens +Total billed: 250 tokens +``` + +**2. Context Limits** +Each model has a maximum token limit: +``` +gpt-4o: 128,000 tokens (≈96,000 words) +gpt-3.5-turbo: 16,384 tokens (≈12,000 words) +``` + +**3. Performance** +More tokens = longer processing time and higher cost + +### Managing Token Usage + +**Monitor usage:** +```javascript +console.log(response.usage.total_tokens); +// Track cumulative usage for budgeting +``` + +**Limit response length:** +```javascript +max_tokens: 150 // Cap the response +``` + +**Trim conversation history:** +```javascript +// Keep only recent messages +if (messages.length > 20) { + messages = messages.slice(-20); +} +``` + +**Estimate before sending:** +```javascript +import { encode } from 'gpt-tokenizer'; + +const text = "Your message here"; +const tokens = encode(text).length; +console.log(`Estimated tokens: ${tokens}`); +``` + +--- + +## Model Selection: Choosing the Right Tool + +### GPT-4o: The Powerhouse + +**Best for:** +- Complex reasoning tasks +- Code generation and debugging +- Technical content +- Tasks requiring high accuracy +- Working with structured data + +**Characteristics:** +- Most capable model +- Higher cost +- Slower than GPT-3.5 +- Best for quality-critical applications + +**Example use cases:** +- Legal document analysis +- Complex code refactoring +- Research and analysis +- Educational tutoring + +### GPT-4o-mini: The Balanced Choice + +**Best for:** +- General-purpose applications +- Good balance of cost and performance +- Most everyday tasks + +**Characteristics:** +- Good performance +- Moderate cost +- Fast response times +- Sweet spot for many applications + +**Example use cases:** +- Customer support chatbots +- Content summarization +- General Q&A +- Moderate complexity tasks + +### GPT-3.5-turbo: The Speed Demon + +**Best for:** +- High-volume, simple tasks +- Speed-critical applications +- Budget-conscious projects +- Classification and extraction + +**Characteristics:** +- Very fast +- Lowest cost +- Good for simple tasks +- Less capable reasoning + +**Example use cases:** +- Sentiment analysis +- Text classification +- Simple formatting +- High-throughput processing + +### Decision Framework + +``` +Is task critical and complex? +├─ YES → GPT-4o +└─ NO + └─ Is speed important and task simple? + ├─ YES → GPT-3.5-turbo + └─ NO → GPT-4o-mini +``` + +--- + +## Error Handling and Resilience + +### Common Error Scenarios + +**1. Authentication Errors (401)** +```javascript +// Invalid API key +Error: Incorrect API key provided +``` + +**2. Rate Limiting (429)** +```javascript +// Too many requests +Error: Rate limit exceeded +``` + +**3. Token Limits (400)** +```javascript +// Context too long +Error: This model's maximum context length is 16385 tokens +``` + +**4. Service Errors (500)** +```javascript +// OpenAI service issue +Error: The server had an error processing your request +``` + +### Best Practices + +**1. Always use try-catch:** +```javascript +try { + const response = await client.chat.completions.create({...}); +} catch (error) { + if (error.status === 429) { + // Implement backoff and retry + } else if (error.status === 500) { + // Retry with exponential backoff + } else { + // Log and handle appropriately + } +} +``` + +**2. Implement retry logic:** +```javascript +async function retryWithBackoff(fn, maxRetries = 3) { + for (let i = 0; i < maxRetries; i++) { + try { + return await fn(); + } catch (error) { + if (i === maxRetries - 1) throw error; + await sleep(Math.pow(2, i) * 1000); // Exponential backoff + } + } +} +``` + +**3. Monitor token usage:** +```javascript +let totalTokens = 0; +totalTokens += response.usage.total_tokens; + +if (totalTokens > MONTHLY_BUDGET_TOKENS) { + throw new Error('Monthly token budget exceeded'); +} +``` + +--- + +## Architectural Patterns + +### Pattern 1: Simple Request-Response + +**Use case:** One-off queries, simple automation + +```javascript +const response = await client.chat.completions.create({ + model: 'gpt-4o', + messages: [{ role: 'user', content: query }] +}); +``` + +**Pros:** Simple, easy to understand +**Cons:** No context, no memory + +### Pattern 2: Stateful Conversation + +**Use case:** Chat applications, tutoring, customer support + +```javascript +class Conversation { + constructor() { + this.messages = [ + { role: 'system', content: 'Your behavior' } + ]; + } + + async ask(userMessage) { + this.messages.push({ role: 'user', content: userMessage }); + + const response = await client.chat.completions.create({ + model: 'gpt-4o', + messages: this.messages + }); + + this.messages.push(response.choices[0].message); + return response.choices[0].message.content; + } +} +``` + +**Pros:** Maintains context, natural conversation +**Cons:** Token costs grow, needs management + +### Pattern 3: Specialized Agents + +**Use case:** Domain-specific applications + +```javascript +class PythonTutor { + async help(question) { + return await client.chat.completions.create({ + model: 'gpt-4o', + messages: [ + { + role: 'system', + content: 'You are an expert Python tutor. Explain concepts clearly with code examples.' + }, + { role: 'user', content: question } + ], + temperature: 0.3 // Focused responses + }); + } +} +``` + +**Pros:** Consistent behavior, optimized for domain +**Cons:** Less flexible + +--- + +## Hybrid Approach: Combining Proprietary and Open Source Models + +In real-world projects, the best solution often isn't choosing between OpenAI and local LLMs - it's using **both strategically**. + +### Why Use a Hybrid Approach? + +**Cost optimization:** Use expensive models only when necessary +**Privacy compliance:** Keep sensitive data local while leveraging cloud for general tasks +**Performance balance:** Fast local models for simple tasks, powerful cloud models for complex ones +**Reliability:** Fallback options when one service is down +**Flexibility:** Match the right tool to each specific task + +### Common Hybrid Architectures + +#### Pattern 1: Tiered Processing + +``` +Simple tasks → Local LLM (fast, free, private) + ↓ If complex +Complex tasks → OpenAI API (powerful, accurate) +``` + +**Example workflow:** +```javascript +async function processQuery(query) { + const complexity = await assessComplexity(query); + + if (complexity < 0.5) { + // Use local model for simple queries + return await localLLM.generate(query); + } else { + // Use OpenAI for complex reasoning + return await openai.chat.completions.create({ + model: 'gpt-4o', + messages: [{ role: 'user', content: query }] + }); + } +} +``` + +**Use cases:** +- Customer support: Local model for FAQs, GPT-4 for complex issues +- Code generation: Local for simple scripts, GPT-4 for architecture +- Content moderation: Local for obvious cases, cloud for edge cases + +#### Pattern 2: Privacy-Based Routing + +``` +Public data → OpenAI (best quality) +Sensitive data → Local LLM (private, secure) +``` + +**Example:** +```javascript +async function handleRequest(data, containsSensitiveInfo) { + if (containsSensitiveInfo) { + // Process locally - data never leaves your infrastructure + return await localLLM.generate(data, { + systemPrompt: "You are a HIPAA-compliant assistant" + }); + } else { + // Use cloud for better quality + return await openai.chat.completions.create({ + model: 'gpt-4o', + messages: [{ role: 'user', content: data }] + }); + } +} +``` + +**Use cases:** +- Healthcare: Patient data → Local, General medical info → OpenAI +- Finance: Transaction details → Local, Market analysis → OpenAI +- Legal: Client communications → Local, Legal research → OpenAI + +#### Pattern 3: Specialized Agent Ecosystem + +``` +Agent 1 (Local): Fast classifier + ↓ Routes to +Agent 2 (OpenAI): Deep analyzer + ↓ Routes to +Agent 3 (Local): Action executor +``` + +**Example:** +```javascript +class MultiModelAgent { + async process(input) { + // Step 1: Local model classifies intent (fast, cheap) + const intent = await localLLM.classify(input); + + // Step 2: Route to appropriate handler + if (intent.requiresReasoning) { + // Complex reasoning with GPT-4 + const analysis = await openai.chat.completions.create({ + model: 'gpt-4o', + messages: [{ role: 'user', content: input }] + }); + return analysis.choices[0].message.content; + } else { + // Simple response with local model + return await localLLM.generate(input); + } + } +} +``` + +**Use cases:** +- Multi-stage pipelines with different complexity levels +- Agent systems where each agent has specialized capabilities +- Workflows requiring both speed and intelligence + +#### Pattern 4: Development vs Production + +``` +Development → OpenAI (fast iteration, best results) + ↓ Optimize +Production → Local LLM (cost-effective, private) +``` + +**Workflow:** +```javascript +const MODEL_PROVIDER = process.env.NODE_ENV === 'production' + ? 'local' + : 'openai'; + +async function generateResponse(prompt) { + if (MODEL_PROVIDER === 'local') { + return await localLLM.generate(prompt); + } else { + return await openai.chat.completions.create({ + model: 'gpt-4o', + messages: [{ role: 'user', content: prompt }] + }); + } +} +``` + +**Strategy:** +1. Develop with GPT-4 to get best results quickly +2. Fine-tune prompts and test thoroughly +3. Switch to local model for production +4. Fall back to OpenAI for edge cases + +#### Pattern 5: Ensemble Approach + +``` +Query → [Local Model, OpenAI, Another API] + ↓ ↓ ↓ + Response Response Response + ↓ ↓ ↓ + Aggregator / Validator + ↓ + Best Response +``` + +**Example:** +```javascript +async function ensembleGenerate(prompt) { + // Get responses from multiple sources + const [local, openai, backup] = await Promise.allSettled([ + localLLM.generate(prompt), + openaiClient.chat.completions.create({ + model: 'gpt-4o', + messages: [{ role: 'user', content: prompt }] + }), + backupAPI.generate(prompt) + ]); + + // Use validator to pick best or combine + return validator.selectBest([local, openai, backup]); +} +``` + +**Use cases:** +- Critical applications requiring high confidence +- Fact-checking and verification +- Reducing hallucinations through consensus + +### Cost-Benefit Analysis + +#### Scenario: Customer Support Chatbot (10,000 queries/day) + +**Option A: OpenAI Only** +``` +10,000 queries × 500 tokens avg = 5M tokens/day +Cost: ~$25-50/day = ~$750-1500/month +Pros: Highest quality, zero infrastructure +Cons: Expensive at scale, privacy concerns +``` + +**Option B: Local LLM Only** +``` +Infrastructure: $100-500/month (server/GPU) +Cost: $100-500/month +Pros: Predictable costs, private, unlimited usage +Cons: Setup complexity, maintenance, lower quality +``` + +**Option C: Hybrid (80% local, 20% OpenAI)** +``` +8,000 simple queries → Local LLM (free after setup) +2,000 complex queries → OpenAI (~$5-10/day) +Infrastructure: $100-500/month +API costs: $150-300/month +Total: $250-800/month +Pros: Cost-effective, high quality when needed, flexible +Cons: More complex architecture +``` + +**Winner for most projects: Hybrid approach** ✓ + +### Decision Framework + +``` +START: New query arrives + ↓ +Is data sensitive/regulated? +├─ YES → Use local model (privacy first) +└─ NO → Continue + ↓ +Is task simple/repetitive? +├─ YES → Use local model (cost-effective) +└─ NO → Continue + ↓ +Is high accuracy critical? +├─ YES → Use OpenAI (quality first) +└─ NO → Continue + ↓ +Is it high volume? +├─ YES → Use local model (cost at scale) +└─ NO → Use OpenAI (simplicity) +``` + +### The Future: Intelligent Model Selection + +Advanced systems will automatically choose models based on real-time factors: + +```javascript +class IntelligentModelSelector { + async selectModel(query, context) { + const factors = { + complexity: await this.analyzeComplexity(query), + latency: context.userTolerance, + budget: context.remainingBudget, + accuracy: context.requiredConfidence, + privacy: context.dataClassification + }; + + // ML model predicts best provider + const selection = await this.mlSelector.predict(factors); + + return { + provider: selection.provider, // 'local' | 'openai-mini' | 'openai-4' + confidence: selection.confidence, + reasoning: selection.reasoning + }; + } +} +``` + +### Key Takeaway + +**You don't have to choose.** Modern AI applications benefit from using the right model for each task: +- **OpenAI / Claude / Host own big open source models:** Complex reasoning, critical accuracy, rapid development +- **Local for scale:** Privacy, cost control, high volume, offline operation +- **Both for success:** Cost-effective, flexible, reliable production systems + +The best architecture leverages the strengths of each approach while mitigating their weaknesses. + +--- + +## Preparing for Agents + +The concepts covered here are **foundational** for building AI agents: + +### You now understand: + +- **How to communicate with LLMs** (API basics) +- **How to shape behavior** (system prompts) +- **How to maintain context** (message history) +- **How to control output** (temperature, tokens) +- **How to handle responses** (streaming, errors) + +### What's next for agents: + +- **Function calling / Tool use** - Let the AI take actions +- **Memory systems** - Persistent state across sessions +- **ReAct patterns** - Iterative reasoning and observation + +**Bottom line:** You can't build good agents without mastering these fundamentals. Every agent pattern builds on this foundation. + +--- + +## Key Insights + +1. **Statelessness is power and burden:** You control context, but you must manage it +2. **System prompts are your secret weapon:** Same model → different behaviors +3. **Temperature changes everything:** Match it to your task type +4. **Tokens are the real currency:** Monitor and optimize usage +5. **Model choice matters:** Don't use a sledgehammer for a nail +6. **Streaming improves UX:** Use it for user-facing applications +7. **Error handling is not optional:** The network will fail, plan for it + +--- + +## Further Reading + +- [OpenAI API Documentation](https://platform.openai.com/docs/api-reference) +- [OpenAI Cookbook](https://cookbook.openai.com/) +- [Best Practices for Prompt Engineering](https://platform.openai.com/docs/guides/prompt-engineering) +- [Token Counting](https://platform.openai.com/tokenizer) diff --git a/examples/02_openai-intro/openai-intro.js b/examples/02_openai-intro/openai-intro.js new file mode 100644 index 0000000000000000000000000000000000000000..4a3b18c26d06312dc22c5b8d98678cc545b47bd3 --- /dev/null +++ b/examples/02_openai-intro/openai-intro.js @@ -0,0 +1,205 @@ +import OpenAI from 'openai'; +import 'dotenv/config'; + +// Initialize OpenAI client +// Create an API key at https://platform.openai.com/api-keys +const client = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); + +console.log("=== OpenAI Intro: Understanding the Basics ===\n"); + +// ============================================ +// EXAMPLE 1: Basic Chat Completion +// ============================================ +async function basicCompletion() { + console.log("--- Example 1: Basic Chat Completion ---"); + + const response = await client.chat.completions.create({ + model: 'gpt-4o', + messages: [ + { role: 'user', content: 'What is node-llama-cpp?' } + ], + }); + + console.log("AI: " + response.choices[0].message.content); + console.log("\n"); +} + +// ============================================ +// EXAMPLE 2: Using System Prompts +// ============================================ +async function systemPromptExample() { + console.log("--- Example 2: System Prompts (Behavioral Control) ---"); + + const response = await client.chat.completions.create({ + model: 'gpt-4o', + messages: [ + { role: 'system', content: 'You are a coding assistant that talks like a pirate.' }, + { role: 'user', content: 'Explain what async/await does in JavaScript.' } + ], + }); + + console.log("AI: " + response.choices[0].message.content); + console.log("\n"); +} + +// ============================================ +// EXAMPLE 3: Temperature and Creativity +// ============================================ +async function temperatureExample() { + console.log("--- Example 3: Temperature Control ---"); + + const prompt = "Write a one-sentence tagline for a coffee shop."; + + // Low temperature = more focused and deterministic + const focusedResponse = await client.chat.completions.create({ + model: 'gpt-4o', + messages: [{ role: 'user', content: prompt }], + temperature: 0.2, + }); + + // High temperature = more creative and varied + const creativeResponse = await client.chat.completions.create({ + model: 'gpt-4o', + messages: [{ role: 'user', content: prompt }], + temperature: 1.5, + }); + + console.log("Low temp (0.2): " + focusedResponse.choices[0].message.content); + console.log("High temp (1.5): " + creativeResponse.choices[0].message.content); + console.log("\n"); +} + +// ============================================ +// EXAMPLE 4: Conversation with Context +// ============================================ +async function conversationContext() { + console.log("--- Example 4: Multi-turn Conversation ---"); + + // Build conversation history + const messages = [ + { role: 'system', content: 'You are a helpful coding tutor.' }, + { role: 'user', content: 'What is a Promise in JavaScript?' }, + ]; + + // First response + const response1 = await client.chat.completions.create({ + model: 'gpt-4o', + messages: messages, + max_tokens: 150, + }); + + console.log("User: What is a Promise in JavaScript?"); + console.log("AI: " + response1.choices[0].message.content); + + // Add AI response to history + messages.push(response1.choices[0].message); + + // Add follow-up question + messages.push({ role: 'user', content: 'Can you show me a simple example?' }); + + // Second response (with context) + const response2 = await client.chat.completions.create({ + model: 'gpt-4o', + messages: messages, + }); + + console.log("\nUser: Can you show me a simple example?"); + console.log("AI: " + response2.choices[0].message.content); + console.log("\n"); +} + +// ============================================ +// EXAMPLE 5: Streaming Responses +// ============================================ +async function streamingExample() { + console.log("--- Example 5: Streaming Response ---"); + console.log("AI: "); + + const stream = await client.chat.completions.create({ + model: 'gpt-4o', + messages: [ + { role: 'user', content: 'Write a haiku about programming.' } + ], + stream: true, + }); + + for await (const chunk of stream) { + const content = chunk.choices[0]?.delta?.content || ''; + process.stdout.write(content); + } + + console.log("\n\n"); +} + +// ============================================ +// EXAMPLE 6: Token Usage and Limits +// ============================================ +async function tokenUsageExample() { + console.log("--- Example 6: Understanding Token Usage ---"); + + const response = await client.chat.completions.create({ + model: 'gpt-4o', + messages: [ + { role: 'user', content: 'Explain recursion in 3 sentences.' } + ], + max_tokens: 100, + }); + + console.log("AI: " + response.choices[0].message.content); + console.log("\nToken usage:"); + console.log("- Prompt tokens: " + response.usage.prompt_tokens); + console.log("- Completion tokens: " + response.usage.completion_tokens); + console.log("- Total tokens: " + response.usage.total_tokens); + console.log("\n"); +} + +// ============================================ +// EXAMPLE 7: Model Comparison +// ============================================ +async function modelComparison() { + console.log("--- Example 7: Different Models ---"); + + const prompt = "What's 25 * 47?"; + + // GPT-4o - Most capable + const gpt4Response = await client.chat.completions.create({ + model: 'gpt-4o', + messages: [{ role: 'user', content: prompt }], + }); + + // GPT-3.5-turbo - Faster and cheaper + const gpt35Response = await client.chat.completions.create({ + model: 'gpt-3.5-turbo', + messages: [{ role: 'user', content: prompt }], + }); + + console.log("GPT-4o: " + gpt4Response.choices[0].message.content); + console.log("GPT-3.5-turbo: " + gpt35Response.choices[0].message.content); + console.log("\n"); +} + +// ============================================ +// Run all examples +// ============================================ +async function main() { + try { + await basicCompletion(); + await systemPromptExample(); + await temperatureExample(); + await conversationContext(); + await streamingExample(); + await tokenUsageExample(); + await modelComparison(); + + console.log("=== All examples completed! ==="); + } catch (error) { + console.error("Error:", error.message); + if (error.message.includes('API key')) { + console.error("\nMake sure to set your OPENAI_API_KEY in a .env file"); + } + } +} + +main(); \ No newline at end of file diff --git a/examples/03_translation/CODE.md b/examples/03_translation/CODE.md new file mode 100644 index 0000000000000000000000000000000000000000..2dee9710fb4d0dd2eabe7f0ea7d780afda483cad --- /dev/null +++ b/examples/03_translation/CODE.md @@ -0,0 +1,231 @@ +# Code Explanation: translation.js + +This file demonstrates how to use **system prompts** to specialize an AI agent for a specific task - in this case, professional German translation. + +## Step-by-Step Code Breakdown + +### 1. Import Required Modules +```javascript +import { + getLlama, LlamaChatSession, +} from "node-llama-cpp"; +import {fileURLToPath} from "url"; +import path from "path"; +``` +- Imports are the same as the intro example + +### 2. Initialize and Load Model +```javascript +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const llama = await getLlama(); +const model = await llama.loadModel({ + modelPath: path.join( + __dirname, + "../", + "models", + "hf_giladgd_Apertus-8B-Instruct-2509.Q6_K.gguf" + ) +}); +``` + +#### Why Apertus-8B? +Apertus-8B is a multilingual language model specifically trained to support over 1,000 languages, with 40% of its training data in non-English languages. This makes it an excellent choice for translation tasks because: + +1. **Massive Multilingual Coverage**: The model was trained on 15 trillion tokens across 1,811 natively supported languages, including underrepresented languages like Swiss German and Romansh +2. **Larger Size**: With 8 billion parameters, it's larger than the intro.js example, providing better understanding and output quality +3. **Translation-Focused Training**: The model was explicitly designed for applications including translation systems +4. **Q6_K Quantization**: 6-bit quantization provides a good balance between quality and file size + +**Experiment suggestion**: Try swapping this model with others to compare translation quality! For example: +- Use a smaller 3B model to see how size affects translation accuracy +- Use a monolingual model to demonstrate why multilingual training matters +- Use a general-purpose model without translation-specific training + +Read more about Apertus [arXiv](https://arxiv.org/abs/2509.14233) + +### 3. Create Context and Chat Session with System Prompt +```javascript +const context = await model.createContext(); +const session = new LlamaChatSession({ + contextSequence: context.getSequence(), + systemPrompt: `Du bist ein erfahrener wissenschaftlicher Übersetzer...` +}); +``` + +**Key difference from intro.js**: The **systemPrompt**! + +#### What is a System Prompt? +The system prompt defines the agent's role, behavior, and rules. It's like giving the AI a job description: + +``` +┌─────────────────────────────────────┐ +│ System Prompt │ +│ "You are a professional translator"│ +│ + Detailed instructions │ +│ + Rules to follow │ +└─────────────────────────────────────┘ + ↓ + Affects every response +``` + +### 4. The System Prompt Breakdown + +The system prompt (in German) tells the model: + +**Role:** +``` +"Du bist ein erfahrener wissenschaftlicher Übersetzer für technische Texte +aus dem Englischen ins Deutsche." +``` +Translation: "You are an experienced scientific translator for technical texts from English to German." + +**Task:** +``` +"Deine Aufgabe: Erstelle eine inhaltlich exakte Übersetzung..." +``` +Translation: "Your task: Create a content-accurate translation that maintains full meaning and technical precision." + +**Rules (Lines 33-41):** +1. Preserve every technical statement exactly +2. Use idiomatic, fluent German +3. Avoid literal sentence structures +4. Use correct terminology (e.g., "Multi-Agenten-System") +5. Use German typography for numbers (e.g., "54 %") +6. Adapt compound terms to German grammar +7. Shorten overly complex sentences while preserving meaning +8. Use neutral, scientific style + +**Critical Instruction (Line 48):** +``` +"DO NOT add any addition text or explanation. ONLY respond with the translated text" +``` +- Forces the model to return ONLY the translation +- No "Here's the translation:" prefix +- No explanations or commentary + +### 5. The Translation Query +```javascript +const q1 = `Translate this text into german: + +We address the long-horizon gap in large language model (LLM) agents by en- +abling them to sustain coherent strategies in adversarial, stochastic environments. +... +`; +``` +- Contains a scientific abstract about LLM agents (HexMachina paper) +- Complex technical content with specialized terms +- Tests the model's ability to: + - Understand technical AI/ML concepts + - Translate accurately + - Follow the detailed system prompt rules + +### 6. Execute Translation +```javascript +const a1 = await session.prompt(q1); +console.log("AI: " + a1); +``` +- Sends the translation request to the model +- The model will: + 1. Read the system prompt (its "role") + 2. Read the user's request + 3. Apply all the rules from the system prompt + 4. Generate a German translation + +### 7. Cleanup +```javascript +session.dispose() +context.dispose() +model.dispose() +llama.dispose() +``` +- Same cleanup as intro.js +- Always dispose resources when done + +## Key Concepts Demonstrated + +### 1. System Prompts for Specialization +System prompts transform a general-purpose LLM into a specialized agent: + +``` +General LLM + System Prompt = Specialized Agent + (Translator, Coder, Analyst, etc.) +``` + +### 2. Detailed Instructions Matter +Compare these approaches: + +**❌ Minimal approach:** +```javascript +systemPrompt: "Translate to German" +``` + +**✅ This example (detailed):** +```javascript +systemPrompt: ` + You are a professional translator + Follow these rules: + - Rule 1 + - Rule 2 + - Rule 3 + ... +` +``` + +The detailed approach gives much better, more consistent results. + +### 3. Constraining Output Format +The line "DO NOT add any addition text" demonstrates output control: + +**Without constraint:** +``` +AI: Here's the translation of the text you provided: + +[German text] + +I hope this helps! Let me know if you need anything else. +``` + +**With constraint:** +``` +AI: [German text only] +``` + +## What Makes This an "Agent"? + +This is a **specialized agent** because: + +1. **Specific Role**: Has a defined purpose (translation) +2. **Constrained Behavior**: Follows specific rules and guidelines +3. **Consistent Output**: Produces predictable, formatted results +4. **Domain Expertise**: Optimized for scientific/technical content + +## Expected Output + +When run, you'll see a German translation of the English abstract, following all the rules: +- Proper German scientific style +- Correct technical terminology +- German number formatting (e.g., "54 %") +- No extra commentary + +The quality depends on the model's training and size. + +## Experimentation Ideas + +1. **Try different models**: + - Swap Apertus-8B with a smaller model (3B) to see size impact + - Try a monolingual English model to demonstrate the importance of multilingual training + - Use models with different quantization levels (Q4, Q6, Q8) to compare quality vs. size + +2. **Modify the system prompt**: + - Remove specific rules one by one to see their impact + - Change the translation target language + - Adjust the style (formal vs. casual) + +3. **Test with different content**: + - Technical documentation + - Creative writing + - Business communications + - Simple vs. complex sentences + +Each experiment will help you understand how system prompts, model selection, and prompt engineering work together to create effective AI agents. \ No newline at end of file diff --git a/examples/03_translation/CONCEPT.md b/examples/03_translation/CONCEPT.md new file mode 100644 index 0000000000000000000000000000000000000000..630056e3ed822c3f947ef27e41b5159e0d61ea77 --- /dev/null +++ b/examples/03_translation/CONCEPT.md @@ -0,0 +1,302 @@ +# Concept: System Prompts & Agent Specialization + +## Overview + +This example demonstrates how to transform a general-purpose LLM into a **specialized agent** using **system prompts**. The key insight: you don't need different models for different tasks—you need different instructions. + +## What is a System Prompt? + +A **system prompt** is a persistent instruction that shapes the AI's behavior for an entire conversation session. + +### Analogy +Think of hiring someone for a job: + +``` +Without System Prompt With System Prompt +───────────────────── ────────────────────── +"Hi, I'm an AI." "I'm a professional translator + with expertise in scientific +What do you want?" German. I follow strict quality + guidelines and output format." +``` + +## How System Prompts Work + +### The Context Structure + +``` +┌─────────────────────────────────────────────┐ +│ CONTEXT WINDOW │ +│ │ +│ ┌───────────────────────────────────────┐ │ +│ │ SYSTEM PROMPT (Always present) │ │ +│ │ "You are a professional translator..." │ +│ │ "Follow these rules..." │ │ +│ └───────────────────────────────────────┘ │ +│ ↓ │ +│ ┌───────────────────────────────────────┐ │ +│ │ USER MESSAGES │ │ +│ │ "Translate this text..." │ │ +│ └───────────────────────────────────────┘ │ +│ ↓ │ +│ ┌───────────────────────────────────────┐ │ +│ │ AI RESPONSES │ │ +│ │ (Shaped by system prompt) │ │ +│ └───────────────────────────────────────┘ │ +└─────────────────────────────────────────────┘ +``` + +The system prompt sits at the top of the context and influences **every** response. + +## Agent Specialization Pattern + +### Transformation Flow + +``` +┌──────────────────┐ ┌─────────────────┐ ┌──────────────────┐ +│ General Model │ + │ System Prompt │ = │ Specialized Agent│ +│ │ │ │ │ │ +│ • Knows many │ │ • Define role │ │ • Translation │ +│ things │ │ • Set rules │ │ Agent │ +│ • No specific │ │ • Constrain │ │ • Coding Agent │ +│ role │ │ output │ │ • Analysis Agent │ +└──────────────────┘ └─────────────────┘ └──────────────────┘ +``` + +### Example Specializations + +**Translation Agent (this example):** +``` +System Prompt = Role + Rules + Output Format +``` + +**Code Assistant:** +```javascript +systemPrompt: "You are an expert programmer. +Always provide working code with comments. +Explain complex logic." +``` + +**Data Analyst:** +```javascript +systemPrompt: "You are a data analyst. +Always show your calculations step-by-step. +Cite data sources when available." +``` + +## Anatomy of an Effective System Prompt + +### The 5 Components + +``` +┌─────────────────────────────────────────┐ +│ 1. ROLE DEFINITION │ +│ "You are a [specific role]..." │ +├─────────────────────────────────────────┤ +│ 2. TASK DESCRIPTION │ +│ "Your goal is to..." │ +├─────────────────────────────────────────┤ +│ 3. BEHAVIORAL RULES │ +│ "Always do X, Never do Y..." │ +├─────────────────────────────────────────┤ +│ 4. OUTPUT FORMAT │ +│ "Format your response as..." │ +├─────────────────────────────────────────┤ +│ 5. CONSTRAINTS │ +│ "Do NOT include..." │ +└─────────────────────────────────────────┘ +``` + +### This Example's Structure + +``` +Role: "Professional scientific translator" +Task: "Translate English to German with precision" +Rules: 8 specific translation guidelines +Format: Idiomatic German, scientific style +Constraints: "ONLY translated text, no explanation" +``` + +## Why Detailed System Prompts Matter + +### Comparison Study + +**Minimal System Prompt:** +```javascript +systemPrompt: "Translate to German" +``` + +**Result:** +- May add unnecessary explanations +- Inconsistent terminology +- Mixed formality levels +- Extra conversational text + +**Detailed System Prompt (this example):** +```javascript +systemPrompt: `You are a professional translator... +- Rule 1: Preserve technical accuracy +- Rule 2: Use idiomatic German +- Rule 3: Follow scientific conventions +... +DO NOT add any explanations` +``` + +**Result:** +- ✅ Consistent quality +- ✅ Correct terminology +- ✅ Proper formatting +- ✅ Only translation output + +### Quality Impact + +``` +Detail Level Output Quality +─────────── ───────────────── +Very minimal → Unpredictable +Basic role → Somewhat consistent +Detailed → Highly consistent ⭐ +Over-detailed → May confuse model +``` + +## System Prompt Design Patterns + +### Pattern 1: Role-Playing +``` +"You are a [profession] with expertise in [domain]..." +``` +Makes the model adopt that perspective. + +### Pattern 2: Rule-Based +``` +"Follow these rules: +1. Always... +2. Never... +3. When X, do Y..." +``` +Explicit constraints lead to predictable behavior. + +### Pattern 3: Output Formatting +``` +"Format your response as: +- JSON +- Markdown +- Plain text only +- Step-by-step list" +``` +Controls the structure of responses. + +### Pattern 4: Contextual Awareness +``` +"You remember: [previous facts] +You know that: [domain knowledge] +Current situation: [context]" +``` +Primes the model with relevant information. + +## How This Relates to AI Agents + +### Agent = Model + System Prompt + Tools + +``` +┌────────────────────────────────────────────┐ +│ AI Agent │ +│ │ +│ ┌──────────────────────────────────────┐ │ +│ │ System Prompt (Agent's "Identity") │ │ +│ └──────────────────────────────────────┘ │ +│ ↓ │ +│ ┌──────────────────────────────────────┐ │ +│ │ LLM (Agent's "Brain") │ │ +│ └──────────────────────────────────────┘ │ +│ ↓ │ +│ ┌──────────────────────────────────────┐ │ +│ │ Tools (Agent's "Hands") [Optional] │ │ +│ └──────────────────────────────────────┘ │ +└────────────────────────────────────────────┘ +``` + +**In this example:** +- System Prompt: "You are a translator..." +- LLM: Apertus-8B model +- Tools: None (translation is done by the model itself) + +**In more complex agents:** +- System Prompt: "You are a research assistant..." +- LLM: Any model +- Tools: Web search, calculator, file access, etc. + +## Practical Applications + +### 1. Domain Specialization +``` +Medical → "You are a medical professional..." +Legal → "You are a legal expert..." +Technical → "You are an engineer..." +``` + +### 2. Output Control +``` +JSON API → "Always respond in valid JSON" +Markdown → "Format all responses as markdown" +Code → "Only output executable code" +``` + +### 3. Behavioral Constraints +``` +Concise → "Use maximum 2 sentences" +Detailed → "Explain thoroughly with examples" +Neutral → "Avoid opinions, state only facts" +``` + +### 4. Multi-Language Support +``` +systemPrompt: `You are a multilingual assistant. +Respond in the same language as the input.` +``` + +## Chat Wrappers Explained + +Different models need different conversation formats: + +``` +Model Type Format Needed Wrapper +────────────── ─────────────────── ───────────────── +Llama 2/3 Llama format LlamaChatWrapper +GPT-style ChatML format ChatMLWrapper +Harmony models Harmony format HarmonyChatWrapper +``` + +**What they do:** +``` +Your Message → [Chat Wrapper] → Formatted Prompt → Model + ↓ + Adds special tokens: + <|system|>, <|user|>, <|assistant|> +``` + +The wrapper ensures the model understands which part is the system prompt, which is the user message, etc. + +## Key Takeaways + +1. **System prompts are powerful**: They fundamentally change how the model behaves +2. **Detailed is better**: More specific instructions = more consistent results +3. **Structure matters**: Role + Rules + Format + Constraints +4. **No retraining needed**: Same model, different behaviors +5. **Foundation for agents**: System prompts are the first step in building specialized agents + +## Evolution Path + +``` +1. Basic Prompting (intro.js) + ↓ +2. System Prompts (translation.js) ← You are here + ↓ +3. System Prompts + Tools (simple-agent.js) + ↓ +4. Multi-turn reasoning (react-agent.js) + ↓ +5. Full Agent Systems +``` + +This example bridges the gap between basic LLM usage and true agent behavior by showing how to specialize through instructions. diff --git a/examples/03_translation/translation.js b/examples/03_translation/translation.js new file mode 100644 index 0000000000000000000000000000000000000000..918eecb6a681f1d370a8305b7669218cf0b5ae61 --- /dev/null +++ b/examples/03_translation/translation.js @@ -0,0 +1,82 @@ +import { + getLlama, + LlamaChatSession, +} from "node-llama-cpp"; +import {fileURLToPath} from "url"; +import path from "path"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const llama = await getLlama({ + logLevel: 'error' +}); +const model = await llama.loadModel({ + modelPath: path.join( + __dirname, + '..', + '..', + 'models', + 'hf_giladgd_Apertus-8B-Instruct-2509.Q6_K.gguf' + ) +}); + +const context = await model.createContext(); +const session = new LlamaChatSession({ + contextSequence: context.getSequence(), + systemPrompt: `Du bist ein erfahrener wissenschaftlicher Übersetzer für technische Texte aus dem Englischen ins + Deutsche. + + Deine Aufgabe: Erstelle eine inhaltlich exakte Übersetzung, die den vollen Sinn und die technische Präzision + des Originaltexts erhält. + + Gleichzeitig soll die Übersetzung klar, natürlich und leicht lesbar auf Deutsch klingen – also so, wie ein + deutscher Wissenschaftler oder Ingenieur denselben Text schreiben würde. + + Befolge diese Regeln: + Bewahre jede fachliche Aussage und Nuance exakt. Kein Inhalt darf verloren gehen oder verändert werden. + Verwende idiomatisches, flüssiges Deutsch, wie es in wissenschaftlichen Abstracts (z. B. NeurIPS, ICLR, AAAI) üblich ist. + Vermeide wörtliche Satzstrukturen. Formuliere so, wie ein deutscher Wissenschaftler denselben Inhalt selbst schreiben würde. + Verwende korrekte Terminologie (z. B. Multi-Agenten-System, Adapterlayer, Baseline, Strategieverbesserung). + Verwende bei Zahlen, Einheiten und Prozentangaben deutsche Typografie (z. B. „54 %“, „3 m“, „2 000“). + Passe zusammengesetzte Begriffe an die deutsche Grammatik an (z. B. „kontinuierlich lernendes System“ statt „kontinuierliches Lernen System“). + Kürze lange oder verschachtelte Sätze behutsam, ohne Bedeutung zu verändern, um Lesbarkeit zu verbessern. + Verwende einen neutralen, wissenschaftlichen Stil, ohne Werbesprache oder unnötige Ausschmückung. + + Zusatzinstruktion: + Wenn der Originaltext englische Satzlogik enthält, restrukturiere den Satz so, dass er auf Deutsch elegant und klar klingt, aber denselben Inhalt vermittelt. + + Zielqualität: Eine Übersetzung, die sich wie ein Originaltext liest – technisch präzise, flüssig und grammatikalisch einwandfrei. + + DO NOT add any addition text or explanation. ONLY respond with the translated text + ` +}); + +const q1 = `Translate this text into german: + +We address the long-horizon gap in large language model (LLM) agents by en- +abling them to sustain coherent strategies in adversarial, stochastic environments. +Settlers of Catan provides a challenging benchmark: success depends on balanc- +ing short- and long-term goals amid randomness, trading, expansion, and block- +ing. Prompt-centric LLM agents (e.g., ReAct, Reflexion) must re-interpret large, +evolving game states each turn, quickly saturating context windows and losing +strategic consistency. We propose HexMachina, a continual learning multi-agent +system that separates environment discovery (inducing an adapter layer without +documentation) from strategy improvement (evolving a compiled player through +code refinement and simulation). This design preserves executable artifacts, al- +lowing the LLM to focus on high-level strategy rather than per-turn reasoning. In +controlled Catanatron experiments, HexMachina learns from scratch and evolves +players that outperform the strongest human-crafted baseline (AlphaBeta), achiev- +ing a 54% win rate and surpassing prompt-driven and no-discovery baselines. Ab- +lations confirm that isolating pure strategy learning improves performance. Over- +all, artifact-centric continual learning transforms LLMs from brittle stepwise de- +ciders into stable strategy designers, advancing long-horizon autonomy. +`; + +console.log('Translation started...') +const a1 = await session.prompt(q1); +console.log("AI: " + a1); + +session.dispose() +context.dispose() +model.dispose() +llama.dispose() \ No newline at end of file diff --git a/examples/04_think/CODE.md b/examples/04_think/CODE.md new file mode 100644 index 0000000000000000000000000000000000000000..92707657e46a0abc1494d1f7b238ab7b1a6d3f80 --- /dev/null +++ b/examples/04_think/CODE.md @@ -0,0 +1,257 @@ +# Code Explanation: think.js + +This file demonstrates using system prompts for **logical reasoning** and **quantitative problem-solving**, showing how to configure an LLM as a specialized reasoning agent. + +## Step-by-Step Code Breakdown + +### 1. Import and Setup (Lines 1-8) +```javascript +import { + getLlama, + LlamaChatSession, +} from "node-llama-cpp"; +import {fileURLToPath} from "url"; +import path from "path"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +``` +- Standard imports for LLM interaction +- Path setup for locating the model file + +### 2. Initialize and Load Model (Lines 10-18) +```javascript +const llama = await getLlama(); +const model = await llama.loadModel({ + modelPath: path.join( + __dirname, + "../", + "models", + "Qwen3-1.7B-Q6_K.gguf" + ) +}); +``` +- Uses **Qwen3-1.7B-Q6_K**: A 1.7B parameter model with 6-bit quantization +- Smaller than the translation example (1.7B vs 8B parameters) +- Q6_K quantization provides a balance between size and quality + +### 3. Define the System Prompt (Lines 19-24) +```javascript +const systemPrompt = `You are an expert logical and quantitative reasoner. + Your goal is to analyze real-world word problems involving families, quantities, averages, and relationships + between entities, and compute the exact numeric answer. + + Goal: Return the correct final number as a single value — no explanation, no reasoning steps, just the answer. + ` +``` + +**Key elements:** + +1. **Role**: "expert logical and quantitative reasoner" + - Sets expectations for mathematical/analytical thinking + +2. **Task Scope**: "real-world word problems involving families, quantities, averages, and relationships" + - Tells the model what type of problems to expect + - Primes it for complex counting and calculation tasks + +3. **Output Constraint**: "Return the correct final number as a single value — no explanation" + - Forces concise output + - Just the answer, not the work + +### Why This System Prompt Design? + +The prompt is designed for the specific problem type: +- Word problems with complex family relationships +- Multiple nested conditions +- Requires careful tracking of people and quantities +- Needs arithmetic calculation + +### 4. Create Context and Session (Lines 25-29) +```javascript +const context = await model.createContext(); +const session = new LlamaChatSession({ + contextSequence: context.getSequence(), + systemPrompt +}); +``` +- Creates context for the conversation +- Initializes session with the reasoning system prompt +- No chat wrapper needed (using model's default format) + +### 5. The Complex Word Problem (Lines 31-40) +```javascript +const prompt = `My family reunion is this week, and I was assigned the mashed potatoes to bring. +The attendees include my married mother and father, my twin brother and his family, my aunt and her family, my grandma +and her brother, her brother's daughter, and his daughter's family. All the adults but me have been married, and no one +is divorced or remarried, but my grandpa and my grandma's sister-in-law passed away last year. All living spouses are attending. +My brother has two children that are still kids, my aunt has one six-year-old, and my grandma's brother's daughter has +three kids under 12. I figure each adult will eat about 1.5 potatoes and each kid will eat about 1/2 a potato, except my +second cousins don't eat carbs. The average potato is about half a pound, and potatoes are sold in 5-pound bags. + +How many whole bags of potatoes do I need? +`; +``` + +**This is intentionally complex to test reasoning:** + +**People to count:** +- Speaker (1) +- Mother and father (2) +- Twin brother + spouse (2) +- Brother's 2 kids (2) +- Aunt + spouse (2) +- Aunt's 1 kid (1) +- Grandma (1) +- Grandma's brother + spouse (2) +- Brother's daughter + spouse (2) +- Their 3 kids (3, but don't eat carbs) + +**Calculations needed:** +1. Count total adults +2. Count total kids +3. Subtract non-eating kids +4. Calculate potato needs: (adults × 1.5) + (eating kids × 0.5) +5. Convert to pounds: total potatoes × 0.5 lbs +6. Convert to bags: pounds ÷ 5, round up + +**The complexity:** +- Family relationships (who's married to whom) +- Deceased people (subtract from count) +- Special dietary needs (second cousins don't eat carbs) +- Unit conversions (potatoes → pounds → bags) + +### 6. Execute and Display (Lines 42-43) +```javascript +const answer = await session.prompt(prompt); +console.log(`AI: ${answer}`); +``` +- Sends the complex problem to the model +- The model uses its reasoning abilities to work through the problem +- Outputs just the final number (based on system prompt) + +### 7. Cleanup (Lines 45-48) +```javascript +session.dispose() +context.dispose() +model.dispose() +llama.dispose() +``` +- Standard resource cleanup + +## Key Concepts Demonstrated + +### 1. Reasoning Agent Configuration +This shows how to configure an LLM for analytical thinking: + +``` +System Prompt → LLM becomes a "reasoning engine" +``` + +Instead of conversational AI, we get: +- Focused analytical processing +- Mathematical computation +- Logical deduction + +### 2. Output Format Control +Compare these approaches: + +**Without constraint:** +``` +AI: Let me work through this step by step. +First, I'll count the adults... +[lengthy explanation] +So the answer is 3 bags. +``` + +**With constraint (this example):** +``` +AI: 3 +``` + +### 3. Problem Complexity Testing +This example tests the model's ability to: +- Parse complex natural language +- Track multiple entities and relationships +- Apply arithmetic operations +- Handle edge cases (deceased people, dietary restrictions) +- Perform unit conversions + +### 4. Specialized Task Agents +This demonstrates creating task-specific agents: + +``` +General LLM + "Reasoning Agent" System Prompt = Math Problem Solver +``` + +Same pattern works for: +- Logic puzzles +- Data analysis +- Scientific calculations +- Statistical reasoning + +## Challenges & Limitations + +### 1. Model Size Matters +The 1.7B parameter model may struggle with: +- Very complex counting problems +- Multi-step reasoning requiring working memory +- Edge cases in the problem + +Larger models (7B, 13B+) generally perform better on reasoning tasks. + +### 2. Hidden Reasoning +The system prompt asks for "just the answer," so we don't see: +- The model's reasoning process +- Where it might have made mistakes +- Its confidence level + +### 3. No Tool Use +The model must do all calculations "in its head" without: +- A calculator +- Note-taking +- Step-by-step verification + +Later examples (like react-agent) address this by giving the model tools. + +## Why This Matters for AI Agents + +### Reasoning is Fundamental +All useful agents need reasoning capabilities: +- **Planning agents**: Reason about sequences of actions +- **Research agents**: Analyze and synthesize information +- **Decision agents**: Evaluate options and consequences + +### System Prompt Shapes Behavior +This example shows that the same model can behave differently based on instructions: +- Translator agent (previous example) +- Reasoning agent (this example) +- Code agent (later examples) + +### Foundation for Complex Agents +Understanding how to prompt for reasoning is essential before adding: +- Tools (giving the model a calculator) +- Memory (remembering previous calculations) +- Multi-step processes (ReAct pattern) + +## Expected Output + +Running this script should output something like: +``` +AI: 3 +``` + +The exact answer depends on the model's ability to: +- Correctly count all family members +- Apply the eating rates +- Convert units +- Round up for whole bags + +## Improving This Approach + +To get better reasoning: +1. **Use larger models**: 7B+ parameters +2. **Add step-by-step prompting**: "Show your work" +3. **Provide tools**: Give the model a calculator +4. **Use chain-of-thought**: Encourage explicit reasoning +5. **Verify answers**: Run multiple times or use multiple models + +The react-agent example demonstrates some of these improvements. diff --git a/examples/04_think/CONCEPT.md b/examples/04_think/CONCEPT.md new file mode 100644 index 0000000000000000000000000000000000000000..b98a2483fd0f1a3c394b9793be32ba73c528d9fc --- /dev/null +++ b/examples/04_think/CONCEPT.md @@ -0,0 +1,368 @@ +# Concept: Reasoning & Problem-Solving Agents + +## Overview + +This example demonstrates how to configure an LLM as a **reasoning agent** capable of analytical thinking and quantitative problem-solving. It shows the bridge between simple text generation and complex cognitive tasks. + +## What is a Reasoning Agent? + +A **reasoning agent** is an LLM configured to perform logical analysis, mathematical computation, and multi-step problem-solving through careful system prompt design. + +### Human Analogy + +``` +Regular Chat Reasoning Agent +───────────── ────────────────── +"Can you help me?" "I am a mathematician. +"Sure! What do you need?" I analyze problems methodically + and compute exact answers." +``` + +## The Reasoning Challenge + +### Why Reasoning is Hard for LLMs + +LLMs are trained on text prediction, not explicit reasoning: + +``` +┌───────────────────────────────────────┐ +│ LLM Training │ +│ "Predict next word in text" │ +│ │ +│ NOT explicitly trained for: │ +│ • Step-by-step logic │ +│ • Arithmetic computation │ +│ • Tracking multiple variables │ +│ • Systematic problem decomposition │ +└───────────────────────────────────────┘ +``` + +However, they can learn reasoning patterns from training data and be guided by system prompts. + +## Reasoning Through System Prompts + +### Configuration Pattern + +``` +┌─────────────────────────────────────────┐ +│ System Prompt Components │ +├─────────────────────────────────────────┤ +│ 1. Role: "Expert reasoner" │ +│ 2. Task: "Analyze and solve problems" │ +│ 3. Method: "Compute exact answers" │ +│ 4. Output: "Single numeric value" │ +└─────────────────────────────────────────┘ + ↓ + Reasoning Behavior +``` + +### Types of Reasoning Tasks + +**Quantitative Reasoning (this example):** +``` +Problem → Count entities → Calculate → Convert units → Answer +``` + +**Logical Reasoning:** +``` +Premises → Apply rules → Deduce conclusions → Answer +``` + +**Analytical Reasoning:** +``` +Data → Identify patterns → Form hypothesis → Conclude +``` + +## How LLMs "Reason" + +### Pattern Matching vs. True Reasoning + +LLMs don't reason like humans, but they can: + +``` +┌─────────────────────────────────────────────┐ +│ What LLMs Actually Do │ +│ │ +│ 1. Pattern Recognition │ +│ "This looks like a counting problem" │ +│ │ +│ 2. Template Application │ +│ "Similar problems follow this pattern" │ +│ │ +│ 3. Statistical Inference │ +│ "These numbers likely combine this way" │ +│ │ +│ 4. Learned Procedures │ +│ "I've seen this type of calculation" │ +└─────────────────────────────────────────────┘ +``` + +### The Reasoning Process + +``` +Input: Complex Word Problem + ↓ + ┌────────────┐ + │ Parse │ Identify entities and relationships + └────────────┘ + ↓ + ┌────────────┐ + │ Decompose │ Break into sub-problems + └────────────┘ + ↓ + ┌────────────┐ + │ Calculate │ Apply arithmetic operations + └────────────┘ + ↓ + ┌────────────┐ + │ Synthesize│ Combine results + └────────────┘ + ↓ + Final Answer +``` + +## Problem Complexity Hierarchy + +### Levels of Reasoning Difficulty + +``` +Easy Hard +│ │ +│ Simple Multi-step Nested Implicit │ +│ Arithmetic Logic Conditions Reasoning│ +│ │ +└─────────────────────────────────────────────┘ + +Examples: +Easy: "What is 5 + 3?" +Medium: "If 3 apples cost $2 each, what's the total?" +Hard: "Count family members with complex relationships" +``` + +### This Example's Complexity + +The potato problem is **highly complex**: + +``` +┌─────────────────────────────────────────┐ +│ Complexity Factors │ +├─────────────────────────────────────────┤ +│ ✓ Multiple entities (15+ people) │ +│ ✓ Relationship reasoning (family tree)│ +│ ✓ Conditional logic (if married then..)│ +│ ✓ Negative conditions (deceased people)│ +│ ✓ Special cases (dietary restrictions)│ +│ ✓ Multiple calculations │ +│ ✓ Unit conversions │ +└─────────────────────────────────────────┘ +``` + +## Limitations of Pure LLM Reasoning + +### Why This Approach Has Issues + +``` +┌────────────────────────────────────┐ +│ Problem: No External Tools │ +│ │ +│ LLM must hold everything in │ +│ "mental" context: │ +│ • All entity counts │ +│ • Intermediate calculations │ +│ • Conversion factors │ +│ • Final arithmetic │ +│ │ +│ Result: Prone to errors │ +└────────────────────────────────────┘ +``` + +### Common Failure Modes + +**1. Counting Errors:** +``` +Problem: "Count 15 people with complex relationships" +LLM: "14" or "16" (off by one) +``` + +**2. Arithmetic Mistakes:** +``` +Problem: "13 adults × 1.5 + 3 kids × 0.5" +LLM: May get intermediate steps wrong +``` + +**3. Lost Context:** +``` +Problem: Multi-step with many facts +LLM: Forgets earlier information +``` + +## Improving Reasoning: Evolution Path + +### Level 1: Pure Prompting (This Example) +``` +User → LLM → Answer + ↑ + System Prompt +``` + +**Limitations:** +- All reasoning internal to LLM +- No verification +- No tools +- Hidden process + +### Level 2: Chain-of-Thought +``` +User → LLM → Show Work → Answer + ↑ + "Explain your reasoning" +``` + +**Improvements:** +- Visible reasoning steps +- Can catch some errors +- Still no tools + +### Level 3: Tool-Augmented (simple-agent) +``` +User → LLM ⟷ Tools → Answer + ↑ (Calculator) + System Prompt +``` + +**Improvements:** +- External computation +- Reduced errors +- Verifiable steps + +### Level 4: ReAct Pattern (react-agent) +``` +User → LLM → Think → Act → Observe + ↑ ↓ ↓ ↓ + System Reason Tool Result + Prompt Use + ↑ ↓ ↓ + └───────────Iterate──┘ +``` + +**Best approach:** +- Explicit reasoning loop +- Tool use at each step +- Self-correction possible + +## System Prompt Design for Reasoning + +### Key Elements + +**1. Role Definition:** +``` +"You are an expert logical and quantitative reasoner" +``` +Sets the mental framework. + +**2. Task Specification:** +``` +"Analyze real-world word problems involving..." +``` +Defines the problem domain. + +**3. Output Format:** +``` +"Return the correct final number as a single value" +``` +Controls response structure. + +### Design Patterns + +**Pattern A: Direct Answer (This Example)** +``` +Prompt: [Problem] +Output: [Number] +``` +Pros: Concise, fast +Cons: No insight into reasoning + +**Pattern B: Show Work** +``` +Prompt: [Problem] "Show your steps" +Output: Step 1: ... Step 2: ... Answer: [Number] +``` +Pros: Transparent, debuggable +Cons: Longer, may still have errors + +**Pattern C: Self-Verification** +``` +Prompt: [Problem] "Solve, then verify" +Output: Solution + Verification + Final Answer +``` +Pros: More reliable +Cons: Slower, uses more tokens + +## Real-World Applications + +### Use Cases for Reasoning Agents + +**1. Data Analysis:** +``` +Input: Dataset summary +Task: Compute statistics, identify trends +Output: Numerical insights +``` + +**2. Planning:** +``` +Input: Goal + constraints +Task: Reason about optimal sequence +Output: Action plan +``` + +**3. Decision Support:** +``` +Input: Options + criteria +Task: Evaluate and compare +Output: Recommended choice +``` + +**4. Problem Solving:** +``` +Input: Complex scenario +Task: Break down and solve +Output: Solution +``` + +## Comparison: Different Agent Types + +``` + Reasoning Tools Memory Multi-turn + ───────── ───── ────── ────────── +intro.js ✗ ✗ ✗ ✗ +translation.js ~ ✗ ✗ ✗ +think.js (here) ✓ ✗ ✗ ✗ +simple-agent.js ✓ ✓ ✗ ~ +memory-agent.js ✓ ✓ ✓ ✓ +react-agent.js ✓✓ ✓ ~ ✓ +``` + +Legend: +- ✗ = Not present +- ~ = Limited/implicit +- ✓ = Present +- ✓✓ = Advanced/explicit + +## Key Takeaways + +1. **System prompts enable reasoning**: Proper configuration transforms an LLM into a reasoning agent +2. **Limitations exist**: Pure LLM reasoning is prone to errors on complex problems +3. **Tools help**: External computation (calculators, etc.) improves accuracy +4. **Iteration matters**: Multi-step reasoning patterns (like ReAct) work better +5. **Transparency is valuable**: Seeing the reasoning process helps debug and verify + +## Next Steps + +After understanding basic reasoning: +- **Add tools**: Let the agent use calculators, databases, APIs +- **Implement verification**: Check answers, retry on errors +- **Use chain-of-thought**: Make reasoning explicit +- **Apply ReAct pattern**: Combine reasoning and tool use systematically + +This example is the foundation for more sophisticated agent architectures that combine reasoning with external capabilities. diff --git a/examples/04_think/think.js b/examples/04_think/think.js new file mode 100644 index 0000000000000000000000000000000000000000..ccde1eee93f8c1ffd2fc1187e9edc0cd19347ed1 --- /dev/null +++ b/examples/04_think/think.js @@ -0,0 +1,49 @@ +import { + getLlama, + LlamaChatSession, +} from "node-llama-cpp"; +import {fileURLToPath} from "url"; +import path from "path"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const llama = await getLlama(); +const model = await llama.loadModel({ + modelPath: path.join( + __dirname, + '..', + '..', + 'models', + 'Qwen3-1.7B-Q8_0.gguf' + ) +}); +const systemPrompt = `You are an expert logical and quantitative reasoner. + Your goal is to analyze real-world word problems involving families, quantities, averages, and relationships + between entities, and compute the exact numeric answer. + + Goal: Return the correct final number as a single value — no explanation, no reasoning steps, just the answer. + ` +const context = await model.createContext(); +const session = new LlamaChatSession({ + contextSequence: context.getSequence(), + systemPrompt +}); + +const prompt = `My family reunion is this week, and I was assigned the mashed potatoes to bring. +The attendees include my married mother and father, my twin brother and his family, my aunt and her family, my grandma +and her brother, her brother's daughter, and his daughter's family. All the adults but me have been married, and no one +is divorced or remarried, but my grandpa and my grandma's sister-in-law passed away last year. All living spouses are attending. +My brother has two children that are still kids, my aunt has one six-year-old, and my grandma's brother's daughter has +three kids under 12. I figure each adult will eat about 1.5 potatoes and each kid will eat about 1/2 a potato, except my +second cousins don't eat carbs. The average potato is about half a pound, and potatoes are sold in 5-pound bags. + +How many whole bags of potatoes do I need? +`; + +const answer = await session.prompt(prompt); +console.log(`AI: ${answer}`); + +llama.dispose() +model.dispose() +context.dispose() +session.dispose() \ No newline at end of file diff --git a/examples/05_batch/CODE.md b/examples/05_batch/CODE.md new file mode 100644 index 0000000000000000000000000000000000000000..83d05d9d1101eea3ce549a7bf745a8a25b462617 --- /dev/null +++ b/examples/05_batch/CODE.md @@ -0,0 +1,323 @@ +# Code Explanation: batch.js + +This file demonstrates **parallel execution** of multiple LLM prompts using separate context sequences, enabling concurrent processing for better performance. + +## Step-by-Step Code Breakdown + +### 1. Import and Setup (Lines 1-10) +```javascript +import {getLlama, LlamaChatSession} from "node-llama-cpp"; +import path from "path"; +import {fileURLToPath} from "url"; + +/** + * Asynchronous execution improves performance in GAIA benchmarks, + * multi-agent applications, and other high-throughput scenarios. + */ + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +``` +- Standard imports for LLM interaction +- Comment explains the performance benefit +- **GAIA benchmark**: A standard for testing AI agent performance +- Useful for multi-agent systems that need to handle many requests + +### 2. Model Path Configuration (Lines 11-16) +```javascript +const modelPath = path.join( + __dirname, + "../", + "models", + "DeepSeek-R1-0528-Qwen3-8B-Q6_K.gguf" +) +``` +- Uses **DeepSeek-R1**: An 8B parameter model optimized for reasoning +- **Q6_K quantization**: Balance between quality and size +- Model is loaded once and shared between sequences + +### 3. Initialize Llama and Load Model (Lines 18-19) +```javascript +const llama = await getLlama(); +const model = await llama.loadModel({modelPath}); +``` +- Standard initialization +- Model is loaded into memory once +- Will be used by multiple sequences simultaneously + +### 4. Create Context with Multiple Sequences (Lines 20-23) +```javascript +const context = await model.createContext({ + sequences: 2, + batchSize: 1024 // The number of tokens that can be processed at once by the GPU. +}); +``` + +**Key parameters:** + +- **sequences: 2**: Creates 2 independent conversation sequences + - Each sequence has its own conversation history + - Both share the same model and context memory pool + - Can be processed in parallel + +- **batchSize: 1024**: Maximum tokens processed per GPU batch + - Larger = better GPU utilization + - Smaller = lower memory usage + - 1024 is a good balance for most GPUs + +### Why Multiple Sequences? + +``` +Single Sequence (Sequential) Multiple Sequences (Parallel) +───────────────────────── ────────────────────────────── +Process Prompt 1 → Response 1 Process Prompt 1 ──┐ +Wait... ├→ Both responses +Process Prompt 2 → Response 2 Process Prompt 2 ──┘ in parallel! + +Total Time: T1 + T2 Total Time: max(T1, T2) +``` + +### 5. Get Individual Sequences (Lines 25-26) +```javascript +const sequence1 = context.getSequence(); +const sequence2 = context.getSequence(); +``` +- Retrieves two separate sequence objects from the context +- Each sequence maintains its own state +- They can be used independently for different conversations + +### 6. Create Separate Sessions (Lines 28-33) +```javascript +const session1 = new LlamaChatSession({ + contextSequence: sequence1 +}); +const session2 = new LlamaChatSession({ + contextSequence: sequence2 +}); +``` +- Creates a chat session for each sequence +- Each session has its own conversation history +- Sessions are completely independent +- No system prompts in this example (could be added) + +### 7. Define Questions (Lines 35-36) +```javascript +const q1 = "Hi there, how are you?"; +const q2 = "How much is 6+6?"; +``` +- Two completely different questions +- Will be processed simultaneously +- Different types: conversational vs. computational + +### 8. Parallel Execution with Promise.all (Lines 38-44) +```javascript +const [ + a1, + a2 +] = await Promise.all([ + session1.prompt(q1), + session2.prompt(q2) +]); +``` + +**How this works:** + +1. `session1.prompt(q1)` starts asynchronously +2. `session2.prompt(q2)` starts asynchronously (doesn't wait for #1) +3. `Promise.all()` waits for BOTH to complete +4. Returns results in array: [response1, response2] +5. Destructures into `a1` and `a2` + +**Key benefit**: Both prompts are processed at the same time, not one after another! + +### 9. Display Results (Lines 46-50) +```javascript +console.log("User: " + q1); +console.log("AI: " + a1); + +console.log("User: " + q2); +console.log("AI: " + a2); +``` +- Outputs both question-answer pairs +- Results appear in order despite parallel processing + +## Key Concepts Demonstrated + +### 1. Parallel Processing +Instead of: +```javascript +// Sequential (slow) +const a1 = await session1.prompt(q1); // Wait +const a2 = await session2.prompt(q2); // Wait again +``` + +We use: +```javascript +// Parallel (fast) +const [a1, a2] = await Promise.all([ + session1.prompt(q1), + session2.prompt(q2) +]); +``` + +### 2. Context Sequences +A context can hold multiple independent sequences: + +``` +┌─────────────────────────────────────┐ +│ Context (Shared) │ +│ ┌───────────────────────────────┐ │ +│ │ Model Weights (8B params) │ │ +│ └───────────────────────────────┘ │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ │ +│ │ Sequence 1 │ │ Sequence 2 │ │ +│ │ "Hi there" │ │ "6+6?" │ │ +│ │ History... │ │ History... │ │ +│ └─────────────┘ └─────────────┘ │ +└─────────────────────────────────────┘ +``` + +## Performance Comparison + +### Sequential Execution +``` +Request 1: 2 seconds +Request 2: 2 seconds +Total: 4 seconds +``` + +### Parallel Execution (This Example) +``` +Request 1: 2 seconds ──┐ +Request 2: 2 seconds ──┤ Both running +Total: ~2 seconds └─ simultaneously +``` + +**Speedup**: ~2x for 2 sequences, scales with more sequences + +## Use Cases + +### 1. Multi-User Applications +```javascript +// Handle multiple users simultaneously +const [user1Response, user2Response, user3Response] = await Promise.all([ + session1.prompt(user1Query), + session2.prompt(user2Query), + session3.prompt(user3Query) +]); +``` + +### 2. Multi-Agent Systems +```javascript +// Multiple agents working on different tasks +const [ + plannerResponse, + analyzerResponse, + executorResponse +] = await Promise.all([ + plannerSession.prompt("Plan the task"), + analyzerSession.prompt("Analyze the data"), + executorSession.prompt("Execute step 1") +]); +``` + +### 3. Benchmarking +```javascript +// Test multiple prompts for evaluation +const results = await Promise.all( + testPrompts.map(prompt => session.prompt(prompt)) +); +``` + +### 4. A/B Testing +```javascript +// Test different system prompts +const [responseA, responseB] = await Promise.all([ + sessionWithPromptA.prompt(query), + sessionWithPromptB.prompt(query) +]); +``` + +## Resource Considerations + +### Memory Usage +Each sequence needs memory for: +- Conversation history +- Intermediate computations +- KV cache (key-value cache for transformer attention) + +**Rule of thumb**: More sequences = more memory needed + +### GPU Utilization +- **Single sequence**: May underutilize GPU +- **Multiple sequences**: Better GPU utilization +- **Too many sequences**: May exceed VRAM, causing slowdown + +### Optimal Number of Sequences +Depends on: +- Available VRAM +- Model size +- Context length +- Batch size + +**Typical**: 2-8 sequences for consumer GPUs + +## Limitations & Considerations + +### 1. Shared Context Limit +All sequences share the same context memory pool: +``` +Total context size: 8192 tokens +Sequence 1: 4096 tokens +Sequence 2: 4096 tokens +Maximum distribution! +``` + +### 2. Not True Parallelism for CPU +On CPU-only systems, sequences are interleaved, not truly parallel. Still provides better overall throughput. + +### 3. Model Loading Overhead +The model is loaded once and shared, which is efficient. But initial loading still takes time. + +## Why This Matters for AI Agents + +### Efficiency in Production +Real-world agent systems need to: +- Handle multiple requests concurrently +- Respond quickly to users +- Make efficient use of hardware + +### Multi-Agent Architectures +Complex agent systems often have: +- **Planner agent**: Thinks about strategy +- **Executor agent**: Takes actions +- **Critic agent**: Evaluates results + +These can run in parallel using separate sequences. + +### Scalability +This pattern is the foundation for: +- Web services with multiple users +- Batch processing of data +- Distributed agent systems + +## Best Practices + +1. **Match sequences to workload**: Don't create more than you need +2. **Monitor memory usage**: Each sequence consumes VRAM +3. **Use appropriate batch size**: Balance speed vs. memory +4. **Clean up resources**: Always dispose when done +5. **Handle errors**: Wrap Promise.all in try-catch + +## Expected Output + +Running this script should output something like: +``` +User: Hi there, how are you? +AI: Hello! I'm doing well, thank you for asking... + +User: How much is 6+6? +AI: 12 +``` + +Both responses appear quickly because they were processed simultaneously! diff --git a/examples/05_batch/CONCEPT.md b/examples/05_batch/CONCEPT.md new file mode 100644 index 0000000000000000000000000000000000000000..d6a67f40810cde554175870069ccc0250c013859 --- /dev/null +++ b/examples/05_batch/CONCEPT.md @@ -0,0 +1,365 @@ +# Concept: Parallel Processing & Performance Optimization + +## Overview + +This example demonstrates **concurrent execution** of multiple LLM requests using separate context sequences, a critical technique for building scalable AI agent systems. + +## The Performance Problem + +### Sequential Processing (Slow) + +Traditional approach processes one request at a time: + +``` +Request 1 ────────→ Response 1 (2s) + ↓ + Request 2 ────────→ Response 2 (2s) + ↓ + Total: 4 seconds +``` + +### Parallel Processing (Fast) + +This example processes multiple requests simultaneously: + +``` +Request 1 ────────→ Response 1 (2s) ──┐ + ├→ Total: 2 seconds +Request 2 ────────→ Response 2 (2s) ──┘ + (Both running at the same time) +``` + +**Performance gain: 2x speedup!** + +## Core Concept: Context Sequences + +### Single vs. Multiple Sequences + +``` +┌────────────────────────────────────────────────┐ +│ Model (Loaded Once) │ +├────────────────────────────────────────────────┤ +│ Context │ +│ ┌──────────────┐ ┌──────────────┐ │ +│ │ Sequence 1 │ │ Sequence 2 │ │ +│ │ │ │ │ │ +│ │ Conversation │ │ Conversation │ │ +│ │ History A │ │ History B │ │ +│ └──────────────┘ └──────────────┘ │ +└────────────────────────────────────────────────┘ +``` + +**Key insights:** +- Model weights are shared (memory efficient) +- Each sequence has independent history +- Sequences can process in parallel +- Both use the same underlying model + +## How Parallel Processing Works + +### Promise.all Pattern + +JavaScript's `Promise.all()` enables concurrent execution: + +``` +Sequential: +──────────────────────────────────── +await fn1(); // Wait 2s +await fn2(); // Wait 2s more +Total: 4s + +Parallel: +──────────────────────────────────── +await Promise.all([ + fn1(), // Start immediately + fn2() // Start immediately (don't wait!) +]); +Total: 2s (whichever finishes last) +``` + +### Execution Timeline + +``` +Time → 0s 1s 2s 3s 4s + │ │ │ │ │ +Seq 1: ├───────Processing───────┤ + │ └─ Response 1 + │ +Seq 2: ├───────Processing───────┤ + └─ Response 2 + + Both complete at ~2s instead of 4s! +``` + +## GPU Batch Processing + +### Why Batching Matters + +Modern GPUs process multiple operations efficiently: + +``` +Without Batching (Inefficient) +────────────────────────────── +GPU: [Token 1] ... wait ... +GPU: [Token 2] ... wait ... +GPU: [Token 3] ... wait ... + └─ GPU underutilized + +With Batching (Efficient) +───────────────────────── +GPU: [Tokens 1-1024] ← Full batch + └─ GPU fully utilized! +``` + +**batchSize parameter**: Controls how many tokens process together. + +### Trade-offs + +``` +Small Batch (e.g., 128) Large Batch (e.g., 2048) +─────────────────────── ──────────────────────── +✓ Lower memory ✓ Better GPU utilization +✓ More flexible ✓ Faster throughput +✗ Slower throughput ✗ Higher memory usage +✗ GPU underutilized ✗ May exceed VRAM +``` + +**Sweet spot**: Usually 512-1024 for consumer GPUs. + +## Architecture Patterns + +### Pattern 1: Multi-User Service + +``` +┌─────────┐ ┌─────────┐ ┌─────────┐ +│ User A │ │ User B │ │ User C │ +└────┬────┘ └────┬────┘ └────┬────┘ + │ │ │ + └────────────┼────────────┘ + ↓ + ┌────────────────┐ + │ Load Balancer │ + └────────────────┘ + ↓ + ┌────────────┼────────────┐ + ↓ ↓ ↓ +┌─────────┐ ┌─────────┐ ┌─────────┐ +│ Seq 1 │ │ Seq 2 │ │ Seq 3 │ +└─────────┘ └─────────┘ └─────────┘ + └────────────┼────────────┘ + ↓ + ┌────────────────┐ + │ Shared Model │ + └────────────────┘ +``` + +### Pattern 2: Multi-Agent System + +``` + ┌──────────────┐ + │ Task │ + └──────┬───────┘ + │ + ┌────────┼────────┐ + ↓ ↓ ↓ + ┌────────┐ ┌──────┐ ┌──────────┐ + │Planner │ │Critic│ │ Executor │ + │ Agent │ │Agent │ │ Agent │ + └───┬────┘ └──┬───┘ └────┬─────┘ + │ │ │ + └─────────┼──────────┘ + ↓ + (All run in parallel) +``` + +### Pattern 3: Pipeline Processing + +``` +Input Queue: [Task1, Task2, Task3, ...] + ↓ + ┌───────────────┐ + │ Dispatcher │ + └───────────────┘ + ↓ + ┌───────────┼───────────┐ + ↓ ↓ ↓ + Sequence 1 Sequence 2 Sequence 3 + ↓ ↓ ↓ + └───────────┼───────────┘ + ↓ + Output: [R1, R2, R3] +``` + +## Resource Management + +### Memory Allocation + +Each sequence consumes memory: + +``` +┌──────────────────────────────────┐ +│ Total VRAM: 8GB │ +├──────────────────────────────────┤ +│ Model Weights: 4.0 GB │ +│ Context Base: 1.0 GB │ +│ Sequence 1 (KV Cache): 0.8 GB │ +│ Sequence 2 (KV Cache): 0.8 GB │ +│ Sequence 3 (KV Cache): 0.8 GB │ +│ Overhead: 0.6 GB │ +├──────────────────────────────────┤ +│ Total Used: 8.0 GB │ +│ Remaining: 0.0 GB │ +└──────────────────────────────────┘ + Maximum capacity! +``` + +**Formula**: +``` +Required VRAM = Model + Context + (NumSequences × KVCache) +``` + +### Finding Optimal Sequence Count + +``` +Too Few (1-2) Optimal (4-8) Too Many (16+) +───────────── ───────────── ────────────── +GPU underutilized Balanced use Memory overflow +↓ ↓ ↓ +Slow throughput Best performance Thrashing/crashes +``` + +**Test your system**: +1. Start with 2 sequences +2. Monitor VRAM usage +3. Increase until performance plateaus +4. Back off if memory issues occur + +## Real-World Scenarios + +### Scenario 1: Chatbot Service + +``` +Challenge: 100 users, each waiting 2s per response +Sequential: 100 × 2s = 200s (3.3 minutes!) +Parallel (10 seq): 10 batches × 2s = 20s + 10x speedup! +``` + +### Scenario 2: Batch Analysis + +``` +Task: Analyze 1000 documents +Sequential: 1000 × 3s = 50 minutes +Parallel (8 seq): 125 batches × 3s = 6.25 minutes + 8x speedup! +``` + +### Scenario 3: Multi-Agent Collaboration + +``` +Agents: Planner, Analyzer, Executor (all needed) +Sequential: Wait for each → Slow pipeline +Parallel: All work together → Fast decision-making +``` + +## Limitations & Considerations + +### 1. Context Capacity Sharing + +``` +Problem: Sequences share total context space +─────────────────────────────────────────── +Total context: 4096 tokens +2 sequences: Each gets ~2048 tokens max +4 sequences: Each gets ~1024 tokens max + +More sequences = Less history per sequence! +``` + +### 2. CPU vs GPU Parallelism + +``` +With GPU: CPU Only: +True parallel processing Interleaved processing +Multiple CUDA streams Single thread context-switching + (Still helps throughput!) +``` + +### 3. Not Always Faster + +``` +When parallel helps: When it doesn't: +• Independent requests • Dependent requests (must wait) +• I/O-bound operations • Very short prompts (overhead) +• Multiple users • Single sequential conversation +``` + +## Best Practices + +### 1. Design for Independence +``` +✓ Good: Separate user conversations +✓ Good: Independent analysis tasks +✗ Bad: Sequential reasoning steps (use ReAct instead) +``` + +### 2. Monitor Resources +``` +Track: +• VRAM usage per sequence +• Processing time per request +• Queue depths +• Error rates +``` + +### 3. Implement Graceful Degradation +``` +if (vramExceeded) { + reduceSequenceCount(); + // or queue requests instead +} +``` + +### 4. Handle Errors Properly +```javascript +try { + const results = await Promise.all([...]); +} catch (error) { + // One failure doesn't crash all sequences + handlePartialResults(); +} +``` + +## Comparison: Evolution of Performance + +``` +Stage Requests/Min Pattern +───────────────── ───────────── ─────────────── +1. Basic (intro) 30 Sequential +2. Batch (this) 120 4 sequences +3. Load balanced 240 8 sequences + queue +4. Distributed 1000+ Multiple machines +``` + +## Key Takeaways + +1. **Parallelism is essential** for production AI agent systems +2. **Sequences share model** but maintain independent state +3. **Promise.all** enables concurrent JavaScript execution +4. **Batch size** affects GPU utilization and throughput +5. **Memory is the limit** - more sequences need more VRAM +6. **Not magic** - only helps with independent tasks + +## Practical Formula + +``` +Speedup = min( + Number_of_Sequences, + Available_VRAM / Memory_Per_Sequence, + GPU_Compute_Limit +) +``` + +Typically: 2-10x speedup for well-designed systems. + +This technique is foundational for building scalable agent architectures that can handle real-world workloads efficiently. diff --git a/examples/05_batch/batch.js b/examples/05_batch/batch.js new file mode 100644 index 0000000000000000000000000000000000000000..641d15e2f0272f27ee82201729be8c3a765c7fe6 --- /dev/null +++ b/examples/05_batch/batch.js @@ -0,0 +1,60 @@ +import {getLlama, LlamaChatSession} from "node-llama-cpp"; +import path from "path"; +import {fileURLToPath} from "url"; + +/** + * Asynchronous execution improves performance in GAIA benchmarks, + * multi-agent applications, and other high-throughput scenarios. + */ + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const modelPath = path.join( + __dirname, + '..', + '..', + 'models', + 'DeepSeek-R1-0528-Qwen3-8B-Q6_K.gguf' +) + +const llama = await getLlama({ + logLevel: 'error' +}); +const model = await llama.loadModel({modelPath}); +const context = await model.createContext({ + sequences: 2, + batchSize: 1024 // The number of tokens that can be processed at once by the GPU. +}); + +const sequence1 = context.getSequence(); +const sequence2 = context.getSequence(); + +const session1 = new LlamaChatSession({ + contextSequence: sequence1 +}); +const session2 = new LlamaChatSession({ + contextSequence: sequence2 +}); + +const q1 = "Hi there, how are you?"; +const q2 = "How much is 6+6?"; + +console.log('Batching started...') +const [ + a1, + a2 +] = await Promise.all([ + session1.prompt(q1), + session2.prompt(q2) +]); + +console.log("User: " + q1); +console.log("AI: " + a1); + +console.log("User: " + q2); +console.log("AI: " + a2); + +session1.dispose(); +session2.dispose(); +context.dispose(); +model.dispose(); +llama.dispose(); \ No newline at end of file diff --git a/examples/06_coding/CODE.md b/examples/06_coding/CODE.md new file mode 100644 index 0000000000000000000000000000000000000000..c7d520c7232f8b7163a7f69d9957c1ed6ddb82a5 --- /dev/null +++ b/examples/06_coding/CODE.md @@ -0,0 +1,380 @@ +# Code Explanation: coding.js + +This file demonstrates **streaming responses** with token limits and real-time output, showing how to get immediate feedback from the LLM as it generates text. + +## Step-by-Step Code Breakdown + +### 1. Import and Setup (Lines 1-8) +```javascript +import { + getLlama, + HarmonyChatWrapper, + LlamaChatSession, +} from "node-llama-cpp"; +import {fileURLToPath} from "url"; +import path from "path"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +``` +- Standard setup for LLM interaction +- **HarmonyChatWrapper**: A chat format wrapper for models that use the Harmony format (more on this below) + +### 2. Understanding the Harmony Chat Format + +#### What is Harmony? +Harmony is a structured message format used for multi-role chat interactions designed by OpenAI for their gpt-oss models. It's not just a prompt format - it's a complete rethinking of how models should structure their outputs, especially for complex reasoning and tool use. + +#### Harmony Format Structure + +The format uses special tokens and syntax to define roles such as `system`, `developer`, `user`, `assistant`, and `tool`, as well as output "channels" (`analysis`, `commentary`, `final`) that let the model reason internally, call tools, and produce clean user-facing responses. + +**Basic message structure:** +``` +<|start|>ROLE<|message|>CONTENT<|end|> +<|start|>assistant<|channel|>CHANNEL<|message|>CONTENT<|end|> +``` + +**The five roles in hierarchy order** (system > developer > user > assistant > tool): + +1. **system**: Global identity, guardrails, and model configuration +2. **developer**: Product policy and style instructions (what you typically think of as "system prompt") +3. **user**: User messages and queries +4. **assistant**: Model responses +5. **tool**: Tool execution results + +**The three output channels:** + +1. **analysis**: Private chain-of-thought reasoning not shown to users +2. **commentary**: Tool calling preambles and process updates +3. **final**: Clean user-facing responses + +**Example of Harmony in action:** +``` +<|start|>system<|message|>You are a helpful assistant.<|end|> +<|start|>developer<|message|>Always be concise.<|end|> +<|start|>user<|message|>What time is it?<|end|> +<|start|>assistant<|channel|>commentary<|message|>{"tool_use": {"name": "get_current_time", "arguments": {}}}<|end|> +<|start|>tool<|message|>{"time": "2025-10-25T13:47:00Z"}<|end|> +<|start|>assistant<|channel|>final<|message|>The current time is 1:47 PM UTC.<|end|> +``` + +#### Why Use Harmony? + +Harmony separates how the model thinks, what actions it takes, and what finally goes to the user, resulting in cleaner tool use, safer defaults for UI, and better observability. For our translation example: + +- The `final` channel ensures we only get the translation, not explanations +- The structured format helps the model follow instructions more reliably +- The role hierarchy prevents instruction conflicts + +**Important Note**: Models need to be specifically trained or fine-tuned to produce Harmony output correctly. You can't just apply this format to any model. Apertus and other models not explicitly trained on Harmony may be confused by this structure, but the HarmonyChatWrapper in node-llama-cpp handles the necessary formatting automatically. + + +### 3. Load Model (Lines 10-18) +```javascript +const llama = await getLlama(); +const model = await llama.loadModel({ + modelPath: path.join( + __dirname, + "../", + "models", + "hf_giladgd_gpt-oss-20b.MXFP4.gguf" + ) +}); +``` +- Uses **gpt-oss-20b**: A 20 billion parameter model +- **MXFP4**: Mixed precision 4-bit quantization for smaller size +- Larger model = better code explanations + +### 4. Create Context and Session (Lines 19-22) +```javascript +const context = await model.createContext(); +const session = new LlamaChatSession({ + chatWrapper: new HarmonyChatWrapper(), + contextSequence: context.getSequence(), +}); +``` +Basic session setup with no system prompt. + +### 5. Define the Question (Line 24) +```javascript +const q1 = `What is hoisting in JavaScript? Explain with examples.`; +``` +A technical programming question that requires detailed explanation. + +### 6. Display Context Size (Line 26) +```javascript +console.log('context.contextSize', context.contextSize) +``` +- Shows the maximum context window size +- Helps understand memory limitations +- Useful for debugging + +### 7. Streaming Prompt Execution (Lines 28-36) +```javascript +const a1 = await session.prompt(q1, { + // Tip: let the lib choose or cap reasonably; using the whole context size can be wasteful + maxTokens: 2000, + + // Fires as soon as the first characters arrive + onTextChunk: (text) => { + process.stdout.write(text); // optional: live print + }, +}); +``` + +**Key parameters:** + +**maxTokens: 2000** +- Limits response length to 2000 tokens (~1500 words) +- Prevents runaway generation +- Saves time and compute +- Without limit: model uses entire context + +**onTextChunk callback** +- Fires **as each token is generated** +- Receives text as it's produced +- `process.stdout.write()`: Prints without newlines +- Creates real-time "typing" effect + +### How Streaming Works + +``` +Without streaming: +User → [Wait 10 seconds...] → Complete response appears + +With streaming: +User → [Token 1] → [Token 2] → [Token 3] → ... → Complete + "What" "is" "hoisting" + (Immediate feedback!) +``` + +### 8. Display Final Answer (Line 38) +```javascript +console.log("\n\nFinal answer:\n", a1); +``` +- Prints the complete response again +- Useful for logging or verification +- Shows full text after streaming + +### 9. Cleanup (Lines 41-44) +```javascript +session.dispose() +context.dispose() +model.dispose() +llama.dispose() +``` +Standard resource cleanup. + +## Key Concepts Demonstrated + +### 1. Streaming Responses + +**Why streaming matters:** +- **Better UX**: Users see progress immediately +- **Early termination**: Can stop if response is off-track +- **Perceived speed**: Feels faster than waiting +- **Debugging**: See generation in real-time + +**Comparison:** +``` +Non-streaming: Streaming: +═══════════════ ═══════════════ +Request sent Request sent +[10s wait...] "What" (0.1s) +Complete response "is" (0.2s) + "hoisting" (0.3s) + ... continues + (Same total time, better experience!) +``` + +### 2. Token Limits + +**maxTokens controls generation length:** + +``` +No limit: With limit (2000): +───────── ───────────────── +May generate forever Stops at 2000 tokens +Uses entire context Saves computation +Unpredictable cost Predictable cost +``` + +**Token approximation:** +- 1 token ≈ 0.75 words (English) +- 2000 tokens ≈ 1500 words +- 4-5 paragraphs of detailed explanation + +### 3. Real-Time Feedback Pattern + +The `onTextChunk` callback enables: +```javascript +onTextChunk: (text) => { + // Do anything with each chunk: + process.stdout.write(text); // Console output + // socket.emit('chunk', text); // WebSocket to client + // buffer += text; // Accumulate for processing + // analyzePartial(text); // Real-time analysis +} +``` + +### 4. Context Size Awareness + +```javascript +console.log('context.contextSize', context.contextSize) +``` + +Shows model's memory capacity: +- Small models: 2048-4096 tokens +- Medium models: 8192-16384 tokens +- Large models: 32768+ tokens + +**Why it matters:** +``` +Context Size: 4096 tokens +Prompt: 100 tokens +Max response: 2000 tokens +History: Up to 1996 tokens +``` + +## Use Cases + +### 1. Code Explanations (This Example) +```javascript +prompt: "Explain hoisting in JavaScript" +→ Streams detailed explanation with examples +``` + +### 2. Long-Form Content Generation +```javascript +prompt: "Write a blog post about AI agents" +maxTokens: 3000 +→ Streams article as it's written +``` + +### 3. Interactive Tutoring +```javascript +// User sees explanation being built +prompt: "Teach me about closures" +onTextChunk: (text) => displayToUser(text) +``` + +### 4. Web Applications +```javascript +// Server-Sent Events or WebSocket +onTextChunk: (text) => { + websocket.send(text); // Send to browser +} +``` + +## Performance Considerations + +### Token Generation Speed + +Depends on: +- **Model size**: Larger = slower per token +- **Hardware**: GPU > CPU +- **Quantization**: Lower bits = faster +- **Context length**: Longer context = slower + +**Typical speeds:** +``` +Model Size GPU (RTX 4090) CPU (M2 Max) +────────── ────────────── ──────────── +1.7B 50-80 tok/s 15-25 tok/s +8B 20-35 tok/s 5-10 tok/s +20B 10-15 tok/s 2-4 tok/s +``` + +### When to Use maxTokens + +``` +✓ Use maxTokens when: + • Response length is predictable + • You want to save computation + • Testing/debugging + • API rate limiting + +✗ Don't limit when: + • Need complete answer + • Length varies greatly + • Using stop sequences instead +``` + +## Advanced Streaming Patterns + +### Pattern 1: Progressive Enhancement +```javascript +let buffer = ''; +onTextChunk: (text) => { + buffer += text; + if (buffer.includes('\n\n')) { + // Complete paragraph ready + processParagraph(buffer); + buffer = ''; + } +} +``` + +### Pattern 2: Early Stopping +```javascript +let isRelevant = true; +onTextChunk: (text) => { + if (text.includes('irrelevant_keyword')) { + isRelevant = false; + // Stop generation (would need additional API) + } +} +``` + +### Pattern 3: Multi-Consumer +```javascript +onTextChunk: (text) => { + console.log(text); // Console + logFile.write(text); // File + websocket.send(text); // Client + analyzer.process(text); // Analysis +} +``` + +## Expected Output + +When run, you'll see: +1. Context size logged (e.g., "context.contextSize 32768") +2. Streaming response appearing token-by-token +3. Complete final answer printed again + +Example output flow: +``` +context.contextSize 32768 +Hoisting is a JavaScript mechanism where variables and function +declarations are moved to the top of their scope before code +execution. For example: + +console.log(x); // undefined (not an error!) +var x = 5; + +This works because... +[continues streaming...] + +Final answer: +[Complete response printed again] +``` + +## Why This Matters for AI Agents + +### User Experience +- Real-time agents feel more responsive +- Users can interrupt if going wrong direction +- Better for conversational interfaces + +### Resource Management +- Token limits prevent runaway generation +- Predictable costs and timing +- Can cancel expensive operations early + +### Integration Patterns +- Web UIs show "typing" effect +- CLIs display progressive output +- APIs stream to clients efficiently + +This pattern is essential for production agent systems where user experience and resource control matter. diff --git a/examples/06_coding/CONCEPT.md b/examples/06_coding/CONCEPT.md new file mode 100644 index 0000000000000000000000000000000000000000..3fa2ffd97b93b38b3406b3d4cdedd7613ab89e6a --- /dev/null +++ b/examples/06_coding/CONCEPT.md @@ -0,0 +1,400 @@ +# Concept: Streaming & Response Control + +## Overview + +This example demonstrates **streaming responses** and **token limits**, two essential techniques for building responsive AI agents with controlled output. + +## The Streaming Problem + +### Traditional (Non-Streaming) Approach + +``` +User sends prompt + ↓ + [Wait 10 seconds...] + ↓ +Complete response appears all at once +``` + +**Problems:** +- Poor user experience (long wait) +- No progress indication +- Can't interrupt bad responses +- Feels unresponsive + +### Streaming Approach (This Example) + +``` +User sends prompt + ↓ +"Hoisting" (0.1s) → User sees first word! + ↓ +"is a" (0.2s) → More text appears + ↓ +"JavaScript" (0.3s) → Continuous feedback + ↓ +[Continues token by token...] +``` + +**Benefits:** +- Immediate feedback +- Progress visible +- Can interrupt early +- Feels interactive + +## How Streaming Works + +### Token-by-Token Generation + +LLMs generate one token at a time internally. Streaming exposes this: + +``` +Internal LLM Process: +┌─────────────────────────────────────┐ +│ Token 1: "Hoisting" │ +│ Token 2: "is" │ +│ Token 3: "a" │ +│ Token 4: "JavaScript" │ +│ Token 5: "mechanism" │ +│ ... │ +└─────────────────────────────────────┘ + +Without Streaming: With Streaming: +Wait for all tokens Emit each token immediately +└─→ Buffer → Return └─→ Callback → Display +``` + +### The onTextChunk Callback + +``` +┌────────────────────────────────────┐ +│ Model Generation │ +└────────────┬───────────────────────┘ + │ + ┌────────┴─────────┐ + │ Each new token │ + └────────┬─────────┘ + ↓ + ┌────────────────────┐ + │ onTextChunk(text) │ ← Your callback + └────────┬───────────┘ + ↓ + Your code processes it: + • Display to user + • Send over network + • Log to file + • Analyze content +``` + +## Token Limits: maxTokens + +### Why Limit Output? + +Without limits, models might generate: +``` +User: "Explain hoisting" +Model: [Generates 10,000 words including: + - Complete JavaScript history + - Every edge case + - Unrelated examples + - Never stops...] +``` + +With limits: +``` +User: "Explain hoisting" +Model: [Generates ~1500 words + - Core concept + - Key examples + - Stops at 2000 tokens] +``` + +### Token Budgeting + +``` +Context Window: 4096 tokens +├─ System Prompt: 200 tokens +├─ User Message: 100 tokens +├─ Response (maxTokens): 2000 tokens +└─ Remaining for history: 1796 tokens + +Total used: 2300 tokens +Available: 1796 tokens for future conversation +``` + +### Cost vs Quality + +``` +Token Limit Output Quality Use Case +─────────── ─────────────── ───────────────── +100 Brief, may be cut Quick answers +500 Concise but complete Short explanations +2000 (example) Detailed Full explanations +No limit Risk of rambling When length unknown +``` + +## Real-Time Applications + +### Pattern 1: Interactive CLI + +``` +User: "Explain closures" + ↓ +Terminal: "A closure is a function..." + (Appears word by word, like typing) + ↓ +User sees progress, knows it's working +``` + +### Pattern 2: Web Application + +``` +Browser Server + │ │ + ├─── Send prompt ────────→│ + │ │ + │←── Chunk 1: "Closures"──┤ + │ (Display immediately) │ + │ │ + │←── Chunk 2: "are"───────┤ + │ (Append to display) │ + │ │ + │←── Chunk 3: "functions"─┤ + │ (Keep appending...) │ +``` + +Implementation: +- Server-Sent Events (SSE) +- WebSockets +- HTTP streaming + +### Pattern 3: Multi-Consumer + +``` + onTextChunk(text) + │ + ┌───────┼───────┐ + ↓ ↓ ↓ + Console WebSocket Log File + Display → Client → Storage +``` + +## Performance Characteristics + +### Latency vs Throughput + +``` +Time to First Token (TTFT): +├─ Small model (1.7B): ~100ms +├─ Medium model (8B): ~200ms +└─ Large model (20B): ~500ms + +Tokens Per Second: +├─ Small model: 50-80 tok/s +├─ Medium model: 20-35 tok/s +└─ Large model: 10-15 tok/s + +User Experience: +TTFT < 500ms → Feels instant +Tok/s > 20 → Reads naturally +``` + +### Resource Trade-offs + +``` +Model Size Memory Speed Quality +────────── ──────── ───── ─────── +1.7B ~2GB Fast Good +8B ~6GB Medium Better +20B ~12GB Slower Best +``` + +## Advanced Concepts + +### Buffering Strategies + +**No Buffer (Immediate)** +``` +Every token → callback → display +└─ Smoothest UX but more overhead +``` + +**Line Buffer** +``` +Accumulate until newline → flush +└─ Better for paragraph-based output +``` + +**Time Buffer** +``` +Accumulate for 50ms → flush batch +└─ Reduces callback frequency +``` + +### Early Stopping + +``` +Generation in progress: +"The answer is clearly... wait, actually..." + ↑ + onTextChunk detects issue + ↓ + Stop generation + ↓ + "Let me reconsider" +``` + +Useful for: +- Detecting off-topic responses +- Safety filters +- Relevance checking + +### Progressive Enhancement + +``` +Partial Response Analysis: +┌─────────────────────────────────┐ +│ "To implement this feature..." │ +│ │ +│ ← Already useful information │ +│ │ +│ "...you'll need: 1) Node.js" │ +│ │ +│ ← Can start acting on this │ +│ │ +│ "2) Express framework" │ +└─────────────────────────────────┘ + +Agent can begin working before response completes! +``` + +## Context Size Awareness + +### Why It Matters + +``` +┌────────────────────────────────┐ +│ Context Window (4096) │ +├────────────────────────────────┤ +│ System Prompt 200 tokens │ +│ Conversation History 1000 │ +│ Current Prompt 100 │ +│ Response Space 2796 │ +└────────────────────────────────┘ + +If maxTokens > 2796: +└─→ Error or truncation! +``` + +### Dynamic Adjustment + +``` +Available = contextSize - (prompt + history) + +if (maxTokens > available) { + maxTokens = available; + // or clear old history +} +``` + +## Streaming in Agent Architectures + +### Simple Agent + +``` +User → LLM (streaming) → Display + └─ onTextChunk shows progress +``` + +### Multi-Step Agent + +``` +Step 1: Plan (stream) → Show thinking +Step 2: Act (stream) → Show action +Step 3: Result (stream) → Show outcome + └─ User sees agent's process +``` + +### Collaborative Agents + +``` +Agent A (streaming) ──┐ + ├─→ Coordinator → User +Agent B (streaming) ──┘ + └─ Both stream simultaneously +``` + +## Best Practices + +### 1. Always Set maxTokens + +``` +✓ Good: +session.prompt(query, { maxTokens: 2000 }) + +✗ Risky: +session.prompt(query) +└─ May use entire context! +``` + +### 2. Handle Partial Updates + +``` +let fullResponse = ''; +onTextChunk: (chunk) => { + fullResponse += chunk; + display(chunk); // Show immediately + logComplete = false; // Mark incomplete +} +// After completion: +saveToDatabase(fullResponse); +``` + +### 3. Provide Feedback + +``` +onTextChunk: (chunk) => { + if (firstChunk) { + showLoadingDone(); + firstChunk = false; + } + appendToDisplay(chunk); +} +``` + +### 4. Monitor Performance + +``` +const startTime = Date.now(); +let tokenCount = 0; + +onTextChunk: (chunk) => { + tokenCount += estimateTokens(chunk); + const elapsed = (Date.now() - startTime) / 1000; + const tokensPerSecond = tokenCount / elapsed; + updateMetrics(tokensPerSecond); +} +``` + +## Key Takeaways + +1. **Streaming improves UX**: Users see progress immediately +2. **maxTokens controls cost**: Prevents runaway generation +3. **Token-by-token generation**: LLMs produce one token at a time +4. **onTextChunk callback**: Your hook into the generation process +5. **Context awareness matters**: Monitor available space +6. **Essential for production**: Real-time systems need streaming + +## Comparison + +``` +Feature intro.js coding.js (this) +──────────────── ───────── ───────────────── +Streaming ✗ ✓ +Token limit ✗ ✓ (2000) +Real-time output ✗ ✓ +Progress visible ✗ ✓ +User control ✗ ✓ +``` + +This pattern is foundational for building responsive, user-friendly AI agent interfaces. diff --git a/examples/06_coding/coding.js b/examples/06_coding/coding.js new file mode 100644 index 0000000000000000000000000000000000000000..9fb86e52e50ad957e923b06d19d23d456692ecb6 --- /dev/null +++ b/examples/06_coding/coding.js @@ -0,0 +1,47 @@ +import { + getLlama, + HarmonyChatWrapper, + LlamaChatSession, +} from "node-llama-cpp"; +import {fileURLToPath} from "url"; +import path from "path"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const llama = await getLlama(); +const model = await llama.loadModel({ + modelPath: path.join( + __dirname, + '..', + '..', + 'models', + 'hf_giladgd_gpt-oss-20b.MXFP4.gguf' + ) +}); +const context = await model.createContext(); +const session = new LlamaChatSession({ + chatWrapper: new HarmonyChatWrapper(), + contextSequence: context.getSequence(), +}); + +const q1 = `What is hoisting in JavaScript? Explain with examples.`; + +console.log('context.contextSize', context.contextSize) + +const a1 = await session.prompt(q1, { + // Tip: let the lib choose or cap reasonably; using the whole context size can be wasteful + maxTokens: 2000, + + // Fires as soon as the first characters arrive + onTextChunk: (text) => { + process.stdout.write(text); // optional: live print + }, +}); + +console.log("\n\nFinal answer:\n", a1); + + +session.dispose() +context.dispose() +model.dispose() +llama.dispose() \ No newline at end of file diff --git a/examples/07_simple-agent/CODE.md b/examples/07_simple-agent/CODE.md new file mode 100644 index 0000000000000000000000000000000000000000..ea6f0eb67347a07d47e508e75dbdbfd229230710 --- /dev/null +++ b/examples/07_simple-agent/CODE.md @@ -0,0 +1,368 @@ +# Code Explanation: simple-agent.js + +This file demonstrates **function calling** - the core feature that transforms an LLM from a text generator into an agent that can take actions using tools. + +## Step-by-Step Code Breakdown + +### 1. Import and Setup (Lines 1-7) +```javascript +import {defineChatSessionFunction, getLlama, LlamaChatSession} from "node-llama-cpp"; +import {fileURLToPath} from "url"; +import path from "path"; +import {PromptDebugger} from "../helper/prompt-debugger.js"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const debug = false; +``` +- **defineChatSessionFunction**: Key import for creating callable functions +- **PromptDebugger**: Helper for debugging prompts (covered at the end) +- **debug**: Controls verbose logging + +### 2. Initialize and Load Model (Lines 9-17) +```javascript +const llama = await getLlama({debug}); +const model = await llama.loadModel({ + modelPath: path.join( + __dirname, + "../", + "models", + "Qwen3-1.7B-Q8_0.gguf" + ) +}); +const context = await model.createContext({contextSize: 2000}); +``` +- Uses Qwen3-1.7B model (good for function calling) +- Sets context size to 2000 tokens explicitly + +### 3. System Prompt for Time Conversion (Lines 20-23) +```javascript +const systemPrompt = `You are a professional chronologist who standardizes time representations across different systems. + +Always convert times from 12-hour format (e.g., "1:46:36 PM") to 24-hour format (e.g., "13:46") without seconds +before returning them.`; +``` + +**Purpose:** +- Defines agent's role and behavior +- Instructs on output format (24-hour, no seconds) +- Ensures consistency in time representation + +### 4. Create Session (Lines 25-28) +```javascript +const session = new LlamaChatSession({ + contextSequence: context.getSequence(), + systemPrompt, +}); +``` +Standard session with system prompt. + +### 5. Define a Tool Function (Lines 30-39) +```javascript +const getCurrentTime = defineChatSessionFunction({ + description: "Get the current time", + params: { + type: "object", + properties: {} + }, + async handler() { + return new Date().toLocaleTimeString(); + } +}); +``` + +**Breaking it down:** + +**description:** +- Tells the LLM what this function does +- LLM reads this to decide when to call it + +**params:** +- Defines function parameters (JSON Schema format) +- Empty `properties: {}` means no parameters needed +- Type must be "object" even if no properties + +**handler:** +- The actual JavaScript function that executes +- Returns current time as string (e.g., "1:46:36 PM") +- Can be async (use await inside) + +### How Function Calling Works + +``` +1. User asks: "What time is it?" +2. LLM reads: + - System prompt + - Available functions (getCurrentTime) + - Function description +3. LLM decides: "I should call getCurrentTime()" +4. Library executes: handler() +5. Handler returns: "1:46:36 PM" +6. LLM receives result as "tool output" +7. LLM processes: Converts to 24-hour format per system prompt +8. LLM responds: "13:46" +``` + +### 6. Register Functions (Line 41) +```javascript +const functions = {getCurrentTime}; +``` +- Creates object with all available functions +- Multiple functions: `{getCurrentTime, getWeather, calculate, ...}` +- LLM can choose which function(s) to call + +### 7. Define User Prompt (Line 42) +```javascript +const prompt = `What time is it right now?`; +``` +A question that requires using the tool. + +### 8. Execute with Functions (Line 45) +```javascript +const a1 = await session.prompt(prompt, {functions}); +console.log("AI: " + a1); +``` +- **{functions}** makes tools available to the LLM +- LLM will automatically call getCurrentTime if needed +- Response includes tool result processed by LLM + +### 9. Debug Prompt Context (Lines 49-55) +```javascript +const promptDebugger = new PromptDebugger({ + outputDir: './logs', + filename: 'qwen_prompts.txt', + includeTimestamp: true, + appendMode: false +}); +await promptDebugger.debugContextState({session, model}); +``` + +**What this does:** +- Saves the entire prompt sent to the model +- Shows exactly what the LLM sees (including function definitions) +- Useful for debugging why model does/doesn't call functions +- Writes to `./logs/qwen_prompts_[timestamp].txt` + +### 10. Cleanup (Lines 58-61) +```javascript +session.dispose() +context.dispose() +model.dispose() +llama.dispose() +``` +Standard cleanup. + +## Key Concepts Demonstrated + +### 1. Function Calling (Tool Use) + +This is what makes it an "agent": +``` +Without tools: With tools: +LLM → Text only LLM → Can take actions + ↓ + Call functions + Access data + Execute code +``` + +### 2. Function Definition Pattern + +```javascript +defineChatSessionFunction({ + description: "What the function does", // LLM reads this + params: { // Expected parameters + type: "object", + properties: { + paramName: { + type: "string", + description: "What this param is for" + } + }, + required: ["paramName"] + }, + handler: async (params) => { // Your code + // Do something with params + return result; + } +}); +``` + +### 3. JSON Schema for Parameters + +Uses standard JSON Schema: +```javascript +// No parameters +properties: {} + +// One string parameter +properties: { + city: { + type: "string", + description: "City name" + } +} + +// Multiple parameters +properties: { + a: { type: "number" }, + b: { type: "number" } +}, +required: ["a", "b"] +``` + +### 4. Agent Decision Making + +``` +User: "What time is it?" + ↓ + LLM thinks: + "I need current time" + "I see function: getCurrentTime" + "Description matches what I need" + ↓ + LLM outputs special format: + {function_call: "getCurrentTime"} + ↓ + Library intercepts and runs handler() + ↓ + Handler returns: "1:46:36 PM" + ↓ + LLM receives: Tool result + ↓ + LLM applies system prompt: + Convert to 24-hour format + ↓ + Final answer: "13:46" +``` + +## Use Cases + +### 1. Information Retrieval +```javascript +const getWeather = defineChatSessionFunction({ + description: "Get weather for a city", + params: { + type: "object", + properties: { + city: { type: "string" } + } + }, + handler: async ({city}) => { + return await fetchWeather(city); + } +}); +``` + +### 2. Calculations +```javascript +const calculate = defineChatSessionFunction({ + description: "Perform arithmetic calculation", + params: { + type: "object", + properties: { + expression: { type: "string" } + } + }, + handler: async ({expression}) => { + return eval(expression); // (Be careful with eval!) + } +}); +``` + +### 3. Data Access +```javascript +const queryDatabase = defineChatSessionFunction({ + description: "Query user database", + params: { + type: "object", + properties: { + userId: { type: "string" } + } + }, + handler: async ({userId}) => { + return await db.users.findById(userId); + } +}); +``` + +### 4. External APIs +```javascript +const searchWeb = defineChatSessionFunction({ + description: "Search the web", + params: { + type: "object", + properties: { + query: { type: "string" } + } + }, + handler: async ({query}) => { + return await googleSearch(query); + } +}); +``` + +## Expected Output + +When run: +``` +AI: 13:46 +``` + +The LLM: +1. Called getCurrentTime() internally +2. Got "1:46:36 PM" +3. Converted to 24-hour format +4. Removed seconds +5. Returned "13:46" + +## Debugging with PromptDebugger + +The debug output shows the full prompt including function schemas: +``` +System: You are a professional chronologist... + +Functions available: +- getCurrentTime: Get the current time + Parameters: (none) + +User: What time is it right now? +``` + +This helps debug: +- Did the model see the function? +- Was the description clear? +- Did parameters match expectations? + +## Why This Matters for AI Agents + +### Agents = LLMs + Tools + +``` +LLM alone: LLM + Tools: +├─ Generate text ├─ Generate text +└─ That's it ├─ Access real data + ├─ Perform calculations + ├─ Call APIs + ├─ Execute actions + └─ Interact with world +``` + +### Foundation for Complex Agents + +This simple example is the foundation for: +- **Research agents**: Search web, read documents +- **Coding agents**: Run code, check errors +- **Personal assistants**: Calendar, email, reminders +- **Analysis agents**: Query databases, compute statistics + +All start with basic function calling! + +## Best Practices + +1. **Clear descriptions**: LLM uses these to decide when to call +2. **Type safety**: Use JSON Schema properly +3. **Error handling**: Handler should catch errors +4. **Return strings**: LLM processes text best +5. **Keep functions focused**: One clear purpose per function + +This is the minimum viable agent: one LLM + one tool + proper configuration. diff --git a/examples/07_simple-agent/CONCEPT.md b/examples/07_simple-agent/CONCEPT.md new file mode 100644 index 0000000000000000000000000000000000000000..23697c2c105be543fab29b079aac10dccbf8caf6 --- /dev/null +++ b/examples/07_simple-agent/CONCEPT.md @@ -0,0 +1,69 @@ +# Concept: Function Calling & Tool Use + +## Overview + +Function calling transforms LLMs from text generators into agents that can take actions and interact with the world. + +## What Makes an Agent? + +``` +Text Generator Agent +────────────── ────── +LLM → Text only LLM + Tools → Can act +``` + +**Function calling** lets the LLM invoke predefined functions to access data or perform actions it cannot do alone. + +## The Core Idea + +``` +User: "What time is it?" + ↓ +LLM thinks: "I need current time" + ↓ +LLM calls: getCurrentTime() + ↓ +Tool returns: "1:46:36 PM" + ↓ +LLM responds: "It's 13:46" +``` + +This is agency - the ability to DO, not just SAY. + +## How It Works + +### 1. Function Definition +```javascript +getCurrentTime = { + description: "Get the current time", + handler: () => new Date().toLocaleTimeString() +} +``` + +### 2. LLM Sees Available Tools +``` +Available functions: +- getCurrentTime: "Get the current time" +- getWeather: "Get weather for a city" +- calculate: "Perform math" +``` + +### 3. LLM Decides When to Use +``` +"What time?" → getCurrentTime() ✓ +"What's 5+5?" → calculate() ✓ +"Tell a joke" → No tool needed +``` + +## Real-World Applications + +**Personal Assistant**: Calendar, email, reminders +**Research Agent**: Web search, document reading +**Coding Assistant**: File operations, code execution +**Data Analyst**: Database queries, calculations + +## Key Takeaway + +Function calling is THE feature that enables AI agents. Without it, LLMs can only talk. With it, they can act. + +This is the foundation of all modern agent systems. diff --git a/examples/07_simple-agent/simple-agent.js b/examples/07_simple-agent/simple-agent.js new file mode 100644 index 0000000000000000000000000000000000000000..0f0ac8d494ef89977e3eae9aba809052169158d2 --- /dev/null +++ b/examples/07_simple-agent/simple-agent.js @@ -0,0 +1,62 @@ +import {defineChatSessionFunction, getLlama, LlamaChatSession} from "node-llama-cpp"; +import {fileURLToPath} from "url"; +import path from "path"; +import {PromptDebugger} from "../../helper/prompt-debugger.js"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const debug = false; + +const llama = await getLlama({debug}); +const model = await llama.loadModel({ + modelPath: path.join( + __dirname, + '..', + '..', + 'models', + 'Qwen3-1.7B-Q8_0.gguf' + ) +}); +const context = await model.createContext({contextSize: 2000}); + +const systemPrompt = `You are a professional chronologist who standardizes time representations across different systems. + +Always convert times from 12-hour format (e.g., "1:46:36 PM") to 24-hour format (e.g., "13:46") without seconds +before returning them.`; + +const session = new LlamaChatSession({ + contextSequence: context.getSequence(), + systemPrompt, +}); + +const getCurrentTime = defineChatSessionFunction({ + description: "Get the current time", + params: { + type: "object", + properties: {} + }, + async handler() { + return new Date().toLocaleTimeString(); + } +}); + +const functions = {getCurrentTime}; +const prompt = `What time is it right now?`; + +// Execute the prompt +const a1 = await session.prompt(prompt, {functions}); +console.log("AI: " + a1); + +// Debug after the prompt execution +const promptDebugger = new PromptDebugger({ + outputDir: './logs', + filename: 'qwen_prompts.txt', + includeTimestamp: true, // adds timestamp to filename + appendMode: false // overwrites file each time +}); +await promptDebugger.debugContextState({session, model}); + +// Clean up +session.dispose() +context.dispose() +model.dispose() +llama.dispose() \ No newline at end of file diff --git a/examples/08_simple-agent-with-memory/CODE.md b/examples/08_simple-agent-with-memory/CODE.md new file mode 100644 index 0000000000000000000000000000000000000000..3a4434dd63458b8fe7f8755255b39bc46055480b --- /dev/null +++ b/examples/08_simple-agent-with-memory/CODE.md @@ -0,0 +1,247 @@ +# Code Explanation: simple-agent-with-memory.js + +This example extends the simple agent with **persistent memory**, enabling it to remember information across sessions while intelligently avoiding duplicate saves. + +## Key Components + +### 1. MemoryManager Import +```javascript +import {MemoryManager} from "./memory-manager.js"; +``` +Custom class for persisting agent memories to JSON files with unified memory storage. + +### 2. Initialize Memory Manager +```javascript +const memoryManager = new MemoryManager('./agent-memory.json'); +const memorySummary = await memoryManager.getMemorySummary(); +``` +- Loads existing memories from file +- Generates formatted summary for system prompt +- Handles migration from old memory schemas + +### 3. Memory-Aware System Prompt with Reasoning +```javascript +const systemPrompt = ` +You are a helpful assistant with long-term memory. + +Before calling any function, always follow this reasoning process: + +1. **Compare** new user statements against existing memories below. +2. **If the same key and value already exist**, do NOT call saveMemory again. + - Instead, simply acknowledge the known information. + - Example: if the user says "My name is Malua" and memory already says "user_name: Malua", reply "Yes, I remember your name is Malua." +3. **If the user provides an updated value** (e.g., "I actually prefer sushi now"), + then call saveMemory once to update the value. +4. **Only call saveMemory for genuinely new information.** + +When saving new data, call saveMemory with structured fields: +- type: "fact" or "preference" +- key: short descriptive identifier (e.g., "user_name", "favorite_food") +- value: the specific information (e.g., "Malua", "chinua") + +Examples: +saveMemory({ type: "fact", key: "user_name", value: "Malua" }) +saveMemory({ type: "preference", key: "favorite_food", value: "chinua" }) + +${memorySummary} +`; +``` + +**What this does:** +- Includes existing memories in the prompt +- Provides explicit reasoning guidelines to prevent duplicate saves +- Teaches the agent to compare before saving +- Instructs when to update vs. acknowledge existing data + +### 4. saveMemory Function +```javascript +const saveMemory = defineChatSessionFunction({ + description: "Save important information to long-term memory (user preferences, facts, personal details)", + params: { + type: "object", + properties: { + type: { + type: "string", + enum: ["fact", "preference"] + }, + key: { type: "string" }, + value: { type: "string" } + }, + required: ["type", "key", "value"] + }, + async handler({ type, key, value }) { + await memoryManager.addMemory({ type, key, value }); + return `Memory saved: ${key} = ${value}`; + } +}); +``` + +**What it does:** +- Uses structured key-value format for all memories +- Saves both facts and preferences with the same method +- Automatically handles duplicates (updates if value changes) +- Persists to JSON file +- Returns confirmation message + +**Parameter Structure:** +- `type`: Either "fact" or "preference" +- `key`: Short identifier (e.g., "user_name", "favorite_food") +- `value`: The actual information (e.g., "Alex", "pizza") + +### 5. Example Conversation +```javascript +const prompt1 = "Hi! My name is Alex and I love pizza."; +const response1 = await session.prompt(prompt1, {functions}); +// Agent calls saveMemory twice: +// - saveMemory({ type: "fact", key: "user_name", value: "Alex" }) +// - saveMemory({ type: "preference", key: "favorite_food", value: "pizza" }) + +const prompt2 = "What's my favorite food?"; +const response2 = await session.prompt(prompt2, {functions}); +// Agent recalls from memory: "Pizza" +``` + +## How Memory Works + +### Flow Diagram +``` +Session 1: +User: "My name is Alex and I love pizza" + ↓ +Agent calls: saveMemory({ type: "fact", key: "user_name", value: "Alex" }) +Agent calls: saveMemory({ type: "preference", key: "favorite_food", value: "pizza" }) + ↓ +Saved to: agent-memory.json + +Session 2 (after restart): +1. Load memories from agent-memory.json +2. Add to system prompt +3. Agent sees: "user_name: Alex" and "favorite_food: pizza" +4. Can use this information in responses + +Session 3: +User: "My name is Alex" + ↓ +Agent compares: user_name already = "Alex" + ↓ +No function call! Just acknowledges: "Yes, I remember your name is Alex." +``` + +## The MemoryManager Class + +Located in `memory-manager.js`: +```javascript +class MemoryManager { + async loadMemories() // Load from JSON (handles schema migration) + async saveMemories() // Write to JSON + async addMemory() // Unified method for all memory types + async getMemorySummary() // Format memories for system prompt + extractKey() // Helper for migration + extractValue() // Helper for migration +} +``` + +**Benefits:** +- Single unified method for all memory types +- Automatic duplicate detection and prevention +- Automatic value updates when information changes + +## Key Concepts + +### 1. Structured Memory Format +All memories now use a consistent structure: +```javascript +{ + type: "fact" | "preference", + key: "user_name", // Identifier + value: "Alex", // The actual data + source: "user", // Where it came from + timestamp: "2025-10-29..." // When it was saved/updated +} +``` + +### 2. Intelligent Duplicate Prevention +The agent is trained to: +- **Compare** before saving +- **Skip** if data is identical +- **Update** if value changed +- **Acknowledge** existing memories instead of re-saving + +### 3. Persistent State +- Memories survive script restarts +- Stored in JSON file with metadata +- Loaded at startup and injected into prompt + +### 4. Memory Integration in System Prompt +Memories are automatically formatted and injected: +``` +=== LONG-TERM MEMORY === + +Known Facts: +- user_name: Alex +- location: Paris + +User Preferences: +- favorite_food: pizza +- preferred_language: French +``` + +## Why This Matters + +**Without memory:** Agent starts fresh every time, asks same questions repeatedly + +**With basic memory:** Agent remembers, but may save duplicates wastefully + +**With smart memory:** Agent remembers AND avoids redundant saves by reasoning first + +This enables: +- **Personalized responses** based on user history +- **Efficient memory usage** (no duplicate entries) +- **Natural conversations** that feel continuous +- **Stateful agents** that maintain context +- **Automatic updates** when information changes + +## Expected Output + +**First run:** +``` +User: "Hi! My name is Alex and I love pizza." +AI: "Nice to meet you, Alex! I've noted that you love pizza." +[Calls saveMemory twice - new information saved] +``` + +**Second run (after restart):** +``` +User: "What's my favorite food?" +AI: "Your favorite food is pizza! You mentioned that you love it." +[No function calls - recalls from loaded memory] +``` + +**Third run (duplicate statement):** +``` +User: "My name is Alex." +AI: "Yes, I remember your name is Alex!" +[No function call - recognizes duplicate, just acknowledges] +``` + +**Fourth run (updated information):** +``` +User: "I actually prefer sushi now." +AI: "Got it! I've updated your favorite food to sushi." +[Calls saveMemory once - updates existing value] +``` + +## Reasoning Process + +The system prompt explicitly guides the agent through this decision tree: +``` +New user statement + ↓ +Compare to existing memories + ↓ + ├─→ Exact match? → Acknowledge only (no save) + ├─→ Updated value? → Save to update + └─→ New information? → Save as new +``` + +This reasoning-first approach makes the agent more intelligent and efficient with memory operations! \ No newline at end of file diff --git a/examples/08_simple-agent-with-memory/CONCEPT.md b/examples/08_simple-agent-with-memory/CONCEPT.md new file mode 100644 index 0000000000000000000000000000000000000000..4b7f9f060d86c41980d1edabb63bf537e780b50f --- /dev/null +++ b/examples/08_simple-agent-with-memory/CONCEPT.md @@ -0,0 +1,249 @@ +# Concept: Persistent Memory & State Management + +## Overview + +Adding persistent memory transforms agents from stateless responders into systems that can maintain context and relationships across sessions. + +## The Memory Problem + +``` +Without Memory With Memory +────────────── ───────────── +Session 1: Session 1: +"I'm Alex" "I'm Alex" → Saved +"I love pizza" "I love pizza" → Saved + +Session 2: Session 2: +"What's my name?" "What's my name?" +"I don't know" "Alex!" ✓ +``` + +## Architecture + +``` +┌─────────────────────────────────┐ +│ Agent Session │ +├─────────────────────────────────┤ +│ System Prompt │ +│ + Loaded Memories │ +│ + saveMemory Tool │ +└────────┬────────────────────────┘ + │ + ↓ +┌─────────────────────────────────┐ +│ Memory Manager │ +├─────────────────────────────────┤ +│ • Load from storage │ +│ • Save to storage │ +│ • Format for prompt │ +└────────┬────────────────────────┘ + │ + ↓ +┌─────────────────────────────────┐ +│ Persistent Storage │ +│ (agent-memory.json) │ +└─────────────────────────────────┘ +``` + +## How It Works + +### 1. Startup +``` +1. Load agent-memory.json +2. Extract facts and preferences +3. Add to system prompt +4. Agent "remembers" past information +``` + +### 2. During Conversation +``` +User shares information + ↓ +Agent recognizes important fact + ↓ +Agent calls saveMemory() + ↓ +Saved to JSON file + ↓ +Available in future sessions +``` + +### 3. Memory Types + +**Facts**: General information +```json +{ + "memories": [ + { + "type": "fact", + "key": "user_name", + "value": "Alex", + "source": "user", + "timestamp": "2025-10-29T11:22:57.372Z" + } + ] +} +``` + +**Preferences**: +```json +{ + "memories": [ + { + "type": "preference", + "key": "favorite_food", + "value": "pizza", + "source": "user", + "timestamp": "2025-10-29T11:22:58.022Z" + } + ] +} +``` + +## Memory Integration Pattern + +### System Prompt Enhancement +``` +Base Prompt: +"You are a helpful assistant." + +Enhanced with Memory: +"You are a helpful assistant with long-term memory. + +=== LONG-TERM MEMORY === +Known Facts: +- User's name is Alex +- User loves pizza" +``` + +### Tool-Assisted Saving +``` +Agent decides when to save: +User: "My favorite color is blue" + ↓ +Agent: "I should remember this" + ↓ +Calls: saveMemory(type="preference", key="color", content="blue") +``` + +## Real-World Applications + +**Personal Assistant** +- Remember appointments, preferences, contacts +- Personalized responses based on history + +**Customer Service** +- Past interactions and issues +- Customer preferences and context + +**Learning Tutor** +- Student progress and weak areas +- Adapted teaching based on history + +**Healthcare Assistant** +- Medical history +- Medication reminders +- Health tracking + +## Memory Strategies + +### 1. Episodic Memory +Store specific events and conversations: +``` +- "On 2025-01-15, user asked about Python" +- "User struggled with async concepts" +``` + +### 2. Semantic Memory +Store facts and knowledge: +``` +- "User is a software engineer" +- "User prefers TypeScript over JavaScript" +``` + +### 3. Procedural Memory +Store how-to information: +``` +- "User's workflow: design → code → test" +- "User's preferred tools: VS Code, Git" +``` + +## Challenges & Solutions + +### Challenge 1: Memory Bloat +**Problem**: Too many memories slow down agent +**Solution**: +- Importance scoring +- Periodic cleanup +- Summary compression + +### Challenge 2: Conflicting Information +**Problem**: "User likes pizza" vs "User is vegan" +**Solution**: +- Timestamps for recency +- Explicit updates +- Conflict resolution logic + +### Challenge 3: Privacy +**Problem**: Sensitive information in memory +**Solution**: +- Encryption at rest +- Access controls +- Expiration policies + +## Key Concepts + +### 1. Persistence +Memory survives: +- Application restarts +- System reboots +- Time gaps + +### 2. Context Augmentation +Memories enhance system prompt: +``` +Prompt = Base + Memories + User Input +``` + +### 3. Agent-Driven Storage +Agent decides what to remember: +``` +Important? → Save +Trivial? → Ignore +``` + +## Evolution Path + +``` +1. Stateless → Each interaction independent +2. Session memory → Remember during conversation +3. Persistent memory → Remember across sessions +4. Distributed memory → Share across instances +5. Semantic search → Find relevant memories +``` + +## Best Practices + +1. **Structure memory**: Use types (facts, preferences, events) +2. **Add timestamps**: Know when information was saved +3. **Enable updates**: Allow overwriting old information +4. **Implement search**: Find relevant memories efficiently +5. **Monitor size**: Prevent unbounded growth + +## Comparison + +``` +Feature Simple Agent Memory Agent +─────────────────── ───────────── ────────────── +Remembers names ✗ ✓ +Recalls preferences ✗ ✓ +Personalization ✗ ✓ +Context continuity ✗ ✓ +Cross-session state ✗ ✓ +``` + +## Key Takeaway + +Memory transforms agents from tools into assistants. They can build relationships, provide personalized experiences, and maintain context over time. + +This is essential for production AI agent systems. diff --git a/examples/08_simple-agent-with-memory/agent-memory.json b/examples/08_simple-agent-with-memory/agent-memory.json new file mode 100644 index 0000000000000000000000000000000000000000..0823afb73a0648d7d7613f89d202df7be544cc6b --- /dev/null +++ b/examples/08_simple-agent-with-memory/agent-memory.json @@ -0,0 +1,19 @@ +{ + "memories": [ + { + "type": "fact", + "key": "user_name", + "value": "Alex", + "source": "user", + "timestamp": "2025-11-05T20:24:58.220Z" + }, + { + "type": "preference", + "key": "favorite_food", + "value": "pizza", + "source": "user", + "timestamp": "2025-11-05T20:24:58.848Z" + } + ], + "conversationHistory": [] +} \ No newline at end of file diff --git a/examples/08_simple-agent-with-memory/memory-manager.js b/examples/08_simple-agent-with-memory/memory-manager.js new file mode 100644 index 0000000000000000000000000000000000000000..60575959cce8b058e9516a36aad08cc54b864875 --- /dev/null +++ b/examples/08_simple-agent-with-memory/memory-manager.js @@ -0,0 +1,137 @@ +import fs from 'fs/promises'; +import path from 'path'; +import {fileURLToPath} from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +export class MemoryManager { + constructor(memoryFileName = './memory.json') { + this.memoryFilePath = path.resolve(__dirname, memoryFileName); + } + + async loadMemories() { + try { + const data = await fs.readFile(this.memoryFilePath, 'utf-8'); + const json = JSON.parse(data); + + // 🔧 Migrate old schema if needed + if (!json.memories) { + const upgraded = {memories: [], conversationHistory: []}; + + if (Array.isArray(json.facts)) { + for (const f of json.facts) { + upgraded.memories.push({ + type: 'fact', + key: this.extractKey(f.content), + value: this.extractValue(f.content), + source: 'migration', + timestamp: f.timestamp || new Date().toISOString() + }); + } + } + + if (json.preferences && typeof json.preferences === 'object') { + for (const [key, val] of Object.entries(json.preferences)) { + upgraded.memories.push({ + type: 'preference', + key, + value: this.extractValue(val), + source: 'migration', + timestamp: new Date().toISOString() + }); + } + } + + await this.saveMemories(upgraded); + return upgraded; + } + + if (!Array.isArray(json.memories)) json.memories = []; + if (!Array.isArray(json.conversationHistory)) json.conversationHistory = []; + + return json; + } catch { + return {memories: [], conversationHistory: []}; + } + } + + async saveMemories(memories) { + await fs.writeFile(this.memoryFilePath, JSON.stringify(memories, null, 2)); + } + + // Add or update memory without duplicates + async addMemory({type, key, value, source = 'user'}) { + const data = await this.loadMemories(); + + // Normalize for comparison + const normType = type.trim().toLowerCase(); + const normKey = key.trim().toLowerCase(); + const normValue = value.trim(); + + // Check if same key+type already exists + const existingIndex = data.memories.findIndex( + m => m.type === normType && m.key.toLowerCase() === normKey + ); + + if (existingIndex >= 0) { + const existing = data.memories[existingIndex]; + // Update value if changed + if (existing.value !== normValue) { + existing.value = normValue; + existing.timestamp = new Date().toISOString(); + existing.source = source; + console.log(`Updated memory: ${normKey} → ${normValue}`); + } else { + console.log(`Skipped duplicate memory: ${normKey}`); + } + } else { + // Add new memory + data.memories.push({ + type: normType, + key: normKey, + value: normValue, + source, + timestamp: new Date().toISOString() + }); + console.log(`Added memory: ${normKey} = ${normValue}`); + } + + await this.saveMemories(data); + } + + async getMemorySummary() { + const data = await this.loadMemories(); + const facts = Array.isArray(data.memories) + ? data.memories.filter(m => m.type === 'fact') + : []; + const prefs = Array.isArray(data.memories) + ? data.memories.filter(m => m.type === 'preference') + : []; + + let summary = "\n=== LONG-TERM MEMORY ===\n"; + + if (facts.length > 0) { + summary += "\nKnown Facts:\n"; + for (const f of facts) summary += `- ${f.key}: ${f.value}\n`; + } + + if (prefs.length > 0) { + summary += "\nUser Preferences:\n"; + for (const p of prefs) summary += `- ${p.key}: ${p.value}\n`; + } + + return summary; + } + + extractKey(content) { + if (typeof content !== 'string') return 'unknown'; + const [key] = content.split(':').map(s => s.trim()); + return key || 'unknown'; + } + + extractValue(content) { + if (typeof content !== 'string') return ''; + const parts = content.split(':').map(s => s.trim()); + return parts.length > 1 ? parts.slice(1).join(':') : content; + } +} \ No newline at end of file diff --git a/examples/08_simple-agent-with-memory/simple-agent-with-memory.js b/examples/08_simple-agent-with-memory/simple-agent-with-memory.js new file mode 100644 index 0000000000000000000000000000000000000000..accb44cf87b01d6e1bb3ecf387a6ef0f5aa9b507 --- /dev/null +++ b/examples/08_simple-agent-with-memory/simple-agent-with-memory.js @@ -0,0 +1,93 @@ +import {defineChatSessionFunction, getLlama, LlamaChatSession} from "node-llama-cpp"; +import {fileURLToPath} from "url"; +import path from "path"; +import {MemoryManager} from "./memory-manager.js"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const llama = await getLlama({debug: false}); +const model = await llama.loadModel({ + modelPath: path.join( + __dirname, + '..', + '..', + 'models', + 'Qwen3-1.7B-Q8_0.gguf' + ) +}); +const context = await model.createContext({contextSize: 2000}); + +// Initialize memory manager +const memoryManager = new MemoryManager('./agent-memory.json'); + +// Load existing memories and add to system prompt +const memorySummary = await memoryManager.getMemorySummary(); + +const systemPrompt = ` +You are a helpful assistant with long-term memory. + +Before calling any function, always follow this reasoning process: + +1. **Compare** new user statements against existing memories below. +2. **If the same key and value already exist**, do NOT call saveMemory again. + - Instead, simply acknowledge the known information. + - Example: if the user says "My name is Malua" and memory already says "user_name: Malua", reply "Yes, I remember your name is Malua." +3. **If the user provides an updated value** (e.g., "I actually prefer sushi now"), + then call saveMemory once to update the value. +4. **Only call saveMemory for genuinely new information.** + +When saving new data, call saveMemory with structured fields: +- type: "fact" or "preference" +- key: short descriptive identifier (e.g., "user_name", "favorite_food") +- value: the specific information (e.g., "Malua", "chinua") + +Examples: +saveMemory({ type: "fact", key: "user_name", value: "Malua" }) +saveMemory({ type: "preference", key: "favorite_food", value: "chinua" }) + +${memorySummary} +`; + +const session = new LlamaChatSession({ + contextSequence: context.getSequence(), + systemPrompt, +}); + +// Function to save memories +const saveMemory = defineChatSessionFunction({ + description: "Save important information to long-term memory (user preferences, facts, personal details)", + params: { + type: "object", + properties: { + type: { + type: "string", + enum: ["fact", "preference"] + }, + key: {type: "string"}, + value: {type: "string"} + }, + required: ["type", "key", "value"] + }, + async handler({type, key, value}) { + await memoryManager.addMemory({type, key, value}); + return `Memory saved: ${key} = ${value}`; + } +}); + +const functions = {saveMemory}; + +// Example conversation +const prompt1 = "Hi! My name is Alex and I love pizza."; +const response1 = await session.prompt(prompt1, {functions}); +console.log("AI: " + response1); + +// Later conversation (even after restarting the script) +const prompt2 = "What's my favorite food?"; +const response2 = await session.prompt(prompt2, {functions}); +console.log("AI: " + response2); + +// Clean up +session.dispose() +context.dispose() +model.dispose() +llama.dispose() \ No newline at end of file diff --git a/examples/09_react-agent/CODE.md b/examples/09_react-agent/CODE.md new file mode 100644 index 0000000000000000000000000000000000000000..27071688b5b34593c8d4be538761a6c4f83da1e6 --- /dev/null +++ b/examples/09_react-agent/CODE.md @@ -0,0 +1,278 @@ +# Code Explanation: react-agent.js + +This example implements the **ReAct pattern** (Reasoning + Acting), a powerful approach for multi-step problem-solving with tools. + +## What is ReAct? + +ReAct = **Rea**soning + **Act**ing + +The agent alternates between: +1. **Thinking** (reasoning about what to do) +2. **Acting** (using tools) +3. **Observing** (seeing tool results) +4. Repeat until problem is solved + +## Key Components + +### 1. ReAct System Prompt (Lines 20-52) +```javascript +const systemPrompt = `You are a mathematical assistant that uses the ReAct approach. + +CRITICAL: You must follow this EXACT pattern: + +Thought: [Explain what calculation you need] +Action: [Call ONE tool] +Observation: [Wait for result] +Thought: [Analyze result] +Action: [Call another tool if needed] +... +Thought: [Once you have all information] +Answer: [Final answer and STOP] +``` + +**Key instructions:** +- Explicit step-by-step pattern +- One tool call at a time +- Continue until final answer +- Stop after "Answer:" + +### 2. Calculator Tools (Lines 60-159) + +Four basic math operations: +```javascript +const add = defineChatSessionFunction({...}); +const multiply = defineChatSessionFunction({...}); +const subtract = defineChatSessionFunction({...}); +const divide = defineChatSessionFunction({...}); +``` + +Each tool: +- Takes two numbers (a, b) +- Performs operation +- Logs the call +- Returns result as string + +### 3. ReAct Agent Loop (Lines 164-212) + +```javascript +async function reactAgent(userPrompt, maxIterations = 10) { + let iteration = 0; + let fullResponse = ""; + + while (iteration < maxIterations) { + iteration++; + + // Prompt the LLM + const response = await session.prompt( + iteration === 1 ? userPrompt : "Continue your reasoning.", + { + functions, + maxTokens: 300, + onTextChunk: (chunk) => { + process.stdout.write(chunk); // Stream output + currentChunk += chunk; + } + } + ); + + fullResponse += currentChunk; + + // Check if final answer reached + if (response.toLowerCase().includes("answer:")) { + return fullResponse; + } + } +} +``` + +**How it works:** +1. Loop up to maxIterations times +2. On first iteration: send user's question +3. On subsequent iterations: ask to continue +4. Stream output in real-time +5. Stop when "Answer:" appears +6. Return full reasoning trace + +### 4. Example Query (Lines 215-220) + +```javascript +const queries = [ + "A store sells 15 items Monday at $8 each, 20 items Tuesday at $8 each, + 10 items Wednesday at $8 each. What's the average items per day and total revenue?" +]; +``` + +Complex problem requiring multiple calculations: +- 15 × 8 +- 20 × 8 +- 10 × 8 +- Sum results +- Calculate average +- Format answer + +## The ReAct Flow + +### Example Execution + +``` +USER: "A store sells 15 items at $8 each and 20 items at $8 each. Total revenue?" + +Iteration 1: +Thought: First I need to calculate 15 × 8 +Action: multiply(15, 8) +Observation: 120 + +Iteration 2: +Thought: Now I need to calculate 20 × 8 +Action: multiply(20, 8) +Observation: 160 + +Iteration 3: +Thought: Now I need to add both results +Action: add(120, 160) +Observation: 280 + +Iteration 4: +Thought: I have the total revenue +Answer: The total revenue is $280 +``` + +**Loop stops** because "Answer:" was detected. + +## Why ReAct Works + +### Traditional Approach (Fails) +``` +User: "Complex math problem" +LLM: [Tries to calculate in head] +→ Often wrong due to arithmetic errors +``` + +### ReAct Approach (Succeeds) +``` +User: "Complex math problem" +LLM: "I need to calculate X" + → Calls calculator tool + → Gets accurate result + → Uses result for next step + → Continues until solved +``` + +## Key Concepts + +### 1. Explicit Reasoning +The agent must "show its work": +``` +Thought: What do I need to do? +Action: Do it +Observation: What happened? +``` + +### 2. Tool Use at Each Step +``` +Don't calculate: 15 × 8 = 120 (may be wrong) +Do calculate: multiply(15, 8) → 120 (always correct) +``` + +### 3. Iterative Problem Solving +``` +Complex Problem → Break into steps → Solve each step → Combine results +``` + +### 4. Self-Correction +Agent can observe bad results and try again: +``` +Thought: That doesn't look right +Action: Let me recalculate +``` + +## Debug Output + +The code includes PromptDebugger (lines 228-234): +```javascript +const promptDebugger = new PromptDebugger({ + outputDir: './logs', + filename: 'react_calculator.txt', + includeTimestamp: true +}); +await promptDebugger.debugContextState({session, model}); +``` + +Saves complete prompt history to logs for debugging. + +## Expected Output + +``` +======================================================== +USER QUESTION: [Problem statement] +======================================================== + +--- Iteration 1 --- +Thought: First I need to multiply 15 by 8 +Action: multiply(15, 8) + + 🔧 TOOL CALLED: multiply(15, 8) + 📊 RESULT: 120 + +Observation: 120 + +--- Iteration 2 --- +Thought: Now I need to multiply 20 by 8 +Action: multiply(20, 8) + + 🔧 TOOL CALLED: multiply(20, 8) + 📊 RESULT: 160 + +... continues ... + +--- Iteration N --- +Thought: I have all the information +Answer: [Final answer] + +======================================================== +FINAL ANSWER REACHED +======================================================== +``` + +## Why This Matters + +### Enables Complex Tasks +- Multi-step reasoning +- Accurate calculations +- Self-correction +- Transparent process + +### Foundation of Modern Agents +This pattern powers: +- LangChain agents +- AutoGPT +- BabyAGI +- Most production agent frameworks + +### Observable Reasoning +Unlike "black box" LLMs, you see: +- What the agent is thinking +- Which tools it uses +- Why it makes decisions +- Where it might fail + +## Best Practices + +1. **Clear system prompt**: Define exact pattern +2. **One tool per action**: Don't combine operations +3. **Limit iterations**: Prevent infinite loops +4. **Stream output**: Show progress +5. **Debug thoroughly**: Use PromptDebugger + +## Comparison + +``` +Simple Agent vs ReAct Agent +──────────────────────────── +Single prompt/response Multi-step iteration +One tool call (maybe) Multiple tool calls +No visible reasoning Explicit reasoning +Works for simple tasks Handles complex problems +``` + +This is the state-of-the-art pattern for building capable AI agents! diff --git a/examples/09_react-agent/CONCEPT.md b/examples/09_react-agent/CONCEPT.md new file mode 100644 index 0000000000000000000000000000000000000000..46c28bd8df99826d7e2aefab81e5854ca72db702 --- /dev/null +++ b/examples/09_react-agent/CONCEPT.md @@ -0,0 +1,372 @@ +# Concept: ReAct Pattern for AI Agents + +## What is ReAct? + +**ReAct** (Reasoning + Acting) is a framework that combines: +- **Reasoning**: Thinking through problems step-by-step +- **Acting**: Using tools to accomplish subtasks +- **Observing**: Learning from tool results + +This creates agents that can solve complex, multi-step problems reliably. + +## The Core Pattern + +``` +┌─────────────┐ +│ Problem │ +└──────┬──────┘ + │ + ▼ +┌─────────────────────────────────────┐ +│ ReAct Loop │ +│ │ +│ ┌──────────────────────────────┐ │ +│ │ 1. THOUGHT │ │ +│ │ "What do I need to do?" │ │ +│ └─────────────┬────────────────┘ │ +│ ▼ │ +│ ┌──────────────────────────────┐ │ +│ │ 2. ACTION │ │ +│ │ Call tool with parameters │ │ +│ └─────────────┬────────────────┘ │ +│ ▼ │ +│ ┌──────────────────────────────┐ │ +│ │ 3. OBSERVATION │ │ +│ │ Receive tool result │ │ +│ └─────────────┬────────────────┘ │ +│ │ │ +│ └──► Repeat or │ +│ Final Answer │ +└─────────────────────────────────────┘ +``` + +## Why ReAct Matters + +### Traditional LLMs Struggle With: +1. **Complex calculations** - arithmetic errors +2. **Multi-step problems** - lose track of progress +3. **Using tools** - don't know when/how +4. **Explaining decisions** - black box reasoning + +### ReAct Solves This: +1. **Reliable calculations** - delegates to tools +2. **Structured progress** - explicit steps +3. **Tool orchestration** - knows when to use what +4. **Transparent reasoning** - visible thought process + +## The Three Components + +### 1. Thought (Reasoning) + +The agent reasons about: +- What information is needed +- Which tool to use +- Whether the result makes sense +- What to do next + +Example: +``` +Thought: I need to calculate 15 × 8 to find revenue +``` + +### 2. Action (Tool Use) + +The agent calls a tool with specific parameters: + +Example: +``` +Action: multiply(15, 8) +``` + +### 3. Observation (Learning) + +The agent receives and interprets the tool result: + +Example: +``` +Observation: 120 +``` + +## Complete Example + +``` +Problem: "If 15 items cost $8 each and 20 items cost $8 each, + what's the total revenue?" + +Thought: First I need to calculate revenue from 15 items +Action: multiply(15, 8) +Observation: 120 + +Thought: Now I need revenue from 20 items +Action: multiply(20, 8) +Observation: 160 + +Thought: Now I add both revenues +Action: add(120, 160) +Observation: 280 + +Thought: I have the final answer +Answer: The total revenue is $280 +``` + +## Key Benefits + +### 1. Reliability +- Tools provide accurate results +- No arithmetic mistakes +- Verifiable calculations + +### 2. Transparency +- See each reasoning step +- Understand decision-making +- Debug easily + +### 3. Scalability +- Handle complex problems +- Break into manageable steps +- Add more tools as needed + +### 4. Flexibility +- Works with any tools +- Adapts to problem complexity +- Self-corrects when needed + +## Comparison with Other Approaches + +### Zero-Shot Prompting +``` +User: "Calculate 15×8 + 20×8" +LLM: "The answer is 279" ❌ Wrong! +``` +**Problem**: LLM calculates in head, makes errors + +### Chain-of-Thought +``` +User: "Calculate 15×8 + 20×8" +LLM: "Let me think step by step: + 15×8 = 120 + 20×8 = 160 + 120+160 = 279" ❌ Still wrong! +``` +**Problem**: Shows work but still miscalculates + +### ReAct (This Implementation) +``` +User: "Calculate 15×8 + 20×8" +Agent: + Thought: Calculate 15×8 + Action: multiply(15, 8) + Observation: 120 + + Thought: Calculate 20×8 + Action: multiply(20, 8) + Observation: 160 + + Thought: Add results + Action: add(120, 160) + Observation: 280 + + Answer: 280 ✅ Correct! +``` +**Success**: Uses tools, gets accurate results + +## Architecture Diagram + +``` +┌──────────────────────────────────────┐ +│ User Question │ +└──────────────┬───────────────────────┘ + │ + ▼ +┌──────────────────────────────────────┐ +│ LLM with ReAct Prompt │ +│ │ +│ "Think, Act, Observe pattern" │ +└──────┬───────────────────────────────┘ + │ + ├──► Generates: "Thought: ..." + │ + ├──► Generates: "Action: tool(params)" + │ │ + │ ▼ + │ ┌─────────────────┐ + │ │ Tool Executor │ + │ │ │ + │ │ - multiply() │ + │ │ - add() │ + │ │ - divide() │ + │ │ - subtract() │ + │ └─────────┬───────┘ + │ │ + │ ▼ + └───────── "Observation: result" + │ + ├──► Next iteration or Final Answer + │ + ▼ +┌──────────────────────────────────────┐ +│ Final Answer │ +└──────────────────────────────────────┘ +``` + +## Implementation Strategies + +### 1. Explicit Pattern Enforcement + +Force the LLM to follow structure: +```javascript +systemPrompt: `CRITICAL: Follow this EXACT pattern: +Thought: [reasoning] +Action: [tool call] +Observation: [result] +... +Answer: [final answer]` +``` + +### 2. Iteration Control + +Prevent infinite loops: +```javascript +maxIterations = 10 // Safety limit +``` + +### 3. Streaming Output + +Show progress in real-time: +```javascript +onTextChunk: (chunk) => { + process.stdout.write(chunk); +} +``` + +### 4. Answer Detection + +Know when to stop: +```javascript +if (response.includes("Answer:")) { + return fullResponse; // Done! +} +``` + +## Real-World Applications + +### 1. Math & Science +- Complex calculations +- Multi-step derivations +- Unit conversions + +### 2. Data Analysis +- Query databases +- Process results +- Generate reports + +### 3. Research Assistants +- Search multiple sources +- Synthesize information +- Cite sources + +### 4. Coding Agents +- Read code +- Run tests +- Fix bugs +- Refactor + +### 5. Customer Support +- Query knowledge base +- Check order status +- Process refunds +- Escalate issues + +## Limitations & Considerations + +### 1. Iteration Cost +Each thought/action/observation cycle costs tokens and time. + +**Solution**: Use efficient models, limit iterations + +### 2. Tool Quality +ReAct is only as good as its tools. + +**Solution**: Build robust, well-tested tools + +### 3. Prompt Engineering +System prompt must be very clear. + +**Solution**: Test extensively, iterate on prompt + +### 4. Error Handling +Tools can fail or return unexpected results. + +**Solution**: Add error handling, validation + +## Advanced Patterns + +### Self-Correction +``` +Thought: That result seems wrong +Action: verify(previous_result) +Observation: Error detected +Thought: Let me recalculate +Action: multiply(15, 8) # Try again +``` + +### Meta-Reasoning +``` +Thought: I've used 5 iterations, I should finish soon +Action: summarize_progress() +Observation: Still need to add final numbers +Thought: One more step should do it +``` + +### Dynamic Tool Selection +``` +Thought: This is a division problem +Action: divide(10, 2) # Chooses right tool + +Thought: Now I need to add +Action: add(5, 3) # Switches tools +``` + +## Research Origins + +ReAct was introduced in: +> **"ReAct: Synergizing Reasoning and Acting in Language Models"** +> Yao et al., 2022 +> Paper: https://arxiv.org/abs/2210.03629 + +Key insight: Combining reasoning traces with task-specific actions creates more powerful agents than either alone. + +## Modern Frameworks Using ReAct + +1. **LangChain** - AgentExecutor with ReAct +2. **AutoGPT** - Autonomous task execution +3. **BabyAGI** - Task management system +4. **GPT Engineer** - Code generation +5. **ChatGPT Plugins** - Tool-using chatbots + +## Why Learn This Pattern? + +### 1. Foundation of Modern Agents +Nearly all production agent systems use ReAct or similar patterns. + +### 2. Understandable AI +Unlike black-box models, you see exactly what's happening. + +### 3. Extendable +Easy to add new tools and capabilities. + +### 4. Debuggable +When things go wrong, you can see where and why. + +### 5. Production-Ready +This pattern scales from demos to real applications. + +## Summary + +ReAct transforms LLMs from: +- **Brittle calculators** → Reliable problem solvers +- **Black boxes** → Transparent reasoners +- **Single-shot answerers** → Iterative thinkers +- **Isolated models** → Tool-using agents + +It's the bridge between language models and autonomous agents that can actually accomplish complex tasks reliably. diff --git a/examples/09_react-agent/react-agent.js b/examples/09_react-agent/react-agent.js new file mode 100644 index 0000000000000000000000000000000000000000..7f712991064772159ee72776fe0edf5038355319 --- /dev/null +++ b/examples/09_react-agent/react-agent.js @@ -0,0 +1,241 @@ +import {defineChatSessionFunction, getLlama, LlamaChatSession} from "node-llama-cpp"; +import {fileURLToPath} from "url"; +import path from "path"; +import {PromptDebugger} from "../../helper/prompt-debugger.js"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const debug = false; + +const llama = await getLlama({debug}); +const model = await llama.loadModel({ + modelPath: path.join( + __dirname, + '..', + '..', + 'models', + 'hf_giladgd_gpt-oss-20b.MXFP4.gguf' + ) +}); +const context = await model.createContext({contextSize: 2000}); + +// ReAct-style system prompt for mathematical reasoning +const systemPrompt = `You are a mathematical assistant that uses the ReAct (Reasoning + Acting) approach. + +CRITICAL: You must follow this EXACT pattern for every problem: + +Thought: [Explain what calculation you need to do next and why] +Action: [Call ONE tool with specific numbers] +Observation: [Wait for the tool result] +Thought: [Analyze the result and decide next step] +Action: [Call another tool if needed] +Observation: [Wait for the tool result] +... (repeat as many times as needed) +Thought: [Once you have ALL the information needed to answer the question] +Answer: [Give the final answer and STOP] + +RULES: +1. Only write "Answer:" when you have the complete final answer to the user's question +2. After writing "Answer:", DO NOT continue calculating or thinking +3. Break complex problems into the smallest possible steps +4. Use tools for ALL calculations - never calculate in your head +5. Each Action should call exactly ONE tool + +EXAMPLE: +User: "What is 5 + 3, then multiply that by 2?" + +Thought: First I need to add 5 and 3 +Action: add(5, 3) +Observation: 8 +Thought: Now I need to multiply that result by 2 +Action: multiply(8, 2) +Observation: 16 +Thought: I now have the final result +Answer: 16`; + +const session = new LlamaChatSession({ + contextSequence: context.getSequence(), + systemPrompt, +}); + +// Simple calculator tools that force step-by-step reasoning +const add = defineChatSessionFunction({ + description: "Add two numbers together", + params: { + type: "object", + properties: { + a: { + type: "number", + description: "First number" + }, + b: { + type: "number", + description: "Second number" + } + }, + required: ["a", "b"] + }, + async handler(params) { + const result = params.a + params.b; + console.log(`\n 🔧 TOOL CALLED: add(${params.a}, ${params.b})`); + console.log(` 📊 RESULT: ${result}\n`); + return result.toString(); + } +}); + +const multiply = defineChatSessionFunction({ + description: "Multiply two numbers together", + params: { + type: "object", + properties: { + a: { + type: "number", + description: "First number" + }, + b: { + type: "number", + description: "Second number" + } + }, + required: ["a", "b"] + }, + async handler(params) { + const result = params.a * params.b; + console.log(`\n 🔧 TOOL CALLED: multiply(${params.a}, ${params.b})`); + console.log(` 📊 RESULT: ${result}\n`); + return result.toString(); + } +}); + +const subtract = defineChatSessionFunction({ + description: "Subtract second number from first number", + params: { + type: "object", + properties: { + a: { + type: "number", + description: "Number to subtract from" + }, + b: { + type: "number", + description: "Number to subtract" + } + }, + required: ["a", "b"] + }, + async handler(params) { + const result = params.a - params.b; + console.log(`\n 🔧 TOOL CALLED: subtract(${params.a}, ${params.b})`); + console.log(` 📊 RESULT: ${result}\n`); + return result.toString(); + } +}); + +const divide = defineChatSessionFunction({ + description: "Divide first number by second number", + params: { + type: "object", + properties: { + a: { + type: "number", + description: "Dividend (number to be divided)" + }, + b: { + type: "number", + description: "Divisor (number to divide by)" + } + }, + required: ["a", "b"] + }, + async handler(params) { + if (params.b === 0) { + console.log(`\n 🔧 TOOL CALLED: divide(${params.a}, ${params.b})`); + console.log(` ❌ ERROR: Division by zero\n`); + return "Error: Cannot divide by zero"; + } + const result = params.a / params.b; + console.log(`\n 🔧 TOOL CALLED: divide(${params.a}, ${params.b})`); + console.log(` 📊 RESULT: ${result}\n`); + return result.toString(); + } +}); + +const functions = {add, multiply, subtract, divide}; + +// ReAct Agent execution loop with proper output handling +async function reactAgent(userPrompt, maxIterations = 10) { + console.log("\n" + "=".repeat(70)); + console.log("USER QUESTION:", userPrompt); + console.log("=".repeat(70) + "\n"); + + let iteration = 0; + let fullResponse = ""; + + while (iteration < maxIterations) { + iteration++; + console.log(`--- Iteration ${iteration} ---`); + + // Prompt with onTextChunk to capture streaming output + let currentChunk = ""; + const response = await session.prompt( + iteration === 1 ? userPrompt : "Continue your reasoning. What's the next step?", + { + functions, + maxTokens: 300, + onTextChunk: (chunk) => { + // Print each chunk as it arrives + process.stdout.write(chunk); + currentChunk += chunk; + } + } + ); + + console.log(); // New line after streaming + + fullResponse += currentChunk; + + // If no output was generated in this iteration, something's wrong + if (!currentChunk.trim() && !response.trim()) { + console.log(" (No output generated this iteration)\n"); + } + + // Check if we have a final answer + if (response.toLowerCase().includes("answer:") || + fullResponse.toLowerCase().includes("answer:")) { + console.log("\n" + "=".repeat(70)); + console.log("FINAL ANSWER REACHED"); + console.log("=".repeat(70)); + return fullResponse; + } + } + + console.log("\n⚠️ Max iterations reached without final answer"); + return fullResponse || "Could not complete reasoning within iteration limit."; +} + +// Test queries that require multi-step reasoning +const queries = [ + // "If I buy 3 apples at $2 each and 4 oranges at $3 each, how much do I spend in total?", + // "Calculate: (15 + 7) × 3 - 10", + //"A pizza costs $20. If 4 friends split it equally, how much does each person pay?", + "A store sells 15 items on Monday at $8 each, 20 items on Tuesday at $8 each, and 10 items on Wednesday at $8 each. What's the average number of items sold per day, and what's the total revenue?", +]; + +for (const query of queries) { + await reactAgent(query, 3); + console.log("\n"); +} + +// Debug +const promptDebugger = new PromptDebugger({ + outputDir: './logs', + filename: 'react_calculator.txt', + includeTimestamp: true, + appendMode: false +}); +await promptDebugger.debugContextState({session, model}); + +// Clean up +session.dispose() +context.dispose() +model.dispose() +llama.dispose() \ No newline at end of file diff --git a/examples/10_aot-agent/CODE.md b/examples/10_aot-agent/CODE.md new file mode 100644 index 0000000000000000000000000000000000000000..fb5174a0a068d638f69763d31eb911837072cbad --- /dev/null +++ b/examples/10_aot-agent/CODE.md @@ -0,0 +1,178 @@ +# Code Explanation: aot-agent.js + +This example demonstrates the **Atom of Thought** prompting pattern using a mathematical calculator as the domain. + +## Three-Phase Architecture + +### Phase 1: Planning (LLM) +```javascript +async function generatePlan(userPrompt) { + const grammar = await llama.createGrammarForJsonSchema(planSchema); + const planText = await session.prompt(userPrompt, { grammar }); + return grammar.parse(planText); +} +``` + +**Key points:** +- LLM outputs **structured JSON** (enforced by grammar) +- LLM does NOT execute calculations +- Each atom represents one operation +- Dependencies are explicit (`dependsOn` array) + +**Example output:** +```json +{ + "atoms": [ + {"id": 1, "kind": "tool", "name": "add", "input": {"a": 15, "b": 7}}, + {"id": 2, "kind": "tool", "name": "multiply", "input": {"a": "", "b": 3}}, + {"id": 3, "kind": "tool", "name": "subtract", "input": {"a": "", "b": 10}}, + {"id": 4, "kind": "final", "name": "report", "dependsOn": [3]} + ] +} +``` + +### Phase 2: Validation (System) +```javascript +function validatePlan(plan) { + const allowedTools = new Set(Object.keys(tools)); + + for (const atom of plan.atoms) { + if (ids.has(atom.id)) throw new Error(`Duplicate ID`); + if (atom.kind === "tool" && !allowedTools.has(atom.name)) { + throw new Error(`Unknown tool: ${atom.name}`); + } + } +} +``` + +**Validates:** +- No duplicate atom IDs +- Only allowed tools are referenced +- Dependencies make sense +- JSON structure is correct + +### Phase 3: Execution (System) +```javascript +function executePlan(plan) { + const state = {}; + + for (const atom of sortedAtoms) { + // Resolve dependencies + let resolvedInput = {}; + for (const [key, value] of Object.entries(atom.input)) { + if (value.startsWith('` references from state +- Each atom stores its result in `state[atom.id]` +- Execution is **deterministic** (same plan + same state = same result) + +## Why This Matters + +### Comparison with ReAct + +| Aspect | ReAct | Atom of Thought | +|--------|-------|-----------------| +| **Planning** | Implicit (in LLM reasoning) | Explicit (JSON structure) | +| **Execution** | LLM decides next step | System follows plan | +| **Validation** | None | Before execution | +| **Debugging** | Hard (trace through text) | Easy (inspect atoms) | +| **Testing** | Hard (mock LLM) | Easy (test executor) | +| **Failures** | May hallucinate | Fail at specific atom | + +### Benefits + +1. **No hidden reasoning**: Every operation is an explicit atom +2. **Testable**: Execute plan without LLM involvement +3. **Debuggable**: Know exactly which atom failed +4. **Auditable**: Plan is a data structure, not text +5. **Deterministic**: Same input = same output (given same plan) + +## Tool Implementation + +Tools are **pure functions** with no side effects: +```javascript +const tools = { + add: (a, b) => { + const result = a + b; + console.log(`EXECUTING: add(${a}, ${b}) = ${result}`); + return result; + }, + // ... more tools +}; +``` + +**Why pure functions?** +- Easy to test +- Easy to replay +- No hidden state +- Composable + +## State Flow +``` +User Question + ↓ +[LLM generates plan] + ↓ +{atoms: [...]} ← JSON plan + ↓ +[System validates] + ↓ +Plan valid + ↓ +[System executes atom 1] → state[1] = result + ↓ +[System executes atom 2] → state[2] = result (uses state[1]) + ↓ +[System executes atom 3] → state[3] = result (uses state[2]) + ↓ +Final Answer +``` + +## Error Handling +```javascript +// Atom validation fails → re-prompt LLM +validatePlan(plan); // throws if invalid + +// Tool execution fails → stop at that atom +if (b === 0) throw new Error("Division by zero"); + +// Dependency missing → clear error message +if (!(depId in state)) { + throw new Error(`Atom ${atom.id} depends on incomplete atom ${depId}`); +} +``` + +## When to Use AoT + +✅ **Use AoT when:** +- Execution must be auditable +- Failures must be recoverable +- Multiple steps with dependencies +- Testing is important +- Compliance matters + +❌ **Don't use AoT when:** +- Single-step tasks +- Creative/exploratory tasks +- Brainstorming +- Natural conversation + +## Extension Ideas + +1. **Add compensation atoms** for rollback +2. **Add retry logic** per atom +3. **Parallelize independent atoms** (atoms with no shared dependencies) +4. **Persist plan** for debugging +5. **Visualize atom graph** (dependency tree) \ No newline at end of file diff --git a/examples/10_aot-agent/CONCEPT.md b/examples/10_aot-agent/CONCEPT.md new file mode 100644 index 0000000000000000000000000000000000000000..d88fc70155a8c6f7d98fccba7187324bd85aa207 --- /dev/null +++ b/examples/10_aot-agent/CONCEPT.md @@ -0,0 +1,265 @@ +# Concept: Atom of Thought (AoT) Pattern for AI Agents + +## The Core Idea + +**Atom of Thought = "SQL for Reasoning"** + +Just as SQL breaks complex data operations into atomic, composable statements, AoT breaks reasoning into minimal, executable steps. + +## What is an Atom? + +An atom is the **smallest unit of reasoning** that: +1. Expresses exactly **one** idea +2. Can be **validated independently** +3. Can be **executed deterministically** +4. **Cannot hide** a mistake + +### Examples + +❌ **Not atomic** (compound statement): +``` +"Search for rooms in Graz and filter by capacity" +``` + +✅ **Atomic** (separate steps): +``` +1. Search for rooms in Graz +2. Filter rooms by minimum capacity of 30 +``` + +## The Three Layers +``` +┌─────────────────────────────────┐ +│ LLM (Planning Layer) │ +│ - Proposes atomic plan │ +│ - Does NOT execute │ +└─────────────────────────────────┘ + ↓ +┌─────────────────────────────────┐ +│ Validator (Safety Layer) │ +│ - Checks plan structure │ +│ - Validates dependencies │ +└─────────────────────────────────┘ + ↓ +┌─────────────────────────────────┐ +│ Executor (Execution Layer) │ +│ - Runs atoms deterministically│ +│ - Manages state │ +└─────────────────────────────────┘ +``` + +## Why Separation Matters + +### Traditional LLM Approach (ReAct) +``` +LLM thinks → LLM acts → LLM thinks → LLM acts +``` +**Problem:** Execution logic lives inside the model (black box) + +### Atom of Thought Approach +``` +LLM plans → System validates → System executes +``` +**Benefit:** Execution logic lives in code (white box) + +## Mental Model + +Think of AoT as the difference between: + +| Cooking | Programming | +|---------|------------| +| **Recipe** (AoT plan) | **Algorithm** | +| "Boil water" | `boilWater()` | +| "Add pasta" | `addPasta()` | +| "Cook 8 minutes" | `cook(8)` | + +vs. + +| Improvising | Natural Language | +|-------------|------------------| +| "Make dinner" | "Figure it out" | +| (figure it out) | (hallucinate) | + +## The Atom Structure +```javascript +{ + "id": 2, + "kind": "tool", // tool | decision | final + "name": "multiply", // operation name + "input": { // explicit inputs + "a": "", // reference to previous result + "b": 3 + }, + "dependsOn": [1] // must wait for atom 1 +} +``` + +**Why this structure?** +- `id`: Establishes order +- `kind`: Categorizes operation type +- `name`: References executable function +- `input`: Makes data flow explicit +- `dependsOn`: Declares dependencies + +## Dependency Graph + +Atoms form a **directed acyclic graph (DAG)**: +``` + ┌─────┐ + │ 1 │ add(15, 7) + └──┬──┘ + │ + ┌──▼──┐ + │ 2 │ multiply(result_1, 3) + └──┬──┘ + │ + ┌──▼──┐ + │ 3 │ subtract(result_2, 10) + └──┬──┘ + │ + ┌──▼──┐ + │ 4 │ final + └─────┘ +``` + +**Properties:** +- Can be executed in topological order +- Can parallelize independent branches +- Failures stop at failed node +- Easy to visualize and debug + +## State Management +```javascript +const state = {}; + +// After atom 1 +state[1] = 22; // result of add(15, 7) + +// After atom 2 +state[2] = 66; // result of multiply(22, 3) + +// After atom 3 +state[3] = 56; // result of subtract(66, 10) +``` + +**State is:** +- Explicit (key-value map) +- Immutable per atom (no overwrites) +- Traceable (full history) +- Inspectable (debugging) + +## Comparison: AoT vs ReAct + +### Question: "What is (15 + 7) × 3 - 10?" + +#### ReAct Output (text): +``` +Thought: I need to add 15 and 7 first +Action: add(15, 7) +Observation: 22 +Thought: Now multiply by 3 +Action: multiply(22, 3) +Observation: 66 +Thought: Finally subtract 10 +Action: subtract(66, 10) +Observation: 56 +Answer: 56 +``` + +#### AoT Output (JSON): +```json +{ + "atoms": [ + {"id": 1, "kind": "tool", "name": "add", "input": {"a": 15, "b": 7}}, + {"id": 2, "kind": "tool", "name": "multiply", "input": {"a": "", "b": 3}, "dependsOn": [1]}, + {"id": 3, "kind": "tool", "name": "subtract", "input": {"a": "", "b": 10}, "dependsOn": [2]}, + {"id": 4, "kind": "final", "name": "report", "dependsOn": [3]} + ] +} +``` + +### Key Differences + +| Aspect | ReAct | AoT | +|--------|-------|-----| +| **Format** | Natural language | Structured data | +| **Validation** | Impossible | Before execution | +| **Testing** | Mock entire LLM | Test executor independently | +| **Debugging** | Read through text | Inspect atom N | +| **Replay** | Re-run entire conversation | Re-run from any atom | +| **Audit trail** | Conversational history | Data structure | + +## When AoT Shines + +### ✅ Perfect for: +- **Multi-step workflows** (booking, pipelines) +- **API orchestration** (call A, then B with A's result) +- **Financial transactions** (auditable, reversible) +- **Compliance-sensitive systems** (every step logged) +- **Production agents** (failures must be clean) + +### ❌ Not ideal for: +- **Creative writing** +- **Open-ended exploration** +- **Brainstorming** +- **Single-step queries** + +## Real-World Analogy + +**ReAct is like a chef improvising:** +- Flexible +- Creative +- Hard to replicate exactly +- Mistakes hidden in process + +**AoT is like following a recipe:** +- Repeatable +- Testable +- Step X failed? Start from step X-1 +- Every ingredient and action is explicit + +## The Hidden Benefit: Debuggability + +When something goes wrong: + +**ReAct:** +``` +"The model said something weird in iteration 7" +→ Re-read entire conversation +→ Guess where it went wrong +→ Hope it doesn't happen again +``` + +**AoT:** +``` +"Atom 3 failed with 'Division by zero'" +→ Look at atom 3's inputs +→ Check where those inputs came from (atom 1, 2) +→ Fix tool or add validation +→ Re-run from atom 3 +``` + +## Implementation Checklist + +✅ **LLM side:** +- [ ] System prompt enforces JSON output +- [ ] Grammar constrains to valid schema +- [ ] Atoms are minimal (one operation each) +- [ ] Dependencies are explicit + +✅ **System side:** +- [ ] Validator checks tool names +- [ ] Validator checks dependencies +- [ ] Executor resolves references +- [ ] Executor is deterministic +- [ ] State is immutable + +## The Bottom Line + +**ReAct asks:** +"What would an intelligent agent say next?" + +**AoT asks:** +"What is the minimal, executable plan?" + +For production systems, you want the second question. diff --git a/examples/10_aot-agent/aot-agent.js b/examples/10_aot-agent/aot-agent.js new file mode 100644 index 0000000000000000000000000000000000000000..83faedfb8997a1c6f9563ae4aa1f684078ad28b6 --- /dev/null +++ b/examples/10_aot-agent/aot-agent.js @@ -0,0 +1,416 @@ +import { getLlama, LlamaChatSession } from "node-llama-cpp"; +import { fileURLToPath } from "url"; +import path from "path"; +import { PromptDebugger } from "../../helper/prompt-debugger.js"; +import { JsonParser } from "../../helper/json-parser.js"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const debug = false; + +const llama = await getLlama({ debug }); +const model = await llama.loadModel({ + modelPath: path.join( + __dirname, + '..', + '..', + 'models', + 'Qwen3-1.7B-Q8_0.gguf' + ) +}); +const context = await model.createContext({ contextSize: 2000 }); + +// Atom of Thought system prompt - LLM only plans, doesn't execute +const systemPrompt = `You are a mathematical planning assistant using Atom of Thought methodology. + +CRITICAL RULES: +1. Extract every number from the user's question and put it in the "input" field. +2. Each atom expresses EXACTLY ONE operation: add, subtract, multiply, divide. +3. NEVER combine operations in one atom. For example, "(5 + 3) × 2" → must be TWO atoms: one for add, one for multiply. +4. The "final" atom reports only the result of the last computational atom; it must NOT have its own input. Do not include an "input" field in final atoms. +5. Use "" to reference previous atom results; never invent calculations in the final atom. +6. Output ONLY valid JSON matching the schema, with no explanation or extra text. + +CORRECT EXAMPLE for "What is (15 + 7) × 3 - 10?": +{ + "atoms": [ + {"id": 1, "kind": "tool", "name": "add", "input": {"a": 15, "b": 7}, "dependsOn": []}, + {"id": 2, "kind": "tool", "name": "multiply", "input": {"a": "", "b": 3}, "dependsOn": [1]}, + {"id": 3, "kind": "tool", "name": "subtract", "input": {"a": "", "b": 10}, "dependsOn": [2]}, + {"id": 4, "kind": "final", "name": "report", "dependsOn": [3]} + ] +} + +WRONG EXAMPLES: +- Empty input: {"input": {}} +- Missing numbers: {"input": {"a": ""}} +- Combined operations: "add then multiply" → must be TWO atoms +- Final atom with input: {"kind": "final", "input": {"a": 5}} is INVALID + +Available tools: add, subtract, multiply, divide +- Each tool requires: {"a": , "b": } +- kind options: "tool", "decision", "final" +- dependsOn: array of atom IDs that must complete first + +Always extract the actual numbers from the question and put them in the input fields! Never combine operations or invent calculations in final atoms.`; + +// Define JSON schema for plan validation +const planSchema = { + type: "object", + properties: { + atoms: { + type: "array", + items: { + type: "object", + properties: { + id: { type: "number" }, + kind: { enum: ["tool", "decision", "final"] }, + name: { type: "string" }, + input: { + type: "object", + properties: { + a: { + oneOf: [ + { type: "number" }, + { type: "string", pattern: "^$" } + ] + }, + b: { + oneOf: [ + { type: "number" }, + { type: "string", pattern: "^$" } + ] + } + } + }, + dependsOn: { + type: "array", + items: { type: "number" } + } + }, + required: ["id", "kind", "name"] + } + } + }, + required: ["atoms"] +}; + +const session = new LlamaChatSession({ + contextSequence: context.getSequence(), + systemPrompt, +}); + +// Tool implementations (pure functions, deterministic) +const tools = { + add: (a, b) => { + const result = a + b; + console.log(`EXECUTING: add(${a}, ${b}) = ${result}`); + return result; + }, + + subtract: (a, b) => { + const result = a - b; + console.log(`EXECUTING: subtract(${a}, ${b}) = ${result}`); + return result; + }, + + multiply: (a, b) => { + const result = a * b; + console.log(`EXECUTING: multiply(${a}, ${b}) = ${result}`); + return result; + }, + + divide: (a, b) => { + if (b === 0) { + console.log(`ERROR: divide(${a}, ${b}) - Division by zero`); + throw new Error("Division by zero"); + } + const result = a / b; + console.log(`EXECUTING: divide(${a}, ${b}) = ${result}`); + return result; + } +}; + +// Decision handlers (for complex logic) +const decisions = { + average: (values) => { + const sum = values.reduce((acc, v) => acc + v, 0); + const avg = sum / values.length; + console.log(`DECISION: average([${values}]) = ${avg}`); + return avg; + }, + + chooseCheapest: (values) => { + const min = Math.min(...values); + console.log(`DECISION: chooseCheapest([${values}]) = ${min}`); + return min; + } +}; + +// Phase 1: LLM generates atomic plan +async function generatePlan(userPrompt) { + console.log("\n" + "=".repeat(70)); + console.log("PHASE 1: PLANNING (LLM generates atomic plan)"); + console.log("=".repeat(70)); + console.log("USER QUESTION:", userPrompt); + console.log("-".repeat(70) + "\n"); + + const grammar = await llama.createGrammarForJsonSchema(planSchema); + + // Add reminder about extracting numbers + const enhancedPrompt = `${userPrompt} + +Remember: Extract the actual numbers from this question and put them in the input fields!`; + + const planText = await session.prompt(enhancedPrompt, { + grammar, + maxTokens: 1000 + }); + + let plan; + try { + // Use the robust JSON parser + plan = JsonParser.parse(planText, { + debug: debug, + expectObject: true, + repairAttempts: true + }); + + // Validate the plan structure + JsonParser.validatePlan(plan, debug); + + // Pretty print the plan + if (debug) { + JsonParser.prettyPrint(plan); + } else { + console.log("GENERATED PLAN:"); + console.log(JSON.stringify(plan, null, 2)); + console.log(); + } + } catch (error) { + console.error("Failed to parse plan:", error.message); + console.log("\nRaw LLM output:"); + console.log(planText); + throw error; + } + + return plan; +} + +// Phase 2: System validates plan +function validatePlan(plan) { + console.log("\n" + "=".repeat(70)); + console.log("PHASE 2: VALIDATION (System checks plan)"); + console.log("=".repeat(70) + "\n"); + + const allowedTools = new Set(Object.keys(tools)); + const allowedDecisions = new Set(Object.keys(decisions)); + const ids = new Set(); + + for (const atom of plan.atoms) { + // Check for duplicate IDs + if (ids.has(atom.id)) { + throw new Error(`Validation failed: Duplicate atom ID ${atom.id}`); + } + ids.add(atom.id); + + // Check tool names + if (atom.kind === "tool" && !allowedTools.has(atom.name)) { + throw new Error(`Validation failed: Unknown tool "${atom.name}" in atom ${atom.id}`); + } + + // Check decision names + if (atom.kind === "decision" && !allowedDecisions.has(atom.name)) { + throw new Error(`Validation failed: Unknown decision "${atom.name}" in atom ${atom.id}`); + } + + // NEW: Validate tool inputs have actual values + if (atom.kind === "tool") { + if (!atom.input || typeof atom.input !== 'object') { + throw new Error( + `Validation failed: Tool atom ${atom.id} (${atom.name}) must have an input object\n` + + ` Current: ${JSON.stringify(atom.input)}` + ); + } + + // Check if a and b are present + if (atom.input.a === undefined || atom.input.b === undefined) { + throw new Error( + `Validation failed: Tool atom ${atom.id} (${atom.name}) missing required parameters\n` + + ` Expected: {"a": , "b": }\n` + + ` Current: ${JSON.stringify(atom.input)}\n` + + ` Tip: The LLM must extract numbers from the user's question` + ); + } + + // For first operations, ensure we have concrete numbers (not references) + if (atom.dependsOn.length === 0) { + const hasConcreteNumbers = + (typeof atom.input.a === 'number') && + (typeof atom.input.b === 'number'); + + if (!hasConcreteNumbers) { + throw new Error( + `Validation failed: First atom ${atom.id} must have concrete numbers\n` + + ` Expected: {"a": , "b": }\n` + + ` Current: ${JSON.stringify(atom.input)}\n` + + ` The LLM failed to extract numbers from the question` + ); + } + } + } + + // Check dependencies exist + if (atom.dependsOn) { + for (const depId of atom.dependsOn) { + if (!ids.has(depId) && depId < atom.id) { + console.warn(`Warning: atom ${atom.id} depends on ${depId} which hasn't been validated yet`); + } + } + } + + console.log(`Atom ${atom.id} (${atom.kind}:${atom.name}) validated`); + } + + console.log("\nPlan validation successful\n"); + return true; +} + +// Phase 3: System executes plan deterministically +function executePlan(plan) { + console.log("\n" + "=".repeat(70)); + console.log("PHASE 3: EXECUTION (System runs atoms)"); + console.log("=".repeat(70) + "\n"); + + const state = {}; + const sortedAtoms = [...plan.atoms].sort((a, b) => a.id - b.id); + + for (const atom of sortedAtoms) { + console.log(`\nExecuting atom ${atom.id} (${atom.kind}:${atom.name})`); + + // Check dependencies + if (atom.dependsOn && atom.dependsOn.length > 0) { + const missingDeps = atom.dependsOn.filter(id => !(id in state)); + if (missingDeps.length > 0) { + throw new Error(`Atom ${atom.id} depends on incomplete atoms: ${missingDeps}`); + } + console.log(`Dependencies satisfied: ${atom.dependsOn.join(', ')}`); + } + + // Resolve input values (replace references) + let resolvedInput = { a: undefined, b: undefined }; + if (atom.input) { + // Deep clone to avoid mutations + resolvedInput = JSON.parse(JSON.stringify(atom.input)); + + for (const [key, value] of Object.entries(resolvedInput)) { + if (typeof value === 'string' && value.startsWith(' but atom ${refId} hasn't executed yet` + ); + } + + resolvedInput[key] = state[refId]; + console.log(`Resolved ${key}: ${value} → ${state[refId]}`); + } + } + } + + // Execute based on kind + if (atom.kind === "tool") { + const tool = tools[atom.name]; + if (!tool) { + throw new Error(`Tool not found: ${atom.name}`); + } + + // Show input before execution + console.log(`Input: a=${resolvedInput.a}, b=${resolvedInput.b}`); + + // Safety check + if (resolvedInput.a === undefined || resolvedInput.b === undefined) { + throw new Error( + `Cannot execute ${atom.name}: undefined input values\n` + + ` This means the LLM didn't extract numbers from your question.\n` + + ` Original input: ${JSON.stringify(atom.input)}` + ); + } + + state[atom.id] = tool(resolvedInput.a, resolvedInput.b); + } + else if (atom.kind === "decision") { + const decision = decisions[atom.name]; + if (!decision) { + throw new Error(`Decision not found: ${atom.name}`); + } + + // Collect results from dependencies + const depResults = atom.dependsOn.map(id => state[id]); + state[atom.id] = decision(depResults); + } + else if (atom.kind === "final") { + const finalValue = state[atom.dependsOn[0]]; + console.log(`\n FINAL RESULT: ${finalValue}`); + state[atom.id] = finalValue; + } + } + + return state; +} + +// Main AoT Agent execution +async function aotAgent(userPrompt) { + try { + // Phase 1: Plan + const plan = await generatePlan(userPrompt); + + // Phase 2: Validate + validatePlan(plan); + + // Phase 3: Execute + const result = executePlan(plan); + + console.log("\n" + "=".repeat(70)); + console.log("EXECUTION COMPLETE"); + console.log("=".repeat(70)); + + // Find final atom + const finalAtom = plan.atoms.find(a => a.kind === "final"); + if (finalAtom) { + console.log(`\nANSWER: ${result[finalAtom.id]}\n`); + } + + return result; + } catch (error) { + console.error("\nEXECUTION FAILED:", error.message); + throw error; + } +} + +// Test queries +const queries = [ + // "What is (15 + 7) multiplied by 3 minus 10?", + // "A pizza costs 20 dollars. If 4 friends split it equally, how much does each person pay?", + "Calculate: 100 divided by 5, then add 3, then multiply by 2", +]; + +for (const query of queries) { + await aotAgent(query); + console.log("\n"); +} + +// Debug +const promptDebugger = new PromptDebugger({ + outputDir: './logs', + filename: 'aot_calculator.txt', + includeTimestamp: true, + appendMode: false +}); +await promptDebugger.debugContextState({ session, model }); + +// Clean up +session.dispose(); +context.dispose(); +model.dispose(); +llama.dispose(); \ No newline at end of file diff --git a/helper/json-parser.js b/helper/json-parser.js new file mode 100644 index 0000000000000000000000000000000000000000..072e5ecf65c7bc0a2afe00d4c8b4e24d1702b99b --- /dev/null +++ b/helper/json-parser.js @@ -0,0 +1,282 @@ +/** + * Robust JSON parser for LLM outputs + * Handles common issues like: + * - Missing opening/closing braces + * - Markdown code blocks + * - Extra text before/after JSON + * - Escaped quotes + * - Trailing commas + */ + +export class JsonParser { + /** + * Extract and parse JSON from potentially messy LLM output + * @param {string} text - Raw text from LLM + * @param {object} options - Parsing options + * @returns {object} Parsed JSON object + */ + static parse(text, options = {}) { + const { + debug = false, + expectArray = false, + expectObject = true, + repairAttempts = true + } = options; + + if (debug) { + console.log("\nRAW LLM OUTPUT:"); + console.log("-".repeat(70)); + console.log(text); + console.log("-".repeat(70) + "\n"); + } + + // Step 1: Clean the text + let cleaned = this.cleanText(text, debug); + + // Step 2: Extract JSON + let extracted = this.extractJson(cleaned, expectArray, expectObject, debug); + + // Step 3: Attempt to parse + try { + const parsed = JSON.parse(extracted); + if (debug) console.log("Successfully parsed JSON\n"); + return parsed; + } catch (firstError) { + if (debug) { + console.log("First parse attempt failed:", firstError.message); + } + + if (!repairAttempts) { + throw new Error(`JSON parse failed: ${firstError.message}\n\nExtracted text:\n${extracted}`); + } + + // Step 4: Attempt repairs + return this.attemptRepairs(extracted, debug); + } + } + + /** + * Clean text from common LLM artifacts + */ + static cleanText(text, debug = false) { + let cleaned = text; + + // Remove markdown code blocks + cleaned = cleaned.replace(/```json\s*/gi, ''); + cleaned = cleaned.replace(/```\s*/g, ''); + + // Remove common prefixes + cleaned = cleaned.replace(/^(Here's the plan:|JSON output:|Plan:|Output:)\s*/i, ''); + + // Trim whitespace + cleaned = cleaned.trim(); + + if (debug && cleaned !== text) { + console.log("Cleaned text (removed markdown/prefixes)\n"); + } + + return cleaned; + } + + /** + * Extract JSON from text (handles text before/after JSON) + */ + static extractJson(text, expectArray = false, expectObject = true, debug = false) { + // Try to find JSON boundaries + const startChar = expectArray ? '[' : '{'; + const endChar = expectArray ? ']' : '}'; + + const startIdx = text.indexOf(startChar); + const lastIdx = text.lastIndexOf(endChar); + + if (startIdx === -1 || lastIdx === -1 || startIdx >= lastIdx) { + if (debug) { + console.log(`Could not find valid ${startChar}...${endChar} boundaries`); + console.log(`Start index: ${startIdx}, End index: ${lastIdx}`); + } + + // Maybe it's missing braces - try to add them + if (expectObject && !text.trim().startsWith('{')) { + const withBraces = '{' + text.trim() + '}'; + if (debug) console.log("Added missing opening brace"); + return withBraces; + } + + return text; + } + + const extracted = text.substring(startIdx, lastIdx + 1); + + if (debug && extracted !== text) { + console.log("Extracted JSON from surrounding text:"); + console.log(extracted.substring(0, 100) + (extracted.length > 100 ? '...' : '')); + console.log(); + } + + return extracted; + } + + /** + * Attempt various repair strategies + */ + static attemptRepairs(jsonString, debug = false) { + const repairs = [ + // Repair 1: Remove trailing commas + (str) => { + const fixed = str.replace(/,(\s*[}\]])/g, '$1'); + if (debug && fixed !== str) console.log("Repair 1: Removed trailing commas"); + return fixed; + }, + + // Repair 2: Fix missing quotes around property names + (str) => { + const fixed = str.replace(/([{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)\s*:/g, '$1"$2":'); + if (debug && fixed !== str) console.log("Repair 2: Added quotes around property names"); + return fixed; + }, + + // Repair 3: Fix single quotes to double quotes + (str) => { + const fixed = str.replace(/'/g, '"'); + if (debug && fixed !== str) console.log("Repair 3: Converted single quotes to double quotes"); + return fixed; + }, + + // Repair 4: Add missing closing braces + (str) => { + const openBraces = (str.match(/{/g) || []).length; + const closeBraces = (str.match(/}/g) || []).length; + if (openBraces > closeBraces) { + const fixed = str + '}'.repeat(openBraces - closeBraces); + if (debug) console.log(`Repair 4: Added ${openBraces - closeBraces} missing closing brace(s)`); + return fixed; + } + return str; + }, + + // Repair 5: Add missing closing brackets + (str) => { + const openBrackets = (str.match(/\[/g) || []).length; + const closeBrackets = (str.match(/]/g) || []).length; + if (openBrackets > closeBrackets) { + const fixed = str + ']'.repeat(openBrackets - closeBrackets); + if (debug) console.log(`Repair 5: Added ${openBrackets - closeBrackets} missing closing bracket(s)`); + return fixed; + } + return str; + }, + + // Repair 6: Fix escaped quotes that shouldn't be escaped + (str) => { + const fixed = str.replace(/\\"/g, '"'); + if (debug && fixed !== str) console.log("Repair 6: Fixed escaped quotes"); + return fixed; + }, + + // Repair 7: Remove control characters + (str) => { + // eslint-disable-next-line no-control-regex + const fixed = str.replace(/[\x00-\x1F\x7F]/g, ''); + if (debug && fixed !== str) console.log("Repair 7: Removed control characters"); + return fixed; + } + ]; + + let current = jsonString; + + // Try each repair in sequence + for (const repair of repairs) { + current = repair(current); + } + + // Try parsing after all repairs + try { + const parsed = JSON.parse(current); + if (debug) console.log("Successfully parsed after repairs\n"); + return parsed; + } catch (error) { + // Last resort: try to extract just the atoms array if it's there + const atomsMatch = current.match(/"atoms"\s*:\s*(\[[\s\S]*\])/); + if (atomsMatch) { + try { + const atomsOnly = { atoms: JSON.parse(atomsMatch[1]) }; + if (debug) console.log("Extracted and parsed atoms array\n"); + return atomsOnly; + } catch (innerError) { + // Fall through to final error + } + } + + // If all repairs fail, throw detailed error + throw new Error( + `JSON parse failed after all repair attempts.\n\n` + + `Original error: ${error.message}\n\n` + + `Attempted repairs:\n${current.substring(0, 500)}${current.length > 500 ? '...' : ''}\n\n` + + `Tip: Check if the LLM is following the JSON schema correctly.` + ); + } + } + + /** + * Validate parsed plan structure + */ + static validatePlan(plan, debug = false) { + if (!plan || typeof plan !== 'object') { + throw new Error('Plan must be an object'); + } + + if (!Array.isArray(plan.atoms)) { + throw new Error('Plan must have an "atoms" array'); + } + + if (plan.atoms.length === 0) { + throw new Error('Plan must have at least one atom'); + } + + for (const atom of plan.atoms) { + if (typeof atom.id !== 'number') { + throw new Error(`Atom missing or invalid id: ${JSON.stringify(atom)}`); + } + + if (!atom.kind || !['tool', 'decision', 'final'].includes(atom.kind)) { + throw new Error(`Atom ${atom.id} has invalid kind: ${atom.kind}`); + } + + if (!atom.name || typeof atom.name !== 'string') { + throw new Error(`Atom ${atom.id} missing or invalid name`); + } + + if (atom.dependsOn && !Array.isArray(atom.dependsOn)) { + throw new Error(`Atom ${atom.id} dependsOn must be an array`); + } + } + + if (debug) { + console.log(`Plan structure validated: ${plan.atoms.length} atoms\n`); + } + + return true; + } + + /** + * Pretty print plan for debugging + */ + static prettyPrint(plan) { + console.log("\nPLAN STRUCTURE:"); + console.log("=".repeat(70)); + + for (const atom of plan.atoms) { + const deps = atom.dependsOn && atom.dependsOn.length > 0 + ? ` (depends on: ${atom.dependsOn.join(', ')})` + : ''; + + console.log(` ${atom.id}. [${atom.kind}] ${atom.name}${deps}`); + + if (atom.input && Object.keys(atom.input).length > 0) { + console.log(` Input: ${JSON.stringify(atom.input)}`); + } + } + + console.log("=".repeat(70) + "\n"); + } +} \ No newline at end of file diff --git a/helper/prompt-debugger.js b/helper/prompt-debugger.js new file mode 100644 index 0000000000000000000000000000000000000000..1c93aa612b53119c9a1b619053db83dc7db61661 --- /dev/null +++ b/helper/prompt-debugger.js @@ -0,0 +1,350 @@ +import {LlamaText} from "node-llama-cpp"; +import path from "path"; +import fs from "fs/promises"; + +/** + * Output types for debugging + */ +const OutputTypes = { + EXACT_PROMPT: 'exactPrompt', + CONTEXT_STATE: 'contextState', + STRUCTURED: 'structured' +}; + +/** + * Helper class for debugging and logging LLM prompts + */ +export class PromptDebugger { + constructor(options = {}) { + this.outputDir = options.outputDir || './'; + this.filename = options.filename; + this.includeTimestamp = options.includeTimestamp ?? false; + this.appendMode = options.appendMode ?? false; + // Configure which outputs to include + this.outputTypes = options.outputTypes || [OutputTypes.EXACT_PROMPT]; + // Ensure outputTypes is always an array + if (!Array.isArray(this.outputTypes)) { + this.outputTypes = [this.outputTypes]; + } + } + + /** + * Captures only the exact prompt (user input + system + functions) + * @param {Object} params + * @param {Object} params.session - The chat session + * @param {string} params.prompt - The user prompt + * @param {string} params.systemPrompt - System prompt (optional) + * @param {Object} params.functions - Available functions (optional) + * @returns {Object} The exact prompt data + */ + captureExactPrompt(params) { + const { session, prompt, systemPrompt, functions } = params; + + const chatWrapper = session.chatWrapper; + + // Build minimal history for exact prompt + const history = [{ type: 'user', text: prompt }]; + + if (systemPrompt) { + history.unshift({ type: 'system', text: systemPrompt }); + } + + // Generate the context state with just the current prompt + const state = chatWrapper.generateContextState({ + chatHistory: history, + availableFunctions: functions, + systemPrompt: systemPrompt + }); + + const formattedPrompt = state.contextText.toString(); + + return { + exactPrompt: formattedPrompt, + timestamp: new Date().toISOString(), + prompt, + systemPrompt, + functions: functions ? Object.keys(functions) : [] + }; + } + + /** + * Captures the full context state (includes assistant responses) + * @param {Object} params + * @param {Object} params.session - The chat session + * @param {Object} params.model - The loaded model + * @returns {Object} The context state data + */ + captureContextState(params) { + const { session, model } = params; + + // Get the actual context from the session after responses + const contextState = model.detokenize(session.sequence.contextTokens, true); + + return { + contextState, + timestamp: new Date().toISOString(), + tokenCount: session.sequence.contextTokens.length + }; + } + + /** + * Captures the structured token representation + * @param {Object} params + * @param {Object} params.session - The chat session + * @param {Object} params.model - The loaded model + * @returns {Object} The structured token data + */ + captureStructured(params) { + const { session, model } = params; + + const structured = LlamaText.fromTokens(model.tokenizer, session.sequence.contextTokens); + + return { + structured, + timestamp: new Date().toISOString(), + tokenCount: session.sequence.contextTokens.length + }; + } + + /** + * Captures all configured output types + * @param {Object} params - Contains all possible parameters + * @returns {Object} Combined captured data based on configuration + */ + captureAll(params) { + const result = { + timestamp: new Date().toISOString() + }; + + if (this.outputTypes.includes(OutputTypes.EXACT_PROMPT)) { + const exactData = this.captureExactPrompt(params); + result.exactPrompt = exactData.exactPrompt; + result.prompt = exactData.prompt; + result.systemPrompt = exactData.systemPrompt; + result.functions = exactData.functions; + } + + if (this.outputTypes.includes(OutputTypes.CONTEXT_STATE)) { + const contextData = this.captureContextState(params); + result.contextState = contextData.contextState; + result.contextTokenCount = contextData.tokenCount; + } + + if (this.outputTypes.includes(OutputTypes.STRUCTURED)) { + const structuredData = this.captureStructured(params); + result.structured = structuredData.structured; + result.structuredTokenCount = structuredData.tokenCount; + } + + return result; + } + + /** + * Formats the captured data based on configuration + * @param {Object} capturedData - Data from capture methods + * @returns {string} Formatted output + */ + formatOutput(capturedData) { + let output = `\n========== PROMPT DEBUG OUTPUT ==========\n`; + output += `Timestamp: ${capturedData.timestamp}\n`; + + if (capturedData.prompt) { + output += `Original Prompt: ${capturedData.prompt}\n`; + } + + if (capturedData.systemPrompt) { + output += `System Prompt: ${capturedData.systemPrompt.substring(0, 50)}...\n`; + } + + if (capturedData.functions && capturedData.functions.length > 0) { + output += `Functions: ${capturedData.functions.join(', ')}\n`; + } + + if (capturedData.exactPrompt) { + output += `\n=== EXACT PROMPT ===\n`; + output += capturedData.exactPrompt; + output += `\n`; + } + + if (capturedData.contextState) { + output += `Token Count: ${capturedData.contextTokenCount || 'N/A'}\n`; + + output += `\n=== CONTEXT STATE ===\n`; + output += capturedData.contextState; + output += `\n`; + } + + if (capturedData.structured) { + output += `\n=== STRUCTURED ===\n`; + output += `Token Count: ${capturedData.structuredTokenCount || 'N/A'}\n`; + output += JSON.stringify(capturedData.structured, null, 2); + output += `\n`; + } + + output += `==========================================\n`; + return output; + } + + /** + * Saves data to file + * @param {Object} capturedData - Data to save + * @param {null} customFilename - Optional custom filename + */ + async saveToFile(capturedData, customFilename = null) { + const content = this.formatOutput(capturedData); + + let filename = customFilename || this.filename; + + if (this.includeTimestamp) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const ext = path.extname(filename); + const base = path.basename(filename, ext); + filename = `${base}_${timestamp}${ext}`; + } + + const filepath = path.join(this.outputDir, filename); + + if (this.appendMode) { + await fs.appendFile(filepath, content, 'utf8'); + } else { + await fs.writeFile(filepath, content, 'utf8'); + } + + console.log(`Prompt debug output written to ${filepath}`); + return filepath; + } + + /** + * Debug exact prompt only - minimal params needed + * @param {Object} params - session, prompt, systemPrompt (optional), functions (optional) + * @param customFilename + */ + async debugExactPrompt(params, customFilename = null) { + const oldOutputTypes = this.outputTypes; + this.outputTypes = [OutputTypes.EXACT_PROMPT]; + const capturedData = this.captureAll(params); + const filepath = await this.saveToFile(capturedData, customFilename); + this.outputTypes = oldOutputTypes; + return { capturedData, filepath }; + } + + /** + * Debug context state only - needs session and model + * @param {Object} params - session, model + * @param customFilename + */ + async debugContextState(params, customFilename = null) { + const oldOutputTypes = this.outputTypes; + this.outputTypes = [OutputTypes.CONTEXT_STATE]; + const capturedData = this.captureAll(params); + const filepath = await this.saveToFile(capturedData, customFilename); + this.outputTypes = oldOutputTypes; + return { capturedData, filepath }; + } + + /** + * Debug structured only - needs session and model + * @param {Object} params - session, model + * @param customFilename + */ + async debugStructured(params, customFilename = null) { + const oldOutputTypes = this.outputTypes; + this.outputTypes = [OutputTypes.STRUCTURED]; + const capturedData = this.captureAll(params); + const filepath = await this.saveToFile(capturedData, customFilename); + this.outputTypes = oldOutputTypes; + return { capturedData, filepath }; + } + + /** + * Debug with configured output types + * @param {Object} params - All parameters (session, model, prompt, etc.) + * @param customFilename + */ + async debug(params, customFilename = null) { + const capturedData = this.captureAll(params); + //const filepath = await this.saveToFile(capturedData, customFilename); + return { capturedData }; + } + + /** + * Log to console only + * @param {Object} params - Parameters based on configured output types + */ + logToConsole(params) { + const capturedData = this.captureAll(params); + console.log(this.formatOutput(capturedData)); + return capturedData; + } + + /** + * Log exact prompt to console + */ + logExactPrompt(params) { + const capturedData = this.captureExactPrompt(params); + console.log(this.formatOutput(capturedData)); + return capturedData; + } + + /** + * Log context state to console + */ + logContextState(params) { + const capturedData = this.captureContextState(params); + console.log(this.formatOutput(capturedData)); + return capturedData; + } + + /** + * Log structured to console + */ + logStructured(params) { + const capturedData = this.captureStructured(params); + console.log(this.formatOutput(capturedData)); + return capturedData; + } +} + +/** + * Quick function to debug exact prompt only + */ +async function debugExactPrompt(params, options = {}) { + const promptDebugger = new PromptDebugger({ + ...options, + outputTypes: [OutputTypes.EXACT_PROMPT] + }); + return await promptDebugger.debug(params); +} + +/** + * Quick function to debug context state only + */ +async function debugContextState(params, options = {}) { + const promptDebugger = new PromptDebugger({ + ...options, + outputTypes: [OutputTypes.CONTEXT_STATE] + }); + return await promptDebugger.debug(params); +} + +/** + * Quick function to debug structured only + */ +async function debugStructured(params, options = {}) { + const promptDebugger = new PromptDebugger({ + ...options, + outputTypes: [OutputTypes.STRUCTURED] + }); + return await promptDebugger.debug(params); +} + +/** + * Quick function to debug all outputs + */ +async function debugAll(params, options = {}) { + const promptDebugger = new PromptDebugger({ + ...options, + outputTypes: [OutputTypes.EXACT_PROMPT, OutputTypes.CONTEXT_STATE, OutputTypes.STRUCTURED] + }); + return await promptDebugger.debug(params); +} \ No newline at end of file diff --git a/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..d79b4ea5420a458ed37123e7480106d444ffcda3 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2700 @@ +{ + "name": "ai-agents", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ai-agents", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "dotenv": "^17.2.3", + "node-llama-cpp": "^3.14.0", + "openai": "^6.7.0" + } + }, + "node_modules/@huggingface/jinja": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.1.tgz", + "integrity": "sha512-yUZLld4lrM9iFxHCwFQ7D1HW2MWMwSbeB7WzWqFYDWK+rEb+WldkLdAJxUPOmgICMHZLzZGVcVjFh3w/YGubng==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "license": "MIT" + }, + "node_modules/@node-llama-cpp/linux-arm64": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-arm64/-/linux-arm64-3.14.0.tgz", + "integrity": "sha512-Ayn+boDjmfu+6arJn0kh9tlFFhA6hB4YVflrxF9O9rqc7htRd+JsF3rzSd6FLDiPBP62mKtg6R4xDqbC7Ut9vg==", + "cpu": [ + "arm64", + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@node-llama-cpp/linux-armv7l": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-armv7l/-/linux-armv7l-3.14.0.tgz", + "integrity": "sha512-+9kHl9/7zgEbqYnRlXD41OSBZA19nlUUbTewR7GnRuU47iFScp4FklijPfivaDxg8Xlp+g6M47GduByifMPqKg==", + "cpu": [ + "arm", + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@node-llama-cpp/linux-x64": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-x64/-/linux-x64-3.14.0.tgz", + "integrity": "sha512-flqBnK0PQcdrQQA/+eNl+nCJyXWpqItdkiPdiE9MxpF6PVoU07lBupkf/s3vXZpLC2ZU+8y/5me36tB4JGH23A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@node-llama-cpp/linux-x64-cuda": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-x64-cuda/-/linux-x64-cuda-3.14.0.tgz", + "integrity": "sha512-BhRoNvUSM+63Z6TczvCSvLHNX4kIRVaABw7547htrPabyBNkfRD604Zq+vACHjADGNLp2AlXTqJHMmszA7KfdA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@node-llama-cpp/linux-x64-cuda-ext": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-x64-cuda-ext/-/linux-x64-cuda-ext-3.14.0.tgz", + "integrity": "sha512-/OMo+ElsEejDxRaOHZjpw7wolmRllnk7J4mQKt+yk+LjkQZe6a3fQlM1YiCexpBGFSAu+1exU8L+oXdhOrK8OQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@node-llama-cpp/linux-x64-vulkan": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-x64-vulkan/-/linux-x64-vulkan-3.14.0.tgz", + "integrity": "sha512-0Gp1VAbeWIFc7Hkbvs5yuWw5i1A3Yo+7Q6uWXDzNUg50m0PrrF8evQWL2arp/lZINkDxI0JfpEoc+jFvdj3a8g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@node-llama-cpp/mac-arm64-metal": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@node-llama-cpp/mac-arm64-metal/-/mac-arm64-metal-3.14.0.tgz", + "integrity": "sha512-u+s8CLceE1yN86wSxGgHqrnKNkJKLtVMxGcP1lWdY43RNQvM+Rb63zfc54L1NG9LNgldYbAEs/8HObRf7/i3Lw==", + "cpu": [ + "arm64", + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@node-llama-cpp/mac-x64": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@node-llama-cpp/mac-x64/-/mac-x64-3.14.0.tgz", + "integrity": "sha512-caaOePiO0Rp4pxJRO2yeRs2K3FHF2DxJYHdEN6iqL4rK6n3vuy72nnHe1IaIV8CNmKQkv6n1DELM1WvLhs/sQA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@node-llama-cpp/win-arm64": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@node-llama-cpp/win-arm64/-/win-arm64-3.14.0.tgz", + "integrity": "sha512-w3u5WK9dvOQXqyquulbvY8Az/Aw/mufTSfM/Xpy7bBMygEVZU2BE+GB0xh4U5sKYdA22dFocrrNpjm6QXb8uzA==", + "cpu": [ + "arm64", + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@node-llama-cpp/win-x64": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@node-llama-cpp/win-x64/-/win-x64-3.14.0.tgz", + "integrity": "sha512-/NLu7IiwGldXV3CIlz/fnlagb1PZELDypYybsq9Jktz+7Dtl/r9urQ7oaea9Gjbj/DJIRKSd+2rAbfEXbEigGg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@node-llama-cpp/win-x64-cuda": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@node-llama-cpp/win-x64-cuda/-/win-x64-cuda-3.14.0.tgz", + "integrity": "sha512-fTDnohKivAsq7A6uB5GO+RqwLhVHIy96Emr1XgYqLjt5hIugJkVocNFSSmNMq2ZFF3cNkWGG/2uTTGJX1GysFA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@node-llama-cpp/win-x64-cuda-ext": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@node-llama-cpp/win-x64-cuda-ext/-/win-x64-cuda-ext-3.14.0.tgz", + "integrity": "sha512-S35Sk1F50lgR19g3blBRDAj+VZFbPd3wZ9q+EfF4qddjR8ArXHrhm6mc6yqHyBusZtG0yx4LAcWFiUHolY2N5A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@node-llama-cpp/win-x64-vulkan": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@node-llama-cpp/win-x64-vulkan/-/win-x64-vulkan-3.14.0.tgz", + "integrity": "sha512-JcmLhC0cb6BQwDFy7NaT/jV2HcRIJnW8zmZX2Y1WXuvXNYzI2W0a0JqjLXKHbsbgk64RSam95+QjQEOV0eL6JA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@octokit/app": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@octokit/app/-/app-16.1.1.tgz", + "integrity": "sha512-pcvKSN6Q6aT3gU5heoDFs3ywU5xejxeqs1rQpUwgN7CmBlxCSy9aCoqFuC6GpVv71O/Qq/VuYfCNzrOZp/9Ycw==", + "license": "MIT", + "dependencies": { + "@octokit/auth-app": "^8.1.1", + "@octokit/auth-unauthenticated": "^7.0.2", + "@octokit/core": "^7.0.5", + "@octokit/oauth-app": "^8.0.2", + "@octokit/plugin-paginate-rest": "^13.2.0", + "@octokit/types": "^15.0.0", + "@octokit/webhooks": "^14.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-app": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-8.1.1.tgz", + "integrity": "sha512-yW9YUy1cuqWlz8u7908ed498wJFt42VYsYWjvepjojM4BdZSp4t+5JehFds7LfvYi550O/GaUI94rgbhswvxfA==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-app": "^9.0.2", + "@octokit/auth-oauth-user": "^6.0.1", + "@octokit/request": "^10.0.5", + "@octokit/request-error": "^7.0.1", + "@octokit/types": "^15.0.0", + "toad-cache": "^3.7.0", + "universal-github-app-jwt": "^2.2.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-oauth-app": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-9.0.2.tgz", + "integrity": "sha512-vmjSHeuHuM+OxZLzOuoYkcY3OPZ8erJ5lfswdTmm+4XiAKB5PmCk70bA1is4uwSl/APhRVAv4KHsgevWfEKIPQ==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^8.0.2", + "@octokit/auth-oauth-user": "^6.0.1", + "@octokit/request": "^10.0.5", + "@octokit/types": "^15.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-oauth-device": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-8.0.2.tgz", + "integrity": "sha512-KW7Ywrz7ei7JX+uClWD2DN1259fnkoKuVdhzfpQ3/GdETaCj4Tx0IjvuJrwhP/04OhcMu5yR6tjni0V6LBihdw==", + "license": "MIT", + "dependencies": { + "@octokit/oauth-methods": "^6.0.1", + "@octokit/request": "^10.0.5", + "@octokit/types": "^15.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-oauth-user": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-6.0.1.tgz", + "integrity": "sha512-vlKsL1KUUPvwXpv574zvmRd+/4JiDFXABIZNM39+S+5j2kODzGgjk7w5WtiQ1x24kRKNaE7v9DShNbw43UA3Hw==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^8.0.2", + "@octokit/oauth-methods": "^6.0.1", + "@octokit/request": "^10.0.5", + "@octokit/types": "^15.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-token": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-unauthenticated": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-7.0.2.tgz", + "integrity": "sha512-vjcPRP1xsKWdYKiyKmHkLFCxeH4QvVTv05VJlZxwNToslBFcHRJlsWRaoI2+2JGCf9tIM99x8cN0b1rlAHJiQw==", + "license": "MIT", + "dependencies": { + "@octokit/request-error": "^7.0.1", + "@octokit/types": "^15.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.5.tgz", + "integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.2", + "@octokit/request": "^10.0.4", + "@octokit/request-error": "^7.0.1", + "@octokit/types": "^15.0.0", + "before-after-hook": "^4.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/endpoint": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.1.tgz", + "integrity": "sha512-7P1dRAZxuWAOPI7kXfio88trNi/MegQ0IJD3vfgC3b+LZo1Qe6gRJc2v0mz2USWWJOKrB2h5spXCzGbw+fAdqA==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^15.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/graphql": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.2.tgz", + "integrity": "sha512-iz6KzZ7u95Fzy9Nt2L8cG88lGRMr/qy1Q36ih/XVzMIlPDMYwaNLE/ENhqmIzgPrlNWiYJkwmveEetvxAgFBJw==", + "license": "MIT", + "dependencies": { + "@octokit/request": "^10.0.4", + "@octokit/types": "^15.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/oauth-app": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-8.0.3.tgz", + "integrity": "sha512-jnAjvTsPepyUaMu9e69hYBuozEPgYqP4Z3UnpmvoIzHDpf8EXDGvTY1l1jK0RsZ194oRd+k6Hm13oRU8EoDFwg==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-app": "^9.0.2", + "@octokit/auth-oauth-user": "^6.0.1", + "@octokit/auth-unauthenticated": "^7.0.2", + "@octokit/core": "^7.0.5", + "@octokit/oauth-authorization-url": "^8.0.0", + "@octokit/oauth-methods": "^6.0.1", + "@types/aws-lambda": "^8.10.83", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/oauth-authorization-url": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-8.0.0.tgz", + "integrity": "sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ==", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/oauth-methods": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-6.0.1.tgz", + "integrity": "sha512-xi6Iut3izMCFzXBJtxxJehxJmAKjE8iwj6L5+raPRwlTNKAbOOBJX7/Z8AF5apD4aXvc2skwIdOnC+CQ4QuA8Q==", + "license": "MIT", + "dependencies": { + "@octokit/oauth-authorization-url": "^8.0.0", + "@octokit/request": "^10.0.5", + "@octokit/request-error": "^7.0.1", + "@octokit/types": "^15.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-26.0.0.tgz", + "integrity": "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA==", + "license": "MIT" + }, + "node_modules/@octokit/openapi-webhooks-types": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/@octokit/openapi-webhooks-types/-/openapi-webhooks-types-12.0.3.tgz", + "integrity": "sha512-90MF5LVHjBedwoHyJsgmaFhEN1uzXyBDRLEBe7jlTYx/fEhPAk3P3DAJsfZwC54m8hAIryosJOL+UuZHB3K3yA==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-graphql": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-graphql/-/plugin-paginate-graphql-6.0.0.tgz", + "integrity": "sha512-crfpnIoFiBtRkvPqOyLOsw12XsveYuY2ieP6uYDosoUegBJpSVxGwut9sxUgFFcll3VTOTqpUf8yGd8x1OmAkQ==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-13.2.1.tgz", + "integrity": "sha512-Tj4PkZyIL6eBMYcG/76QGsedF0+dWVeLhYprTmuFVVxzDW7PQh23tM0TP0z+1MvSkxB29YFZwnUX+cXfTiSdyw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^15.0.1" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-16.1.1.tgz", + "integrity": "sha512-VztDkhM0ketQYSh5Im3IcKWFZl7VIrrsCaHbDINkdYeiiAsJzjhS2xRFCSJgfN6VOcsoW4laMtsmf3HcNqIimg==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^15.0.1" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-retry": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-8.0.2.tgz", + "integrity": "sha512-mVPCe77iaD8g1lIX46n9bHPUirFLzc3BfIzsZOpB7bcQh1ecS63YsAgcsyMGqvGa2ARQWKEFTrhMJX2MLJVHVw==", + "license": "MIT", + "dependencies": { + "@octokit/request-error": "^7.0.1", + "@octokit/types": "^15.0.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=7" + } + }, + "node_modules/@octokit/plugin-throttling": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-11.0.2.tgz", + "integrity": "sha512-ntNIig4zZhQVOZF4fG9Wt8QCoz9ehb+xnlUwp74Ic2ANChCk8oKmRwV9zDDCtrvU1aERIOvtng8wsalEX7Jk5Q==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^15.0.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": "^7.0.0" + } + }, + "node_modules/@octokit/request": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.5.tgz", + "integrity": "sha512-TXnouHIYLtgDhKo+N6mXATnDBkV05VwbR0TtMWpgTHIoQdRQfCSzmy/LGqR1AbRMbijq/EckC/E3/ZNcU92NaQ==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.1", + "@octokit/request-error": "^7.0.1", + "@octokit/types": "^15.0.0", + "fast-content-type-parse": "^3.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/request-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.1.tgz", + "integrity": "sha512-CZpFwV4+1uBrxu7Cw8E5NCXDWFNf18MSY23TdxCBgjw1tXXHvTrZVsXlW8hgFTOLw8RQR1BBrMvYRtuyaijHMA==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^15.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/types": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-15.0.1.tgz", + "integrity": "sha512-sdiirM93IYJ9ODDCBgmRPIboLbSkpLa5i+WLuXH8b8Atg+YMLAyLvDDhNWLV4OYd08tlvYfVm/dw88cqHWtw1Q==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^26.0.0" + } + }, + "node_modules/@octokit/webhooks": { + "version": "14.1.3", + "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-14.1.3.tgz", + "integrity": "sha512-gcK4FNaROM9NjA0mvyfXl0KPusk7a1BeA8ITlYEZVQCXF5gcETTd4yhAU0Kjzd8mXwYHppzJBWgdBVpIR9wUcQ==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-webhooks-types": "12.0.3", + "@octokit/request-error": "^7.0.0", + "@octokit/webhooks-methods": "^6.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/webhooks-methods": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-6.0.0.tgz", + "integrity": "sha512-MFlzzoDJVw/GcbfzVC1RLR36QqkTLUf79vLVO3D+xn7r0QgxnFoLZgtrzxiQErAjFUOdH6fas2KeQJ1yr/qaXQ==", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@reflink/reflink": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@reflink/reflink/-/reflink-0.1.19.tgz", + "integrity": "sha512-DmCG8GzysnCZ15bres3N5AHCmwBwYgp0As6xjhQ47rAUTUXxJiK+lLUxaGsX3hd/30qUpVElh05PbGuxRPgJwA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@reflink/reflink-darwin-arm64": "0.1.19", + "@reflink/reflink-darwin-x64": "0.1.19", + "@reflink/reflink-linux-arm64-gnu": "0.1.19", + "@reflink/reflink-linux-arm64-musl": "0.1.19", + "@reflink/reflink-linux-x64-gnu": "0.1.19", + "@reflink/reflink-linux-x64-musl": "0.1.19", + "@reflink/reflink-win32-arm64-msvc": "0.1.19", + "@reflink/reflink-win32-x64-msvc": "0.1.19" + } + }, + "node_modules/@reflink/reflink-darwin-arm64": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@reflink/reflink-darwin-arm64/-/reflink-darwin-arm64-0.1.19.tgz", + "integrity": "sha512-ruy44Lpepdk1FqDz38vExBY/PVUsjxZA+chd9wozjUH9JjuDT/HEaQYA6wYN9mf041l0yLVar6BCZuWABJvHSA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@reflink/reflink-darwin-x64": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@reflink/reflink-darwin-x64/-/reflink-darwin-x64-0.1.19.tgz", + "integrity": "sha512-By85MSWrMZa+c26TcnAy8SDk0sTUkYlNnwknSchkhHpGXOtjNDUOxJE9oByBnGbeuIE1PiQsxDG3Ud+IVV9yuA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@reflink/reflink-linux-arm64-gnu": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@reflink/reflink-linux-arm64-gnu/-/reflink-linux-arm64-gnu-0.1.19.tgz", + "integrity": "sha512-7P+er8+rP9iNeN+bfmccM4hTAaLP6PQJPKWSA4iSk2bNvo6KU6RyPgYeHxXmzNKzPVRcypZQTpFgstHam6maVg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@reflink/reflink-linux-arm64-musl": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@reflink/reflink-linux-arm64-musl/-/reflink-linux-arm64-musl-0.1.19.tgz", + "integrity": "sha512-37iO/Dp6m5DDaC2sf3zPtx/hl9FV3Xze4xoYidrxxS9bgP3S8ALroxRK6xBG/1TtfXKTvolvp+IjrUU6ujIGmA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@reflink/reflink-linux-x64-gnu": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@reflink/reflink-linux-x64-gnu/-/reflink-linux-x64-gnu-0.1.19.tgz", + "integrity": "sha512-jbI8jvuYCaA3MVUdu8vLoLAFqC+iNMpiSuLbxlAgg7x3K5bsS8nOpTRnkLF7vISJ+rVR8W+7ThXlXlUQ93ulkw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@reflink/reflink-linux-x64-musl": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@reflink/reflink-linux-x64-musl/-/reflink-linux-x64-musl-0.1.19.tgz", + "integrity": "sha512-e9FBWDe+lv7QKAwtKOt6A2W/fyy/aEEfr0g6j/hWzvQcrzHCsz07BNQYlNOjTfeytrtLU7k449H1PI95jA4OjQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@reflink/reflink-win32-arm64-msvc": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@reflink/reflink-win32-arm64-msvc/-/reflink-win32-arm64-msvc-0.1.19.tgz", + "integrity": "sha512-09PxnVIQcd+UOn4WAW73WU6PXL7DwGS6wPlkMhMg2zlHHG65F3vHepOw06HFCq+N42qkaNAc8AKIabWvtk6cIQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@reflink/reflink-win32-x64-msvc": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@reflink/reflink-win32-x64-msvc/-/reflink-win32-x64-msvc-0.1.19.tgz", + "integrity": "sha512-E//yT4ni2SyhwP8JRjVGWr3cbnhWDiPLgnQ66qqaanjjnMiu3O/2tjCPQXlcGc/DEYofpDc9fvhv6tALQsMV9w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tinyhttp/content-disposition": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@tinyhttp/content-disposition/-/content-disposition-2.2.2.tgz", + "integrity": "sha512-crXw1txzrS36huQOyQGYFvhTeLeG0Si1xu+/l6kXUVYpE0TjFjEZRqTbuadQLfKGZ0jaI+jJoRyqaWwxOSHW2g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "funding": { + "type": "individual", + "url": "https://github.com/tinyhttp/tinyhttp?sponsor=1" + } + }, + "node_modules/@types/aws-lambda": { + "version": "8.10.156", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.156.tgz", + "integrity": "sha512-LElQP+QliVWykC7OF8dNr04z++HJCMO2lF7k9HuKoSDARqhcjHq8MzbrRwujCSDeBHIlvaimbuY/tVZL36KXFQ==", + "license": "MIT" + }, + "node_modules/ansi-escapes": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz", + "integrity": "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "license": "MIT", + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/before-after-hook": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", + "license": "Apache-2.0" + }, + "node_modules/bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chmodrp": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/chmodrp/-/chmodrp-1.0.2.tgz", + "integrity": "sha512-TdngOlFV1FLTzU0o1w8MB6/BFywhtLC0SzRTGJU7T9lmdjlCWeMRt1iVo0Ki+ldwNk0BqNiKoc8xpLZEQ8mY1w==", + "license": "MIT" + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cmake-js": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/cmake-js/-/cmake-js-7.3.1.tgz", + "integrity": "sha512-aJtHDrTFl8qovjSSqXT9aC2jdGfmP8JQsPtjdLAXFfH1BF4/ImZ27Jx0R61TFg8Apc3pl6e2yBKMveAeRXx2Rw==", + "license": "MIT", + "dependencies": { + "axios": "^1.6.5", + "debug": "^4", + "fs-extra": "^11.2.0", + "memory-stream": "^1.0.0", + "node-api-headers": "^1.1.0", + "npmlog": "^6.0.2", + "rc": "^1.2.7", + "semver": "^7.5.4", + "tar": "^6.2.0", + "url-join": "^4.0.1", + "which": "^2.0.2", + "yargs": "^17.7.2" + }, + "bin": { + "cmake-js": "bin/cmake-js" + }, + "engines": { + "node": ">= 14.15.0" + } + }, + "node_modules/cmake-js/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/cmake-js/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/env-var": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/env-var/-/env-var-7.5.0.tgz", + "integrity": "sha512-mKZOzLRN0ETzau2W2QXefbFjo5EF4yWq28OyKb9ICdeNhHJlOE/pHHnz4hdYJ9cNZXcJHo5xN4OT4pzuSHSNvA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/fast-content-type-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", + "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/filename-reserved-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-3.0.0.tgz", + "integrity": "sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/filenamify": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-6.0.0.tgz", + "integrity": "sha512-vqIlNogKeyD3yzrm0yhRMQg8hOVwYcYRfjEoODd49iCprMn4HL85gK3HcykQE53EPIpX3HcAbGA5ELQv216dAQ==", + "license": "MIT", + "dependencies": { + "filename-reserved-regex": "^3.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-extra": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", + "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/gauge/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ipull": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/ipull/-/ipull-3.9.3.tgz", + "integrity": "sha512-ZMkxaopfwKHwmEuGDYx7giNBdLxbHbRCWcQVA1D2eqE4crUguupfxej6s7UqbidYEwT69dkyumYkY8DPHIxF9g==", + "license": "MIT", + "dependencies": { + "@tinyhttp/content-disposition": "^2.2.0", + "async-retry": "^1.3.3", + "chalk": "^5.3.0", + "ci-info": "^4.0.0", + "cli-spinners": "^2.9.2", + "commander": "^10.0.0", + "eventemitter3": "^5.0.1", + "filenamify": "^6.0.0", + "fs-extra": "^11.1.1", + "is-unicode-supported": "^2.0.0", + "lifecycle-utils": "^2.0.1", + "lodash.debounce": "^4.0.8", + "lowdb": "^7.0.1", + "pretty-bytes": "^6.1.0", + "pretty-ms": "^8.0.0", + "sleep-promise": "^9.1.0", + "slice-ansi": "^7.1.0", + "stdout-update": "^4.0.1", + "strip-ansi": "^7.1.0" + }, + "bin": { + "ipull": "dist/cli/cli.js" + }, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/ido-pluto/ipull?sponsor=1" + }, + "optionalDependencies": { + "@reflink/reflink": "^0.1.16" + } + }, + "node_modules/ipull/node_modules/lifecycle-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lifecycle-utils/-/lifecycle-utils-2.1.0.tgz", + "integrity": "sha512-AnrXnE2/OF9PHCyFg0RSqsnQTzV991XaZA/buhFDoc58xU7rhSCDgCz/09Lqpsn4MpoPHt7TRAXV1kWZypFVsA==", + "license": "MIT" + }, + "node_modules/ipull/node_modules/parse-ms": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-3.0.0.tgz", + "integrity": "sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ipull/node_modules/pretty-ms": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-8.0.0.tgz", + "integrity": "sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q==", + "license": "MIT", + "dependencies": { + "parse-ms": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/lifecycle-utils": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lifecycle-utils/-/lifecycle-utils-3.0.1.tgz", + "integrity": "sha512-Qt/Jl5dsNIsyCAZsHB6x3mbwHFn0HJbdmvF49sVX/bHgX2cW7+G+U+I67Zw+TPM1Sr21Gb2nfJMd2g6iUcI1EQ==", + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lowdb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-7.0.1.tgz", + "integrity": "sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==", + "license": "MIT", + "dependencies": { + "steno": "^4.0.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memory-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/memory-stream/-/memory-stream-1.0.0.tgz", + "integrity": "sha512-Wm13VcsPIMdG96dzILfij09PvuS3APtcKNh7M28FsCA/w6+1mjR7hhPmfFNoilX9xU7wTdhsH5lJAm6XNzdtww==", + "license": "MIT", + "dependencies": { + "readable-stream": "^3.4.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", + "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-api-headers": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-api-headers/-/node-api-headers-1.6.0.tgz", + "integrity": "sha512-81T99+mWLZnxX0LlZPYuafyFlxVVaWKQ0BDAbSrOqLO+v+gzCzu0GTAVNeVK8lucqjqo9L/1UcK9cpkem8Py4Q==", + "license": "MIT" + }, + "node_modules/node-llama-cpp": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/node-llama-cpp/-/node-llama-cpp-3.14.0.tgz", + "integrity": "sha512-k6scfmM2zAQVqDAn3HYB+dq/Dl7Mj7SQyPv7TVRZxS7rbkl+P8u44MKXYYkzdU2GD5TcWdkApJ1Facu5Pzlqxg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@huggingface/jinja": "^0.5.1", + "async-retry": "^1.3.3", + "bytes": "^3.1.2", + "chalk": "^5.4.1", + "chmodrp": "^1.0.2", + "cmake-js": "^7.3.1", + "cross-spawn": "^7.0.6", + "env-var": "^7.5.0", + "filenamify": "^6.0.0", + "fs-extra": "^11.3.0", + "ignore": "^7.0.4", + "ipull": "^3.9.2", + "is-unicode-supported": "^2.1.0", + "lifecycle-utils": "^3.0.1", + "log-symbols": "^7.0.0", + "nanoid": "^5.1.5", + "node-addon-api": "^8.3.1", + "octokit": "^5.0.3", + "ora": "^8.2.0", + "pretty-ms": "^9.2.0", + "proper-lockfile": "^4.1.2", + "semver": "^7.7.1", + "simple-git": "^3.27.0", + "slice-ansi": "^7.1.0", + "stdout-update": "^4.0.1", + "strip-ansi": "^7.1.0", + "validate-npm-package-name": "^6.0.0", + "which": "^5.0.0", + "yargs": "^17.7.2" + }, + "bin": { + "nlc": "dist/cli/cli.js", + "node-llama-cpp": "dist/cli/cli.js" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/giladgd" + }, + "optionalDependencies": { + "@node-llama-cpp/linux-arm64": "3.14.0", + "@node-llama-cpp/linux-armv7l": "3.14.0", + "@node-llama-cpp/linux-x64": "3.14.0", + "@node-llama-cpp/linux-x64-cuda": "3.14.0", + "@node-llama-cpp/linux-x64-cuda-ext": "3.14.0", + "@node-llama-cpp/linux-x64-vulkan": "3.14.0", + "@node-llama-cpp/mac-arm64-metal": "3.14.0", + "@node-llama-cpp/mac-x64": "3.14.0", + "@node-llama-cpp/win-arm64": "3.14.0", + "@node-llama-cpp/win-x64": "3.14.0", + "@node-llama-cpp/win-x64-cuda": "3.14.0", + "@node-llama-cpp/win-x64-cuda-ext": "3.14.0", + "@node-llama-cpp/win-x64-vulkan": "3.14.0" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/octokit": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/octokit/-/octokit-5.0.4.tgz", + "integrity": "sha512-4n/mMoLQs2npBE+aTG5o4H+hZhFKu8aDqZFP/nmUNRUYrTpXpaqvX1ppK5eiCtQ+uP/8jI6vbdfCB2udlBgccA==", + "license": "MIT", + "dependencies": { + "@octokit/app": "^16.1.1", + "@octokit/core": "^7.0.5", + "@octokit/oauth-app": "^8.0.2", + "@octokit/plugin-paginate-graphql": "^6.0.0", + "@octokit/plugin-paginate-rest": "^13.2.0", + "@octokit/plugin-rest-endpoint-methods": "^16.1.0", + "@octokit/plugin-retry": "^8.0.2", + "@octokit/plugin-throttling": "^11.0.2", + "@octokit/request-error": "^7.0.1", + "@octokit/types": "^15.0.0", + "@octokit/webhooks": "^14.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openai": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.7.0.tgz", + "integrity": "sha512-mgSQXa3O/UXTbA8qFzoa7aydbXBJR5dbLQXCRapAOtoNT+v69sLdKMZzgiakpqhclRnhPggPAXoniVGn2kMY2A==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/ora/node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-bytes": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", + "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", + "license": "MIT", + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/simple-git": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.28.0.tgz", + "integrity": "sha512-Rs/vQRwsn1ILH1oBUy8NucJlXmnnLeLCfcvbSehkPzbv3wwoFWIdtfd6Ndo6ZPhlPsCZ60CPI4rxurnwAa+a2w==", + "license": "MIT", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, + "node_modules/sleep-promise": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/sleep-promise/-/sleep-promise-9.1.0.tgz", + "integrity": "sha512-UHYzVpz9Xn8b+jikYSD6bqvf754xL2uBUzDFwiU6NcdZeifPr6UfgU43xpkPu67VMS88+TI2PSI7Eohgqf2fKA==", + "license": "MIT" + }, + "node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stdout-update": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/stdout-update/-/stdout-update-4.0.1.tgz", + "integrity": "sha512-wiS21Jthlvl1to+oorePvcyrIkiG/6M3D3VTmDUlJm7Cy6SbFhKkAvX+YBuHLxck/tO3mrdpC/cNesigQc3+UQ==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^6.2.0", + "ansi-styles": "^6.2.1", + "string-width": "^7.1.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/stdout-update/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/stdout-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/steno": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/steno/-/steno-4.0.2.tgz", + "integrity": "sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/universal-github-app-jwt": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-2.2.2.tgz", + "integrity": "sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw==", + "license": "MIT" + }, + "node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "license": "ISC" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/validate-npm-package-name": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.2.tgz", + "integrity": "sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000000000000000000000000000000000000..3e58a4c74d767b45d08e1d00cff4e38dec46c8ea --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "ai-agents", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "type": "module", + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "dotenv": "^17.2.3", + "node-llama-cpp": "^3.14.0", + "openai": "^6.7.0" + } +} diff --git a/run_classifier.js b/run_classifier.js new file mode 100644 index 0000000000000000000000000000000000000000..29302d9460aea80fcdc7dd4e089435b3929790dd --- /dev/null +++ b/run_classifier.js @@ -0,0 +1,349 @@ +/** + * Part 1 Capstone Solution: Smart Email Classifier + * + * Build an AI system that organizes your inbox by classifying emails into categories. + * + * Skills Used: + * - Runnables for processing pipeline + * - Messages for structured classification + * - LLM wrapper for flexible model switching + * - Context for classification history + * + * Difficulty: ⭐⭐☆☆☆ + */ + +import { SystemMessage, HumanMessage, Runnable, LlamaCppLLM } from './src/index.js'; +import { BaseCallback } from './src/utils/callbacks.js'; +import { readFileSync } from 'fs'; + +// ============================================================================ +// EMAIL CLASSIFICATION CATEGORIES +// ============================================================================ + +const CATEGORIES = { + SPAM: 'Spam', + INVOICE: 'Invoice', + MEETING: 'Meeting Request', + URGENT: 'Urgent', + PERSONAL: 'Personal', + OTHER: 'Other' +}; + +// ============================================================================ +// Email Parser Runnable +// ============================================================================ + +/** + * Parses raw email text into structured format + * + * Input: { subject: string, body: string, from: string } + * Output: { subject, body, from, timestamp } + */ +class EmailParserRunnable extends Runnable { + async _call(input, config) { + // Validate required fields + if (!input.subject || !input.body || !input.from) { + throw new Error('Email must have subject, body, and from fields'); + } + + // Parse and structure the email + return { + subject: input.subject.trim(), + body: input.body.trim(), + from: input.from.trim(), + timestamp: new Date().toISOString() + }; + } +} + +// ============================================================================ +// Email Classifier Runnable +// ============================================================================ + +/** + * Classifies email using LLM + * + * Input: { subject, body, from, timestamp } + * Output: { ...email, category, confidence, reason } + */ +class EmailClassifierRunnable extends Runnable { + constructor(llm) { + super(); + this.llm = llm; + } + + async _call(input, config) { + // Build the classification prompt + const messages = this._buildPrompt(input); + + // Call the LLM + const response = await this.llm.invoke(messages, config); + + // Parse the LLM response + const classification = this._parseClassification(response.content); + + // Return email with classification + return { + ...input, + category: classification.category, + confidence: classification.confidence, + reason: classification.reason + }; + } + + _buildPrompt(email) { + const systemPrompt = new SystemMessage(`You are an email classification assistant. Your task is to classify emails into one of these categories: + +Categories: +- Spam: Unsolicited promotional emails, advertisements with excessive punctuation/caps, phishing attempts, scams +- Invoice: Bills, payment requests, financial documents, receipts +- Meeting Request: Meeting invitations, calendar requests, scheduling, availability inquiries +- Urgent: Time-sensitive matters requiring immediate attention, security alerts, critical notifications +- Personal: Personal correspondence from friends/family (look for personal tone and familiar email addresses) +- Other: Legitimate newsletters, updates, informational content, everything else that doesn't fit above + +Important distinctions: +- Legitimate newsletters (tech updates, subscriptions) should be "Other", not Spam +- Spam has excessive punctuation (!!!, ALL CAPS), pushy language, or suspicious intent +- Personal emails have familiar sender addresses and casual tone + +Respond in this exact JSON format: +{ + "category": "Category Name", + "confidence": 0.95, + "reason": "Brief explanation" +} + +Confidence should be between 0 and 1.`); + + const userPrompt = new HumanMessage(`Classify this email: + +From: ${email.from} +Subject: ${email.subject} +Body: ${email.body} + +Provide your classification in JSON format.`); + + return [systemPrompt, userPrompt]; + } + + _parseClassification(response) { + try { + // Try to find JSON in the response + const jsonMatch = response.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + throw new Error('No JSON found in response'); + } + + const parsed = JSON.parse(jsonMatch[0]); + + // Validate the parsed response + if (!parsed.category || parsed.confidence === undefined || !parsed.reason) { + throw new Error('Invalid classification format'); + } + + // Ensure confidence is a number between 0 and 1 + const confidence = Math.max(0, Math.min(1, parseFloat(parsed.confidence))); + + return { + category: parsed.category, + confidence: confidence, + reason: parsed.reason + }; + } catch (error) { + // Fallback classification if parsing fails + console.warn('Failed to parse LLM response, using fallback:', error.message); + return { + category: CATEGORIES.OTHER, + confidence: 0.5, + reason: 'Failed to parse classification' + }; + } + } +} + +// ============================================================================ +// Classification History Callback +// ============================================================================ + +/** + * Tracks classification history using callbacks + */ +class ClassificationHistoryCallback extends BaseCallback { + constructor() { + super(); + this.history = []; + } + + async onEnd(runnable, output, config) { + // Only track EmailClassifierRunnable results + if (runnable.name === 'EmailClassifierRunnable' && output.category) { + this.history.push({ + timestamp: output.timestamp, + from: output.from, + subject: output.subject, + category: output.category, + confidence: output.confidence, + reason: output.reason + }); + } + } + + getHistory() { + return this.history; + } + + getStatistics() { + if (this.history.length === 0) { + return { + total: 0, + byCategory: {}, + averageConfidence: 0 + }; + } + + // Count by category + const byCategory = {}; + let totalConfidence = 0; + + for (const entry of this.history) { + byCategory[entry.category] = (byCategory[entry.category] || 0) + 1; + totalConfidence += entry.confidence; + } + + return { + total: this.history.length, + byCategory: byCategory, + averageConfidence: totalConfidence / this.history.length + }; + } + + printHistory() { + console.log('\n📧 Classification History:'); + console.log('─'.repeat(70)); + + for (const entry of this.history) { + console.log(`\n✉️ From: ${entry.from}`); + console.log(` Subject: ${entry.subject}`); + console.log(` Category: ${entry.category}`); + console.log(` Confidence: ${(entry.confidence * 100).toFixed(1)}%`); + console.log(` Reason: ${entry.reason}`); + } + } + + printStatistics() { + const stats = this.getStatistics(); + + console.log('\n📊 Classification Statistics:'); + console.log('─'.repeat(70)); + console.log(`Total Emails: ${stats.total}\n`); + + if (stats.total > 0) { + console.log('By Category:'); + for (const [category, count] of Object.entries(stats.byCategory)) { + const percentage = ((count / stats.total) * 100).toFixed(1); + console.log(` ${category}: ${count} (${percentage}%)`); + } + + console.log(`\nAverage Confidence: ${(stats.averageConfidence * 100).toFixed(1)}%`); + } + } +} + +// ============================================================================ +// Email Classification Pipeline +// ============================================================================ + +/** + * Complete pipeline: Parse → Classify → Store + */ +class EmailClassificationPipeline { + constructor(llm) { + this.parser = new EmailParserRunnable(); + this.classifier = new EmailClassifierRunnable(llm); + this.historyCallback = new ClassificationHistoryCallback(); + + // Build the pipeline: parser -> classifier + this.pipeline = this.parser.pipe(this.classifier); + } + + async classify(email) { + // Run the email through the pipeline with history callback + const config = { + callbacks: [this.historyCallback] + }; + + return await this.pipeline.invoke(email, config); + } + + getHistory() { + return this.historyCallback.getHistory(); + } + + getStatistics() { + return this.historyCallback.getStatistics(); + } + + printHistory() { + this.historyCallback.printHistory(); + } + + printStatistics() { + this.historyCallback.printStatistics(); + } +} + +// ============================================================================ +// TEST DATA +// ============================================================================ + +const TEST_EMAILS = JSON.parse( + readFileSync(new URL('./test-emails.json', import.meta.url), 'utf-8') +); + +// ============================================================================ +// MAIN FUNCTION +// ============================================================================ + +async function main() { + console.log('=== Part 1 Capstone: Smart Email Classifier ===\n'); + + // Initialize the LLM + const llm = new LlamaCppLLM({ + modelPath: './models/Qwen3-1.7B-Q8_0.gguf', // Adjust to your model + temperature: 0.1, // Low temperature for consistent classification + maxTokens: 200 + }); + + // Create the classification pipeline + const pipeline = new EmailClassificationPipeline(llm); + + console.log('📬 Processing emails...\n'); + + // Classify each test email + for (const email of TEST_EMAILS) { + try { + const result = await pipeline.classify(email); + + console.log(`✉️ Email from: ${result.from}`); + console.log(` Subject: ${result.subject}`); + console.log(` Category: ${result.category}`); + console.log(` Confidence: ${(result.confidence * 100).toFixed(1)}%`); + console.log(` Reason: ${result.reason}\n`); + } catch (error) { + console.error(`❌ Error classifying email from ${email.from}:`, error.message); + } + } + + // Print history and statistics + pipeline.printHistory(); + pipeline.printStatistics(); + + // Cleanup + await llm.dispose(); + + console.log('\n✓ Capstone Project Complete!'); +} + +// Run the project +main().catch(console.error); diff --git a/secrets.local.md b/secrets.local.md new file mode 100644 index 0000000000000000000000000000000000000000..5edb724bb1c9934cb33a1e7f4896e7d44fc8f167 --- /dev/null +++ b/secrets.local.md @@ -0,0 +1,22 @@ +# 🔐 ANTIGRAVITY SECURE VAULT (LOCAL ONLY) + +> [!CAUTION] +> **KHÔNG BAO GIỜ PUSH FILE NÀY LÊN GITHUB.** +> File này chứa thông tin nhạy cảm. Đã được cấu hình để Git bỏ qua. + +## 🔑 GITHUB TOKENS +- **dahanhstudio**: `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` +- **NungLon01**: `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` +- **lenzcomvth**: `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` + +--- + +## ☁️ CLOUDFLARE TOKENS +- **API Token (Workers)**: `UE6zJ6_3uwlSZbrqhxeJieEnsHO01frIjxRyBlA7` + + +##HuggingFace TOKEN : +- ** lenzcom account ** : `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` +--- +*Generated by Antigravity Secure Protocol* + diff --git a/server.js b/server.js new file mode 100644 index 0000000000000000000000000000000000000000..f70c63f1c30a26200425718a759ed7129c064bff --- /dev/null +++ b/server.js @@ -0,0 +1,80 @@ +import express from 'express'; +import { SystemMessage, HumanMessage, Runnable, LlamaCppLLM } from '../src/index.js'; +import bodyParser from 'body-parser'; + +// ============================================================================ +// COPIED LOGIC FROM SOLUTION (Simplification for API) +// ============================================================================ + +class EmailClassifierRunnable extends Runnable { + constructor(llm) { + super(); + this.llm = llm; + } + + async _call(input, config) { + const messages = this._buildPrompt(input); + const response = await this.llm.invoke(messages, config); + return this._parseClassification(response.content); + } + + _buildPrompt(email) { + return [ + new SystemMessage(`You are an email classification assistant. Classify into: Spam, Invoice, Meeting Request, Urgent, Personal, Other. + Respond in strict JSON format: {"category": "Name", "confidence": 0.95, "reason": "Explanation"}`), + new HumanMessage(`Classify this email:\nSubject: ${email.subject}\nBody: ${email.body}`) + ]; + } + + _parseClassification(response) { + try { + const jsonMatch = response.match(/\{[\s\S]*\}/); + if (!jsonMatch) throw new Error('No JSON found'); + return JSON.parse(jsonMatch[0]); + } catch (error) { + return { category: 'Other', confidence: 0.0, reason: 'Failed to parse', raw: response }; + } + } +} + +// ============================================================================ +// SERVER SETUP +// ============================================================================ + +const app = express(); +const PORT = 7860; // Standard port for Hugging Face Spaces + +app.use(bodyParser.json()); + +// Initialize LLM Global +console.log("Loading model..."); +const llm = new LlamaCppLLM({ + modelPath: './models/Qwen3-1.7B-Q8_0.gguf', + temperature: 0.1, + maxTokens: 200 +}); +const classifier = new EmailClassifierRunnable(llm); +console.log("Model loaded!"); + +app.post('/classify', async (req, res) => { + try { + const { subject, body, from } = req.body; + if (!subject || !body) { + return res.status(400).json({ error: 'Missing subject or body' }); + } + + const result = await classifier.invoke({ subject, body, from: from || 'unknown' }); + res.json(result); + } catch (error) { + console.error(error); + res.status(500).json({ error: error.message }); + } +}); + +app.get('/', (req, res) => { + res.send('AI Agent Email Classifier is Running. POST to /classify to use.'); +}); + +app.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); +}); diff --git a/src/agents/agent-executor.js b/src/agents/agent-executor.js new file mode 100644 index 0000000000000000000000000000000000000000..034485727cb4eb31c5351da8122ddc80e2f15664 --- /dev/null +++ b/src/agents/agent-executor.js @@ -0,0 +1,18 @@ +/** + * AgentExecutor + * + * Main agent execution loop + * + * @module src/agents/agent-executor.js + */ + +export class AgentExecutor { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('AgentExecutor not yet implemented'); + } + + // TODO: Add methods +} + +export default AgentExecutor; diff --git a/src/agents/base-agent.js b/src/agents/base-agent.js new file mode 100644 index 0000000000000000000000000000000000000000..97b9c87c23426f5a355b29330485a161674dc558 --- /dev/null +++ b/src/agents/base-agent.js @@ -0,0 +1,18 @@ +/** + * BaseAgent + * + * Abstract agent class + * + * @module src/agents/base-agent.js + */ + +export class BaseAgent { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('BaseAgent not yet implemented'); + } + + // TODO: Add methods +} + +export default BaseAgent; diff --git a/src/agents/conversational-agent.js b/src/agents/conversational-agent.js new file mode 100644 index 0000000000000000000000000000000000000000..77b7c2807e79839006c8400379142670aad30cce --- /dev/null +++ b/src/agents/conversational-agent.js @@ -0,0 +1,18 @@ +/** + * ConversationalAgent + * + * Chat-optimized agent + * + * @module src/agents/conversational-agent.js + */ + +export class ConversationalAgent { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('ConversationalAgent not yet implemented'); + } + + // TODO: Add methods +} + +export default ConversationalAgent; diff --git a/src/agents/index.js b/src/agents/index.js new file mode 100644 index 0000000000000000000000000000000000000000..6098e4a68541251e55306d0b9d258a33c55f7206 --- /dev/null +++ b/src/agents/index.js @@ -0,0 +1,13 @@ +/** + * Module exports + * + * @module src/agents/index.js + */ + +export { BaseAgent } from './base-agent.js'; +export { AgentExecutor } from './agent-executor.js'; +export { ToolCallingAgent } from './tool-calling-agent.js'; +export { ReActAgent } from './react-agent.js'; +export { StructuredChatAgent } from './structured-chat-agent.js'; +export { ConversationalAgent } from './conversational-agent.js'; + diff --git a/src/agents/react-agent.js b/src/agents/react-agent.js new file mode 100644 index 0000000000000000000000000000000000000000..071a9d0ba81958432a30ea793eb16c7712320125 --- /dev/null +++ b/src/agents/react-agent.js @@ -0,0 +1,18 @@ +/** + * ReActAgent + * + * Agent implementing ReAct pattern + * + * @module src/agents/react-agent.js + */ + +export class ReActAgent { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('ReActAgent not yet implemented'); + } + + // TODO: Add methods +} + +export default ReActAgent; diff --git a/src/agents/structured-chat-agent.js b/src/agents/structured-chat-agent.js new file mode 100644 index 0000000000000000000000000000000000000000..8a62414e848e8334df6466d21846debe1b462746 --- /dev/null +++ b/src/agents/structured-chat-agent.js @@ -0,0 +1,18 @@ +/** + * StructuredChatAgent + * + * Agent with structured JSON output + * + * @module src/agents/structured-chat-agent.js + */ + +export class StructuredChatAgent { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('StructuredChatAgent not yet implemented'); + } + + // TODO: Add methods +} + +export default StructuredChatAgent; diff --git a/src/agents/tool-calling-agent.js b/src/agents/tool-calling-agent.js new file mode 100644 index 0000000000000000000000000000000000000000..334c0237f4b54afe4b9734a97147e321c42f41ea --- /dev/null +++ b/src/agents/tool-calling-agent.js @@ -0,0 +1,18 @@ +/** + * ToolCallingAgent + * + * Agent using function calling + * + * @module src/agents/tool-calling-agent.js + */ + +export class ToolCallingAgent { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('ToolCallingAgent not yet implemented'); + } + + // TODO: Add methods +} + +export default ToolCallingAgent; diff --git a/src/chains/base-chain.js b/src/chains/base-chain.js new file mode 100644 index 0000000000000000000000000000000000000000..b9a455e9201b5e04fdfe46f17d6b056ffeeaf316 --- /dev/null +++ b/src/chains/base-chain.js @@ -0,0 +1,18 @@ +/** + * BaseChain + * + * Abstract chain class + * + * @module src/chains/base-chain.js + */ + +export class BaseChain { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('BaseChain not yet implemented'); + } + + // TODO: Add methods +} + +export default BaseChain; diff --git a/src/chains/index.js b/src/chains/index.js new file mode 100644 index 0000000000000000000000000000000000000000..3ed24f912aa617d2d44422e76c609f9781b611f6 --- /dev/null +++ b/src/chains/index.js @@ -0,0 +1,13 @@ +/** + * Module exports + * + * @module src/chains/index.js + */ + +export { BaseChain } from './base-chain.js'; +export { LLMChain } from './llm-chain.js'; +export { SequentialChain } from './sequential-chain.js'; +export { RouterChain } from './router-chain.js'; +export { MapReduceChain } from './map-reduce-chain.js'; +export { TransformChain } from './transform-chain.js'; + diff --git a/src/chains/llm-chain.js b/src/chains/llm-chain.js new file mode 100644 index 0000000000000000000000000000000000000000..21b1ca5d6ea76ef9fec9b276027bb29c06c529e8 --- /dev/null +++ b/src/chains/llm-chain.js @@ -0,0 +1,18 @@ +/** + * LLMChain + * + * Basic chain: Prompt -> LLM -> Parser + * + * @module src/chains/llm-chain.js + */ + +export class LLMChain { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('LLMChain not yet implemented'); + } + + // TODO: Add methods +} + +export default LLMChain; diff --git a/src/chains/map-reduce-chain.js b/src/chains/map-reduce-chain.js new file mode 100644 index 0000000000000000000000000000000000000000..a2627106e2b132d3b96cfaddb8bb9d1b6ffd8371 --- /dev/null +++ b/src/chains/map-reduce-chain.js @@ -0,0 +1,18 @@ +/** + * MapReduceChain + * + * Parallel processing with aggregation + * + * @module src/chains/map-reduce-chain.js + */ + +export class MapReduceChain { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('MapReduceChain not yet implemented'); + } + + // TODO: Add methods +} + +export default MapReduceChain; diff --git a/src/chains/router-chain.js b/src/chains/router-chain.js new file mode 100644 index 0000000000000000000000000000000000000000..4f35cee68a65fb7c2712c9fe0fd866151833347b --- /dev/null +++ b/src/chains/router-chain.js @@ -0,0 +1,18 @@ +/** + * RouterChain + * + * Routes to different chains based on input + * + * @module src/chains/router-chain.js + */ + +export class RouterChain { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('RouterChain not yet implemented'); + } + + // TODO: Add methods +} + +export default RouterChain; diff --git a/src/chains/sequential-chain.js b/src/chains/sequential-chain.js new file mode 100644 index 0000000000000000000000000000000000000000..a623edf360f1598a3ee203aa6b57e4f960dcdbb9 --- /dev/null +++ b/src/chains/sequential-chain.js @@ -0,0 +1,18 @@ +/** + * SequentialChain + * + * Executes chains in sequence + * + * @module src/chains/sequential-chain.js + */ + +export class SequentialChain { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('SequentialChain not yet implemented'); + } + + // TODO: Add methods +} + +export default SequentialChain; diff --git a/src/chains/transform-chain.js b/src/chains/transform-chain.js new file mode 100644 index 0000000000000000000000000000000000000000..0b74367ac69f17efca9ab60e83046818b6db8300 --- /dev/null +++ b/src/chains/transform-chain.js @@ -0,0 +1,18 @@ +/** + * TransformChain + * + * Pure data transformation + * + * @module src/chains/transform-chain.js + */ + +export class TransformChain { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('TransformChain not yet implemented'); + } + + // TODO: Add methods +} + +export default TransformChain; diff --git a/src/core/context.js b/src/core/context.js new file mode 100644 index 0000000000000000000000000000000000000000..0fc2b23f7fc7d2c38db0d6477bc57dd0921a27de --- /dev/null +++ b/src/core/context.js @@ -0,0 +1,47 @@ +/** + * RunnableConfig + * + * Configuration object passed through runnable chains + * + * @module src/core/context.js + */ +export class RunnableConfig { + constructor(options = {}) { + // Callbacks for monitoring + this.callbacks = options.callbacks || []; + + // Metadata (arbitrary data) + this.metadata = options.metadata || {}; + + // Tags for filtering/organization + this.tags = options.tags || []; + + // Recursion limit (prevent infinite loops) + this.recursionLimit = options.recursionLimit ?? 25; + + // Runtime overrides for generation parameters + this.configurable = options.configurable || {}; + } + + /** + * Merge with another config (child inherits from parent) + */ + merge(other) { + return new RunnableConfig({ + callbacks: [...this.callbacks, ...(other.callbacks || [])], + metadata: { ...this.metadata, ...(other.metadata || {}) }, + tags: [...this.tags, ...(other.tags || [])], + recursionLimit: other.recursionLimit ?? this.recursionLimit, + configurable: { ...this.configurable, ...(other.configurable || {}) } + }); + } + + /** + * Create a child config with additional settings + */ + child(options = {}) { + return this.merge(new RunnableConfig(options)); + } +} + +export default RunnableConfig; diff --git a/src/core/index.js b/src/core/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f17412e050495cc966b649c5cd7f4c59c2b9d05c --- /dev/null +++ b/src/core/index.js @@ -0,0 +1,11 @@ +/** + * Module exports + * + * @module src/core/index.js + */ + +export { Runnable, RunnableSequence } from './runnable.js'; +export { RunnableParallel } from './runnable-parallel.js'; +export { BaseMessage, HumanMessage, AIMessage, SystemMessage, ToolMessage } from './message.js'; +export { RunnableConfig } from './context.js'; + diff --git a/src/core/message.js b/src/core/message.js new file mode 100644 index 0000000000000000000000000000000000000000..7b4014b5c1e7de3de2a367e3de6667cbf0455620 --- /dev/null +++ b/src/core/message.js @@ -0,0 +1,284 @@ +/** + * Message System - Typed conversation data structures + * + * Implementation similar to LangChain.js message system + * + * @module src/core/message.js + */ + +/** + * 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}`; + } +} + +/** + * 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 + }; + } +} + +/** + * 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 + }; + } +} + +/** + * 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; + } +} + +/** + * 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 + }; + } +} + +/** + * Registry mapping type strings to message classes + */ +export const MESSAGE_TYPES = { + 'system': SystemMessage, + 'human': HumanMessage, + 'ai': AIMessage, + 'tool': ToolMessage +}; + +/** + * Helper function to convert messages to prompt format + * + * @param {Array} messages - Array of messages + * @returns {Array} Messages in LLM format + */ +export function messagesToPromptFormat(messages) { + return messages.map(msg => msg.toPromptFormat()); +} + +/** + * Helper function to filter messages by type + * + * @param {Array} messages - Array of messages + * @param {string} type - Message type to filter + * @returns {Array} Filtered messages + */ +export function filterMessagesByType(messages, type) { + return messages.filter(msg => msg._type === type); +} + +/** + * Helper function to get the last N messages + * + * @param {Array} messages - Array of messages + * @param {number} n - Number of messages to get + * @returns {Array} Last N messages + */ +export function getLastMessages(messages, n) { + return messages.slice(-n); +} + +/** + * Helper to merge consecutive messages of the same type + * Useful for reducing token count + * + * @param {Array} messages - Array of messages + * @returns {Array} Merged messages + */ +export function mergeConsecutiveMessages(messages) { + if (messages.length === 0) return []; + + const merged = [messages[0]]; + + for (let i = 1; i < messages.length; i++) { + const current = messages[i]; + const last = merged[merged.length - 1]; + + // Merge if same type and both are strings + if ( + current._type === last._type && + typeof current.content === 'string' && + typeof last.content === 'string' && + current._type !== 'tool' // Don't merge tool messages + ) { + // Create new merged message + const MessageClass = MESSAGE_CLASSES[current._type]; + const mergedContent = last.content + '\n' + current.content; + merged[merged.length - 1] = new MessageClass(mergedContent, { + name: last.name, + additionalKwargs: { ...last.additionalKwargs, merged: true } + }); + } else { + merged.push(current); + } + } + + return merged; +} + +export default { + BaseMessage, + SystemMessage, + HumanMessage, + AIMessage, + ToolMessage, + MESSAGE_TYPES +}; \ No newline at end of file diff --git a/src/core/runnable-parallel.js b/src/core/runnable-parallel.js new file mode 100644 index 0000000000000000000000000000000000000000..8077cc47f4e76ac717ac490db7a63d0486687ef2 --- /dev/null +++ b/src/core/runnable-parallel.js @@ -0,0 +1,18 @@ +/** + * RunnableParallel + * + * Executes multiple runnables in parallel + * + * @module src/core/runnable-parallel.js + */ + +export class RunnableParallel { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('RunnableParallel not yet implemented'); + } + + // TODO: Add methods +} + +export default RunnableParallel; diff --git a/src/core/runnable.js b/src/core/runnable.js new file mode 100644 index 0000000000000000000000000000000000000000..f1a35a16324d5707026f22843aea706f23c9beaf --- /dev/null +++ b/src/core/runnable.js @@ -0,0 +1,148 @@ +import RunnableConfig from "./context.js"; +import {CallbackManager} from "../utils/index.js"; + +/** + * Runnable - Base class for all composable components + * + * Every Runnable must implement the _call() method. + * This base class provides invoke, stream, batch, and pipe. + */ +export class Runnable { + constructor() { + this.name = this.constructor.name; + } + + /** + * Main execution method - processes a single input + * + * @param {any} input - The input to process + * @param {Object} config - Optional configuration + * @returns {Promise} The processed output + */ + async invoke(input, config = {}) { + // Normalize config to RunnableConfig instance + const runnableConfig = config instanceof RunnableConfig + ? config + : new RunnableConfig(config); + + // Create callback manager + const callbackManager = new CallbackManager(runnableConfig.callbacks); + + try { + // Notify callbacks: starting + await callbackManager.handleStart(this, input, runnableConfig); + + // Execute the runnable + const output = await this._call(input, runnableConfig); + + // Notify callbacks: success + await callbackManager.handleEnd(this, output, runnableConfig); + + return output; + } catch (error) { + // Notify callbacks: error + await callbackManager.handleError(this, error, runnableConfig); + throw error; + } + } + + /** + * Internal method that subclasses must implement + * + * @param {any} input - The input to process + * @param {Object} config - Optional configuration + * @returns {Promise} The processed output + */ + async _call(input, config) { + throw new Error( + `${this.name} must implement _call() method` + ); + } + + /** + * Stream output in chunks + * + * @param {any} input - The input to process + * @param {Object} config - Optional configuration + * @yields {any} Output chunks + */ + async* stream(input, config = {}) { + // Default implementation: just yield the full result + // Subclasses can override for true streaming + const result = await this.invoke(input, config); + yield result; + } + + /** + * Internal streaming method for subclasses + * Override this for custom streaming behavior + */ + async* _stream(input, config) { + yield await this._call(input, config); + } + + /** + * Process multiple inputs in parallel + * + * @param {Array} inputs - Array of inputs to process + * @param {Object} config - Optional configuration + * @returns {Promise>} Array of outputs + */ + async batch(inputs, config = {}) { + // Use Promise.all for parallel execution + return await Promise.all( + inputs.map(input => this.invoke(input, config)) + ); + } + + /** + * Compose this Runnable with another + * Creates a new Runnable that runs both in sequence + * + * @param {Runnable} other - The Runnable to pipe to + * @returns {RunnableSequence} A new composed Runnable + */ + pipe(other) { + return new RunnableSequence([this, other]); + } +} + +/** + * RunnableSequence - Chains multiple Runnables together + * + * Output of one becomes input of the next + */ +export class RunnableSequence extends Runnable { + constructor(steps) { + super(); + this.steps = steps; // Array of Runnables + } + + async _call(input, config) { + let output = input; + + // Run through each step sequentially + for (const step of this.steps) { + output = await step.invoke(output, config); + } + + return output; + } + + async *_stream(input, config) { + let output = input; + + // Stream through all steps + for (let i = 0; i < this.steps.length - 1; i++) { + output = await this.steps[i].invoke(output, config); + } + + // Only stream the last step + yield* this.steps[this.steps.length - 1].stream(output, config); + } + + // pipe() returns a new sequence with the added step + pipe(other) { + return new RunnableSequence([...this.steps, other]); + } +} \ No newline at end of file diff --git a/src/graph/checkpoint.js b/src/graph/checkpoint.js new file mode 100644 index 0000000000000000000000000000000000000000..4b6fc8fea9bfd70843ffeb7b071ac9bcc308c2be --- /dev/null +++ b/src/graph/checkpoint.js @@ -0,0 +1,18 @@ +/** + * Checkpoint + * + * State checkpoint manager + * + * @module src/graph/checkpoint.js + */ + +export class Checkpoint { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('Checkpoint not yet implemented'); + } + + // TODO: Add methods +} + +export default Checkpoint; diff --git a/src/graph/checkpointer/base-checkpointer.js b/src/graph/checkpointer/base-checkpointer.js new file mode 100644 index 0000000000000000000000000000000000000000..49676d79e26fcd7cfc294f2a76458685fc398ced --- /dev/null +++ b/src/graph/checkpointer/base-checkpointer.js @@ -0,0 +1,18 @@ +/** + * BaseCheckpointer + * + * Abstract checkpointer + * + * @module src/graph/checkpointer/base-checkpointer.js + */ + +export class BaseCheckpointer { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('BaseCheckpointer not yet implemented'); + } + + // TODO: Add methods +} + +export default BaseCheckpointer; diff --git a/src/graph/checkpointer/file-checkpointer.js b/src/graph/checkpointer/file-checkpointer.js new file mode 100644 index 0000000000000000000000000000000000000000..a9e001b0067f12fe90ce8d148e1243c5e96b4026 --- /dev/null +++ b/src/graph/checkpointer/file-checkpointer.js @@ -0,0 +1,18 @@ +/** + * FileCheckpointer + * + * File-based checkpoint storage + * + * @module src/graph/checkpointer/file-checkpointer.js + */ + +export class FileCheckpointer { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('FileCheckpointer not yet implemented'); + } + + // TODO: Add methods +} + +export default FileCheckpointer; diff --git a/src/graph/checkpointer/index.js b/src/graph/checkpointer/index.js new file mode 100644 index 0000000000000000000000000000000000000000..6ce31c2d88768bc6b977b19ccd5be12945252181 --- /dev/null +++ b/src/graph/checkpointer/index.js @@ -0,0 +1,10 @@ +/** + * Module exports + * + * @module src/graph/checkpointer/index.js + */ + +export { BaseCheckpointer } from './base-checkpointer.js'; +export { MemoryCheckpointer } from './memory-checkpointer.js'; +export { FileCheckpointer } from './file-checkpointer.js'; + diff --git a/src/graph/checkpointer/memory-checkpointer.js b/src/graph/checkpointer/memory-checkpointer.js new file mode 100644 index 0000000000000000000000000000000000000000..bf53ed2042057b41a7ca46bd35c987e8426fde71 --- /dev/null +++ b/src/graph/checkpointer/memory-checkpointer.js @@ -0,0 +1,18 @@ +/** + * MemoryCheckpointer + * + * In-memory checkpoint storage + * + * @module src/graph/checkpointer/memory-checkpointer.js + */ + +export class MemoryCheckpointer { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('MemoryCheckpointer not yet implemented'); + } + + // TODO: Add methods +} + +export default MemoryCheckpointer; diff --git a/src/graph/compiled-graph.js b/src/graph/compiled-graph.js new file mode 100644 index 0000000000000000000000000000000000000000..ee1fc5ceb6859debf91f98cc691dd1ff200c5e85 --- /dev/null +++ b/src/graph/compiled-graph.js @@ -0,0 +1,18 @@ +/** + * CompiledGraph + * + * Compiled and executable graph + * + * @module src/graph/compiled-graph.js + */ + +export class CompiledGraph { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('CompiledGraph not yet implemented'); + } + + // TODO: Add methods +} + +export default CompiledGraph; diff --git a/src/graph/conditional-edge.js b/src/graph/conditional-edge.js new file mode 100644 index 0000000000000000000000000000000000000000..bb0f2f0db6730847a0cbf34da8b27f5c445a906e --- /dev/null +++ b/src/graph/conditional-edge.js @@ -0,0 +1,18 @@ +/** + * ConditionalEdge + * + * Conditional routing edge + * + * @module src/graph/conditional-edge.js + */ + +export class ConditionalEdge { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('ConditionalEdge not yet implemented'); + } + + // TODO: Add methods +} + +export default ConditionalEdge; diff --git a/src/graph/edge.js b/src/graph/edge.js new file mode 100644 index 0000000000000000000000000000000000000000..ee5ecbd3c6861cd33152d643e50e465178614aee --- /dev/null +++ b/src/graph/edge.js @@ -0,0 +1,18 @@ +/** + * GraphEdge + * + * Graph edge representation + * + * @module src/graph/edge.js + */ + +export class GraphEdge { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('GraphEdge not yet implemented'); + } + + // TODO: Add methods +} + +export default GraphEdge; diff --git a/src/graph/index.js b/src/graph/index.js new file mode 100644 index 0000000000000000000000000000000000000000..19b781c906172435cb43be1db7d5d271166548d0 --- /dev/null +++ b/src/graph/index.js @@ -0,0 +1,17 @@ +/** + * Module exports + * + * @module src/graph/index.js + */ + +export { StateGraph } from './state-graph.js'; +export { MessageGraph } from './message-graph.js'; +export { CompiledGraph } from './compiled-graph.js'; +export { GraphNode } from './node.js'; +export { GraphEdge } from './edge.js'; +export { ConditionalEdge } from './conditional-edge.js'; +export { Checkpoint } from './checkpoint.js'; +export * from './checkpointer/index.js'; + +export const END = '__end__'; + diff --git a/src/graph/message-graph.js b/src/graph/message-graph.js new file mode 100644 index 0000000000000000000000000000000000000000..cef7c297ed287411ba35ab1073119b7187c1b61c --- /dev/null +++ b/src/graph/message-graph.js @@ -0,0 +1,18 @@ +/** + * MessageGraph + * + * Message-centric graph + * + * @module src/graph/message-graph.js + */ + +export class MessageGraph { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('MessageGraph not yet implemented'); + } + + // TODO: Add methods +} + +export default MessageGraph; diff --git a/src/graph/node.js b/src/graph/node.js new file mode 100644 index 0000000000000000000000000000000000000000..2b4f9d7bb009175c92d12c2288042fd971d9bba7 --- /dev/null +++ b/src/graph/node.js @@ -0,0 +1,18 @@ +/** + * GraphNode + * + * Graph node representation + * + * @module src/graph/node.js + */ + +export class GraphNode { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('GraphNode not yet implemented'); + } + + // TODO: Add methods +} + +export default GraphNode; diff --git a/src/graph/state-graph.js b/src/graph/state-graph.js new file mode 100644 index 0000000000000000000000000000000000000000..97a637694f64c4f4731aa43b92dd7fd868108f94 --- /dev/null +++ b/src/graph/state-graph.js @@ -0,0 +1,18 @@ +/** + * StateGraph + * + * State machine builder + * + * @module src/graph/state-graph.js + */ + +export class StateGraph { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('StateGraph not yet implemented'); + } + + // TODO: Add methods +} + +export default StateGraph; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000000000000000000000000000000000000..41fd85b380778be57071f450b3e84c4166aa808b --- /dev/null +++ b/src/index.js @@ -0,0 +1,117 @@ +/** + * AI Agents Framework + * + * A lightweight, educational implementation of LangChain/LangGraph + * using node-llama-cpp for local inference. + * + * @module ai-agents-framework + */ + +// Core +export { + Runnable, + RunnableSequence, + RunnableParallel, + BaseMessage, + HumanMessage, + AIMessage, + SystemMessage, + ToolMessage, + RunnableConfig +} from './core/index.js'; + +// LLM +export { + BaseLLM, + LlamaCppLLM, + ChatModel, + StreamingLLM +} from './llm/index.js'; + +// Prompts +export { + BasePromptTemplate, + PromptTemplate, + ChatPromptTemplate, + FewShotPromptTemplate, + PipelinePromptTemplate, + SystemMessagePromptTemplate +} from './prompts/index.js'; + +// Output Parsers +export { + BaseOutputParser, + StringOutputParser, + JsonOutputParser, + StructuredOutputParser, + ListOutputParser, + RegexOutputParser +} from './output-parsers/index.js'; + +// Chains +export { + BaseChain, + LLMChain, + SequentialChain, + RouterChain, + MapReduceChain, + TransformChain +} from './chains/index.js'; + +// Tools +export { + BaseTool, + ToolExecutor, + ToolRegistry, + Calculator, + WebSearch, + WebScraper, + FileReader, + FileWriter, + CodeExecutor +} from './tools/index.js'; + +// Agents +export { + BaseAgent, + AgentExecutor, + ToolCallingAgent, + ReActAgent, + StructuredChatAgent, + ConversationalAgent +} from './agents/index.js'; + +// Memory +export { + BaseMemory, + BufferMemory, + WindowMemory, + SummaryMemory, + VectorMemory, + EntityMemory +} from './memory/index.js'; + +// Graph +export { + StateGraph, + MessageGraph, + CompiledGraph, + GraphNode, + GraphEdge, + ConditionalEdge, + Checkpoint, + BaseCheckpointer, + MemoryCheckpointer, + FileCheckpointer, + END +} from './graph/index.js'; + +// Utils +export { + CallbackManager, + TokenCounter, + RetryManager, + TimeoutManager, + Logger, + SchemaValidator +} from './utils/index.js'; diff --git a/src/llm/base-llm.js b/src/llm/base-llm.js new file mode 100644 index 0000000000000000000000000000000000000000..2275629bd7749ff78e8ba3b1f90395ec76286de1 --- /dev/null +++ b/src/llm/base-llm.js @@ -0,0 +1,18 @@ +/** + * BaseLLM + * + * Abstract base class for LLM implementations + * + * @module src/llm/base-llm.js + */ + +export class BaseLLM { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('BaseLLM not yet implemented'); + } + + // TODO: Add methods +} + +export default BaseLLM; diff --git a/src/llm/chat-model.js b/src/llm/chat-model.js new file mode 100644 index 0000000000000000000000000000000000000000..2310bd1fe802a009e6d04693fd488f4bef190065 --- /dev/null +++ b/src/llm/chat-model.js @@ -0,0 +1,18 @@ +/** + * ChatModel + * + * Chat-specific LLM interface with message handling + * + * @module src/llm/chat-model.js + */ + +export class ChatModel { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('ChatModel not yet implemented'); + } + + // TODO: Add methods +} + +export default ChatModel; diff --git a/src/llm/index.js b/src/llm/index.js new file mode 100644 index 0000000000000000000000000000000000000000..441e2abd562354663c12ba3b87b906b4c044daab --- /dev/null +++ b/src/llm/index.js @@ -0,0 +1,11 @@ +/** + * Module exports + * + * @module src/llm/index.js + */ + +export { BaseLLM } from './base-llm.js'; +export { LlamaCppLLM } from './llama-cpp-llm.js'; +export { ChatModel } from './chat-model.js'; +export { StreamingLLM } from './streaming-llm.js'; + diff --git a/src/llm/llama-cpp-llm.js b/src/llm/llama-cpp-llm.js new file mode 100644 index 0000000000000000000000000000000000000000..8f68b80bdf92adc91a2f773e3b5f408c2792c849 --- /dev/null +++ b/src/llm/llama-cpp-llm.js @@ -0,0 +1,647 @@ +/** + * LlamaCppLLM - node-llama-cpp wrapper as a Runnable + * + * @module llm/llama-cpp-llm + */ + +import { Runnable } from '../core/runnable.js'; +import { AIMessage, HumanMessage } from '../core/message.js'; +import { getLlama, LlamaChatSession } from 'node-llama-cpp'; + +/** + * LlamaCppLLM - A Runnable wrapper for node-llama-cpp + * + * Wraps your LLM calls from agent fundamentals into a reusable, + * composable Runnable component. + * + * Key benefits over raw node-llama-cpp: + * - Composable with other Runnables via .pipe() + * - Supports batch processing multiple inputs + * - Built-in streaming support + * - Consistent interface across all LLMs + * - Easy to swap with other LLM providers + */ +export class LlamaCppLLM extends Runnable { + /** + * Create a new LlamaCppLLM instance + * + * @param {Object} options - Configuration options + * @param {string} options.modelPath - Path to your GGUF model file (REQUIRED) + * @param {number} [options.temperature=0.7] - Sampling temperature (0-1) + * - Lower (0.1): More focused, deterministic + * - Higher (0.9): More creative, random + * @param {number} [options.topP=0.9] - Nucleus sampling threshold + * @param {number} [options.topK=40] - Top-K sampling parameter + * @param {number} [options.maxTokens=2048] - Maximum tokens to generate + * @param {number} [options.repeatPenalty=1.1] - Penalty for repeating tokens + * @param {number} [options.contextSize=4096] - Context window size + * @param {number} [options.batchSize=512] - Batch processing size + * @param {boolean} [options.verbose=false] - Enable debug logging + * @param {string[]} [options.stopStrings] - Strings that stop generation + * @param {Object} [options.chatWrapper] - Custom chat wrapper instance (e.g., QwenChatWrapper) + * - If not provided, the library will automatically select the best wrapper for your model + * + * @example Basic Setup + * ```javascript + * const llm = new LlamaCppLLM({ + * modelPath: './models/Meta-Llama-3.1-8B-Instruct-Q5_K_S.gguf', + * temperature: 0.7 + * }); + * ``` + * + * @example With Qwen Chat Wrapper (Discourage Thoughts) + * ```javascript + * import { QwenChatWrapper } from 'node-llama-cpp'; + * + * const llm = new LlamaCppLLM({ + * modelPath: './models/Qwen3-1.7B-Q6_K.gguf', + * temperature: 0.7, + * chatWrapper: new QwenChatWrapper({ + * thoughts: 'discourage' + * }) + * }); + * ``` + * + * @example Different Configurations for Different Tasks + * ```javascript + * // Creative writing (higher temperature) + * const creative = new LlamaCppLLM({ + * modelPath: './model.gguf', + * temperature: 0.9, + * maxTokens: 1000 + * }); + * + * // Factual responses (lower temperature) + * const factual = new LlamaCppLLM({ + * modelPath: './model.gguf', + * temperature: 0.1, + * maxTokens: 500 + * }); + * ``` + */ + constructor(options = {}) { + super(); + + // Validate required options + this.modelPath = options.modelPath; + if (!this.modelPath) { + throw new Error( + 'modelPath is required. Example: new LlamaCppLLM({ modelPath: "./model.gguf" })' + ); + } + + // Generation parameters + // These control how the LLM generates text - same as in your fundamentals! + this.temperature = options.temperature ?? 0.7; + this.topP = options.topP ?? 0.9; + this.topK = options.topK ?? 40; + this.maxTokens = options.maxTokens ?? 2048; + this.repeatPenalty = options.repeatPenalty ?? 1.1; + + // Context configuration + this.contextSize = options.contextSize ?? 4096; + this.batchSize = options.batchSize ?? 512; + + // Behavior + this.verbose = options.verbose ?? false; + + // Chat wrapper configuration + // If not provided, LlamaChatSession will auto-select the best wrapper + this.chatWrapper = options.chatWrapper ?? 'auto'; + + // Stop strings - when the model sees these, it stops generating + // Default includes common chat separators + this.stopStrings = options.stopStrings ?? [ + 'Human:', + 'User:', + '\n\nHuman:', + '\n\nUser:' + ]; + + // Internal state (lazy initialized) + this._llama = null; + this._model = null; + this._context = null; + this._chatSession = null; + this._initialized = false; + } + + /** + * Initialize model (lazy loading) + * + * This loads the model only when first needed, not at construction. + * This pattern is useful because model loading is slow - we only + * want to do it once and only when we actually need it. + * + * @private + * @throws {Error} If model loading fails + */ + async _initialize() { + // Skip if already initialized + if (this._initialized) return; + + if (this.verbose) { + console.log(`Loading model: ${this.modelPath}`); + } + + try { + // Step 1: Get the llama instance + this._llama = await getLlama(); + + // Step 2: Load the model file + this._model = await this._llama.loadModel({ + modelPath: this.modelPath + }); + + // Step 3: Create a context for generation + this._context = await this._model.createContext({ + contextSize: this.contextSize, + batchSize: this.batchSize + }); + + // Step 4: Create a chat session + // This manages conversation history for us + const contextSequence = this._context.getSequence(); + const sessionConfig = { + contextSequence + }; + + // Add chatWrapper if specified (otherwise LlamaChatSession uses "auto") + if (this.chatWrapper !== 'auto') { + sessionConfig.chatWrapper = this.chatWrapper; + } + + this._chatSession = new LlamaChatSession(sessionConfig); + + this._initialized = true; + + if (this.verbose) { + console.log('✓ Model loaded successfully'); + if (this.chatWrapper !== 'auto') { + console.log(`✓ Using custom chat wrapper: ${this.chatWrapper.constructor.name}`); + } else { + console.log('✓ Using auto-detected chat wrapper'); + } + } + } catch (error) { + throw new Error( + `Failed to initialize model at ${this.modelPath}: ${error.message}` + ); + } + } + + /** + * Convert our Message objects to node-llama-cpp chat history format + * + * This bridges between our standardized Message types and what + * node-llama-cpp expects. Think of it as a translator. + * + * @private + * @param {Array} messages - Array of Message objects + * @returns {Array} Chat history in llama.cpp format + * + * @example + * ```javascript + * // Input: Our messages + * [ + * new SystemMessage("You are helpful"), + * new HumanMessage("Hi"), + * new AIMessage("Hello!") + * ] + * + * // Output: llama.cpp format + * [ + * { type: 'system', text: 'You are helpful' }, + * { type: 'user', text: 'Hi' }, + * { type: 'model', response: 'Hello!' } + * ] + * ``` + */ + _messagesToChatHistory(messages) { + return messages.map(msg => { + // System messages: instructions for the AI + if (msg._type === 'system') { + return { type: 'system', text: msg.content }; + } + // Human messages: user input + else if (msg._type === 'human') { + return { type: 'user', text: msg.content }; + } + // AI messages: previous AI responses + else if (msg._type === 'ai') { + return { type: 'model', response: msg.content }; + } + // Tool messages: results from tool execution + else if (msg._type === 'tool') { + // Convert tool results to system messages + return { type: 'system', text: `Tool Result: ${msg.content}` }; + } + + // Fallback: treat unknown types as user messages + return { type: 'user', text: msg.content }; + }); + } + + /** + * Clean up model response + * + * Sometimes models include extra prefixes or suffixes. + * This cleans them up for a better user experience. + * + * @private + * @param {string} response - Raw model response + * @returns {string} Cleaned response + * + * @example + * ```javascript + * // Before: "Assistant: The answer is 42\n\nHuman: " + * // After: "The answer is 42" + * ``` + */ + _cleanResponse(response) { + let cleaned = response.trim(); + + // Remove "Assistant:" or "AI:" prefixes + cleaned = cleaned.replace(/^(Assistant|AI):\s*/i, ''); + + // Remove any conversation continuations + cleaned = cleaned.replace(/\n\n(Human|User):.*$/s, ''); + + return cleaned.trim(); + } + + /** + * Main generation method - this is where your LLM calls happen! + * + * This is the same as calling `llm.chat(messages)` in your fundamentals, + * but wrapped to work with the Runnable interface. + * + * @async + * @param {string|Array} input - User input or message array + * @param {Object} [config={}] - Runtime configuration + * @param {number} [config.temperature] - Override temperature for this call + * @param {number} [config.maxTokens] - Override max tokens for this call + * @param {boolean} [config.clearHistory=false] - Clear chat history before this call + * @returns {Promise} Generated response as AIMessage + * + * @example String Input (Simplest) + * ```javascript + * const response = await llm.invoke("What is AI?"); + * console.log(response.content); // "AI is..." + * ``` + * + * @example Message Array Input (Full Control) + * ```javascript + * const messages = [ + * new SystemMessage("You are a helpful assistant"), + * new HumanMessage("What is AI?") + * ]; + * const response = await llm.invoke(messages); + * ``` + * + * @example Runtime Configuration + * ```javascript + * // Override temperature for this specific call + * const response = await llm.invoke( + * "Write a creative story", + * { temperature: 0.9, maxTokens: 500 } + * ); + * ``` + * + * @example Clear History Before Call + * ```javascript + * // Ensure fresh context with no prior conversation + * const response = await llm.invoke( + * "What is AI?", + * { clearHistory: true } + * ); + * ``` + * + * @example In a Pipeline (Composition) + * ```javascript + * const pipeline = promptFormatter + * .pipe(llm) + * .pipe(outputParser); + * + * const result = await pipeline.invoke("user input"); + * ``` + */ + async _call(input, config = {}) { + // Ensure model is loaded (only happens once) + await this._initialize(); + + // Clear history if requested (important for batch processing) + if (config.clearHistory) { + this._chatSession.setChatHistory([]); + } + + // Handle different input types + let messages; + if (typeof input === 'string') { + messages = [new HumanMessage(input)]; + } else if (Array.isArray(input)) { + messages = input; + } else { + throw new Error( + 'Input must be a string or array of messages. ' + + 'Example: "Hello" or [new HumanMessage("Hello")]' + ); + } + + // Extract system message if present + const systemMessages = messages.filter(msg => msg._type === 'system'); + const systemPrompt = systemMessages.length > 0 + ? systemMessages[0].content + : ''; + + // Convert our Message objects to llama.cpp format + const chatHistory = this._messagesToChatHistory(messages); + this._chatSession.setChatHistory(chatHistory); + + // ALWAYS set system prompt (either new value or empty string to clear) + this._chatSession.systemPrompt = systemPrompt; + + try { + // Build prompt options + const promptOptions = { + temperature: config.temperature ?? this.temperature, + topP: config.topP ?? this.topP, + topK: config.topK ?? this.topK, + maxTokens: config.maxTokens ?? this.maxTokens, + repeatPenalty: config.repeatPenalty ?? this.repeatPenalty, + customStopTriggers: config.stopStrings ?? this.stopStrings + }; + + // Add random seed if temperature > 0 and no seed specified + // This ensures randomness works properly + if (promptOptions.temperature > 0 && config.seed === undefined) { + promptOptions.seed = Math.floor(Math.random() * 1000000); + } else if (config.seed !== undefined) { + promptOptions.seed = config.seed; + } + + // Generate response using prompt (simpler than promptWithMeta for non-streaming) + const response = await this._chatSession.prompt('', promptOptions); + + // Return as AIMessage for consistency + return new AIMessage(response); + } catch (error) { + throw new Error(`Generation failed: ${error.message}`); + } + } + + /** + * Batch processing with history isolation + * + * Processes multiple inputs sequentially, ensuring each gets a clean chat history. + * Note: Local models process requests sequentially, so there's no performance + * benefit compared to calling invoke() multiple times. + * + * @async + * @param {Array>} inputs - Array of inputs to process + * @param {Object} [config={}] - Runtime configuration + * @returns {Promise>} Array of generated responses + * + * @example + * ```javascript + * const questions = ["What is AI?", "What is ML?", "What is DL?"]; + * const answers = await llm.batch(questions); + * ``` + */ + async batch(inputs, config = {}) { + const results = []; + for (const input of inputs) { + // Clear history before each batch item to prevent contamination + const result = await this._call(input, { ...config, clearHistory: true }); + results.push(result); + } + return results; + } + + /** + * Streaming generation - show results as they're generated! + * + * This is the same as _call() but yields chunks as they arrive, + * like the typing effect you see in ChatGPT. + * + * @async + * @generator + * @param {string|Array} input - User input or message array + * @param {Object} [config={}] - Runtime configuration + * @yields {AIMessage} Chunks of generated text + * + * @example Basic Streaming + * ```javascript + * console.log("Response: "); + * for await (const chunk of llm.stream("Tell me a story")) { + * process.stdout.write(chunk.content); // Print without newline + * } + * console.log("\nDone!"); + * ``` + * + * @example Streaming in a Pipeline + * ```javascript + * const pipeline = promptFormatter + * .pipe(llm) + * .pipe(parser); + * + * // Only the last step (parser) gets streamed chunks + * for await (const chunk of pipeline.stream(input)) { + * console.log(chunk); + * } + * ``` + * + * @example Building a Chat UI + * ```javascript + * async function streamToUI(input) { + * let fullResponse = ''; + * + * for await (const chunk of llm.stream(input)) { + * fullResponse += chunk.content; + * updateUI(fullResponse); // Update your UI in real-time + * } + * } + * ``` + */ + async* stream(input, config = {}) { + await this._initialize(); + + // Clear history if requested + if (config.clearHistory) { + this._chatSession.setChatHistory([]); + } + + // Handle different input types (same as _call) + let messages; + if (typeof input === 'string') { + messages = [new HumanMessage(input)]; + } else if (Array.isArray(input)) { + messages = input; + } else { + throw new Error( + 'Input must be a string or array of messages for streaming' + ); + } + + // Extract system message if present + const systemMessages = messages.filter(msg => msg._type === 'system'); + const systemPrompt = systemMessages.length > 0 + ? systemMessages[0].content + : ''; + + // Set up chat history + const chatHistory = this._messagesToChatHistory(messages); + this._chatSession.setChatHistory(chatHistory); + + // ALWAYS set system prompt (either new value or empty string to clear) + this._chatSession.systemPrompt = systemPrompt; + + try { + // Build prompt options + const promptOptions = { + temperature: config.temperature ?? this.temperature, + topP: config.topP ?? this.topP, + topK: config.topK ?? this.topK, + maxTokens: config.maxTokens ?? this.maxTokens, + repeatPenalty: config.repeatPenalty ?? this.repeatPenalty, + customStopTriggers: config.stopStrings ?? this.stopStrings + }; + + // Add random seed if temperature > 0 and no seed specified + if (promptOptions.temperature > 0 && config.seed === undefined) { + promptOptions.seed = Math.floor(Math.random() * 1000000); + } else if (config.seed !== undefined) { + promptOptions.seed = config.seed; + } + + // Use onTextChunk callback to stream chunks as they arrive + const self = this; + promptOptions.onTextChunk = (chunk) => { + // This callback is synchronous, so we can't yield directly + // We'll collect chunks and yield them after + self._currentStreamChunks = self._currentStreamChunks || []; + self._currentStreamChunks.push(chunk); + }; + + // Initialize chunk collection + this._currentStreamChunks = []; + + // Start generation (this will call onTextChunk as it generates) + const responsePromise = this._chatSession.prompt('', promptOptions); + + // Yield chunks as they become available + let lastYieldedIndex = 0; + + // Poll for new chunks + while (true) { + // Yield any new chunks + while (lastYieldedIndex < this._currentStreamChunks.length) { + yield new AIMessage(this._currentStreamChunks[lastYieldedIndex], { + additionalKwargs: { chunk: true } + }); + lastYieldedIndex++; + } + + // Check if generation is complete + const isDone = await Promise.race([ + responsePromise.then(() => true), + new Promise(resolve => setTimeout(() => resolve(false), 10)) + ]); + + if (isDone) { + // Yield any remaining chunks + while (lastYieldedIndex < this._currentStreamChunks.length) { + yield new AIMessage(this._currentStreamChunks[lastYieldedIndex], { + additionalKwargs: { chunk: true } + }); + lastYieldedIndex++; + } + break; + } + } + + // Wait for the full response + await responsePromise; + + // Clean up + delete this._currentStreamChunks; + + } catch (error) { + throw new Error(`Streaming failed: ${error.message}`); + } + } + + /** + * Cleanup resources + * + * LLMs hold resources in memory. Call this when you're done + * to free them up properly. + * + * @async + * @returns {Promise} + * + * @example + * ```javascript + * const llm = new LlamaCppLLM({ modelPath: './model.gguf' }); + * + * try { + * const response = await llm.invoke("Hello"); + * console.log(response.content); + * } finally { + * await llm.dispose(); // Always clean up! + * } + * ``` + * + * @example With Multiple Uses + * ```javascript + * const llm = new LlamaCppLLM({ modelPath: './model.gguf' }); + * + * // Use it many times + * await llm.invoke("Question 1"); + * await llm.invoke("Question 2"); + * await llm.batch(["Q3", "Q4", "Q5"]); + * + * // Clean up when completely done + * await llm.dispose(); + * ``` + */ + async dispose() { + if (this._context) { + await this._context.dispose(); + this._context = null; + } + if (this._model) { + await this._model.dispose(); + this._model = null; + } + this._chatSession = null; + this._initialized = false; + + if (this.verbose) { + console.log('✓ Model resources disposed'); + } + } + + /** + * String representation for debugging + * + * @returns {string} Human-readable representation + * + * @example + * ```javascript + * const llm = new LlamaCppLLM({ modelPath: './llama-2-7b.gguf' }); + * console.log(llm.toString()); + * // "LlamaCppLLM(model=./llama-2-7b.gguf)" + * + * // Useful in pipelines + * const pipeline = formatter.pipe(llm).pipe(parser); + * console.log(pipeline.toString()); + * // "PromptFormatter() | LlamaCppLLM(model=./llama-2-7b.gguf) | OutputParser()" + * ``` + */ + toString() { + return `LlamaCppLLM(model=${this.modelPath})`; + } +} + +export default LlamaCppLLM; \ No newline at end of file diff --git a/src/llm/streaming-llm.js b/src/llm/streaming-llm.js new file mode 100644 index 0000000000000000000000000000000000000000..be1149647881a6441081db1f372371342fbd8957 --- /dev/null +++ b/src/llm/streaming-llm.js @@ -0,0 +1,18 @@ +/** + * StreamingLLM + * + * Mixin for streaming support in LLMs + * + * @module src/llm/streaming-llm.js + */ + +export class StreamingLLM { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('StreamingLLM not yet implemented'); + } + + // TODO: Add methods +} + +export default StreamingLLM; diff --git a/src/memory/base-memory.js b/src/memory/base-memory.js new file mode 100644 index 0000000000000000000000000000000000000000..2bacb2cc858e6f816f31522149a7a50798e586e4 --- /dev/null +++ b/src/memory/base-memory.js @@ -0,0 +1,18 @@ +/** + * BaseMemory + * + * Abstract memory class + * + * @module src/memory/base-memory.js + */ + +export class BaseMemory { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('BaseMemory not yet implemented'); + } + + // TODO: Add methods +} + +export default BaseMemory; diff --git a/src/memory/buffer-memory.js b/src/memory/buffer-memory.js new file mode 100644 index 0000000000000000000000000000000000000000..d7e5ae2a3eeef36f0ee4eb34e387bd6990066a42 --- /dev/null +++ b/src/memory/buffer-memory.js @@ -0,0 +1,18 @@ +/** + * BufferMemory + * + * Simple conversation buffer + * + * @module src/memory/buffer-memory.js + */ + +export class BufferMemory { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('BufferMemory not yet implemented'); + } + + // TODO: Add methods +} + +export default BufferMemory; diff --git a/src/memory/entity-memory.js b/src/memory/entity-memory.js new file mode 100644 index 0000000000000000000000000000000000000000..f5b16cddf1f0204996744c146f004fe6006c25d4 --- /dev/null +++ b/src/memory/entity-memory.js @@ -0,0 +1,18 @@ +/** + * EntityMemory + * + * Entity extraction and tracking + * + * @module src/memory/entity-memory.js + */ + +export class EntityMemory { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('EntityMemory not yet implemented'); + } + + // TODO: Add methods +} + +export default EntityMemory; diff --git a/src/memory/index.js b/src/memory/index.js new file mode 100644 index 0000000000000000000000000000000000000000..deb13cb5c2fbddf7756bac006680f935bcd4495f --- /dev/null +++ b/src/memory/index.js @@ -0,0 +1,13 @@ +/** + * Module exports + * + * @module src/memory/index.js + */ + +export { BaseMemory } from './base-memory.js'; +export { BufferMemory } from './buffer-memory.js'; +export { WindowMemory } from './window-memory.js'; +export { SummaryMemory } from './summary-memory.js'; +export { VectorMemory } from './vector-memory.js'; +export { EntityMemory } from './entity-memory.js'; + diff --git a/src/memory/summary-memory.js b/src/memory/summary-memory.js new file mode 100644 index 0000000000000000000000000000000000000000..ecdd69f5f0cbeee78492db8fd1853a2839a79dfa --- /dev/null +++ b/src/memory/summary-memory.js @@ -0,0 +1,18 @@ +/** + * SummaryMemory + * + * Auto-summarizing memory + * + * @module src/memory/summary-memory.js + */ + +export class SummaryMemory { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('SummaryMemory not yet implemented'); + } + + // TODO: Add methods +} + +export default SummaryMemory; diff --git a/src/memory/vector-memory.js b/src/memory/vector-memory.js new file mode 100644 index 0000000000000000000000000000000000000000..1b2988168dc3b6eeb39365418f76cdded59d0bd1 --- /dev/null +++ b/src/memory/vector-memory.js @@ -0,0 +1,18 @@ +/** + * VectorMemory + * + * Semantic search memory using embeddings + * + * @module src/memory/vector-memory.js + */ + +export class VectorMemory { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('VectorMemory not yet implemented'); + } + + // TODO: Add methods +} + +export default VectorMemory; diff --git a/src/memory/window-memory.js b/src/memory/window-memory.js new file mode 100644 index 0000000000000000000000000000000000000000..7840350b36cc1ad3b4c9b53e1637a9cedce3fa24 --- /dev/null +++ b/src/memory/window-memory.js @@ -0,0 +1,18 @@ +/** + * WindowMemory + * + * Sliding window memory + * + * @module src/memory/window-memory.js + */ + +export class WindowMemory { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('WindowMemory not yet implemented'); + } + + // TODO: Add methods +} + +export default WindowMemory; diff --git a/src/output-parsers/base-parser.js b/src/output-parsers/base-parser.js new file mode 100644 index 0000000000000000000000000000000000000000..9699c8f3af3b46875ac6b09ad25b486724b13944 --- /dev/null +++ b/src/output-parsers/base-parser.js @@ -0,0 +1,69 @@ +import {Runnable} from '../core/runnable.js'; + +/** + * Base class for all output parsers + * Transforms LLM text output into structured data + */ +export class BaseOutputParser extends Runnable { + constructor() { + super(); + this.name = this.constructor.name; + } + + /** + * Parse the LLM output into structured data + * @abstract + * @param {string} text - Raw LLM output + * @returns {Promise} Parsed data + */ + async parse(text) { + throw new Error(`${this.name} must implement parse()`); + } + + /** + * Get instructions for the LLM on how to format output + * @returns {string} Format instructions + */ + getFormatInstructions() { + return ''; + } + + /** + * Runnable interface: parse the output + */ + async _call(input, config) { + // Input can be a string or a Message + const text = typeof input === 'string' + ? input + : input.content; + + return await this.parse(text); + } + + /** + * Parse with error handling + */ + async parseWithPrompt(text, prompt) { + try { + return await this.parse(text); + } catch (error) { + throw new OutputParserException( + `Failed to parse output from prompt: ${error.message}`, + text, + error + ); + } + } +} + +/** + * Exception thrown when parsing fails + */ +export class OutputParserException extends Error { + constructor(message, llmOutput, originalError) { + super(message); + this.name = 'OutputParserException'; + this.llmOutput = llmOutput; + this.originalError = originalError; + } +} \ No newline at end of file diff --git a/src/output-parsers/index.js b/src/output-parsers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..7819672db76913f0c3b632b0a37700989cb74ca6 --- /dev/null +++ b/src/output-parsers/index.js @@ -0,0 +1,13 @@ +/** + * Module exports + * + * @module src/output-parsers/index.js + */ + +export { BaseOutputParser } from './base-parser.js'; +export { StringOutputParser } from './string-parser.js'; +export { JsonOutputParser } from './json-parser.js'; +export { StructuredOutputParser } from './structured-parser.js'; +export { ListOutputParser } from './list-parser.js'; +export { RegexOutputParser } from './regex-parser.js'; + diff --git a/src/output-parsers/json-parser.js b/src/output-parsers/json-parser.js new file mode 100644 index 0000000000000000000000000000000000000000..22d3725fc5311927d48ce5e66062f36c7eda5c3a --- /dev/null +++ b/src/output-parsers/json-parser.js @@ -0,0 +1,107 @@ +import { BaseOutputParser, OutputParserException } from './base-parser.js'; + +/** + * Parser that extracts JSON from LLM output + * Handles markdown code blocks and extra text + * + * Example: + * const parser = new JsonOutputParser(); + * const result = await parser.parse('```json\n{"name": "Alice"}\n```'); + * // { name: "Alice" } + */ +export class JsonOutputParser extends BaseOutputParser { + constructor(options = {}) { + super(); + this.schema = options.schema; + } + + /** + * Parse JSON from text + */ + async parse(text) { + try { + // Try to extract JSON from the text + const jsonText = this._extractJson(text); + const parsed = JSON.parse(jsonText); + + // Validate against schema if provided + if (this.schema) { + this._validateSchema(parsed); + } + + return parsed; + } catch (error) { + throw new OutputParserException( + `Failed to parse JSON: ${error.message}`, + text, + error + ); + } + } + + /** + * Extract JSON from text (handles markdown, extra text) + */ + _extractJson(text) { + // Try direct parse first + try { + JSON.parse(text.trim()); + return text.trim(); + } catch { + // Not direct JSON, try to find it + } + + // Look for JSON in markdown code blocks + const markdownMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/); + if (markdownMatch) { + return markdownMatch[1].trim(); + } + + // Look for JSON object/array patterns + const jsonObjectMatch = text.match(/\{[\s\S]*\}/); + if (jsonObjectMatch) { + return jsonObjectMatch[0]; + } + + const jsonArrayMatch = text.match(/\[[\s\S]*\]/); + if (jsonArrayMatch) { + return jsonArrayMatch[0]; + } + + // Give up, return original + return text.trim(); + } + + /** + * Validate parsed JSON against schema + */ + _validateSchema(parsed) { + if (!this.schema) return; + + for (const [key, type] of Object.entries(this.schema)) { + if (!(key in parsed)) { + throw new Error(`Missing required field: ${key}`); + } + + const actualType = typeof parsed[key]; + if (actualType !== type) { + throw new Error( + `Field ${key} should be ${type}, got ${actualType}` + ); + } + } + } + + getFormatInstructions() { + let instructions = 'Respond with valid JSON.'; + + if (this.schema) { + const schemaDesc = Object.entries(this.schema) + .map(([key, type]) => `"${key}": ${type}`) + .join(', '); + instructions += ` Schema: { ${schemaDesc} }`; + } + + return instructions; + } +} \ No newline at end of file diff --git a/src/output-parsers/list-parser.js b/src/output-parsers/list-parser.js new file mode 100644 index 0000000000000000000000000000000000000000..0a3195c54b32f143545b80d186cb435c7c36042f --- /dev/null +++ b/src/output-parsers/list-parser.js @@ -0,0 +1,98 @@ +import { BaseOutputParser } from './base-parser.js'; + +/** + * Parser that extracts lists from text + * Handles: numbered lists, bullets, comma-separated + * + * Example: + * const parser = new ListOutputParser(); + * const result = await parser.parse("1. Apple\n2. Banana\n3. Orange"); + * // ["Apple", "Banana", "Orange"] + * + * Enhancement: handle multi line csv correctly + */ +export class ListOutputParser extends BaseOutputParser { + constructor(options = {}) { + super(); + this.separator = options.separator; + } + + /** + * Parse list from text + */ + async parse(text) { + const cleaned = text.trim(); + + // If separator specified, use it + if (this.separator) { + return cleaned + .split(this.separator) + .map(item => item.trim()) + .filter(item => item.length > 0); + } + + // Try to detect format + if (this._isNumberedList(cleaned)) { + return this._parseNumberedList(cleaned); + } + + if (this._isBulletList(cleaned)) { + return this._parseBulletList(cleaned); + } + + // Try comma-separated + if (cleaned.includes(',')) { + return cleaned + .split(',') + .map(item => item.trim()) + .filter(item => item.length > 0); + } + + // Try newline-separated + return cleaned + .split('\n') + .map(item => item.trim()) + .filter(item => item.length > 0); + } + + /** + * Check if text is numbered list (1. Item\n2. Item) + */ + _isNumberedList(text) { + return /^\d+\./.test(text); + } + + /** + * Check if text is bullet list (- Item\n- Item or * Item) + */ + _isBulletList(text) { + return /^[-*•]/.test(text); + } + + /** + * Parse numbered list + */ + _parseNumberedList(text) { + return text + .split('\n') + .map(line => line.replace(/^\d+\.\s*/, '').trim()) + .filter(item => item.length > 0); + } + + /** + * Parse bullet list + */ + _parseBulletList(text) { + return text + .split('\n') + .map(line => line.replace(/^[-*•]\s*/, '').trim()) + .filter(item => item.length > 0); + } + + getFormatInstructions() { + if (this.separator) { + return `Respond with items separated by "${this.separator}".`; + } + return 'Respond with a numbered list (1. Item) or bullet list (- Item).'; + } +} \ No newline at end of file diff --git a/src/output-parsers/regex-parser.js b/src/output-parsers/regex-parser.js new file mode 100644 index 0000000000000000000000000000000000000000..f21e9a7bd2fae37898115e0ff7b714f356d4fbbb --- /dev/null +++ b/src/output-parsers/regex-parser.js @@ -0,0 +1,64 @@ +import { BaseOutputParser, OutputParserException } from './base-parser.js'; + +/** + * Parser that uses regex to extract structured data + * + * Example: + * const parser = new RegexOutputParser({ + * regex: /Sentiment: (\w+), Confidence: ([\d.]+)/, + * outputKeys: ["sentiment", "confidence"] + * }); + * + * const result = await parser.parse("Sentiment: positive, Confidence: 0.92"); + * // { sentiment: "positive", confidence: "0.92" } + */ +export class RegexOutputParser extends BaseOutputParser { + constructor(options = {}) { + super(); + this.regex = options.regex; + this.outputKeys = options.outputKeys || []; + this.dotAll = options.dotAll ?? false; + + if (this.dotAll) { + // Add 's' flag for dotAll if not present + const flags = this.regex.flags.includes('s') + ? this.regex.flags + : this.regex.flags + 's'; + this.regex = new RegExp(this.regex.source, flags); + } + } + + /** + * Parse using regex + */ + async parse(text) { + const match = text.match(this.regex); + + if (!match) { + throw new OutputParserException( + `Text does not match regex pattern: ${this.regex}`, + text + ); + } + + // If no output keys, return the groups as array + if (this.outputKeys.length === 0) { + return match.slice(1); // Exclude full match + } + + // Map groups to keys + const result = {}; + for (let i = 0; i < this.outputKeys.length; i++) { + result[this.outputKeys[i]] = match[i + 1]; // +1 to skip full match + } + + return result; + } + + getFormatInstructions() { + if (this.outputKeys.length > 0) { + return `Format your response to match: ${this.outputKeys.join(', ')}`; + } + return 'Follow the specified format exactly.'; + } +} \ No newline at end of file diff --git a/src/output-parsers/string-parser.js b/src/output-parsers/string-parser.js new file mode 100644 index 0000000000000000000000000000000000000000..e317f23ab41bc08f42ccb7aea960f318e997586b --- /dev/null +++ b/src/output-parsers/string-parser.js @@ -0,0 +1,42 @@ +import { BaseOutputParser } from './base-parser.js'; + +/** + * Parser that returns cleaned string output + * Strips whitespace and optionally removes markdown + * + * Example: + * const parser = new StringOutputParser(); + * const result = await parser.parse(" Hello World "); + * // "Hello World" + */ +export class StringOutputParser extends BaseOutputParser { + constructor(options = {}) { + super(); + this.stripMarkdown = options.stripMarkdown ?? true; + } + + /** + * Parse: clean the text + */ + async parse(text) { + let cleaned = text.trim(); + + if (this.stripMarkdown) { + cleaned = this._stripMarkdownCodeBlocks(cleaned); + } + + return cleaned; + } + + /** + * Remove markdown code blocks (```code```) + */ + _stripMarkdownCodeBlocks(text) { + // Remove ```language\ncode\n``` + return text.replace(/```[\w]*\n([\s\S]*?)\n```/g, '$1').trim(); + } + + getFormatInstructions() { + return 'Respond with plain text. No markdown formatting.'; + } +} \ No newline at end of file diff --git a/src/output-parsers/structured-parser.js b/src/output-parsers/structured-parser.js new file mode 100644 index 0000000000000000000000000000000000000000..c775e1674d27e9c237700cc9404199c7394b15b9 --- /dev/null +++ b/src/output-parsers/structured-parser.js @@ -0,0 +1,154 @@ +import { BaseOutputParser, OutputParserException } from './base-parser.js'; + +/** + * Parser with full schema validation + * + * Example: + * const parser = new StructuredOutputParser({ + * responseSchemas: [ + * { + * name: "sentiment", + * type: "string", + * description: "The sentiment (positive/negative/neutral)", + * enum: ["positive", "negative", "neutral"] + * }, + * { + * name: "confidence", + * type: "number", + * description: "Confidence score between 0 and 1" + * } + * ] + * }); + */ +export class StructuredOutputParser extends BaseOutputParser { + constructor(options = {}) { + super(); + this.responseSchemas = options.responseSchemas || []; + } + + /** + * Parse and validate against schema + */ + async parse(text) { + try { + // Extract JSON + const jsonText = this._extractJson(text); + const parsed = JSON.parse(jsonText); + + // Validate against schema + this._validateAgainstSchema(parsed); + + return parsed; + } catch (error) { + throw new OutputParserException( + `Failed to parse structured output: ${error.message}`, + text, + error + ); + } + } + + /** + * Extract JSON from text (same as JsonOutputParser) + */ + _extractJson(text) { + try { + JSON.parse(text.trim()); + return text.trim(); + } catch {} + + const markdownMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/); + if (markdownMatch) return markdownMatch[1].trim(); + + const jsonMatch = text.match(/\{[\s\S]*\}/); + if (jsonMatch) return jsonMatch[0]; + + return text.trim(); + } + + /** + * Validate parsed data against schema + */ + _validateAgainstSchema(parsed) { + for (const schema of this.responseSchemas) { + const { name, type, enum: enumValues, required = true } = schema; + + // Check required fields + if (required && !(name in parsed)) { + throw new Error(`Missing required field: ${name}`); + } + + if (name in parsed) { + const value = parsed[name]; + + // Check type + if (!this._checkType(value, type)) { + throw new Error( + `Field ${name} should be ${type}, got ${typeof value}` + ); + } + + // Check enum values + if (enumValues && !enumValues.includes(value)) { + throw new Error( + `Field ${name} must be one of: ${enumValues.join(', ')}` + ); + } + } + } + } + + /** + * Check if value matches expected type + */ + _checkType(value, type) { + switch (type) { + case 'string': + return typeof value === 'string'; + case 'number': + return typeof value === 'number' && !isNaN(value); + case 'boolean': + return typeof value === 'boolean'; + case 'array': + return Array.isArray(value); + case 'object': + return typeof value === 'object' && value !== null && !Array.isArray(value); + default: + return true; + } + } + + /** + * Generate format instructions for LLM + */ + getFormatInstructions() { + const schemaDescriptions = this.responseSchemas.map(schema => { + let desc = `"${schema.name}": ${schema.type}`; + if (schema.description) { + desc += ` // ${schema.description}`; + } + if (schema.enum) { + desc += ` (one of: ${schema.enum.join(', ')})`; + } + return desc; + }); + + return `Respond with valid JSON matching this schema: +{ +${schemaDescriptions.map(d => ' ' + d).join(',\n')} +}`; + } + + /** + * Static helper to create from simple schema + */ + static fromNamesAndDescriptions(schemas) { + const responseSchemas = Object.entries(schemas).map(([name, description]) => ({ + name, + description, + type: 'string' // Default type + })); + + return new StructuredOutputParser({ responseSchemas }); + } +} \ No newline at end of file diff --git a/src/prompts/base-prompt-template.js b/src/prompts/base-prompt-template.js new file mode 100644 index 0000000000000000000000000000000000000000..93c4ba728a517dc6745480bac90ec7d9a2a824d7 --- /dev/null +++ b/src/prompts/base-prompt-template.js @@ -0,0 +1,56 @@ +/** + * BasePromptTemplate + * + * Abstract prompt template class* + */ + +import { Runnable } from '../core/runnable.js'; + +/** + * Base class for all prompt templates + */ +export class BasePromptTemplate extends Runnable { + constructor(options = {}) { + super(); + this.inputVariables = options.inputVariables || []; + this.partialVariables = options.partialVariables || {}; + } + + /** + * Format the prompt with given values + * @abstract + */ + async format(values) { + throw new Error('Subclasses must implement format()'); + } + + /** + * Runnable interface: invoke returns formatted prompt + */ + async _call(input, config) { + return await this.format(input); + } + + /** + * Validate that all required variables are provided + */ + _validateInput(values) { + const provided = { ...this.partialVariables, ...values }; + const missing = this.inputVariables.filter( + key => !(key in provided) + ); + + if (missing.length > 0) { + throw new Error( + `Missing required input variables: ${missing.join(', ')}` + ); + } + } + + /** + * Merge partial variables with provided values + */ + _mergePartialAndUserVariables(values) { + return { ...this.partialVariables, ...values }; + } +} \ No newline at end of file diff --git a/src/prompts/chat-prompt-template.js b/src/prompts/chat-prompt-template.js new file mode 100644 index 0000000000000000000000000000000000000000..bb33bfcb727480691213cf3726cef7a9b55cdab4 --- /dev/null +++ b/src/prompts/chat-prompt-template.js @@ -0,0 +1,98 @@ +import { BasePromptTemplate } from './base-prompt-template.js'; +import { SystemMessage, HumanMessage, AIMessage } from '../core/message.js'; +import { PromptTemplate } from './prompt-template.js'; + +/** + * Template for chat-based conversations + * Returns an array of Message objects + * + * Example: + * const prompt = ChatPromptTemplate.fromMessages([ + * ["system", "You are a {role}"], + * ["human", "{input}"] + * ]); + * + * const messages = await prompt.format({ + * role: "translator", + * input: "Hello" + * }); + * // [SystemMessage(...), HumanMessage(...)] + */ +export class ChatPromptTemplate extends BasePromptTemplate { + constructor(options = {}) { + super(options); + this.promptMessages = options.promptMessages || []; + } + + /** + * Format into array of Message objects + */ + async format(values) { + this._validateInput(values); + const allValues = this._mergePartialAndUserVariables(values); + + const messages = []; + for (const [role, template] of this.promptMessages) { + const content = await this._formatMessageTemplate(template, allValues); + messages.push(this._createMessage(role, content)); + } + + return messages; + } + + /** + * Format a single message template + */ + async _formatMessageTemplate(template, values) { + if (typeof template === 'string') { + const promptTemplate = new PromptTemplate({ template }); + return await promptTemplate.format(values); + } + return template; + } + + /** + * Create appropriate Message object for role + */ + _createMessage(role, content) { + switch (role.toLowerCase()) { + case 'system': + return new SystemMessage(content); + case 'human': + case 'user': + return new HumanMessage(content); + case 'ai': + case 'assistant': + return new AIMessage(content); + default: + throw new Error(`Unknown message role: ${role}`); + } + } + + /** + * Static helper to create from message list + */ + static fromMessages(messages, options = {}) { + const promptMessages = messages.map(msg => { + if (Array.isArray(msg)) { + return msg; // [role, template] + } + throw new Error('Each message must be [role, template] array'); + }); + + // Extract all input variables from all templates + const inputVariables = new Set(); + for (const [, template] of promptMessages) { + if (typeof template === 'string') { + const matches = template.match(/\{(\w+)\}/g) || []; + matches.forEach(m => inputVariables.add(m.slice(1, -1))); + } + } + + return new ChatPromptTemplate({ + promptMessages, + inputVariables: Array.from(inputVariables), + ...options + }); + } +} \ No newline at end of file diff --git a/src/prompts/few-shot-prompt.js b/src/prompts/few-shot-prompt.js new file mode 100644 index 0000000000000000000000000000000000000000..97626a92a73c3d509736d95a16a48d4da115b9f9 --- /dev/null +++ b/src/prompts/few-shot-prompt.js @@ -0,0 +1,63 @@ +import { BasePromptTemplate } from './base-prompt-template.js'; +import { PromptTemplate } from './prompt-template.js'; + +/** + * Prompt template that includes examples (few-shot learning) + * + * Example: + * const fewShot = new FewShotPromptTemplate({ + * examples: [ + * { input: "2+2", output: "4" }, + * { input: "3+5", output: "8" } + * ], + * examplePrompt: new PromptTemplate({ + * template: "Input: {input}\nOutput: {output}", + * inputVariables: ["input", "output"] + * }), + * prefix: "Solve these math problems:", + * suffix: "Input: {input}\nOutput:", + * inputVariables: ["input"] + * }); + */ +export class FewShotPromptTemplate extends BasePromptTemplate { + constructor(options = {}) { + super(options); + this.examples = options.examples || []; + this.examplePrompt = options.examplePrompt; + this.prefix = options.prefix || ''; + this.suffix = options.suffix || ''; + this.exampleSeparator = options.exampleSeparator || '\n\n'; + } + + /** + * Format the few-shot prompt + */ + async format(values) { + this._validateInput(values); + const allValues = this._mergePartialAndUserVariables(values); + + const parts = []; + + // Add prefix + if (this.prefix) { + const prefixTemplate = new PromptTemplate({ template: this.prefix }); + parts.push(await prefixTemplate.format(allValues)); + } + + // Add formatted examples + if (this.examples.length > 0) { + const exampleStrings = await Promise.all( + this.examples.map(ex => this.examplePrompt.format(ex)) + ); + parts.push(exampleStrings.join(this.exampleSeparator)); + } + + // Add suffix + if (this.suffix) { + const suffixTemplate = new PromptTemplate({ template: this.suffix }); + parts.push(await suffixTemplate.format(allValues)); + } + + return parts.join('\n\n'); + } +} \ No newline at end of file diff --git a/src/prompts/index.js b/src/prompts/index.js new file mode 100644 index 0000000000000000000000000000000000000000..7c7a0eb610e0ce5aea08339a4e2ab4b7aec39783 --- /dev/null +++ b/src/prompts/index.js @@ -0,0 +1,13 @@ +/** + * Module exports + * + * @module src/prompts/index.js + */ + +export { BasePromptTemplate } from './base-prompt-template.js'; +export { PromptTemplate } from './prompt-template.js'; +export { ChatPromptTemplate } from './chat-prompt-template.js'; +export { FewShotPromptTemplate } from './few-shot-prompt.js'; +export { PipelinePromptTemplate } from './pipeline-prompt.js'; +export { SystemMessagePromptTemplate } from './system-message-prompt.js'; + diff --git a/src/prompts/pipeline-prompt.js b/src/prompts/pipeline-prompt.js new file mode 100644 index 0000000000000000000000000000000000000000..9940b5de0da8f0c2875621aa326757a9a7f06deb --- /dev/null +++ b/src/prompts/pipeline-prompt.js @@ -0,0 +1,58 @@ +import { BasePromptTemplate } from './base-prompt-template.js'; + +/** + * Compose multiple prompts into a pipeline + * + * Example: + * const pipeline = new PipelinePromptTemplate({ + * finalPrompt: mainPrompt, + * pipelinePrompts: [ + * { name: "context", prompt: contextPrompt }, + * { name: "instructions", prompt: instructionPrompt } + * ] + * }); + */ +export class PipelinePromptTemplate extends BasePromptTemplate { + constructor(options = {}) { + super(options); + this.finalPrompt = options.finalPrompt; + this.pipelinePrompts = options.pipelinePrompts || []; + + // Collect all input variables + this.inputVariables = this._collectInputVariables(); + } + + /** + * Format by running pipeline prompts first, then final prompt + */ + async format(values) { + this._validateInput(values); + const allValues = this._mergePartialAndUserVariables(values); + + // Format each pipeline prompt and collect results + const pipelineResults = {}; + for (const { name, prompt } of this.pipelinePrompts) { + pipelineResults[name] = await prompt.format(allValues); + } + + // Merge with original values and format final prompt + const finalValues = { ...allValues, ...pipelineResults }; + return await this.finalPrompt.format(finalValues); + } + + /** + * Collect input variables from all prompts + */ + _collectInputVariables() { + const vars = new Set(this.finalPrompt.inputVariables); + + for (const { prompt } of this.pipelinePrompts) { + prompt.inputVariables.forEach(v => vars.add(v)); + } + + // Remove pipeline output names (they're generated) + this.pipelinePrompts.forEach(({ name }) => vars.delete(name)); + + return Array.from(vars); + } +} \ No newline at end of file diff --git a/src/prompts/prompt-template.js b/src/prompts/prompt-template.js new file mode 100644 index 0000000000000000000000000000000000000000..dc2ec64aa5fc40c8e9d096f268455ac5caa06e55 --- /dev/null +++ b/src/prompts/prompt-template.js @@ -0,0 +1,60 @@ +import { BasePromptTemplate } from './base-prompt-template.js'; + +/** + * Simple string template with {variable} placeholders + * + * Example: + * const prompt = new PromptTemplate({ + * template: "Translate to {language}: {text}", + * inputVariables: ["language", "text"] + * }); + * + * await prompt.format({ language: "Spanish", text: "Hello" }); + * // "Translate to Spanish: Hello" + */ +export class PromptTemplate extends BasePromptTemplate { + constructor(options = {}) { + super(options); + this.template = options.template; + + // Auto-detect input variables if not provided + if (!options.inputVariables) { + this.inputVariables = this._extractInputVariables(this.template); + } + } + + /** + * Format the template with provided values + */ + async format(values) { + this._validateInput(values); + const allValues = this._mergePartialAndUserVariables(values); + + let result = this.template; + for (const [key, value] of Object.entries(allValues)) { + const regex = new RegExp(`\\{${key}\\}`, 'g'); + result = result.replace(regex, String(value)); + } + + return result; + } + + /** + * Extract variable names from template string + * Finds all {variable} patterns + */ + _extractInputVariables(template) { + const matches = template.match(/\{(\w+)\}/g) || []; + return matches.map(match => match.slice(1, -1)); + } + + /** + * Static helper to create from template string + */ + static fromTemplate(template, options = {}) { + return new PromptTemplate({ + template, + ...options + }); + } +} \ No newline at end of file diff --git a/src/prompts/system-message-prompt.js b/src/prompts/system-message-prompt.js new file mode 100644 index 0000000000000000000000000000000000000000..594f03d9ff8c0d0bb378c833c4d3f7761b5b63ba --- /dev/null +++ b/src/prompts/system-message-prompt.js @@ -0,0 +1,75 @@ +import { BasePromptTemplate } from './base-prompt-template.js'; +import { SystemMessage } from '../core/message.js'; +import { PromptTemplate } from './prompt-template.js'; + +/** + * Template specifically for system messages + * Returns a single SystemMessage object + * + * Example: + * const systemPrompt = new SystemMessagePromptTemplate({ + * template: "You are a {role} assistant specialized in {domain}. {instructions}", + * inputVariables: ["role", "domain", "instructions"] + * }); + * + * const message = await systemPrompt.format({ + * role: "helpful", + * domain: "cooking", + * instructions: "Always provide recipe alternatives." + * }); + * // SystemMessage("You are a helpful assistant specialized in cooking. Always provide recipe alternatives.") + */ +export class SystemMessagePromptTemplate extends BasePromptTemplate { + constructor(options = {}) { + super(options); + this.prompt = options.prompt || new PromptTemplate({ + template: options.template, + inputVariables: options.inputVariables, + partialVariables: options.partialVariables + }); + + // Inherit input variables from inner prompt + if (!options.inputVariables) { + this.inputVariables = this.prompt.inputVariables; + } + } + + /** + * Format into a SystemMessage object + */ + async format(values) { + this._validateInput(values); + const allValues = this._mergePartialAndUserVariables(values); + + const content = await this.prompt.format(allValues); + return new SystemMessage(content); + } + + /** + * Static helper to create from template string + */ + static fromTemplate(template, options = {}) { + return new SystemMessagePromptTemplate({ + template, + ...options + }); + } + + /** + * Create with partial variables pre-filled + * Useful for setting default context that can be overridden + */ + static fromTemplateWithPartials(template, partialVariables = {}, options = {}) { + const promptTemplate = new PromptTemplate({ template }); + const inputVariables = promptTemplate.inputVariables.filter( + v => !(v in partialVariables) + ); + + return new SystemMessagePromptTemplate({ + template, + inputVariables, + partialVariables, + ...options + }); + } +} \ No newline at end of file diff --git a/src/tools/base-tool.js b/src/tools/base-tool.js new file mode 100644 index 0000000000000000000000000000000000000000..0eb045f1332ce5d3c5c74830442cf0abb9e6689d --- /dev/null +++ b/src/tools/base-tool.js @@ -0,0 +1,18 @@ +/** + * BaseTool + * + * Abstract tool class with JSON Schema support + * + * @module src/tools/base-tool.js + */ + +export class BaseTool { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('BaseTool not yet implemented'); + } + + // TODO: Add methods +} + +export default BaseTool; diff --git a/src/tools/builtin/calculator.js b/src/tools/builtin/calculator.js new file mode 100644 index 0000000000000000000000000000000000000000..d4430b458de3abb3d8218714f7cfa06ee188076b --- /dev/null +++ b/src/tools/builtin/calculator.js @@ -0,0 +1,18 @@ +/** + * Calculator + * + * Basic arithmetic operations tool + * + * @module src/tools/builtin/calculator.js + */ + +export class Calculator { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('Calculator not yet implemented'); + } + + // TODO: Add methods +} + +export default Calculator; diff --git a/src/tools/builtin/code-executor.js b/src/tools/builtin/code-executor.js new file mode 100644 index 0000000000000000000000000000000000000000..1f45244f3e3f5ad47aa7f3f1d93357deb0a377f6 --- /dev/null +++ b/src/tools/builtin/code-executor.js @@ -0,0 +1,18 @@ +/** + * CodeExecutor + * + * Execute code in sandboxed environment + * + * @module src/tools/builtin/code-executor.js + */ + +export class CodeExecutor { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('CodeExecutor not yet implemented'); + } + + // TODO: Add methods +} + +export default CodeExecutor; diff --git a/src/tools/builtin/file-reader.js b/src/tools/builtin/file-reader.js new file mode 100644 index 0000000000000000000000000000000000000000..d80564724b36161c63da6f98298e40fd532f0b84 --- /dev/null +++ b/src/tools/builtin/file-reader.js @@ -0,0 +1,18 @@ +/** + * FileReader + * + * Read files from filesystem + * + * @module src/tools/builtin/file-reader.js + */ + +export class FileReader { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('FileReader not yet implemented'); + } + + // TODO: Add methods +} + +export default FileReader; diff --git a/src/tools/builtin/file-writer.js b/src/tools/builtin/file-writer.js new file mode 100644 index 0000000000000000000000000000000000000000..f134d4a9643235e12502a90e1db20a00518bc2b4 --- /dev/null +++ b/src/tools/builtin/file-writer.js @@ -0,0 +1,18 @@ +/** + * FileWriter + * + * Write files to filesystem + * + * @module src/tools/builtin/file-writer.js + */ + +export class FileWriter { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('FileWriter not yet implemented'); + } + + // TODO: Add methods +} + +export default FileWriter; diff --git a/src/tools/builtin/index.js b/src/tools/builtin/index.js new file mode 100644 index 0000000000000000000000000000000000000000..fc6d919598dd63b4e2d339c483119c34b0d0243f --- /dev/null +++ b/src/tools/builtin/index.js @@ -0,0 +1,13 @@ +/** + * Module exports + * + * @module src/tools/builtin/index.js + */ + +export { Calculator } from './calculator.js'; +export { WebSearch } from './web-search.js'; +export { WebScraper } from './web-scraper.js'; +export { FileReader } from './file-reader.js'; +export { FileWriter } from './file-writer.js'; +export { CodeExecutor } from './code-executor.js'; + diff --git a/src/tools/builtin/web-scraper.js b/src/tools/builtin/web-scraper.js new file mode 100644 index 0000000000000000000000000000000000000000..fa098e8ad0a0804c1a2a52be50eb5baec7f46cfc --- /dev/null +++ b/src/tools/builtin/web-scraper.js @@ -0,0 +1,18 @@ +/** + * WebScraper + * + * Web scraping tool + * + * @module src/tools/builtin/web-scraper.js + */ + +export class WebScraper { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('WebScraper not yet implemented'); + } + + // TODO: Add methods +} + +export default WebScraper; diff --git a/src/tools/builtin/web-search.js b/src/tools/builtin/web-search.js new file mode 100644 index 0000000000000000000000000000000000000000..d3ad72869d627c89824abe9ff04033ed6ce2ddaf --- /dev/null +++ b/src/tools/builtin/web-search.js @@ -0,0 +1,18 @@ +/** + * WebSearch + * + * Web search tool (placeholder for API integration) + * + * @module src/tools/builtin/web-search.js + */ + +export class WebSearch { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('WebSearch not yet implemented'); + } + + // TODO: Add methods +} + +export default WebSearch; diff --git a/src/tools/index.js b/src/tools/index.js new file mode 100644 index 0000000000000000000000000000000000000000..9d1dd4a39e56060bf076ae74828f8c3a061d1205 --- /dev/null +++ b/src/tools/index.js @@ -0,0 +1,11 @@ +/** + * Module exports + * + * @module src/tools/index.js + */ + +export { BaseTool } from './base-tool.js'; +export { ToolExecutor } from './tool-executor.js'; +export { ToolRegistry } from './tool-registry.js'; +export * from './builtin/index.js'; + diff --git a/src/tools/tool-executor.js b/src/tools/tool-executor.js new file mode 100644 index 0000000000000000000000000000000000000000..d1040a3dff14b32ca97853e693b8c00f57339018 --- /dev/null +++ b/src/tools/tool-executor.js @@ -0,0 +1,18 @@ +/** + * ToolExecutor + * + * Executes tools with error handling and timeout + * + * @module src/tools/tool-executor.js + */ + +export class ToolExecutor { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('ToolExecutor not yet implemented'); + } + + // TODO: Add methods +} + +export default ToolExecutor; diff --git a/src/tools/tool-registry.js b/src/tools/tool-registry.js new file mode 100644 index 0000000000000000000000000000000000000000..02ded0354de91da67411f45d43b6445d0febf2b5 --- /dev/null +++ b/src/tools/tool-registry.js @@ -0,0 +1,18 @@ +/** + * ToolRegistry + * + * Manages and organizes tools + * + * @module src/tools/tool-registry.js + */ + +export class ToolRegistry { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('ToolRegistry not yet implemented'); + } + + // TODO: Add methods +} + +export default ToolRegistry; diff --git a/src/utils/callback-manager.js b/src/utils/callback-manager.js new file mode 100644 index 0000000000000000000000000000000000000000..6216ddb5e321c3dace92631735601ff7aed2a1f5 --- /dev/null +++ b/src/utils/callback-manager.js @@ -0,0 +1,88 @@ +/** + * CallbackManager + * + * Event system for logging and monitoring + * + * @module src/utils/callback-manager.js + */ +export class CallbackManager { + constructor(callbacks = []) { + this.callbacks = callbacks; + } + + /** + * Add a callback + */ + add(callback) { + this.callbacks.push(callback); + } + + /** + * Call onStart for all callbacks + */ + async handleStart(runnable, input, config) { + await Promise.all( + this.callbacks.map(cb => + this._safeCall(() => cb.onStart(runnable, input, config)) + ) + ); + } + + /** + * Call onEnd for all callbacks + */ + async handleEnd(runnable, output, config) { + await Promise.all( + this.callbacks.map(cb => + this._safeCall(() => cb.onEnd(runnable, output, config)) + ) + ); + } + + /** + * Call onError for all callbacks + */ + async handleError(runnable, error, config) { + await Promise.all( + this.callbacks.map(cb => + this._safeCall(() => cb.onError(runnable, error, config)) + ) + ); + } + + /** + * Call onLLMNewToken for all callbacks + */ + async handleLLMNewToken(token, config) { + await Promise.all( + this.callbacks.map(cb => + this._safeCall(() => cb.onLLMNewToken(token, config)) + ) + ); + } + + /** + * Call onChainStep for all callbacks + */ + async handleChainStep(stepName, output, config) { + await Promise.all( + this.callbacks.map(cb => + this._safeCall(() => cb.onChainStep(stepName, output, config)) + ) + ); + } + + /** + * Safely call a callback (don't let one callback crash others) + */ + async _safeCall(fn) { + try { + await fn(); + } catch (error) { + console.error('Callback error:', error); + // Don't throw - callbacks shouldn't break the pipeline + } + } +} + +export default CallbackManager; diff --git a/src/utils/callbacks.js b/src/utils/callbacks.js new file mode 100644 index 0000000000000000000000000000000000000000..e47b4a6e0ecc0baf8d4db22104ebeee1cf16bed9 --- /dev/null +++ b/src/utils/callbacks.js @@ -0,0 +1,196 @@ +/** + * BaseCallback - Abstract callback handler + */ +export class BaseCallback { + /** + * Called when a Runnable starts + */ + async onStart(runnable, input, config) { + // Override in subclass + } + + /** + * Called when a Runnable completes successfully + */ + async onEnd(runnable, output, config) { + // Override in subclass + } + + /** + * Called when a Runnable errors + */ + async onError(runnable, error, config) { + // Override in subclass + } + + /** + * Called for LLM token streaming + */ + async onLLMNewToken(token, config) { + // Override in subclass + } + + /** + * Called when a chain step completes + */ + async onChainStep(stepName, output, config) { + // Override in subclass + } +} + +/** + * ConsoleCallback - Logs to console + */ +export class ConsoleCallback extends BaseCallback { + constructor(options = {}) { + super(); + this.verbose = options.verbose ?? true; + this.colors = options.colors ?? true; + } + + async onStart(runnable, input, config) { + if (this.verbose) { + console.log(`\n▶ Starting: ${runnable.name}`); + console.log(` Input:`, this._format(input)); + } + } + + async onEnd(runnable, output, config) { + if (this.verbose) { + console.log(`✓ Completed: ${runnable.name}`); + console.log(` Output:`, this._format(output)); + } + } + + async onError(runnable, error, config) { + console.error(`✗ Error in ${runnable.name}:`, error.message); + } + + async onLLMNewToken(token, config) { + process.stdout.write(token); + } + + _format(value) { + if (typeof value === 'string') { + return value.length > 100 ? value.substring(0, 97) + '...' : value; + } + return JSON.stringify(value, null, 2); + } +} + +/** + * MetricsCallback - Tracks timing and counts + */ +export class MetricsCallback extends BaseCallback { + constructor() { + super(); + this.metrics = { + calls: {}, + totalTime: {}, + errors: {} + }; + this.startTimes = new Map(); + } + + async onStart(runnable, input, config) { + const name = runnable.name; + this.startTimes.set(name, Date.now()); + + this.metrics.calls[name] = (this.metrics.calls[name] || 0) + 1; + } + + async onEnd(runnable, output, config) { + const name = runnable.name; + const startTime = this.startTimes.get(name); + + if (startTime) { + const duration = Date.now() - startTime; + this.metrics.totalTime[name] = (this.metrics.totalTime[name] || 0) + duration; + this.startTimes.delete(name); + } + } + + async onError(runnable, error, config) { + const name = runnable.name; + this.metrics.errors[name] = (this.metrics.errors[name] || 0) + 1; + } + + getReport() { + const report = []; + + for (const [name, calls] of Object.entries(this.metrics.calls)) { + const totalTime = this.metrics.totalTime[name] || 0; + const avgTime = calls > 0 ? (totalTime / calls).toFixed(2) : 0; + const errors = this.metrics.errors[name] || 0; + + report.push({ + runnable: name, + calls, + avgTime: `${avgTime}ms`, + totalTime: `${totalTime}ms`, + errors + }); + } + + return report; + } + + reset() { + this.metrics = {calls: {}, totalTime: {}, errors: {}}; + this.startTimes.clear(); + } +} + +/** + * FileCallback - Logs to file + */ +export class FileCallback extends BaseCallback { + constructor(filename) { + super(); + this.filename = filename; + this.logs = []; + } + + async onStart(runnable, input, config) { + this.logs.push({ + timestamp: new Date().toISOString(), + event: 'start', + runnable: runnable.name, + input: this._serialize(input) + }); + } + + async onEnd(runnable, output, config) { + this.logs.push({ + timestamp: new Date().toISOString(), + event: 'end', + runnable: runnable.name, + output: this._serialize(output) + }); + } + + async onError(runnable, error, config) { + this.logs.push({ + timestamp: new Date().toISOString(), + event: 'error', + runnable: runnable.name, + error: error.message + }); + } + + async flush() { + const fs = await import('fs/promises'); + await fs.writeFile( + this.filename, + JSON.stringify(this.logs, null, 2), + 'utf-8' + ); + this.logs = []; + } + + _serialize(value) { + if (typeof value === 'string') return value; + if (value?.content) return value.content; // Message + return JSON.stringify(value); + } +} \ No newline at end of file diff --git a/src/utils/index.js b/src/utils/index.js new file mode 100644 index 0000000000000000000000000000000000000000..9b046c2ccd251c259ac5c46ddb59248321ac3ce8 --- /dev/null +++ b/src/utils/index.js @@ -0,0 +1,13 @@ +/** + * Module exports + * + * @module src/utils/index.js + */ + +export { CallbackManager } from './callback-manager.js'; +export { TokenCounter } from './token-counter.js'; +export { RetryManager } from './retry.js'; +export { TimeoutManager } from './timeout.js'; +export { Logger } from './logger.js'; +export { SchemaValidator } from './schema-validator.js'; + diff --git a/src/utils/logger.js b/src/utils/logger.js new file mode 100644 index 0000000000000000000000000000000000000000..cceea9718b067b43ce8d33112c2754da643c33bd --- /dev/null +++ b/src/utils/logger.js @@ -0,0 +1,18 @@ +/** + * Logger + * + * Logging utility + * + * @module src/utils/logger.js + */ + +export class Logger { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('Logger not yet implemented'); + } + + // TODO: Add methods +} + +export default Logger; diff --git a/src/utils/retry.js b/src/utils/retry.js new file mode 100644 index 0000000000000000000000000000000000000000..0eb9321df07b4554f70cc9d57f39986d1c2a1875 --- /dev/null +++ b/src/utils/retry.js @@ -0,0 +1,18 @@ +/** + * RetryManager + * + * Retry logic with exponential backoff + * + * @module src/utils/retry.js + */ + +export class RetryManager { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('RetryManager not yet implemented'); + } + + // TODO: Add methods +} + +export default RetryManager; diff --git a/src/utils/schema-validator.js b/src/utils/schema-validator.js new file mode 100644 index 0000000000000000000000000000000000000000..670ac0f109d0fe37f969e941216090889876a4c0 --- /dev/null +++ b/src/utils/schema-validator.js @@ -0,0 +1,18 @@ +/** + * SchemaValidator + * + * JSON Schema validation + * + * @module src/utils/schema-validator.js + */ + +export class SchemaValidator { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('SchemaValidator not yet implemented'); + } + + // TODO: Add methods +} + +export default SchemaValidator; diff --git a/src/utils/timeout.js b/src/utils/timeout.js new file mode 100644 index 0000000000000000000000000000000000000000..ae2a7b8fba71cdb92ed27e16540f864f17aa8120 --- /dev/null +++ b/src/utils/timeout.js @@ -0,0 +1,18 @@ +/** + * TimeoutManager + * + * Timeout wrapper for async operations + * + * @module src/utils/timeout.js + */ + +export class TimeoutManager { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('TimeoutManager not yet implemented'); + } + + // TODO: Add methods +} + +export default TimeoutManager; diff --git a/src/utils/token-counter.js b/src/utils/token-counter.js new file mode 100644 index 0000000000000000000000000000000000000000..e267f29ef00917e57ca89a6d450e8d29a8773fea --- /dev/null +++ b/src/utils/token-counter.js @@ -0,0 +1,18 @@ +/** + * TokenCounter + * + * Token usage tracking + * + * @module src/utils/token-counter.js + */ + +export class TokenCounter { + constructor(options = {}) { + // TODO: Implement constructor + throw new Error('TokenCounter not yet implemented'); + } + + // TODO: Add methods +} + +export default TokenCounter; diff --git a/test-emails.json b/test-emails.json new file mode 100644 index 0000000000000000000000000000000000000000..1db58835325028467df594bd7db55e84cf9f5ca6 --- /dev/null +++ b/test-emails.json @@ -0,0 +1,122 @@ +[ + { + "from": "promotions@shop.com", + "subject": "🎉 70% OFF SALE! Limited Time Only!!!", + "body": "Click here now to get amazing deals! Free shipping! Act fast!" + }, + { + "from": "billing@company.com", + "subject": "Invoice #12345 - Payment Due", + "body": "Your invoice for $1,250.00 is attached. Payment is due by March 15th." + }, + { + "from": "sarah@company.com", + "subject": "Can we schedule a meeting next week?", + "body": "Hi! I wanted to discuss the Q2 planning. Are you available Tuesday or Wednesday?" + }, + { + "from": "security@bank.com", + "subject": "URGENT: Suspicious Activity Detected", + "body": "We detected unusual login attempts on your account. Please verify immediately." + }, + { + "from": "mom@family.com", + "subject": "How are you doing?", + "body": "Hi honey, just checking in. Hope you're having a great week! Love, Mom" + }, + { + "from": "newsletter@tech.com", + "subject": "Weekly Tech News Digest", + "body": "Here are this week's top technology stories and updates..." + }, + { + "from": "accounting@acmecorp.com", + "subject": "Monthly Statement - January 2024", + "body": "Please find attached your monthly statement for January 2024. Total amount due: $3,450.75. Payment terms: Net 30." + }, + { + "from": "noreply@phishing-site.com", + "subject": "Your Amazon account has been locked!!!", + "body": "URGENT!!! Click here NOW to verify your account or it will be PERMANENTLY DELETED!!! Act immediately!!!" + }, + { + "from": "boss@company.com", + "subject": "Need your input by EOD", + "body": "Hi, I need your analysis on the Q4 projections before end of day today. This is blocking the board meeting tomorrow morning." + }, + { + "from": "hr@company.com", + "subject": "Team Lunch - Friday 12pm", + "body": "Hi everyone, we're having a team lunch this Friday at noon in the conference room. Please let me know if you can attend." + }, + { + "from": "dad@family.com", + "subject": "Re: Weekend plans", + "body": "Sounds great! We'll see you Saturday around 3pm. Your mom is making her famous lasagna. Looking forward to it!" + }, + { + "from": "updates@github.com", + "subject": "Your weekly GitHub activity summary", + "body": "Here's your activity from the past week: 15 commits, 3 pull requests merged, 8 issues closed." + }, + { + "from": "WINNER@lottery-scam.com", + "subject": "YOU WON $1,000,000 USD!!!", + "body": "CONGRATULATIONS!!! You have been selected as the WINNER of our international lottery!!! Send your bank details NOW to claim your prize!!!" + }, + { + "from": "it-support@company.com", + "subject": "CRITICAL: Server maintenance tonight at 11pm", + "body": "Critical maintenance alert: We will be performing emergency server maintenance tonight at 11pm. Expected downtime: 2 hours. Please save all work." + }, + { + "from": "john@company.com", + "subject": "Quick sync on project timeline", + "body": "Hey, do you have 15 minutes today to discuss the project timeline? I want to make sure we're aligned before the client call tomorrow." + }, + { + "from": "receipts@stripe.com", + "subject": "Receipt for your payment to CloudService", + "body": "Thank you for your payment. Receipt #RCP-2024-001. Amount: $49.99. Service: Cloud Storage Pro Plan." + }, + { + "from": "friend@personal.com", + "subject": "Coffee next week?", + "body": "Hey! It's been too long. Want to grab coffee next week? I'm free Tuesday afternoon or Thursday morning. Let me know what works!" + }, + { + "from": "notifications@linkedin.com", + "subject": "You have 5 new connection requests", + "body": "5 people want to connect with you on LinkedIn. View your pending invitations and grow your network." + }, + { + "from": "ceo@company.com", + "subject": "All-hands meeting moved to 2pm TODAY", + "body": "Important: The all-hands meeting has been moved from 3pm to 2pm today due to a schedule conflict. Please adjust your calendars accordingly." + }, + { + "from": "support@legitservice.com", + "subject": "Your subscription renewal", + "body": "Your annual subscription will renew on March 30th. Amount: $99.99. If you need to update your payment method, please visit your account settings." + }, + { + "from": "calendar@company.com", + "subject": "Reminder: Performance review meeting tomorrow at 10am", + "body": "This is a reminder about your performance review meeting scheduled for tomorrow (March 15) at 10:00 AM with your manager in Conference Room B." + }, + { + "from": "sister@family.com", + "subject": "Kids' birthday party next month", + "body": "Hi! Just wanted to give you a heads up that we're planning Emma's birthday party for April 20th. Would love for you to be there! More details soon." + }, + { + "from": "webinar@training.com", + "subject": "Webinar Registration Confirmed: AI Best Practices", + "body": "Thank you for registering for our webinar 'AI Best Practices for Developers' on March 25th at 2pm EST. You will receive a reminder email with the joining link." + }, + { + "from": "no-reply@bank.com", + "subject": "Your bank statement is now available", + "body": "Your monthly bank statement for February 2024 is now available. Login to your account to view and download your statement." + } +] \ No newline at end of file diff --git a/tutorial/01-foundation/01-runnable/exercises/01-multiplier-runnable.js b/tutorial/01-foundation/01-runnable/exercises/01-multiplier-runnable.js new file mode 100644 index 0000000000000000000000000000000000000000..d37f7107b6559b8d80d42e4a9b9fd62df36424a0 --- /dev/null +++ b/tutorial/01-foundation/01-runnable/exercises/01-multiplier-runnable.js @@ -0,0 +1,91 @@ +/** + * Exercise 1: Build a Multiplier Runnable + * + * Goal: Create a Runnable that multiplies numbers by a factor + * + * Requirements: + * - Takes a number as input + * - Multiplies by a factor set in constructor + * - Returns the result + * + * Example: + * const times3 = new MultiplierRunnable(3); + * await times3.invoke(5); // Should return 15 + */ + +import { Runnable } from '../../../../src/index.js'; + +/** + * MultiplierRunnable - Multiplies input by a factor + * + * TODO: Implement this class + */ +class MultiplierRunnable extends Runnable { + constructor(factor) { + // TODO: Call super() + // TODO: Store the factor + } + + async _call(input, config) { + // TODO: Validate that input is a number + // TODO: Multiply input by this.factor + // TODO: Return the result + } +} + +// ============================================================================ +// Tests - Run this file to check your implementation +// ============================================================================ + +async function runTests() { + console.log('🧪 Testing MultiplierRunnable...\n'); + + try { + // Test 1: Basic multiplication + console.log('Test 1: Basic multiplication'); + const times3 = new MultiplierRunnable(3); + const result1 = await times3.invoke(5); + console.assert(result1 === 15, `Expected 15, got ${result1}`); + console.log('✅ 3 × 5 = 15\n'); + + // Test 2: Different factor + console.log('Test 2: Different factor'); + const times10 = new MultiplierRunnable(10); + const result2 = await times10.invoke(7); + console.assert(result2 === 70, `Expected 70, got ${result2}`); + console.log('✅ 10 × 7 = 70\n'); + + // Test 3: Negative numbers + console.log('Test 3: Negative numbers'); + const times2 = new MultiplierRunnable(2); + const result3 = await times2.invoke(-5); + console.assert(result3 === -10, `Expected -10, got ${result3}`); + console.log('✅ 2 × -5 = -10\n'); + + // Test 4: Decimal numbers + console.log('Test 4: Decimal numbers'); + const times1_5 = new MultiplierRunnable(1.5); + const result4 = await times1_5.invoke(4); + console.assert(result4 === 6, `Expected 6, got ${result4}`); + console.log('✅ 1.5 × 4 = 6\n'); + + // Test 5: Zero + console.log('Test 5: Multiply by zero'); + const times0 = new MultiplierRunnable(0); + const result5 = await times0.invoke(100); + console.assert(result5 === 0, `Expected 0, got ${result5}`); + console.log('✅ 0 × 100 = 0\n'); + + console.log('🎉 All tests passed!'); + } catch (error) { + console.error('❌ Test failed:', error.message); + console.error(error.stack); + } +} + +// Run tests if this file is executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + runTests(); +} + +export { MultiplierRunnable }; \ No newline at end of file diff --git a/tutorial/01-foundation/01-runnable/exercises/02-json-parser-runnable.js b/tutorial/01-foundation/01-runnable/exercises/02-json-parser-runnable.js new file mode 100644 index 0000000000000000000000000000000000000000..7fd119528e0b8c9c07ea05f47c179cf522e22073 --- /dev/null +++ b/tutorial/01-foundation/01-runnable/exercises/02-json-parser-runnable.js @@ -0,0 +1,102 @@ +/** + * Exercise 2: Build a JSON Parser Runnable + * + * Goal: Create a Runnable that parses JSON strings safely + * + * Requirements: + * - Takes a JSON string as input + * - Parses it to an object + * - Handles errors gracefully (return null if invalid) + * - Optional: Add a default value option + * + * Example: + * const parser = new JsonParserRunnable(); + * await parser.invoke('{"name":"Alice"}'); // Should return { name: "Alice" } + * await parser.invoke('invalid json'); // Should return null + */ + +import { Runnable } from '../../../../src/index.js'; + +/** + * JsonParserRunnable - Safely parses JSON strings + * + * TODO: Implement this class + */ +class JsonParserRunnable extends Runnable { + constructor(options = {}) { + // TODO: Call super() + // TODO: Store options (like defaultValue) + } + + async _call(input, config) { + // TODO: Check if input is a string + // TODO: Try to parse the JSON + // TODO: If parsing fails, return null or defaultValue + // TODO: Return the parsed object + } +} + +// ============================================================================ +// Tests +// ============================================================================ + +async function runTests() { + console.log('🧪 Testing JsonParserRunnable...\n'); + + try { + // Test 1: Valid JSON object + console.log('Test 1: Valid JSON object'); + const parser = new JsonParserRunnable(); + const result1 = await parser.invoke('{"name":"Alice","age":30}'); + console.assert(result1.name === 'Alice', 'Should parse name'); + console.assert(result1.age === 30, 'Should parse age'); + console.log('✅ Parsed: {"name":"Alice","age":30}\n'); + + // Test 2: Valid JSON array + console.log('Test 2: Valid JSON array'); + const result2 = await parser.invoke('[1, 2, 3, 4, 5]'); + console.assert(Array.isArray(result2), 'Should return array'); + console.assert(result2.length === 5, 'Should have 5 elements'); + console.log('✅ Parsed: [1, 2, 3, 4, 5]\n'); + + // Test 3: Invalid JSON returns null + console.log('Test 3: Invalid JSON returns null'); + const result3 = await parser.invoke('this is not json'); + console.assert(result3 === null, 'Should return null for invalid JSON'); + console.log('✅ Returns null for invalid JSON\n'); + + // Test 4: Empty string returns null + console.log('Test 4: Empty string returns null'); + const result4 = await parser.invoke(''); + console.assert(result4 === null, 'Should return null for empty string'); + console.log('✅ Returns null for empty string\n'); + + // Test 5: With default value + console.log('Test 5: With default value'); + const parserWithDefault = new JsonParserRunnable({ + defaultValue: { error: 'Invalid JSON' } + }); + const result5 = await parserWithDefault.invoke('bad json'); + console.assert(result5.error === 'Invalid JSON', 'Should return default value'); + console.log('✅ Returns default value for invalid JSON\n'); + + // Test 6: Nested JSON + console.log('Test 6: Nested JSON'); + const nested = '{"user":{"name":"Bob","address":{"city":"NYC"}}}'; + const result6 = await parser.invoke(nested); + console.assert(result6.user.address.city === 'NYC', 'Should parse nested objects'); + console.log('✅ Parsed nested JSON\n'); + + console.log('🎉 All tests passed!'); + } catch (error) { + console.error('❌ Test failed:', error.message); + console.error(error.stack); + } +} + +// Run tests +if (import.meta.url === `file://${process.argv[1]}`) { + runTests(); +} + +export { JsonParserRunnable }; \ No newline at end of file diff --git a/tutorial/01-foundation/01-runnable/exercises/03-pipeline-composition.js b/tutorial/01-foundation/01-runnable/exercises/03-pipeline-composition.js new file mode 100644 index 0000000000000000000000000000000000000000..b5d3e772ce06123aa278eef2a4101a12adba7b4e --- /dev/null +++ b/tutorial/01-foundation/01-runnable/exercises/03-pipeline-composition.js @@ -0,0 +1,161 @@ +/** + * Exercise 3: Compose a Pipeline + * + * Goal: Use the Runnables you've created to build a pipeline + * + * Requirements: + * Build a pipeline that: + * 1. Takes a number as input + * 2. Multiplies it by a factor + * 3. Converts it to an object: { result: } + * 4. Converts the object to a JSON string + * + * Example: + * const pipeline = // your code + * await pipeline.invoke(5); // Should return '{"result":15}' if factor is 3 + * + * Bonus Challenge: + * - Create a pipeline that can parse JSON, extract a value, multiply it, and format it back + */ + +import { Runnable } from '../../../../src/index.js'; + +// You'll need these helper Runnables for the pipeline +// Some are already implemented above, others you'll create + +/** + * MultiplierRunnable - Multiplies by a factor + */ +class MultiplierRunnable extends Runnable { + constructor(factor) { + super(); + this.factor = factor; + } + + async _call(input, config) { + if (typeof input !== 'number') { + throw new Error('Input must be a number'); + } + return input * this.factor; + } +} + +/** + * ObjectWrapperRunnable - Wraps value in an object + * + * TODO: Implement this class + * Takes input and returns { result: input } + */ +class ObjectWrapperRunnable extends Runnable { + async _call(input, config) { + // TODO: Return an object with 'result' property + } +} + +/** + * JsonStringifyRunnable - Converts object to JSON string + * + * TODO: Implement this class + * Takes an object and returns JSON.stringify(object) + */ +class JsonStringifyRunnable extends Runnable { + async _call(input, config) { + // TODO: Convert input to JSON string + } +} + +// ============================================================================ +// Build Your Pipeline Here +// ============================================================================ + +/** + * TODO: Create the pipeline + * + * Hint: Use the pipe() method to chain Runnables + * + * const pipeline = runnable1 + * .pipe(runnable2) + * .pipe(runnable3); + */ + +function createPipeline(factor = 3) { + // TODO: Create instances of the Runnables + // TODO: Chain them together with pipe() + // TODO: Return the pipeline +} + +// ============================================================================ +// Tests +// ============================================================================ + +async function runTests() { + console.log('🧪 Testing Pipeline Composition...\n'); + + try { + // Test 1: Basic pipeline + console.log('Test 1: Basic pipeline (multiply → wrap → stringify)'); + const pipeline = createPipeline(3); + const result1 = await pipeline.invoke(5); + console.log(` Input: 5`); + console.log(` Output: ${result1}`); + console.assert(result1 === '{"result":15}', `Expected '{"result":15}', got '${result1}'`); + console.log('✅ Pipeline works!\n'); + + // Test 2: Different factor + console.log('Test 2: Different factor'); + const pipeline2 = createPipeline(10); + const result2 = await pipeline2.invoke(4); + console.log(` Input: 4`); + console.log(` Output: ${result2}`); + console.assert(result2 === '{"result":40}', `Expected '{"result":40}', got '${result2}'`); + console.log('✅ Works with different factors!\n'); + + // Test 3: Pipeline with batch + console.log('Test 3: Batch processing through pipeline'); + const pipeline3 = createPipeline(2); + const results3 = await pipeline3.batch([1, 2, 3]); + console.log(` Inputs: [1, 2, 3]`); + console.log(` Outputs: [${results3.join(', ')}]`); + console.assert(results3[0] === '{"result":2}', 'First result should be correct'); + console.assert(results3[1] === '{"result":4}', 'Second result should be correct'); + console.assert(results3[2] === '{"result":6}', 'Third result should be correct'); + console.log('✅ Batch processing works!\n'); + + // Test 4: Individual components work + console.log('Test 4: Testing individual components'); + const multiplier = new MultiplierRunnable(5); + const wrapper = new ObjectWrapperRunnable(); + const stringifier = new JsonStringifyRunnable(); + + const step1 = await multiplier.invoke(3); + console.log(` After multiply: ${step1}`); + console.assert(step1 === 15, 'Multiplier should work'); + + const step2 = await wrapper.invoke(step1); + console.log(` After wrap: ${JSON.stringify(step2)}`); + console.assert(step2.result === 15, 'Wrapper should work'); + + const step3 = await stringifier.invoke(step2); + console.log(` After stringify: ${step3}`); + console.assert(step3 === '{"result":15}', 'Stringifier should work'); + console.log('✅ All components work individually!\n'); + + console.log('🎉 All tests passed!'); + console.log('\n💡 You successfully composed multiple Runnables into a pipeline!'); + } catch (error) { + console.error('❌ Test failed:', error.message); + console.error(error.stack); + } +} + +// Run tests +if (import.meta.url === `file://${process.argv[1]}`) { + runTests(); +} + +export { + MultiplierRunnable, + ObjectWrapperRunnable, + JsonStringifyRunnable, + createPipeline +}; \ No newline at end of file diff --git a/tutorial/01-foundation/01-runnable/exercises/04-batch-processing.js b/tutorial/01-foundation/01-runnable/exercises/04-batch-processing.js new file mode 100644 index 0000000000000000000000000000000000000000..be26e7867c6b40a7b164f03f80641625e58a9651 --- /dev/null +++ b/tutorial/01-foundation/01-runnable/exercises/04-batch-processing.js @@ -0,0 +1,196 @@ +/** + * Exercise 4: Implement Batch Processing + * + * Goal: Test and understand parallel execution with batch() + * + * Requirements: + * - Process multiple inputs in parallel using batch() + * - Measure performance difference vs sequential + * - Handle errors in batch processing + * - Understand Promise.all() behavior + * + * Example: + * const results = await runnable.batch([1, 2, 3, 4, 5]); + * // All 5 inputs process simultaneously + */ + +import { Runnable } from '../../../../src/index.js'; + +/** + * DelayedMultiplierRunnable - Multiplies with a delay + * + * This simulates an async operation (like an API call or LLM inference) + * that takes time to complete. + */ +class DelayedMultiplierRunnable extends Runnable { + constructor(factor, delayMs = 100) { + super(); + this.factor = factor; + this.delayMs = delayMs; + } + + async _call(input, config) { + if (typeof input !== 'number') { + throw new Error('Input must be a number'); + } + + // Simulate async work + await new Promise(resolve => setTimeout(resolve, this.delayMs)); + + return input * this.factor; + } + + toString() { + return `DelayedMultiplier(×${this.factor}, ${this.delayMs}ms)`; + } +} + +/** + * TODO: Create a function that processes sequentially + * + * Process inputs one at a time (not in parallel) + */ +async function processSequentially(runnable, inputs) { + // TODO: Loop through inputs and call invoke() for each + // TODO: Return array of results +} + +/** + * TODO: Create a function that processes in parallel + * + * Process all inputs at the same time using batch() + */ +async function processInParallel(runnable, inputs) { + // TODO: Use batch() to process all inputs at once + // TODO: Return array of results +} + +// ============================================================================ +// Tests +// ============================================================================ + +async function runTests() { + console.log('🧪 Testing Batch Processing...\n'); + + try { + // Test 1: Basic batch processing + console.log('Test 1: Basic batch processing'); + const multiplier = new DelayedMultiplierRunnable(2, 100); + const inputs = [1, 2, 3, 4, 5]; + + const startTime = Date.now(); + const results = await multiplier.batch(inputs); + const duration = Date.now() - startTime; + + console.log(` Inputs: [${inputs.join(', ')}]`); + console.log(` Results: [${results.join(', ')}]`); + console.log(` Time: ${duration}ms`); + console.assert(results.length === 5, 'Should have 5 results'); + console.assert(results[0] === 2, 'First result should be 2'); + console.assert(results[4] === 10, 'Last result should be 10'); + console.assert(duration < 200, 'Should complete in ~100ms (parallel), not 500ms (sequential)'); + console.log('✅ Batch processing works!\n'); + + // Test 2: Compare sequential vs parallel + console.log('Test 2: Sequential vs Parallel comparison'); + const runnable = new DelayedMultiplierRunnable(3, 100); + const testInputs = [1, 2, 3, 4, 5]; + + console.log(' Processing sequentially...'); + const seqStart = Date.now(); + const seqResults = await processSequentially(runnable, testInputs); + const seqDuration = Date.now() - seqStart; + console.log(` Sequential: ${seqDuration}ms`); + + console.log(' Processing in parallel...'); + const parStart = Date.now(); + const parResults = await processInParallel(runnable, testInputs); + const parDuration = Date.now() - parStart; + console.log(` Parallel: ${parDuration}ms`); + + console.log(` Speedup: ${(seqDuration / parDuration).toFixed(1)}x faster`); + console.assert(parDuration < seqDuration / 2, 'Parallel should be much faster'); + console.log('✅ Parallel is significantly faster!\n'); + + // Test 3: Large batch + console.log('Test 3: Large batch (10 items)'); + const largeBatch = Array.from({ length: 10 }, (_, i) => i + 1); + const startLarge = Date.now(); + const largeResults = await multiplier.batch(largeBatch); + const durationLarge = Date.now() - startLarge; + + console.log(` Processed ${largeBatch.length} items in ${durationLarge}ms`); + console.assert(largeResults.length === 10, 'Should process all items'); + console.assert(durationLarge < 200, 'Should complete quickly due to parallelism'); + console.log('✅ Large batch works!\n'); + + // Test 4: Batch with errors + console.log('Test 4: Error handling in batch'); + const mixedInputs = [1, 2, 'invalid', 4, 5]; + + try { + await multiplier.batch(mixedInputs); + console.log('❌ Should have thrown an error'); + } catch (error) { + console.log(` Caught error: ${error.message}`); + console.log('✅ Errors are caught in batch processing!\n'); + } + + // Test 5: Empty batch + console.log('Test 5: Empty batch'); + const emptyResults = await multiplier.batch([]); + console.assert(emptyResults.length === 0, 'Empty batch should return empty array'); + console.log('✅ Empty batch handled correctly!\n'); + + // Test 6: Batch with pipeline + console.log('Test 6: Batch through a pipeline'); + class AddConstant extends Runnable { + constructor(constant) { + super(); + this.constant = constant; + } + async _call(input) { + await new Promise(resolve => setTimeout(resolve, 50)); + return input + this.constant; + } + } + + const pipeline = new DelayedMultiplierRunnable(2, 50) + .pipe(new AddConstant(10)); + + const pipelineInputs = [1, 2, 3]; + const startPipeline = Date.now(); + const pipelineResults = await pipeline.batch(pipelineInputs); + const durationPipeline = Date.now() - startPipeline; + + console.log(` Inputs: [${pipelineInputs.join(', ')}]`); + console.log(` Results: [${pipelineResults.join(', ')}]`); + console.log(` Expected: [12, 14, 16] (multiply by 2, then add 10)`); + console.log(` Time: ${durationPipeline}ms`); + console.assert(pipelineResults[0] === 12, 'First should be 12'); + console.assert(pipelineResults[2] === 16, 'Last should be 16'); + console.log('✅ Batch works through pipelines!\n'); + + console.log('🎉 All tests passed!'); + console.log('\n💡 Key Learnings:'); + console.log(' • batch() processes inputs in parallel'); + console.log(' • Much faster than sequential processing'); + console.log(' • Uses Promise.all() under the hood'); + console.log(' • All inputs must succeed (or all fail)'); + console.log(' • Works with pipelines too!'); + } catch (error) { + console.error('❌ Test failed:', error.message); + console.error(error.stack); + } +} + +// Run tests +if (import.meta.url === `file://${process.argv[1]}`) { + runTests(); +} + +export { + DelayedMultiplierRunnable, + processSequentially, + processInParallel +}; \ No newline at end of file diff --git a/tutorial/01-foundation/01-runnable/lesson.md b/tutorial/01-foundation/01-runnable/lesson.md new file mode 100644 index 0000000000000000000000000000000000000000..0adf92d565dbb02e32d6a311261bd5d2fa5653e6 --- /dev/null +++ b/tutorial/01-foundation/01-runnable/lesson.md @@ -0,0 +1,869 @@ +# The Runnable Contract + +**Part 1: Foundation - Lesson 1** + +> Understanding the single pattern that powers most AI agent framework + +## Overview + +The `Runnable` is the fundamental building block of our framework. It's a simple yet powerful abstraction that allows us to build complex AI systems from composable parts. Think of it as the "contract" that every component in the framework must follow. + +By the end of this lesson, you'll understand why frameworks like LangChain built everything around this single interface, and you'll implement your own Runnable components. + +## Why Does This Matter? + +Imagine you're building with LEGO blocks. Each block has the same connection mechanism (those little bumps), which means any block can connect to any other block. The `Runnable` interface is exactly that for AI agents. + +### The Problem Without Runnable + +```javascript +// Without a common interface, every component is different: +const llmResponse = await llm.generate(prompt); +const parsedOutput = parser.parse(llmResponse); +const memorySaved = memory.store(parsedOutput); + +// Different methods: generate(), parse(), store() +// Hard to compose, hard to test, hard to maintain +``` + +### The Solution With Runnable + +```javascript +// With Runnable, everything uses the same interface: +const result = await prompt + .pipe(llm) + .pipe(parser) + .pipe(memory) + .invoke(input); + +// Same method everywhere: invoke() +// Easy to compose, test, and maintain +``` + +## Learning Objectives + +By the end of this lesson, you will: + +- ✅ Understand what makes a good abstraction +- ✅ Implement the base `Runnable` class +- ✅ Create custom Runnable components +- ✅ Know the three core execution patterns: `invoke`, `stream`, `batch` +- ✅ Understand why this abstraction is powerful for AI systems + +## Core Concepts + +### What is a Runnable? + +A `Runnable` is any component that can: +1. **Take input** +2. **Do something with it** +3. **Return output** + +That's it! But this simplicity is what makes it powerful. + +### The Three Execution Patterns + +Every Runnable supports three ways of execution: + +#### 1. `invoke()` - Single Execution +Run once with one input, get one output. + +```javascript +const result = await runnable.invoke(input); +// Input: "Hello" +// Output: "Hello, World!" +``` + +**Use case**: Normal execution, when you have one thing to process. + +#### 2. `stream()` - Streaming Execution +Process input and receive output in chunks as it's generated. + +```javascript +for await (const chunk of runnable.stream(input)) { + console.log(chunk); // Print each piece as it arrives +} +// Output: "H", "e", "l", "l", "o", "..." +``` + +**Use case**: LLM text generation, where you want to show results in real-time. + +#### 3. `batch()` - Parallel Execution +Process multiple inputs at once. + +```javascript +const results = await runnable.batch([input1, input2, input3]); +// Input: ["Hello", "Hi", "Hey"] +// Output: ["Hello, World!", "Hi, World!", "Hey, World!"] +``` + +**Use case**: Processing many items efficiently. + +### The Magic: Composition + +The real power comes from combining Runnables: + +```javascript +const pipeline = runnableA.pipe(runnableB).pipe(runnableC); +``` + +Because everything is a Runnable, you can chain them together infinitely! + +## Implementation Deep Dive + +Let's build the `Runnable` class step by step. + +### Step 1: The Base Structure + +**Location:** `src/core/runnable.js` +```javascript +/** + * Runnable - Base class for all composable components + * + * Every Runnable must implement the _call() method. + * This base class provides invoke, stream, batch, and pipe. + */ +export class Runnable { + /** + * Main execution method - processes a single input + * + * @param {any} input - The input to process + * @param {Object} config - Optional configuration + * @returns {Promise} The processed output + */ + async invoke(input, config = {}) { + // This is the public interface + return await this._call(input, config); + } + + /** + * Internal method that subclasses must implement + * + * @param {any} input - The input to process + * @param {Object} config - Optional configuration + * @returns {Promise} The processed output + */ + async _call(input, config) { + throw new Error( + `${this.constructor.name} must implement _call() method` + ); + } +} +``` + +**Why this design?** +- `invoke()` is public and consistent across all Runnables +- `_call()` is internal and overridden by subclasses +- This separation allows us to add common behavior in `invoke()` without breaking subclasses + +### Step 2: Adding Streaming + +```javascript +export class Runnable { + // ... previous code ... + + /** + * Stream output in chunks + * + * @param {any} input - The input to process + * @param {Object} config - Optional configuration + * @yields {any} Output chunks + */ + async *stream(input, config = {}) { + // Default implementation: just yield the full result + // Subclasses can override for true streaming + const result = await this.invoke(input, config); + yield result; + } + + /** + * Internal streaming method for subclasses + * Override this for custom streaming behavior + */ + async *_stream(input, config) { + yield await this._call(input, config); + } +} +``` + +**Why generators (`async *`)?** +Generators allow us to yield values one at a time, perfect for streaming! + +```javascript +// Generator function +async *countToThree() { + yield 1; + yield 2; + yield 3; +} + +// Usage +for await (const num of countToThree()) { + console.log(num); // Prints: 1, then 2, then 3 +} +``` + +### Step 3: Adding Batch Processing + +```javascript +export class Runnable { + // ... previous code ... + + /** + * Process multiple inputs in parallel + * + * @param {Array} inputs - Array of inputs to process + * @param {Object} config - Optional configuration + * @returns {Promise>} Array of outputs + */ + async batch(inputs, config = {}) { + // Use Promise.all for parallel execution + return await Promise.all( + inputs.map(input => this.invoke(input, config)) + ); + } +} +``` + +**Key insight**: `Promise.all()` runs all promises concurrently. This means if you have 100 inputs, they all process at the same time (within system limits), not one by one! + +### Step 4: The Power Move - Composition with `pipe()` + +```javascript +export class Runnable { + // ... previous code ... + + /** + * Compose this Runnable with another + * Creates a new Runnable that runs both in sequence + * + * @param {Runnable} other - The Runnable to pipe to + * @returns {RunnableSequence} A new composed Runnable + */ + pipe(other) { + return new RunnableSequence([this, other]); + } +} +``` + +Now we need to create `RunnableSequence`: + +**Location:** `src/core/runnable.js` +```javascript +/** + * RunnableSequence - Chains multiple Runnables together + * + * Output of one becomes input of the next + */ +export class RunnableSequence extends Runnable { + constructor(steps) { + super(); + this.steps = steps; // Array of Runnables + } + + async _call(input, config) { + let output = input; + + // Run through each step sequentially + for (const step of this.steps) { + output = await step.invoke(output, config); + } + + return output; + } + + async *_stream(input, config) { + let output = input; + + // Stream through all steps + for (let i = 0; i < this.steps.length - 1; i++) { + output = await this.steps[i].invoke(output, config); + } + + // Only stream the last step + yield* this.steps[this.steps.length - 1].stream(output, config); + } + + // pipe() returns a new sequence with the added step + pipe(other) { + return new RunnableSequence([...this.steps, other]); + } +} +``` + +**Why is this powerful?** + +```javascript +// Each pipe creates a new Runnable +const step1 = new MyRunnable(); +const step2 = new AnotherRunnable(); +const step3 = new YetAnotherRunnable(); + +// Chain them +const pipeline = step1.pipe(step2).pipe(step3); + +// Now pipeline is itself a Runnable! +await pipeline.invoke(input); + +// And it can be piped to other things +const biggerPipeline = pipeline.pipe(step4); +``` + +## Complete Implementation + +Here's the full `Runnable` class (location: src/core/runnable.js): + +```javascript +/** + * Runnable - The foundation of composable AI components + * + * @module core/runnable + */ + +export class Runnable { + constructor() { + this._name = this.constructor.name; + } + + /** + * Execute with a single input + */ + async invoke(input, config = {}) { + try { + return await this._call(input, config); + } catch (error) { + throw new Error(`${this._name}.invoke() failed: ${error.message}`); + } + } + + /** + * Internal execution method - override this! + */ + async _call(input, config) { + throw new Error(`${this._name} must implement _call() method`); + } + + /** + * Stream output chunks + */ + async *stream(input, config = {}) { + yield* this._stream(input, config); + } + + /** + * Internal streaming method - override for custom streaming + */ + async *_stream(input, config) { + yield await this._call(input, config); + } + + /** + * Execute with multiple inputs in parallel + */ + async batch(inputs, config = {}) { + const batchConfig = { ...config, batch: true }; + return await Promise.all( + inputs.map(input => this.invoke(input, batchConfig)) + ); + } + + /** + * Compose with another Runnable + */ + pipe(other) { + return new RunnableSequence([this, other]); + } + + /** + * Helper for debugging + */ + toString() { + return `${this._name}()`; + } +} + +/** + * RunnableSequence - Sequential composition of Runnables + */ +export class RunnableSequence extends Runnable { + constructor(steps) { + super(); + this.steps = steps; + this._name = `RunnableSequence[${steps.length}]`; + } + + async _call(input, config) { + let output = input; + for (const step of this.steps) { + output = await step.invoke(output, config); + } + return output; + } + + async *_stream(input, config) { + let output = input; + + // Execute all but last step normally + for (let i = 0; i < this.steps.length - 1; i++) { + output = await this.steps[i].invoke(output, config); + } + + // Stream the last step + yield* this.steps[this.steps.length - 1].stream(output, config); + } + + pipe(other) { + return new RunnableSequence([...this.steps, other]); + } + + toString() { + return this.steps.map(s => s.toString()).join(' | '); + } +} + +export default Runnable; +``` + +## Real-World Examples + +### Example 1: Echo Runnable + +The simplest possible Runnable - just returns the input: + +```javascript +class EchoRunnable extends Runnable { + async _call(input, config) { + return input; + } +} + +// Usage +const echo = new EchoRunnable(); +const result = await echo.invoke("Hello!"); +console.log(result); // "Hello!" +``` + +### Example 2: Transform Runnable + +Transforms the input in some way: + +```javascript +class UpperCaseRunnable extends Runnable { + async _call(input, config) { + if (typeof input !== 'string') { + throw new Error('Input must be a string'); + } + return input.toUpperCase(); + } +} + +// Usage +const upper = new UpperCaseRunnable(); +const result = await upper.invoke("hello"); +console.log(result); // "HELLO" +``` + +### Example 3: Composing Runnables + +```javascript +class AddPrefixRunnable extends Runnable { + constructor(prefix) { + super(); + this.prefix = prefix; + } + + async _call(input, config) { + return `${this.prefix}${input}`; + } +} + +class AddSuffixRunnable extends Runnable { + constructor(suffix) { + super(); + this.suffix = suffix; + } + + async _call(input, config) { + return `${input}${this.suffix}`; + } +} + +// Compose them +const pipeline = new AddPrefixRunnable("Hello, ") + .pipe(new AddSuffixRunnable("!")); + +const result = await pipeline.invoke("World"); +console.log(result); // "Hello, World!" +``` + +### Example 4: Async Processing + +```javascript +class DelayedEchoRunnable extends Runnable { + constructor(delayMs) { + super(); + this.delayMs = delayMs; + } + + async _call(input, config) { + // Simulate async work + await new Promise(resolve => setTimeout(resolve, this.delayMs)); + return input; + } +} + +// Usage with batch +const delayed = new DelayedEchoRunnable(1000); +const results = await delayed.batch(["A", "B", "C"]); +// All three process in parallel, takes ~1 second total, not 3! +``` + +### Example 5: Streaming Numbers + +```javascript +class CounterRunnable extends Runnable { + constructor(max) { + super(); + this.max = max; + } + + async _call(input, config) { + return Array.from({ length: this.max }, (_, i) => i + 1); + } + + async *_stream(input, config) { + for (let i = 1; i <= this.max; i++) { + await new Promise(resolve => setTimeout(resolve, 100)); + yield i; + } + } +} + +// Usage +const counter = new CounterRunnable(5); + +// Regular invoke +const all = await counter.invoke(); +console.log(all); // [1, 2, 3, 4, 5] + +// Streaming +for await (const num of counter.stream()) { + console.log(num); // Prints 1... then 2... then 3... etc +} +``` + +## Design Patterns + +### Pattern 1: Configuration Through Constructor + +```javascript +class ConfigurableRunnable extends Runnable { + constructor({ option1, option2 }) { + super(); + this.option1 = option1; + this.option2 = option2; + } + + async _call(input, config) { + // Use this.option1 and this.option2 + return /* processed result */; + } +} +``` + +### Pattern 2: Runtime Configuration + +```javascript +class RuntimeConfigRunnable extends Runnable { + async _call(input, config) { + // Access config at runtime + const temperature = config.temperature || 0.7; + const maxTokens = config.maxTokens || 100; + + return /* processed result */; + } +} + +// Usage +await runnable.invoke(input, { temperature: 0.9, maxTokens: 200 }); +``` + +### Pattern 3: Error Handling + +```javascript +class SafeRunnable extends Runnable { + async _call(input, config) { + try { + return await this.riskyOperation(input); + } catch (error) { + // Handle error gracefully + console.error(`Error in ${this._name}:`, error); + return config.defaultValue || null; + } + } +} +``` + +## Debugging Tips + +### Tip 1: Add Logging + +```javascript +class LoggingRunnable extends Runnable { + async invoke(input, config = {}) { + console.log(`[${this._name}] Input:`, input); + const output = await super.invoke(input, config); + console.log(`[${this._name}] Output:`, output); + return output; + } +} +``` + +### Tip 2: Inspect Pipelines + +```javascript +const pipeline = step1.pipe(step2).pipe(step3); +console.log(pipeline.toString()); +// "Step1() | Step2() | Step3()" +``` + +### Tip 3: Test Each Step + +```javascript +// Don't test the whole pipeline at once +const result1 = await step1.invoke(input); +console.log("After step1:", result1); + +const result2 = await step2.invoke(result1); +console.log("After step2:", result2); + +const result3 = await step3.invoke(result2); +console.log("After step3:", result3); +``` + +## Common Mistakes + +### ❌ Mistake 1: Forgetting `async` + +```javascript +class BadRunnable extends Runnable { + _call(input, config) { // Missing async! + return input.toUpperCase(); + } +} +``` + +**Fix**: Always make `_call()` async, even if you're not using `await`: + +```javascript +async _call(input, config) { + return input.toUpperCase(); +} +``` + +### ❌ Mistake 2: Not Calling `super()` + +```javascript +class BadRunnable extends Runnable { + constructor() { + // Forgot super()! + this.value = 42; + } +} +``` + +**Fix**: Always call `super()`: + +```javascript +constructor() { + super(); + this.value = 42; +} +``` + +### ❌ Mistake 3: Modifying Input + +```javascript +class BadRunnable extends Runnable { + async _call(input, config) { + input.value = 123; // Don't mutate input! + return input; + } +} +``` + +**Fix**: Return new objects: + +```javascript +async _call(input, config) { + return { ...input, value: 123 }; +} +``` + +## Mental Model + +Think of Runnable like this: + +``` +┌─────────────┐ +│ Input │ +└──────┬──────┘ + │ + ▼ +┌─────────────┐ +│ Runnable │ ← Your custom logic lives here +│ _call() │ +└──────┬──────┘ + │ + ▼ +┌─────────────┐ +│ Output │ +└─────────────┘ +``` + +When you pipe Runnables: + +``` +Input → Runnable1 → Runnable2 → Runnable3 → Output + _call() _call() _call() +``` + +## Exercises + +Now it's your turn! Complete these exercises to solidify your understanding. + +### Exercise 1: Build a Multiplier Runnable + +Create a Runnable that multiplies numbers by a factor. + +**Starter code in**: `exercises/01-echo-runnable.js` + +**Requirements**: +- Takes a number as input +- Multiplies by a factor set in constructor +- Returns the result + +**Example**: +```javascript +const times3 = new MultiplierRunnable(3); +await times3.invoke(5); // Should return 15 +``` + +### Exercise 2: Build a JSON Parser Runnable + +Create a Runnable that parses JSON strings. + +**Requirements**: +- Takes a JSON string as input +- Parses it to an object +- Handles errors gracefully (return null if invalid) + +**Example**: +```javascript +const parser = new JsonParserRunnable(); +await parser.invoke('{"name":"Alice"}'); // Should return { name: "Alice" } +await parser.invoke('invalid json'); // Should return null +``` + +### Exercise 3: Compose a Pipeline + +Using the Runnables you've created, build a pipeline that: +1. Takes a number +2. Multiplies it +3. Converts it to an object: `{ result: }` +4. Converts to JSON string + +**Example**: +```javascript +const pipeline = /* your code */; +await pipeline.invoke(5); // Should return '{"result":15}' +``` + +### Exercise 4: Implement Batch Processing + +Test your Multiplier with batch processing: + +**Requirements**: +- Process [1, 2, 3, 4, 5] in parallel +- Each should be multiplied by 10 + +**Example**: +```javascript +const results = await times10.batch([1, 2, 3, 4, 5]); +console.log(results); // [10, 20, 30, 40, 50] +``` + +## Summary + +Congratulations! You now understand the Runnable abstraction. Let's recap: + +### Key Takeaways + +1. **Runnable is a contract**: Every component implements `invoke()`, `stream()`, and `batch()` +2. **Override `_call()`**: This is where your custom logic goes +3. **Composition is powerful**: Use `pipe()` to chain Runnables together +4. **Async by default**: Always use `async/await` +5. **Immutability matters**: Don't modify inputs, return new values + +### What Makes This Powerful + +- ✅ **Consistency**: Everything works the same way +- ✅ **Composability**: Build complex systems from simple parts +- ✅ **Testability**: Test each Runnable independently +- ✅ **Reusability**: Write once, use anywhere +- ✅ **Extensibility**: Easy to add new Runnables + +### The Foundation is Set + +Now that you understand Runnables, everything else in the framework will make sense: +- **Prompts** are Runnables that format text +- **LLMs** are Runnables that generate text +- **Parsers** are Runnables that extract structure +- **Chains** are Runnables that combine other Runnables +- **Agents** are Runnables that make decisions + +It's Runnables all the way down! 🐢 + +## Next Steps + +In the next lesson, we'll explore **Messages** - how to structure conversation data for AI agents. + +**Preview**: You'll learn: +- Different message types (Human, AI, System, Tool) +- Why type systems matter +- How to build a message history + +➡️ [Continue to Lesson 2: Messages & Types](02-messages.md) + +## Additional Resources + +- [LangChain Runnable Documentation](https://python.langchain.com/docs/expression_language/) +- [JavaScript Generators (MDN)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator) +- [Promise.all() Documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all) + +## Questions & Discussion + +**Q: Why use `_call()` instead of just `invoke()`?** + +A: The underscore prefix (`_call`) is a convention meaning "internal method." This separation allows the base class to add common functionality in `invoke()` (like logging, error handling, metrics) without forcing every subclass to implement it. + +**Q: Can I pipe more than two Runnables?** + +A: Absolutely! You can chain as many as you want: +```javascript +a.pipe(b).pipe(c).pipe(d).pipe(e)... +``` + +**Q: What if I don't need streaming?** + +A: That's fine! The default implementation just yields the full result. Only override `_stream()` if you need true streaming behavior. + +**Q: Is this the same as function composition?** + +A: Very similar! It's like function composition but with superpowers (async, streaming, batching). + +--- + +**Built with ❤️ for learners who want to understand AI agents deeply** + +[← Back to Tutorial Index](../README.md) | [Next Lesson: Messages →](02-messages.md) \ No newline at end of file diff --git a/tutorial/01-foundation/01-runnable/solutions/01-multiplier-runnable-solution.js b/tutorial/01-foundation/01-runnable/solutions/01-multiplier-runnable-solution.js new file mode 100644 index 0000000000000000000000000000000000000000..b26ceb673e098f957e956dc47a72eafddef5d9df --- /dev/null +++ b/tutorial/01-foundation/01-runnable/solutions/01-multiplier-runnable-solution.js @@ -0,0 +1,128 @@ +/** + * Solution 1: Multiplier Runnable + * + * This solution demonstrates: + * - Extending the Runnable base class + * - Storing configuration in constructor + * - Implementing the _call() method + * - Input validation + */ + +import { Runnable } from '../../../../src/index.js'; + +/** + * MultiplierRunnable - Multiplies input by a factor + */ +class MultiplierRunnable extends Runnable { + constructor(factor) { + super(); // Always call super() first! + + // Validate factor + if (typeof factor !== 'number') { + throw new Error('Factor must be a number'); + } + + this.factor = factor; + } + + async _call(input, config) { + // Validate input + if (typeof input !== 'number') { + throw new Error('Input must be a number'); + } + + // Perform multiplication + return input * this.factor; + } + + // Optional: Override toString for better debugging + toString() { + return `MultiplierRunnable(×${this.factor})`; + } +} + +// ============================================================================ +// Tests +// ============================================================================ + +async function runTests() { + console.log('🧪 Testing MultiplierRunnable Solution...\n'); + + try { + // Test 1: Basic multiplication + console.log('Test 1: Basic multiplication'); + const times3 = new MultiplierRunnable(3); + const result1 = await times3.invoke(5); + console.assert(result1 === 15, `Expected 15, got ${result1}`); + console.log('✅ 3 × 5 = 15'); + console.log(` Runnable: ${times3.toString()}\n`); + + // Test 2: Different factor + console.log('Test 2: Different factor'); + const times10 = new MultiplierRunnable(10); + const result2 = await times10.invoke(7); + console.assert(result2 === 70, `Expected 70, got ${result2}`); + console.log('✅ 10 × 7 = 70\n'); + + // Test 3: Negative numbers + console.log('Test 3: Negative numbers'); + const times2 = new MultiplierRunnable(2); + const result3 = await times2.invoke(-5); + console.assert(result3 === -10, `Expected -10, got ${result3}`); + console.log('✅ 2 × -5 = -10\n'); + + // Test 4: Decimal numbers + console.log('Test 4: Decimal numbers'); + const times1_5 = new MultiplierRunnable(1.5); + const result4 = await times1_5.invoke(4); + console.assert(result4 === 6, `Expected 6, got ${result4}`); + console.log('✅ 1.5 × 4 = 6\n'); + + // Test 5: Zero + console.log('Test 5: Multiply by zero'); + const times0 = new MultiplierRunnable(0); + const result5 = await times0.invoke(100); + console.assert(result5 === 0, `Expected 0, got ${result5}`); + console.log('✅ 0 × 100 = 0\n'); + + // Test 6: Error handling - invalid factor + console.log('Test 6: Error handling - invalid factor'); + try { + new MultiplierRunnable('not a number'); + console.error('❌ Should have thrown error'); + } catch (error) { + console.log('✅ Correctly throws error for invalid factor\n'); + } + + // Test 7: Error handling - invalid input + console.log('Test 7: Error handling - invalid input'); + try { + const times5 = new MultiplierRunnable(5); + await times5.invoke('not a number'); + console.error('❌ Should have thrown error'); + } catch (error) { + console.log('✅ Correctly throws error for invalid input\n'); + } + + // Test 8: Works with batch + console.log('Test 8: Batch processing'); + const times2_batch = new MultiplierRunnable(2); + const results = await times2_batch.batch([1, 2, 3, 4, 5]); + console.log(` Input: [1, 2, 3, 4, 5]`); + console.log(` Output: [${results.join(', ')}]`); + console.assert(JSON.stringify(results) === JSON.stringify([2, 4, 6, 8, 10])); + console.log('✅ Batch processing works\n'); + + console.log('🎉 All tests passed!'); + } catch (error) { + console.error('❌ Test failed:', error.message); + console.error(error.stack); + } +} + +// Run tests +if (import.meta.url === `file://${process.argv[1]}`) { + runTests(); +} + +export { MultiplierRunnable }; \ No newline at end of file diff --git a/tutorial/01-foundation/01-runnable/solutions/02-json-parser-runnable-solution.js b/tutorial/01-foundation/01-runnable/solutions/02-json-parser-runnable-solution.js new file mode 100644 index 0000000000000000000000000000000000000000..466175574dfcd4fd89f8d98ce8d571772443b289 --- /dev/null +++ b/tutorial/01-foundation/01-runnable/solutions/02-json-parser-runnable-solution.js @@ -0,0 +1,184 @@ +/** + * Solution 2: JSON Parser Runnable + * + * This solution demonstrates: + * - Graceful error handling with try-catch + * - Configuration through constructor options + * - Type checking input + * - Providing default values + */ + +import { Runnable } from '../../../../src/index.js'; + +/** + * JsonParserRunnable - Safely parses JSON strings + */ +class JsonParserRunnable extends Runnable { + constructor(options = {}) { + super(); + + // Store configuration + this.defaultValue = options.defaultValue ?? null; + this.throwOnError = options.throwOnError ?? false; + } + + async _call(input, config) { + // Ensure input is a string + if (typeof input !== 'string') { + if (this.throwOnError) { + throw new Error('Input must be a string'); + } + return this.defaultValue; + } + + // Handle empty strings + if (input.trim().length === 0) { + return this.defaultValue; + } + + // Try to parse JSON + try { + return JSON.parse(input); + } catch (error) { + // If throwOnError is true, propagate the error + if (this.throwOnError) { + throw new Error(`Failed to parse JSON: ${error.message}`); + } + + // Otherwise, return default value + return this.defaultValue; + } + } + + toString() { + return `JsonParserRunnable()`; + } +} + +// ============================================================================ +// Tests +// ============================================================================ + +async function runTests() { + console.log('🧪 Testing JsonParserRunnable Solution...\n'); + + try { + // Test 1: Valid JSON object + console.log('Test 1: Valid JSON object'); + const parser = new JsonParserRunnable(); + const result1 = await parser.invoke('{"name":"Alice","age":30}'); + console.assert(result1.name === 'Alice', 'Should parse name'); + console.assert(result1.age === 30, 'Should parse age'); + console.log('✅ Parsed:', result1); + console.log(); + + // Test 2: Valid JSON array + console.log('Test 2: Valid JSON array'); + const result2 = await parser.invoke('[1, 2, 3, 4, 5]'); + console.assert(Array.isArray(result2), 'Should return array'); + console.assert(result2.length === 5, 'Should have 5 elements'); + console.log('✅ Parsed:', result2); + console.log(); + + // Test 3: Invalid JSON returns null + console.log('Test 3: Invalid JSON returns null'); + const result3 = await parser.invoke('this is not json'); + console.assert(result3 === null, 'Should return null for invalid JSON'); + console.log('✅ Returns:', result3); + console.log(); + + // Test 4: Empty string returns null + console.log('Test 4: Empty string returns null'); + const result4 = await parser.invoke(''); + console.assert(result4 === null, 'Should return null for empty string'); + console.log('✅ Returns:', result4); + console.log(); + + // Test 5: With default value + console.log('Test 5: With default value'); + const parserWithDefault = new JsonParserRunnable({ + defaultValue: { error: 'Invalid JSON' } + }); + const result5 = await parserWithDefault.invoke('bad json'); + console.assert(result5.error === 'Invalid JSON', 'Should return default value'); + console.log('✅ Returns:', result5); + console.log(); + + // Test 6: Nested JSON + console.log('Test 6: Nested JSON'); + const nested = '{"user":{"name":"Bob","address":{"city":"NYC"}}}'; + const result6 = await parser.invoke(nested); + console.assert(result6.user.address.city === 'NYC', 'Should parse nested objects'); + console.log('✅ Parsed:', result6); + console.log(); + + // Test 7: Numbers and booleans + console.log('Test 7: Primitive values'); + const result7a = await parser.invoke('42'); + const result7b = await parser.invoke('true'); + const result7c = await parser.invoke('"hello"'); + console.assert(result7a === 42, 'Should parse number'); + console.assert(result7b === true, 'Should parse boolean'); + console.assert(result7c === 'hello', 'Should parse string'); + console.log('✅ Parsed primitives:', result7a, result7b, result7c); + console.log(); + + // Test 8: Batch processing + console.log('Test 8: Batch processing'); + const inputs = [ + '{"id":1}', + '{"id":2}', + 'invalid', + '{"id":3}' + ]; + const results = await parser.batch(inputs); + console.log(' Inputs:', inputs); + console.log(' Results:', results); + console.assert(results[0].id === 1, 'First should parse'); + console.assert(results[2] === null, 'Invalid should be null'); + console.assert(results[3].id === 3, 'Last should parse'); + console.log('✅ Batch processing works'); + console.log(); + + // Test 9: throwOnError mode + console.log('Test 9: throwOnError mode'); + const strictParser = new JsonParserRunnable({ throwOnError: true }); + try { + await strictParser.invoke('invalid json'); + console.error('❌ Should have thrown error'); + } catch (error) { + console.log('✅ Throws error in strict mode:', error.message); + } + console.log(); + + // Test 10: Complex real-world JSON + console.log('Test 10: Complex real-world JSON'); + const complexJson = `{ + "users": [ + {"id": 1, "name": "Alice", "active": true}, + {"id": 2, "name": "Bob", "active": false} + ], + "metadata": { + "total": 2, + "timestamp": "2024-01-01" + } + }`; + const result10 = await parser.invoke(complexJson); + console.assert(result10.users.length === 2, 'Should have 2 users'); + console.assert(result10.metadata.total === 2, 'Should have metadata'); + console.log('✅ Parsed complex JSON'); + console.log(); + + console.log('🎉 All tests passed!'); + } catch (error) { + console.error('❌ Test failed:', error.message); + console.error(error.stack); + } +} + +// Run tests +if (import.meta.url === `file://${process.argv[1]}`) { + runTests(); +} + +export { JsonParserRunnable }; \ No newline at end of file diff --git a/tutorial/01-foundation/01-runnable/solutions/03-pipeline-composition-solution.js b/tutorial/01-foundation/01-runnable/solutions/03-pipeline-composition-solution.js new file mode 100644 index 0000000000000000000000000000000000000000..0b298a11cf764ffe2b0ba0780bbf280289820b23 --- /dev/null +++ b/tutorial/01-foundation/01-runnable/solutions/03-pipeline-composition-solution.js @@ -0,0 +1,272 @@ +/** + * Solution 3: Pipeline Composition + * + * This solution demonstrates: + * - Creating multiple Runnable components + * - Composing them with pipe() + * - Building reusable pipelines + * - Testing each component individually + */ + +import {Runnable} from '../../../../src/index.js'; + +/** + * MultiplierRunnable - Multiplies by a factor + */ +class MultiplierRunnable extends Runnable { + constructor(factor) { + super(); + this.factor = factor; + } + + async _call(input, config) { + if (typeof input !== 'number') { + throw new Error('Input must be a number'); + } + return input * this.factor; + } + + toString() { + return `Multiply(×${this.factor})`; + } +} + +/** + * ObjectWrapperRunnable - Wraps value in an object + */ +class ObjectWrapperRunnable extends Runnable { + constructor(key = 'result') { + super(); + this.key = key; + } + + async _call(input, config) { + // Wrap any input in an object with the specified key + return { [this.key]: input }; + } + + toString() { + return `Wrap({${this.key}: ...})`; + } +} + +/** + * JsonStringifyRunnable - Converts object to JSON string + */ +class JsonStringifyRunnable extends Runnable { + constructor(options = {}) { + super(); + this.indent = options.indent ?? 0; // 0 = compact, 2 = pretty + } + + async _call(input, config) { + try { + if (this.indent > 0) { + return JSON.stringify(input, null, this.indent); + } + return JSON.stringify(input); + } catch (error) { + throw new Error(`Failed to stringify: ${error.message}`); + } + } + + toString() { + return 'Stringify(JSON)'; + } +} + +// ============================================================================ +// Pipeline Creation +// ============================================================================ + +/** + * Create a pipeline: number → multiply → wrap → stringify + */ +function createPipeline(factor = 3) { + const multiplier = new MultiplierRunnable(factor); + const wrapper = new ObjectWrapperRunnable('result'); + const stringifier = new JsonStringifyRunnable(); + + // Compose the pipeline using pipe() + return multiplier + .pipe(wrapper) + .pipe(stringifier); +} + +/** + * Bonus: Create a reverse pipeline (parse → extract → process) + */ +function createReversePipeline(factor = 2) { + // Parse JSON string + class JsonParserRunnable extends Runnable { + async _call(input, config) { + return JSON.parse(input); + } + } + + // Extract a value from object + class ExtractorRunnable extends Runnable { + constructor(key) { + super(); + this.key = key; + } + async _call(input, config) { + return input[this.key]; + } + } + + const parser = new JsonParserRunnable(); + const extractor = new ExtractorRunnable('result'); + const multiplier = new MultiplierRunnable(factor); + const wrapper = new ObjectWrapperRunnable('doubled'); + const stringifier = new JsonStringifyRunnable(); + + return parser + .pipe(extractor) + .pipe(multiplier) + .pipe(wrapper) + .pipe(stringifier); +} + +// ============================================================================ +// Tests +// ============================================================================ + +async function runTests() { + console.log('🧪 Testing Pipeline Composition Solution...\n'); + + try { + // Test 1: Basic pipeline + console.log('Test 1: Basic pipeline (multiply → wrap → stringify)'); + const pipeline = createPipeline(3); + console.log(` Pipeline: ${pipeline.toString()}`); + const result1 = await pipeline.invoke(5); + console.log(` Input: 5`); + console.log(` Output: ${result1}`); + console.assert(result1 === '{"result":15}', `Expected '{"result":15}', got '${result1}'`); + console.log('✅ Pipeline works!\n'); + + // Test 2: Different factor + console.log('Test 2: Different factor'); + const pipeline2 = createPipeline(10); + const result2 = await pipeline2.invoke(4); + console.log(` Input: 4`); + console.log(` Output: ${result2}`); + console.assert(result2 === '{"result":40}', `Expected '{"result":40}', got '${result2}'`); + console.log('✅ Works with different factors!\n'); + + // Test 3: Pipeline with batch + console.log('Test 3: Batch processing through pipeline'); + const pipeline3 = createPipeline(2); + const results3 = await pipeline3.batch([1, 2, 3]); + console.log(` Inputs: [1, 2, 3]`); + console.log(` Outputs:`); + results3.forEach((r, i) => console.log(` [${i}]: ${r}`)); + console.assert(results3[0] === '{"result":2}', 'First result should be correct'); + console.assert(results3[1] === '{"result":4}', 'Second result should be correct'); + console.assert(results3[2] === '{"result":6}', 'Third result should be correct'); + console.log('✅ Batch processing works!\n'); + + // Test 4: Individual components + console.log('Test 4: Testing individual components'); + const multiplier = new MultiplierRunnable(5); + const wrapper = new ObjectWrapperRunnable(); + const stringifier = new JsonStringifyRunnable(); + + const step1 = await multiplier.invoke(3); + console.log(` After multiply: ${step1}`); + console.assert(step1 === 15, 'Multiplier should work'); + + const step2 = await wrapper.invoke(step1); + console.log(` After wrap: ${JSON.stringify(step2)}`); + console.assert(step2.result === 15, 'Wrapper should work'); + + const step3 = await stringifier.invoke(step2); + console.log(` After stringify: ${step3}`); + console.assert(step3 === '{"result":15}', 'Stringifier should work'); + console.log('✅ All components work individually!\n'); + + // Test 5: Pretty printing + console.log('Test 5: Pretty printing with indent'); + const prettyStringifier = new JsonStringifyRunnable({ indent: 2 }); + const prettyPipeline = new MultiplierRunnable(5) + .pipe(new ObjectWrapperRunnable()) + .pipe(prettyStringifier); + + const result5 = await prettyPipeline.invoke(3); + console.log(' Output with indent:'); + console.log(result5); + console.assert(result5.includes('\n'), 'Should have newlines'); + console.log('✅ Pretty printing works!\n'); + + // Test 6: Complex pipeline with multiple steps + console.log('Test 6: Complex pipeline (5 steps)'); + class AddConstantRunnable extends Runnable { + constructor(constant) { + super(); + this.constant = constant; + } + async _call(input, config) { + return input + this.constant; + } + } + + const complexPipeline = new MultiplierRunnable(2) // 5 * 2 = 10 + .pipe(new AddConstantRunnable(5)) // 10 + 5 = 15 + .pipe(new MultiplierRunnable(3)) // 15 * 3 = 45 + .pipe(new ObjectWrapperRunnable('finalResult')) // { finalResult: 45 } + .pipe(new JsonStringifyRunnable()); // JSON string + + const result6 = await complexPipeline.invoke(5); + console.log(` Input: 5`); + console.log(` Steps: ×2 → +5 → ×3 → wrap → stringify`); + console.log(` Output: ${result6}`); + console.assert(result6 === '{"finalResult":45}', 'Complex pipeline should work'); + console.log('✅ Complex pipeline works!\n'); + + // Test 7: Bonus - Reverse pipeline + console.log('Test 7: Bonus - Reverse pipeline (parse → extract → process)'); + const reversePipeline = createReversePipeline(2); + const result7 = await reversePipeline.invoke('{"result":10}'); + console.log(` Input: '{"result":10}'`); + console.log(` Steps: parse → extract → ×2 → wrap → stringify`); + console.log(` Output: ${result7}`); + console.assert(result7 === '{"doubled":20}', 'Reverse pipeline should work'); + console.log('✅ Reverse pipeline works!\n'); + + // Test 8: Error propagation through pipeline + console.log('Test 8: Error propagation'); + try { + const errorPipeline = createPipeline(5); + await errorPipeline.invoke('not a number'); // Should fail at multiply step + console.error('❌ Should have thrown error'); + } catch (error) { + console.log(` Caught error: ${error.message}`); + console.log('✅ Errors propagate correctly!\n'); + } + + console.log('🎉 All tests passed!'); + console.log('\n💡 Key Learnings:'); + console.log(' • Runnables can be composed with pipe()'); + console.log(' • Each step transforms the data'); + console.log(' • Pipelines are themselves Runnables'); + console.log(' • Errors propagate through the pipeline'); + console.log(' • You can test each step individually'); + } catch (error) { + console.error('❌ Test failed:', error.message); + console.error(error.stack); + } +} + +// Run tests +if (import.meta.url === `file://${process.argv[1]}`) { + runTests(); +} + +export { + MultiplierRunnable, + ObjectWrapperRunnable, + JsonStringifyRunnable, + createPipeline, + createReversePipeline +}; \ No newline at end of file diff --git a/tutorial/01-foundation/01-runnable/solutions/04-batch-processing-solution.js b/tutorial/01-foundation/01-runnable/solutions/04-batch-processing-solution.js new file mode 100644 index 0000000000000000000000000000000000000000..4678991d2647698e0e25d02389f6e14a41036fee --- /dev/null +++ b/tutorial/01-foundation/01-runnable/solutions/04-batch-processing-solution.js @@ -0,0 +1,285 @@ +/** + * Solution 4: Batch Processing + * + * This solution demonstrates: + * - Parallel execution with batch() + * - Performance comparison vs sequential + * - Error handling in parallel operations + * - Understanding Promise.all() behavior + */ + +import { Runnable } from '../../../../src/index.js'; + +/** + * DelayedMultiplierRunnable - Multiplies with a delay + */ +class DelayedMultiplierRunnable extends Runnable { + constructor(factor, delayMs = 100) { + super(); + this.factor = factor; + this.delayMs = delayMs; + } + + async _call(input, config) { + if (typeof input !== 'number') { + throw new Error(`Input must be a number, got ${typeof input}`); + } + + // Log if in debug mode + if (config?.debug) { + console.log(` Processing ${input} (will take ${this.delayMs}ms)...`); + } + + // Simulate async work (like LLM inference or API call) + await new Promise(resolve => setTimeout(resolve, this.delayMs)); + + const result = input * this.factor; + + if (config?.debug) { + console.log(` Completed ${input} → ${result}`); + } + + return result; + } + + toString() { + return `DelayedMultiplier(×${this.factor}, ${this.delayMs}ms)`; + } +} + +/** + * Process inputs sequentially (one at a time) + */ +async function processSequentially(runnable, inputs) { + const results = []; + + for (const input of inputs) { + const result = await runnable.invoke(input); + results.push(result); + } + + return results; +} + +/** + * Process inputs in parallel (all at once) + */ +async function processInParallel(runnable, inputs) { + // This is exactly what batch() does internally + return await runnable.batch(inputs); +} + +/** + * Bonus: Process in chunks (controlled parallelism) + */ +async function processInChunks(runnable, inputs, chunkSize = 3) { + const results = []; + + // Process in chunks to avoid overwhelming the system + for (let i = 0; i < inputs.length; i += chunkSize) { + const chunk = inputs.slice(i, i + chunkSize); + const chunkResults = await runnable.batch(chunk); + results.push(...chunkResults); + } + + return results; +} + +// ============================================================================ +// Tests +// ============================================================================ + +async function runTests() { + console.log('🧪 Testing Batch Processing Solution...\n'); + + try { + // Test 1: Basic batch processing + console.log('Test 1: Basic batch processing'); + const multiplier = new DelayedMultiplierRunnable(2, 100); + const inputs = [1, 2, 3, 4, 5]; + + const startTime = Date.now(); + const results = await multiplier.batch(inputs); + const duration = Date.now() - startTime; + + console.log(` Inputs: [${inputs.join(', ')}]`); + console.log(` Results: [${results.join(', ')}]`); + console.log(` Time: ${duration}ms`); + console.assert(results.length === 5, 'Should have 5 results'); + console.assert(results[0] === 2, 'First result should be 2'); + console.assert(results[4] === 10, 'Last result should be 10'); + console.assert(duration < 200, 'Should complete in ~100ms (parallel), not 500ms (sequential)'); + console.log('✅ Batch processing works!\n'); + + // Test 2: Compare sequential vs parallel + console.log('Test 2: Sequential vs Parallel comparison'); + const runnable = new DelayedMultiplierRunnable(3, 100); + const testInputs = [1, 2, 3, 4, 5]; + + console.log(' Processing sequentially...'); + const seqStart = Date.now(); + const seqResults = await processSequentially(runnable, testInputs); + const seqDuration = Date.now() - seqStart; + console.log(` Sequential: ${seqDuration}ms (${testInputs.length} × 100ms)`); + + console.log(' Processing in parallel...'); + const parStart = Date.now(); + const parResults = await processInParallel(runnable, testInputs); + const parDuration = Date.now() - parStart; + console.log(` Parallel: ${parDuration}ms (max of all operations)`); + + const speedup = (seqDuration / parDuration).toFixed(1); + console.log(` Speedup: ${speedup}x faster 🚀`); + + console.assert( + JSON.stringify(seqResults) === JSON.stringify(parResults), + 'Results should be identical' + ); + console.assert(parDuration < seqDuration / 2, 'Parallel should be much faster'); + console.log('✅ Parallel is significantly faster!\n'); + + // Test 3: Large batch + console.log('Test 3: Large batch (10 items)'); + const largeBatch = Array.from({ length: 10 }, (_, i) => i + 1); + const startLarge = Date.now(); + const largeResults = await multiplier.batch(largeBatch); + const durationLarge = Date.now() - startLarge; + + console.log(` Input: [1, 2, 3, ..., 10]`); + console.log(` Processed ${largeBatch.length} items in ${durationLarge}ms`); + console.log(` Sequential would take: ~${largeBatch.length * 100}ms`); + console.log(` Actual time: ${durationLarge}ms`); + console.assert(largeResults.length === 10, 'Should process all items'); + console.assert(durationLarge < 200, 'Should complete quickly due to parallelism'); + console.log('✅ Large batch works!\n'); + + // Test 4: Batch with errors + console.log('Test 4: Error handling in batch'); + const mixedInputs = [1, 2, 'invalid', 4, 5]; + + try { + await multiplier.batch(mixedInputs); + console.log('❌ Should have thrown an error'); + } catch (error) { + console.log(` Caught error: ${error.message}`); + console.log(` Note: When one fails, all fail (Promise.all behavior)`); + console.log('✅ Errors are caught in batch processing!\n'); + } + + // Test 5: Empty batch + console.log('Test 5: Empty batch'); + const emptyResults = await multiplier.batch([]); + console.assert(emptyResults.length === 0, 'Empty batch should return empty array'); + console.log('✅ Empty batch handled correctly!\n'); + + // Test 6: Batch with pipeline + console.log('Test 6: Batch through a pipeline'); + class AddConstant extends Runnable { + constructor(constant) { + super(); + this.constant = constant; + } + async _call(input) { + await new Promise(resolve => setTimeout(resolve, 50)); + return input + this.constant; + } + } + + const pipeline = new DelayedMultiplierRunnable(2, 50) + .pipe(new AddConstant(10)); + + const pipelineInputs = [1, 2, 3]; + const startPipeline = Date.now(); + const pipelineResults = await pipeline.batch(pipelineInputs); + const durationPipeline = Date.now() - startPipeline; + + console.log(` Inputs: [${pipelineInputs.join(', ')}]`); + console.log(` Results: [${pipelineResults.join(', ')}]`); + console.log(` Expected: [12, 14, 16] (×2, then +10)`); + console.log(` Time: ${durationPipeline}ms`); + console.log(` Sequential: would take ~${pipelineInputs.length * 100}ms`); + console.assert(pipelineResults[0] === 12, 'First should be 12'); + console.assert(pipelineResults[2] === 16, 'Last should be 16'); + console.log('✅ Batch works through pipelines!\n'); + + // Test 7: Chunked processing + console.log('Test 7: Chunked processing (controlled parallelism)'); + const manyInputs = Array.from({ length: 12 }, (_, i) => i + 1); + + console.log(' Processing 12 items in chunks of 3...'); + const startChunked = Date.now(); + const chunkedResults = await processInChunks( + new DelayedMultiplierRunnable(2, 100), + manyInputs, + 3 // chunk size + ); + const durationChunked = Date.now() - startChunked; + + console.log(` Time: ${durationChunked}ms`); + console.log(` Expected: ~400ms (4 chunks × 100ms each)`); + console.log(` All parallel would be: ~100ms`); + console.log(` Sequential would be: ~1200ms`); + console.assert(chunkedResults.length === 12, 'Should process all items'); + console.assert( + durationChunked > 300 && durationChunked < 600, + 'Should take time for 4 chunks' + ); + console.log('✅ Chunked processing works!\n'); + + // Test 8: Debug mode + console.log('Test 8: Debug mode to see parallel execution'); + const debugMultiplier = new DelayedMultiplierRunnable(5, 100); + console.log(' Watch items process in parallel:'); + await debugMultiplier.batch([1, 2, 3], { debug: true }); + console.log('✅ Debug mode shows parallel execution!\n'); + + // Test 9: Performance metrics + console.log('Test 9: Detailed performance analysis'); + const sizes = [1, 2, 5, 10]; + const delay = 100; + + console.log(' Items | Sequential | Parallel | Speedup'); + console.log(' ------|------------|----------|--------'); + + for (const size of sizes) { + const inputs = Array.from({ length: size }, (_, i) => i + 1); + const perf = new DelayedMultiplierRunnable(2, delay); + + const seqStart = Date.now(); + await processSequentially(perf, inputs); + const seqTime = Date.now() - seqStart; + + const parStart = Date.now(); + await processInParallel(perf, inputs); + const parTime = Date.now() - parStart; + + const speedup = (seqTime / parTime).toFixed(1); + console.log(` ${size.toString().padStart(5)} | ${seqTime.toString().padStart(10)}ms | ${parTime.toString().padStart(8)}ms | ${speedup.padStart(6)}x`); + } + console.log('✅ Performance scales with parallelism!\n'); + + console.log('🎉 All tests passed!'); + console.log('\n💡 Key Learnings:'); + console.log(' • batch() uses Promise.all() for parallel execution'); + console.log(' • N items with 100ms delay: sequential = N×100ms, parallel = 100ms'); + console.log(' • One error in batch causes all to fail'); + console.log(' • Chunked processing balances speed and resource usage'); + console.log(' • Pipelines work with batch processing'); + console.log(' • Perfect for processing multiple LLM requests simultaneously'); + } catch (error) { + console.error('❌ Test failed:', error.message); + console.error(error.stack); + } +} + +// Run tests +if (import.meta.url === `file://${process.argv[1]}`) { + runTests(); +} + +export { + DelayedMultiplierRunnable, + processSequentially, + processInParallel, + processInChunks +}; \ No newline at end of file diff --git a/tutorial/01-foundation/02-messages/exercises/05-message-formatter.js b/tutorial/01-foundation/02-messages/exercises/05-message-formatter.js new file mode 100644 index 0000000000000000000000000000000000000000..f0aba7a8d39d82bef7f251a2972c5c3f362023b1 --- /dev/null +++ b/tutorial/01-foundation/02-messages/exercises/05-message-formatter.js @@ -0,0 +1,144 @@ +/** + * Exercise 5: Build a Message Formatter + * + * Goal: Create a function that formats messages for console display + * + * Requirements: + * - Format each message type differently + * - Include timestamp in readable format + * - Add visual indicators (without emojis) + * - Truncate long messages (>100 chars) + * - Show message metadata + * + * Example Output: + * [10:30:45] SYSTEM: You are a helpful assistant + * [10:30:46] HUMAN: What's the weather? + * [10:30:47] AI: Let me check that for you... + */ + +import { HumanMessage, AIMessage, SystemMessage, ToolMessage } from '../../../../src/index.js'; + +/** + * TODO: Implement formatMessage + * + * Should return a formatted string with: + * - Timestamp in [HH:MM:SS] format + * - Message type in UPPERCASE + * - Content (truncated if too long) + * + * @param {BaseMessage} message - The message to format + * @param {Object} options - Formatting options + * @returns {string} Formatted message string + */ +function formatMessage(message, options = {}) { + const maxLength = options.maxLength || 100; + + // TODO: Extract timestamp and format as [HH:MM:SS] + // TODO: Get message type in uppercase + // TODO: Truncate content if longer than maxLength + // TODO: Return formatted string +} + +/** + * TODO: Implement formatConversation + * + * Formats an entire conversation with proper spacing + * + * @param {Array} messages - Array of messages + * @returns {string} Formatted conversation + */ +function formatConversation(messages) { + // TODO: Format each message + // TODO: Join with newlines + // TODO: Add conversation header/footer +} + +/** + * BONUS: Implement colorized output for terminal + * + * Add ANSI color codes for different message types: + * - System: Blue + * - Human: Green + * - AI: Cyan + * - Tool: Yellow + */ +function formatMessageWithColor(message, options = {}) { + // TODO: Add ANSI color codes based on message type + // TODO: Use formatMessage internally +} + +// ============================================================================ +// Tests +// ============================================================================ + +async function runTests() { + console.log('🧪 Testing Message Formatter...\n'); + + try { + // Test 1: Basic formatting + console.log('Test 1: Basic message formatting'); + const msg1 = new HumanMessage("Hello, how are you?"); + const formatted1 = formatMessage(msg1); + console.log(` Output: ${formatted1}`); + console.assert(formatted1.includes('HUMAN'), 'Should include message type'); + console.assert(formatted1.includes('Hello'), 'Should include content'); + console.log('✅ Basic formatting works\n'); + + // Test 2: Long message truncation + console.log('Test 2: Long message truncation'); + const longContent = 'A'.repeat(150); + const msg2 = new AIMessage(longContent); + const formatted2 = formatMessage(msg2, { maxLength: 50 }); + console.log(` Output length: ${formatted2.length}`); + console.assert(formatted2.length < 100, 'Should truncate long messages'); + console.log('✅ Truncation works\n'); + + // Test 3: Different message types + console.log('Test 3: Different message types'); + const messages = [ + new SystemMessage("You are helpful"), + new HumanMessage("Hi"), + new AIMessage("Hello!"), + new ToolMessage("result", "tool_123") + ]; + + messages.forEach(msg => { + const formatted = formatMessage(msg); + console.log(` ${formatted}`); + }); + console.log('✅ All message types format correctly\n'); + + // Test 4: Conversation formatting + console.log('Test 4: Full conversation formatting'); + const conversation = [ + new SystemMessage("You are a helpful assistant"), + new HumanMessage("What's 2+2?"), + new AIMessage("2+2 equals 4") + ]; + + const formattedConv = formatConversation(conversation); + console.log(formattedConv); + console.assert(formattedConv.split('\n').length >= 3, 'Should have multiple lines'); + console.log('✅ Conversation formatting works\n'); + + // Test 5: Timestamp format + console.log('Test 5: Timestamp format'); + const msg5 = new HumanMessage("Test"); + const formatted5 = formatMessage(msg5); + const timestampRegex = /\[\d{2}:\d{2}:\d{2}\]/; + console.assert(timestampRegex.test(formatted5), 'Should have [HH:MM:SS] timestamp'); + console.log('✅ Timestamp format is correct\n'); + + console.log('🎉 All tests passed!'); + } catch (error) { + console.error('❌ Test failed:', error.message); + console.error(error.stack); + } +} + +// Run tests +if (import.meta.url === `file://${process.argv[1]}`) { + runTests(); +} + +export { formatMessage, formatConversation, formatMessageWithColor }; \ No newline at end of file diff --git a/tutorial/01-foundation/02-messages/exercises/06-conversation-validator.js b/tutorial/01-foundation/02-messages/exercises/06-conversation-validator.js new file mode 100644 index 0000000000000000000000000000000000000000..3f4ed4f5777f81b5b988bf5f1b1865eb51b2f291 --- /dev/null +++ b/tutorial/01-foundation/02-messages/exercises/06-conversation-validator.js @@ -0,0 +1,208 @@ +/** + * Exercise 6: Build a Conversation Validator + * + * Goal: Validate conversation structure and message sequences + * + * Requirements: + * - Check if conversation follows proper patterns + * - Validate message ordering + * - Ensure tool messages link to AI tool calls + * - Check for required message types + * - Return detailed validation errors + * + * Validation Rules: + * 1. First message should be a SystemMessage (recommended) + * 2. Tool messages must follow AI messages with tool calls + * 3. No empty message content + * 4. Human and AI messages should alternate (after system) + * 5. Tool messages must have valid toolCallId + */ + +import { HumanMessage, AIMessage, SystemMessage, ToolMessage } from '../../../../src/index.js'; + +/** + * TODO: Implement validateMessage + * + * Validates a single message + * + * @param {BaseMessage} message - The message to validate + * @returns {Object} { valid: boolean, errors: string[] } + */ +function validateMessage(message) { + const errors = []; + + // TODO: Check if message content is not empty + if (message.content !== '') { + throw Error('Empty') + } + // TODO: Check if message has a type + if (!Object.hasOwn(message, 'type')) { + throw Error('No type') + } + // TODO: For tool messages, check toolCallId exists + + // TODO: For AI messages, validate tool calls structure + + return { + valid: errors.length === 0, + errors + }; +} + +/** + * TODO: Implement validateConversation + * + * Validates an entire conversation structure + * + * @param {Array} messages - The conversation to validate + * @returns {Object} { valid: boolean, errors: string[], warnings: string[] } + */ +function validateConversation(messages) { + const errors = []; + const warnings = []; + + // TODO: Check if conversation is empty + // TODO: Warn if first message is not system message + // TODO: Validate each message individually + // TODO: Check message ordering (alternating human/AI) + // TODO: Validate tool message follows AI message with tool calls + // TODO: Check for tool call ID matches + + return { + valid: errors.length === 0, + errors, + warnings + }; +} + +/** + * TODO: Implement validateToolCallSequence + * + * Validates that tool messages correctly reference AI tool calls + * + * @param {Array} messages - Messages to check + * @returns {Object} Validation result + */ +function validateToolCallSequence(messages) { + // TODO: Find all AI messages with tool calls + // TODO: Find all tool messages + // TODO: Check that every tool message references a valid tool call + // TODO: Check that all tool calls have corresponding tool messages +} + +// ============================================================================ +// Tests +// ============================================================================ + +async function runTests() { + console.log('🧪 Testing Conversation Validator...\n'); + + try { + // Test 1: Valid simple conversation + console.log('Test 1: Valid simple conversation'); + const validConv = [ + new SystemMessage("You are helpful"), + new HumanMessage("Hi"), + new AIMessage("Hello!") + ]; + + const result1 = validateConversation(validConv); + console.log(` Valid: ${result1.valid}`); + console.log(` Errors: ${result1.errors.length}`); + console.log(` Warnings: ${result1.warnings.length}`); + console.assert(result1.valid === true, 'Should be valid'); + console.log('✅ Valid conversation passes\n'); + + // Test 2: Missing system message (warning) + console.log('Test 2: Missing system message'); + const noSystem = [ + new HumanMessage("Hi"), + new AIMessage("Hello!") + ]; + + const result2 = validateConversation(noSystem); + console.log(` Valid: ${result2.valid}`); + console.log(` Warnings: ${result2.warnings}`); + console.assert(result2.warnings.length > 0, 'Should have warning'); + console.log('✅ Missing system message triggers warning\n'); + + // Test 3: Empty message content (error) + console.log('Test 3: Empty message content'); + const emptyContent = [ + new SystemMessage(""), + new HumanMessage("Hi") + ]; + + const result3 = validateConversation(emptyContent); + console.log(` Valid: ${result3.valid}`); + console.log(` Errors: ${result3.errors}`); + console.assert(result3.valid === false, 'Should be invalid'); + console.log('✅ Empty content caught\n'); + + // Test 4: Tool message without AI tool call (error) + console.log('Test 4: Tool message without preceding AI tool call'); + const badToolSequence = [ + new SystemMessage("You are helpful"), + new HumanMessage("Calculate 2+2"), + new ToolMessage("4", "call_123") // No AI tool call before this + ]; + + const result4 = validateConversation(badToolSequence); + console.log(` Valid: ${result4.valid}`); + console.log(` Errors: ${result4.errors}`); + console.assert(result4.valid === false, 'Should be invalid'); + console.log('✅ Invalid tool sequence caught\n'); + + // Test 5: Valid tool call sequence + console.log('Test 5: Valid tool call sequence'); + const validToolSeq = [ + new SystemMessage("You are helpful"), + new HumanMessage("Calculate 2+2"), + new AIMessage("Let me calculate", { + toolCalls: [{ + id: 'call_123', + type: 'function', + function: { name: 'calculator', arguments: '{"a":2,"b":2}' } + }] + }), + new ToolMessage("4", "call_123"), + new AIMessage("The answer is 4") + ]; + + const result5 = validateConversation(validToolSeq); + console.log(` Valid: ${result5.valid}`); + console.log(` Errors: ${result5.errors.length}`); + console.assert(result5.valid === true, 'Should be valid'); + console.log('✅ Valid tool sequence passes\n'); + + // Test 6: Single message validation + console.log('Test 6: Single message validation'); + const goodMsg = new HumanMessage("Hello"); + const result6a = validateMessage(goodMsg); + console.assert(result6a.valid === true, 'Valid message should pass'); + + const badMsg = new HumanMessage(""); + const result6b = validateMessage(badMsg); + console.assert(result6b.valid === false, 'Empty message should fail'); + console.log('✅ Single message validation works\n'); + + // Test 7: Empty conversation + console.log('Test 7: Empty conversation'); + const result7 = validateConversation([]); + console.log(` Valid: ${result7.valid}`); + console.assert(result7.valid === false, 'Empty conversation should be invalid'); + console.log('✅ Empty conversation caught\n'); + + console.log('🎉 All tests passed!'); + } catch (error) { + console.error('❌ Test failed:', error.message); + console.error(error.stack); + } +} + +// Run tests +if (import.meta.url === `file://${process.argv[1]}`) { + runTests(); +} + +export { validateMessage, validateConversation, validateToolCallSequence }; \ No newline at end of file diff --git a/tutorial/01-foundation/02-messages/exercises/07-chat-history.js b/tutorial/01-foundation/02-messages/exercises/07-chat-history.js new file mode 100644 index 0000000000000000000000000000000000000000..73a33efe16890e240bc9ab69d61a5a7dbdef0935 --- /dev/null +++ b/tutorial/01-foundation/02-messages/exercises/07-chat-history.js @@ -0,0 +1,252 @@ +/** + * Exercise 7: Create a Chat History Manager + * + * Goal: Build a class to manage conversation history with persistence + * + * Requirements: + * - Add messages to history + * - Get last N messages + * - Filter by message type + * - Implement sliding window (max messages) + * - Save/load from JSON + * - Clear history (but keep system message) + * - Format for LLM consumption + * + * Example Usage: + * const history = new ConversationHistory({ maxMessages: 50 }); + * history.add(new SystemMessage("You are helpful")); + * history.add(new HumanMessage("Hi")); + * const recent = history.getLast(5); + * const json = history.save(); + */ + +import { HumanMessage, AIMessage, SystemMessage, ToolMessage, BaseMessage } from '../../../../src/index.js'; + +/** + * TODO: Implement ConversationHistory class + * + * Manages conversation history with: + * - Sliding window (automatic old message removal) + * - Type filtering + * - Persistence (save/load JSON) + * - LLM formatting + */ +class ConversationHistory { + constructor(options = {}) { + // TODO: Initialize messages array + // TODO: Store maxMessages option (default: 100) + // TODO: Store other options + } + + /** + * TODO: Add a message to history + * Should handle sliding window automatically + */ + add(message) { + // TODO: Add message to array + // TODO: If exceeds maxMessages, remove oldest (but keep system message) + } + + /** + * TODO: Get all messages + * Should return a copy, not the original array + */ + getAll() { + // TODO: Return copy of messages array + } + + /** + * TODO: Get last N messages + */ + getLast(n = 1) { + // TODO: Return last n messages + } + + /** + * TODO: Get messages by type + */ + getByType(type) { + // TODO: Filter messages by type + // TODO: Return filtered array + } + + /** + * TODO: Clear history + * Keep system message if present + */ + clear() { + // TODO: Find system message + // TODO: Reset messages array + // TODO: Keep system message if it existed + } + + /** + * TODO: Format for LLM + * Convert to array of {role, content} objects + */ + toPromptFormat() { + // TODO: Map messages to LLM format + // TODO: Use message.toPromptFormat() method + } + + /** + * TODO: Save to JSON string + */ + save() { + // TODO: Convert messages to JSON + // TODO: Return JSON string + } + + /** + * TODO: Load from JSON string + * Static method + */ + static load(json, options = {}) { + // TODO: Parse JSON + // TODO: Create ConversationHistory instance + // TODO: Recreate messages from JSON + // TODO: Return populated instance + } + + /** + * TODO: Get conversation statistics + */ + getStats() { + // TODO: Count messages by type + // TODO: Calculate total tokens (estimate) + // TODO: Return stats object + } +} + +// ============================================================================ +// Tests +// ============================================================================ + +async function runTests() { + console.log('🧪 Testing Conversation History Manager...\n'); + + try { + // Test 1: Basic add and get + console.log('Test 1: Add and retrieve messages'); + const history = new ConversationHistory(); + history.add(new SystemMessage("You are helpful")); + history.add(new HumanMessage("Hi")); + history.add(new AIMessage("Hello!")); + + const all = history.getAll(); + console.log(` Added: 3 messages`); + console.log(` Retrieved: ${all.length} messages`); + console.assert(all.length === 3, 'Should have 3 messages'); + console.log('✅ Basic operations work\n'); + + // Test 2: Get last N messages + console.log('Test 2: Get last N messages'); + const last2 = history.getLast(2); + console.log(` Last 2 messages:`); + last2.forEach(msg => console.log(` - ${msg.type}: ${msg.content.substring(0, 30)}`)); + console.assert(last2.length === 2, 'Should return 2 messages'); + console.assert(last2[0].type === 'human', 'First should be human'); + console.log('✅ getLast works\n'); + + // Test 3: Filter by type + console.log('Test 3: Filter by message type'); + const humanMessages = history.getByType('human'); + const aiMessages = history.getByType('ai'); + console.log(` Human messages: ${humanMessages.length}`); + console.log(` AI messages: ${aiMessages.length}`); + console.assert(humanMessages.length === 1, 'Should have 1 human message'); + console.assert(aiMessages.length === 1, 'Should have 1 AI message'); + console.log('✅ Filtering works\n'); + + // Test 4: Sliding window + console.log('Test 4: Sliding window (max messages)'); + const limited = new ConversationHistory({ maxMessages: 5 }); + limited.add(new SystemMessage("You are helpful")); + + // Add 10 messages + for (let i = 0; i < 10; i++) { + limited.add(new HumanMessage(`Message ${i}`)); + } + + const count = limited.getAll().length; + console.log(` Added: 11 messages (1 system + 10 human)`); + console.log(` Kept: ${count} messages (max: 5)`); + console.assert(count === 5, 'Should keep only 5 messages'); + + // System message should be preserved + const hasSystem = limited.getAll().some(m => m.type === 'system'); + console.assert(hasSystem, 'Should preserve system message'); + console.log('✅ Sliding window works\n'); + + // Test 5: Clear history + console.log('Test 5: Clear history'); + const hist5 = new ConversationHistory(); + hist5.add(new SystemMessage("You are helpful")); + hist5.add(new HumanMessage("Hi")); + hist5.add(new AIMessage("Hello!")); + + hist5.clear(); + const afterClear = hist5.getAll(); + console.log(` Messages after clear: ${afterClear.length}`); + console.assert(afterClear.length === 1, 'Should keep system message'); + console.assert(afterClear[0].type === 'system', 'Should be system message'); + console.log('✅ Clear preserves system message\n'); + + // Test 6: Save and load + console.log('Test 6: Save and load from JSON'); + const hist6 = new ConversationHistory(); + hist6.add(new SystemMessage("You are helpful")); + hist6.add(new HumanMessage("Hi")); + hist6.add(new AIMessage("Hello!")); + + const json = hist6.save(); + console.log(` Saved JSON length: ${json.length} chars`); + + const loaded = ConversationHistory.load(json); + const loadedMessages = loaded.getAll(); + console.log(` Loaded: ${loadedMessages.length} messages`); + console.assert(loadedMessages.length === 3, 'Should load all messages'); + console.assert(loadedMessages[0].type === 'system', 'Should preserve types'); + console.log('✅ Persistence works\n'); + + // Test 7: Format for LLM + console.log('Test 7: Format for LLM'); + const hist7 = new ConversationHistory(); + hist7.add(new SystemMessage("You are helpful")); + hist7.add(new HumanMessage("Hi")); + hist7.add(new AIMessage("Hello!")); + + const formatted = hist7.toPromptFormat(); + console.log(` Formatted messages:`, JSON.stringify(formatted, null, 2)); + console.assert(formatted[0].role === 'system', 'Should have system role'); + console.assert(formatted[1].role === 'user', 'Should map human to user'); + console.log('✅ LLM formatting works\n'); + + // Test 8: Statistics + console.log('Test 8: Get conversation statistics'); + const hist8 = new ConversationHistory(); + hist8.add(new SystemMessage("You are helpful")); + hist8.add(new HumanMessage("Hi")); + hist8.add(new AIMessage("Hello!")); + hist8.add(new HumanMessage("How are you?")); + hist8.add(new AIMessage("I'm great!")); + + const stats = hist8.getStats(); + console.log(` Statistics:`, stats); + console.assert(stats.total === 5, 'Should count total'); + console.assert(stats.human === 2, 'Should count human messages'); + console.log('✅ Statistics work\n'); + + console.log('🎉 All tests passed!'); + } catch (error) { + console.error('❌ Test failed:', error.message); + console.error(error.stack); + } +} + +// Run tests +if (import.meta.url === `file://${process.argv[1]}`) { + runTests(); +} + +export { ConversationHistory }; \ No newline at end of file diff --git a/tutorial/01-foundation/02-messages/exercises/08-tool-flow.js b/tutorial/01-foundation/02-messages/exercises/08-tool-flow.js new file mode 100644 index 0000000000000000000000000000000000000000..331cbeba3b5c3a424d1105203e35473eabac1a98 --- /dev/null +++ b/tutorial/01-foundation/02-messages/exercises/08-tool-flow.js @@ -0,0 +1,234 @@ +/** + * Exercise 8: Simulate a Complete Tool Call Flow + * + * Goal: Build a complete agent conversation with tool calls + * + * Requirements: + * - Simulate a realistic agent conversation + * - Include system message, user query, AI tool decision + * - Execute a mock tool and return results + * - AI processes results and responds + * - Handle multiple tool calls in sequence + * + * Example Flow: + * 1. System: "You are helpful" + * 2. Human: "What's 5*3?" + * 3. AI: [Decides to use calculator tool] + * 4. Tool: [Returns 15] + * 5. AI: "5*3 = 15" + */ + +import { HumanMessage, AIMessage, SystemMessage, ToolMessage } from '../../../../src/index.js'; + +/** + * Mock Calculator Tool + * Simulates a real tool that an agent would call + */ +class Calculator { + constructor() { + this.name = 'calculator'; + } + + /** + * Execute a calculation + */ + execute(operation, a, b) { + const ops = { + 'add': (x, y) => x + y, + 'subtract': (x, y) => x - y, + 'multiply': (x, y) => x * y, + 'divide': (x, y) => x / y + }; + + if (!ops[operation]) { + throw new Error(`Unknown operation: ${operation}`); + } + + return ops[operation](a, b); + } + + /** + * Get tool definition for LLM + */ + getDefinition() { + return { + name: 'calculator', + description: 'Performs basic arithmetic operations', + parameters: { + type: 'object', + properties: { + operation: { + type: 'string', + enum: ['add', 'subtract', 'multiply', 'divide'], + description: 'The operation to perform' + }, + a: { type: 'number', description: 'First number' }, + b: { type: 'number', description: 'Second number' } + }, + required: ['operation', 'a', 'b'] + } + }; + } +} + +/** + * TODO: Implement simulateToolCallFlow + * + * Simulates a complete tool call conversation: + * 1. System sets context + * 2. Human asks a question + * 3. AI decides to use a tool + * 4. Tool executes and returns result + * 5. AI incorporates result and responds + * + * @param {string} userQuery - The user's question + * @returns {Array} Complete conversation + */ +function simulateToolCallFlow(userQuery) { + const messages = []; + + // TODO: Add system message + // TODO: Add human message with query + // TODO: Create AI message with tool call + // TODO: Execute the tool (mock) + // TODO: Add tool message with result + // TODO: Add final AI message with answer + + return messages; +} + +/** + * TODO: Implement simulateMultiToolFlow + * + * Simulates a conversation requiring multiple tool calls + * + * Example: "What's 5*3 and then add 10 to the result?" + * - First tool call: multiply 5*3 = 15 + * - Second tool call: add 15+10 = 25 + * - Final answer: "The result is 25" + */ +function simulateMultiToolFlow(userQuery) { + const messages = []; + + // TODO: Implement multi-step tool calling + // TODO: First calculation + // TODO: Use result in second calculation + // TODO: Final answer + + return messages; +} + +/** + * TODO: Implement displayConversation + * + * Pretty-print a conversation with tool calls highlighted + */ +function displayConversation(messages) { + // TODO: Format each message + // TODO: Highlight tool calls specially + // TODO: Show tool call/result connections +} + +// ============================================================================ +// Tests +// ============================================================================ + +async function runTests() { + console.log('🧪 Testing Tool Call Flow...\n'); + + try { + // Test 1: Simple tool call + console.log('Test 1: Simple calculation with tool'); + const conversation1 = simulateToolCallFlow("What's 5 * 3?"); + + console.log(` Messages created: ${conversation1.length}`); + console.assert(conversation1.length >= 5, 'Should have at least 5 messages'); + + // Check message types in order + console.assert(conversation1[0].type === 'system', 'Should start with system'); + console.assert(conversation1[1].type === 'human', 'Should have human query'); + console.assert(conversation1[2].type === 'ai', 'Should have AI decision'); + console.assert(conversation1[3].type === 'tool', 'Should have tool result'); + console.assert(conversation1[4].type === 'ai', 'Should have final AI response'); + + // Check AI message has tool calls + const aiWithTool = conversation1[2]; + console.assert(aiWithTool.hasToolCalls(), 'AI message should have tool calls'); + + console.log('\n Conversation:'); + displayConversation(conversation1); + console.log('\n✅ Simple tool call works\n'); + + // Test 2: Multi-step tool calls + console.log('Test 2: Multi-step calculation'); + const conversation2 = simulateMultiToolFlow("What's 5*3 and then add 10?"); + + console.log(` Messages created: ${conversation2.length}`); + + // Should have multiple tool calls + const toolMessages = conversation2.filter(m => m.type === 'tool'); + console.log(` Tool calls made: ${toolMessages.length}`); + console.assert(toolMessages.length >= 2, 'Should have at least 2 tool calls'); + + console.log('\n Conversation:'); + displayConversation(conversation2); + console.log('\n✅ Multi-step tool calls work\n'); + + // Test 3: Tool call ID linking + console.log('Test 3: Tool call IDs match'); + const testConv = simulateToolCallFlow("Calculate 10 + 5"); + + // Find AI message with tool call + const aiMsg = testConv.find(m => m.type === 'ai' && m.hasToolCalls()); + const toolMsg = testConv.find(m => m.type === 'tool'); + + console.assert(aiMsg, 'Should have AI message with tool call'); + console.assert(toolMsg, 'Should have tool message'); + + const toolCallId = aiMsg.toolCalls[0].id; + console.log(` Tool call ID: ${toolCallId}`); + console.log(` Tool message references: ${toolMsg.toolCallId}`); + console.assert(toolCallId === toolMsg.toolCallId, 'IDs should match'); + console.log('✅ Tool call IDs link correctly\n'); + + // Test 4: Calculator tool + console.log('Test 4: Calculator tool execution'); + const calc = new Calculator(); + + const result1 = calc.execute('multiply', 5, 3); + const result2 = calc.execute('add', 10, 5); + const result3 = calc.execute('divide', 20, 4); + + console.log(` 5 * 3 = ${result1}`); + console.log(` 10 + 5 = ${result2}`); + console.log(` 20 / 4 = ${result3}`); + + console.assert(result1 === 15, 'Multiplication should work'); + console.assert(result2 === 15, 'Addition should work'); + console.assert(result3 === 5, 'Division should work'); + console.log('✅ Calculator works\n'); + + // Test 5: Tool definition + console.log('Test 5: Tool definition format'); + const calc5 = new Calculator(); + const definition = calc5.getDefinition(); + + console.log(` Tool name: ${definition.name}`); + console.log(` Parameters: ${Object.keys(definition.parameters.properties).join(', ')}`); + console.assert(definition.name === 'calculator', 'Should have name'); + console.assert(definition.parameters, 'Should have parameters'); + console.log('✅ Tool definition is correct\n'); + + console.log('🎉 All tests passed!'); + } catch (error) { + console.error('❌ Test failed:', error.message); + console.error(error.stack); + } +} + +// Run tests +if (import.meta.url === `file://${process.argv[1]}`) { + runTests(); +} + +export { Calculator, simulateToolCallFlow, simulateMultiToolFlow, displayConversation }; \ No newline at end of file diff --git a/tutorial/01-foundation/02-messages/lesson.md b/tutorial/01-foundation/02-messages/lesson.md new file mode 100644 index 0000000000000000000000000000000000000000..92f51da29ad2537ba0bb7c29d5f8ae7a3a1435ea --- /dev/null +++ b/tutorial/01-foundation/02-messages/lesson.md @@ -0,0 +1,1382 @@ +# 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 += `<>\n${msg.content}\n<>\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) \ No newline at end of file diff --git a/tutorial/01-foundation/02-messages/solutions/05-message-formatter-solution.js b/tutorial/01-foundation/02-messages/solutions/05-message-formatter-solution.js new file mode 100644 index 0000000000000000000000000000000000000000..a232310b7bea1d720673a5446b93e017ec6b9203 --- /dev/null +++ b/tutorial/01-foundation/02-messages/solutions/05-message-formatter-solution.js @@ -0,0 +1,316 @@ +/** + * Solution 5: Message Formatter + * + * This solution demonstrates: + * - Formatting timestamps + * - Handling different message types + * - String truncation + * - ANSI color codes for terminal output + */ + +import { HumanMessage, AIMessage, SystemMessage, ToolMessage } from '../../../../src/index.js'; + +/** + * Format a single message for display + */ +function formatMessage(message, options = {}) { + const maxLength = options.maxLength || 100; + + // Format timestamp + const date = new Date(message.timestamp); + const timestamp = date.toLocaleTimeString('en-US', { + hour12: false, + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); + + // Get message type in uppercase + const type = message.type.toUpperCase().padEnd(6); + + // Truncate content if needed + let content = message.content; + if (content.length > maxLength) { + content = content.substring(0, maxLength - 3) + '...'; + } + + // Build formatted string + return `[${timestamp}] ${type}: ${content}`; +} + +/** + * Format an entire conversation + */ +function formatConversation(messages) { + const separator = '─'.repeat(60); + const lines = []; + + lines.push(separator); + lines.push('CONVERSATION'); + lines.push(separator); + + messages.forEach((msg) => { + lines.push(formatMessage(msg)); + }); + + lines.push(separator); + + return lines.join('\n'); +} + +/** + * ANSI color codes + */ +const COLORS = { + reset: '\x1b[0m', + blue: '\x1b[34m', + green: '\x1b[32m', + cyan: '\x1b[36m', + yellow: '\x1b[33m', + gray: '\x1b[90m' +}; + +/** + * Get color for message type + */ +function getColorForType(type) { + const colorMap = { + 'system': COLORS.blue, + 'human': COLORS.green, + 'ai': COLORS.cyan, + 'tool': COLORS.yellow + }; + return colorMap[type] || COLORS.reset; +} + +/** + * Format message with terminal colors + */ +function formatMessageWithColor(message, options = {}) { + const maxLength = options.maxLength || 100; + + // Format timestamp (gray) + const date = new Date(message.timestamp); + const timestamp = date.toLocaleTimeString('en-US', { + hour12: false, + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); + const coloredTimestamp = `${COLORS.gray}[${timestamp}]${COLORS.reset}`; + + // Get color for message type + const color = getColorForType(message.type); + const type = message.type.toUpperCase().padEnd(6); + const coloredType = `${color}${type}${COLORS.reset}`; + + // Truncate content if needed + let content = message.content; + if (content.length > maxLength) { + content = content.substring(0, maxLength - 3) + '...'; + } + + return `${coloredTimestamp} ${coloredType}: ${content}`; +} + +/** + * Format conversation with colors + */ +function formatConversationWithColor(messages) { + const separator = COLORS.gray + '─'.repeat(60) + COLORS.reset; + const lines = []; + + lines.push(separator); + lines.push(`${COLORS.gray}CONVERSATION${COLORS.reset}`); + lines.push(separator); + + messages.forEach((msg) => { + lines.push(formatMessageWithColor(msg)); + }); + + lines.push(separator); + + return lines.join('\n'); +} + +/** + * Advanced: Format with metadata + */ +function formatMessageDetailed(message, options = {}) { + const basic = formatMessage(message, options); + + // Add metadata if present + const metadata = []; + if (message.id) { + metadata.push(`ID: ${message.id.substring(0, 8)}`); + } + if (message.additionalKwargs && Object.keys(message.additionalKwargs).length > 0) { + metadata.push(`Meta: ${JSON.stringify(message.additionalKwargs)}`); + } + if (message.type === 'ai' && message.hasToolCalls && message.hasToolCalls()) { + metadata.push(`Tools: ${message.toolCalls.length}`); + } + if (message.type === 'tool' && message.toolCallId) { + metadata.push(`CallID: ${message.toolCallId.substring(0, 8)}`); + } + + if (metadata.length > 0) { + return `${basic}\n ${COLORS.gray}[${metadata.join(', ')}]${COLORS.reset}`; + } + + return basic; +} + +// ============================================================================ +// Tests +// ============================================================================ + +async function runTests() { + console.log('🧪 Testing Message Formatter Solution...\n'); + + try { + // Test 1: Basic formatting + console.log('Test 1: Basic message formatting'); + const msg1 = new HumanMessage("Hello, how are you?"); + const formatted1 = formatMessage(msg1); + console.log(` Output: ${formatted1}`); + console.assert(formatted1.includes('HUMAN'), 'Should include message type'); + console.assert(formatted1.includes('Hello'), 'Should include content'); + console.assert(/\[\d{2}:\d{2}:\d{2}\]/.test(formatted1), 'Should have timestamp'); + console.log('✅ Basic formatting works\n'); + + // Test 2: Long message truncation + console.log('Test 2: Long message truncation'); + const longContent = 'A'.repeat(150); + const msg2 = new AIMessage(longContent); + const formatted2 = formatMessage(msg2, { maxLength: 50 }); + console.log(` Original length: 150 chars`); + console.log(` Formatted: ${formatted2.substring(0, 70)}...`); + console.assert(formatted2.includes('...'), 'Should have ellipsis'); + console.log('✅ Truncation works\n'); + + // Test 3: Different message types + console.log('Test 3: Different message types'); + const messages = [ + new SystemMessage("You are helpful"), + new HumanMessage("Hi"), + new AIMessage("Hello!"), + new ToolMessage("result", "tool_123") + ]; + + messages.forEach(msg => { + const formatted = formatMessage(msg); + console.log(` ${formatted}`); + }); + console.log('✅ All message types format correctly\n'); + + // Test 4: Conversation formatting + console.log('Test 4: Full conversation formatting'); + const conversation = [ + new SystemMessage("You are a helpful assistant"), + new HumanMessage("What's 2+2?"), + new AIMessage("2+2 equals 4") + ]; + + console.log(formatConversation(conversation)); + console.log('✅ Conversation formatting works\n'); + + // Test 5: Colored output + console.log('Test 5: Colored terminal output'); + const coloredMessages = [ + new SystemMessage("System message in blue"), + new HumanMessage("Human message in green"), + new AIMessage("AI message in cyan"), + new ToolMessage("Tool message in yellow", "tool_123") + ]; + + console.log(formatConversationWithColor(coloredMessages)); + console.log('✅ Colored output works\n'); + + // Test 6: Detailed formatting with metadata + console.log('Test 6: Detailed formatting with metadata'); + const msgWithMeta = new HumanMessage("Hello", { + userId: "user_123", + sessionId: "sess_456" + }); + const detailed = formatMessageDetailed(msgWithMeta); + console.log(detailed); + console.assert(detailed.includes('Meta:'), 'Should include metadata'); + console.log('✅ Detailed formatting works\n'); + + // Test 7: AI message with tool calls + console.log('Test 7: AI message with tool calls'); + const aiWithTools = new AIMessage("Let me calculate that", { + toolCalls: [ + { id: 'call_123', type: 'function', function: { name: 'calculator' } } + ] + }); + const formattedTools = formatMessageDetailed(aiWithTools); + console.log(formattedTools); + console.log('✅ Tool call formatting works\n'); + + // Test 8: Empty content handling + console.log('Test 8: Edge cases'); + const emptyMsg = new HumanMessage(""); + const formatted8 = formatMessage(emptyMsg); + console.log(` Empty message: ${formatted8}`); + + const specialChars = new AIMessage("Hello\nWorld\tTest"); + const formatted8b = formatMessage(specialChars); + console.log(` Special chars: ${formatted8b}`); + console.log('✅ Edge cases handled\n'); + + // Test 9: Performance with many messages + console.log('Test 9: Performance test'); + const manyMessages = Array.from({ length: 100 }, (_, i) => + new HumanMessage(`Message ${i}`) + ); + + const startTime = Date.now(); + manyMessages.forEach(msg => formatMessage(msg)); + const duration = Date.now() - startTime; + + console.log(` Formatted 100 messages in ${duration}ms`); + console.assert(duration < 100, 'Should be fast'); + console.log('✅ Performance is good\n'); + + // Test 10: Real conversation example + console.log('Test 10: Real conversation example'); + const realConversation = [ + new SystemMessage("You are a helpful Python tutor."), + new HumanMessage("How do I reverse a string in Python?"), + new AIMessage("You can reverse a string using slicing: text[::-1]"), + new HumanMessage("Can you show me an example?"), + new AIMessage("Sure! Here's an example: 'hello'[::-1] returns 'olleh'"), + new HumanMessage("Thanks!") + ]; + + console.log('\n' + formatConversationWithColor(realConversation)); + console.log('\n✅ Real conversation looks great\n'); + + console.log('🎉 All tests passed!'); + console.log('\n💡 Key Features Demonstrated:'); + console.log(' • Timestamp formatting'); + console.log(' • Message type identification'); + console.log(' • Content truncation'); + console.log(' • Terminal colors'); + console.log(' • Metadata display'); + console.log(' • Tool call indicators'); + } catch (error) { + console.error('❌ Test failed:', error.message); + console.error(error.stack); + } +} + +// Run tests +if (import.meta.url === `file://${process.argv[1]}`) { + runTests(); +} + +export { + formatMessage, + formatConversation, + formatMessageWithColor, + formatConversationWithColor, + formatMessageDetailed +}; \ No newline at end of file diff --git a/tutorial/01-foundation/02-messages/solutions/06-conversation-validator-solution.js b/tutorial/01-foundation/02-messages/solutions/06-conversation-validator-solution.js new file mode 100644 index 0000000000000000000000000000000000000000..ee768989f58e21ca0cbf8a2428d01d11e45ab373 --- /dev/null +++ b/tutorial/01-foundation/02-messages/solutions/06-conversation-validator-solution.js @@ -0,0 +1,403 @@ +/** + * Solution 6: Conversation Validator + * + * This solution demonstrates: + * - Validating message structure + * - Checking conversation flow + * - Ensuring tool call sequences are correct + * - Providing detailed error messages + */ + +import { HumanMessage, AIMessage, SystemMessage, ToolMessage } from '../../../../src/index.js'; + +/** + * Validate a single message + */ +function validateMessage(message) { + const errors = []; + + // Check message exists + if (!message) { + errors.push('Message is null or undefined'); + return { valid: false, errors }; + } + + // Check content is not empty + if (!message.content || message.content.trim().length === 0) { + errors.push(`Message content cannot be empty (type: ${message.type})`); + } + + // Check message has a type + if (!message.type) { + errors.push('Message must have a type'); + } + + // Validate tool messages + if (message.type === 'tool') { + if (!message.toolCallId) { + errors.push('Tool message must have a toolCallId'); + } + } + + // Validate AI messages with tool calls + if (message.type === 'ai' && message.toolCalls && message.toolCalls.length > 0) { + message.toolCalls.forEach((toolCall, idx) => { + if (!toolCall.id) { + errors.push(`Tool call ${idx} missing id`); + } + if (!toolCall.function || !toolCall.function.name) { + errors.push(`Tool call ${idx} missing function name`); + } + }); + } + + return { + valid: errors.length === 0, + errors + }; +} + +/** + * Validate an entire conversation + */ +function validateConversation(messages) { + const errors = []; + const warnings = []; + + // Check if conversation is empty + if (!messages || messages.length === 0) { + errors.push('Conversation cannot be empty'); + return { valid: false, errors, warnings }; + } + + // Warn if first message is not system message + if (messages[0].type !== 'system') { + warnings.push('Conversation should start with a SystemMessage'); + } + + // Validate each message individually + messages.forEach((msg, idx) => { + const result = validateMessage(msg); + if (!result.valid) { + result.errors.forEach(err => { + errors.push(`Message ${idx}: ${err}`); + }); + } + }); + + // Check message ordering and sequences + for (let i = 1; i < messages.length; i++) { + const prev = messages[i - 1]; + const curr = messages[i]; + + // Tool messages must follow AI messages with tool calls + if (curr.type === 'tool') { + if (prev.type !== 'ai') { + errors.push(`Message ${i}: Tool message must follow an AI message`); + } else if (!prev.toolCalls || prev.toolCalls.length === 0) { + errors.push(`Message ${i}: Tool message must follow an AI message with tool calls`); + } else { + // Check if tool call ID exists in previous AI message + const toolCallIds = prev.toolCalls.map(tc => tc.id); + if (!toolCallIds.includes(curr.toolCallId)) { + errors.push( + `Message ${i}: Tool message references unknown tool call ID '${curr.toolCallId}'` + ); + } + } + } + } + + // Validate tool call sequences + const toolSeqResult = validateToolCallSequence(messages); + errors.push(...toolSeqResult.errors); + warnings.push(...toolSeqResult.warnings); + + return { + valid: errors.length === 0, + errors, + warnings + }; +} + +/** + * Validate tool call sequences + */ +function validateToolCallSequence(messages) { + const errors = []; + const warnings = []; + + // Collect all tool calls from AI messages + const toolCalls = new Map(); // id -> { message, toolCall } + messages.forEach((msg, idx) => { + if (msg.type === 'ai' && msg.toolCalls) { + msg.toolCalls.forEach(tc => { + toolCalls.set(tc.id, { messageIdx: idx, toolCall: tc }); + }); + } + }); + + // Collect all tool messages + const toolMessages = new Map(); // toolCallId -> message + messages.forEach((msg, idx) => { + if (msg.type === 'tool') { + toolMessages.set(msg.toolCallId, { messageIdx: idx, message: msg }); + } + }); + + // Check every tool call has a corresponding tool message + toolCalls.forEach((value, callId) => { + if (!toolMessages.has(callId)) { + warnings.push( + `Tool call '${callId}' at message ${value.messageIdx} has no corresponding tool message` + ); + } + }); + + // Check every tool message references a valid tool call + toolMessages.forEach((value, callId) => { + if (!toolCalls.has(callId)) { + errors.push( + `Tool message at index ${value.messageIdx} references non-existent tool call '${callId}'` + ); + } + }); + + return { errors, warnings }; +} + +/** + * BONUS: Validate conversation flow (alternating human/AI) + */ +function validateConversationFlow(messages) { + const errors = []; + const warnings = []; + + // Skip system message + let startIdx = messages[0]?.type === 'system' ? 1 : 0; + + for (let i = startIdx; i < messages.length; i++) { + const msg = messages[i]; + + // Skip tool messages in flow check + if (msg.type === 'tool') continue; + + // After system, should start with human + if (i === startIdx && msg.type !== 'human') { + warnings.push('Conversation should start with a human message after system message'); + } + + // Find next non-tool message + let nextIdx = i + 1; + while (nextIdx < messages.length && messages[nextIdx].type === 'tool') { + nextIdx++; + } + + if (nextIdx < messages.length) { + const next = messages[nextIdx]; + + // Human should be followed by AI + if (msg.type === 'human' && next.type !== 'ai') { + warnings.push(`Message ${i}: Human message should be followed by AI message`); + } + + // AI should be followed by Human (unless using tools) + if (msg.type === 'ai' && next.type !== 'human' && !msg.toolCalls) { + warnings.push(`Message ${i}: AI message should be followed by human message`); + } + } + } + + return { errors, warnings }; +} + +// ============================================================================ +// Tests +// ============================================================================ + +async function runTests() { + console.log('🧪 Testing Conversation Validator Solution...\n'); + + try { + // Test 1: Valid simple conversation + console.log('Test 1: Valid simple conversation'); + const validConv = [ + new SystemMessage("You are helpful"), + new HumanMessage("Hi"), + new AIMessage("Hello!") + ]; + + const result1 = validateConversation(validConv); + console.log(` Valid: ${result1.valid}`); + console.log(` Errors: ${result1.errors.length}`); + console.log(` Warnings: ${result1.warnings.length}`); + if (result1.warnings.length > 0) { + console.log(` Warnings: ${result1.warnings[0]}`); + } + console.assert(result1.valid === true, 'Should be valid'); + console.log('✅ Valid conversation passes\n'); + + // Test 2: Missing system message + console.log('Test 2: Missing system message'); + const noSystem = [ + new HumanMessage("Hi"), + new AIMessage("Hello!") + ]; + + const result2 = validateConversation(noSystem); + console.log(` Valid: ${result2.valid}`); + console.log(` Warnings:`, result2.warnings); + console.assert(result2.warnings.length > 0, 'Should have warning'); + console.assert(result2.warnings[0].includes('SystemMessage'), 'Should mention system message'); + console.log('✅ Missing system message triggers warning\n'); + + // Test 3: Empty message content + console.log('Test 3: Empty message content'); + const emptyContent = [ + new SystemMessage(""), + new HumanMessage("Hi") + ]; + + const result3 = validateConversation(emptyContent); + console.log(` Valid: ${result3.valid}`); + console.log(` Errors:`, result3.errors); + console.assert(result3.valid === false, 'Should be invalid'); + console.assert(result3.errors[0].includes('empty'), 'Should mention empty content'); + console.log('✅ Empty content caught\n'); + + // Test 4: Tool message without AI tool call + console.log('Test 4: Tool message without preceding AI tool call'); + const badToolSequence = [ + new SystemMessage("You are helpful"), + new HumanMessage("Calculate 2+2"), + new ToolMessage("4", "call_123") + ]; + + const result4 = validateConversation(badToolSequence); + console.log(` Valid: ${result4.valid}`); + console.log(` Errors:`, result4.errors); + console.assert(result4.valid === false, 'Should be invalid'); + console.log('✅ Invalid tool sequence caught\n'); + + // Test 5: Valid tool call sequence + console.log('Test 5: Valid tool call sequence'); + const validToolSeq = [ + new SystemMessage("You are helpful"), + new HumanMessage("Calculate 2+2"), + new AIMessage("Let me calculate", { + toolCalls: [{ + id: 'call_123', + type: 'function', + function: { name: 'calculator', arguments: '{"a":2,"b":2}' } + }] + }), + new ToolMessage("4", "call_123"), + new AIMessage("The answer is 4") + ]; + + const result5 = validateConversation(validToolSeq); + console.log(` Valid: ${result5.valid}`); + console.log(` Errors: ${result5.errors.length}`); + console.log(` Warnings: ${result5.warnings.length}`); + console.assert(result5.valid === true, 'Should be valid'); + console.log('✅ Valid tool sequence passes\n'); + + // Test 6: Single message validation + console.log('Test 6: Single message validation'); + const goodMsg = new HumanMessage("Hello"); + const result6a = validateMessage(goodMsg); + console.assert(result6a.valid === true, 'Valid message should pass'); + + const badMsg = new HumanMessage(""); + const result6b = validateMessage(badMsg); + console.assert(result6b.valid === false, 'Empty message should fail'); + console.log(` Empty message errors: ${result6b.errors[0]}`); + console.log('✅ Single message validation works\n'); + + // Test 7: Empty conversation + console.log('Test 7: Empty conversation'); + const result7 = validateConversation([]); + console.log(` Valid: ${result7.valid}`); + console.log(` Errors: ${result7.errors[0]}`); + console.assert(result7.valid === false, 'Empty conversation should be invalid'); + console.log('✅ Empty conversation caught\n'); + + // Test 8: Multiple tool calls + console.log('Test 8: Multiple tool calls in sequence'); + const multiTool = [ + new SystemMessage("You are helpful"), + new HumanMessage("Calculate 2+2 and 3*3"), + new AIMessage("Let me calculate both", { + toolCalls: [ + { id: 'call_1', type: 'function', function: { name: 'add', arguments: '{"a":2,"b":2}' } }, + { id: 'call_2', type: 'function', function: { name: 'multiply', arguments: '{"a":3,"b":3}' } } + ] + }), + new ToolMessage("4", "call_1"), + new ToolMessage("9", "call_2"), + new AIMessage("2+2 = 4 and 3*3 = 9") + ]; + + const result8 = validateConversation(multiTool); + console.log(` Valid: ${result8.valid}`); + console.log(` Errors: ${result8.errors.length}`); + console.assert(result8.valid === true, 'Multiple tools should be valid'); + console.log('✅ Multiple tool calls work\n'); + + // Test 9: Missing tool message + console.log('Test 9: Tool call without tool message'); + const missingToolMsg = [ + new SystemMessage("You are helpful"), + new HumanMessage("Calculate 2+2"), + new AIMessage("Let me calculate", { + toolCalls: [{ id: 'call_123', type: 'function', function: { name: 'calc' } }] + }), + // Missing ToolMessage here + new AIMessage("The answer is 4") + ]; + + const result9 = validateConversation(missingToolMsg); + console.log(` Valid: ${result9.valid}`); + console.log(` Warnings:`, result9.warnings); + console.assert(result9.warnings.length > 0, 'Should have warning about missing tool message'); + console.log('✅ Missing tool message warning works\n'); + + // Test 10: Conversation flow validation + console.log('Test 10: Conversation flow validation'); + const flowTest = [ + new SystemMessage("You are helpful"), + new HumanMessage("Hi"), + new AIMessage("Hello!"), + new HumanMessage("How are you?"), + new AIMessage("I'm doing well!") + ]; + + const result10 = validateConversationFlow(flowTest); + console.log(` Flow warnings: ${result10.warnings.length}`); + console.log('✅ Flow validation works\n'); + + console.log('🎉 All tests passed!'); + console.log('\n💡 Validation Rules Implemented:'); + console.log(' • Empty content detection'); + console.log(' • System message recommendations'); + console.log(' • Tool call sequence validation'); + console.log(' • Tool call ID matching'); + console.log(' • Conversation flow checking'); + console.log(' • Detailed error messages'); + } catch (error) { + console.error('❌ Test failed:', error.message); + console.error(error.stack); + } +} + +// Run tests +if (import.meta.url === `file://${process.argv[1]}`) { + runTests(); +} + +export { + validateMessage, + validateConversation, + validateToolCallSequence, + validateConversationFlow +}; \ No newline at end of file diff --git a/tutorial/01-foundation/02-messages/solutions/07-chat-history-solution.js b/tutorial/01-foundation/02-messages/solutions/07-chat-history-solution.js new file mode 100644 index 0000000000000000000000000000000000000000..e63cb28d4c2cfac8949dbf3c1721366b14ddc5d0 --- /dev/null +++ b/tutorial/01-foundation/02-messages/solutions/07-chat-history-solution.js @@ -0,0 +1,378 @@ +/** + * Solution 7: Chat History Manager + * + * This solution demonstrates: + * - Managing conversation state + * - Sliding window implementation + * - JSON persistence + * - Type filtering and statistics + * - LLM format conversion + */ + +import { HumanMessage, AIMessage, SystemMessage, ToolMessage, BaseMessage } from '../../../../src/index.js'; + +/** + * ConversationHistory - Manages conversation messages + */ +class ConversationHistory { + constructor(options = {}) { + this.messages = []; + this.maxMessages = options.maxMessages || 100; + this.preserveSystem = options.preserveSystem !== false; // default true + } + + /** + * Add a message to history + */ + add(message) { + if (!(message instanceof BaseMessage)) { + throw new Error('Message must be an instance of BaseMessage'); + } + + this.messages.push(message); + + // Apply sliding window if needed + if (this.messages.length > this.maxMessages) { + this._applyWindow(); + } + } + + /** + * Apply sliding window to keep only recent messages + */ + _applyWindow() { + // Find system message if present + const systemMsg = this.preserveSystem + ? this.messages.find(m => m.type === 'system') + : null; + + // Keep last maxMessages - 1 (to leave room for system message) + const keepCount = systemMsg ? this.maxMessages - 1 : this.maxMessages; + const recentMessages = this.messages.slice(-keepCount); + + // Rebuild messages array + if (systemMsg && !recentMessages.find(m => m.type === 'system')) { + this.messages = [systemMsg, ...recentMessages.filter(m => m.type !== 'system')]; + } else { + this.messages = recentMessages; + } + } + + /** + * Get all messages (returns copy) + */ + getAll() { + return [...this.messages]; + } + + /** + * Get last N messages + */ + getLast(n = 1) { + return this.messages.slice(-n); + } + + /** + * Get messages by type + */ + getByType(type) { + return this.messages.filter(msg => msg.type === type); + } + + /** + * Get message count + */ + count() { + return this.messages.length; + } + + /** + * Clear history but preserve system message + */ + clear() { + const systemMsg = this.preserveSystem + ? this.messages.find(m => m.type === 'system') + : null; + + this.messages = systemMsg ? [systemMsg] : []; + } + + /** + * Format for LLM consumption + */ + toPromptFormat() { + return this.messages.map(msg => msg.toPromptFormat()); + } + + /** + * Save to JSON string + */ + save() { + const data = { + messages: this.messages.map(msg => msg.toJSON()), + maxMessages: this.maxMessages, + preserveSystem: this.preserveSystem + }; + return JSON.stringify(data); + } + + /** + * Load from JSON string + */ + static load(json, options = {}) { + const data = JSON.parse(json); + + const history = new ConversationHistory({ + maxMessages: data.maxMessages || options.maxMessages || 100, + preserveSystem: data.preserveSystem !== false + }); + + // Recreate messages from JSON + data.messages.forEach(msgData => { + const message = BaseMessage.fromJSON(msgData); + history.messages.push(message); + }); + + return history; + } + + /** + * Get conversation statistics + */ + getStats() { + const stats = { + total: this.messages.length, + system: 0, + human: 0, + ai: 0, + tool: 0, + estimatedTokens: 0 + }; + + this.messages.forEach(msg => { + stats[msg.type]++; + // Rough token estimate: ~4 chars per token + stats.estimatedTokens += Math.ceil(msg.content.length / 4); + }); + + return stats; + } + + /** + * Get messages in a time range + */ + getRange(startTime, endTime) { + return this.messages.filter(msg => + msg.timestamp >= startTime && msg.timestamp <= endTime + ); + } + + /** + * Find messages by content search + */ + search(query) { + const lowerQuery = query.toLowerCase(); + return this.messages.filter(msg => + msg.content.toLowerCase().includes(lowerQuery) + ); + } +} + +// ============================================================================ +// Tests +// ============================================================================ + +async function runTests() { + console.log('🧪 Testing Conversation History Manager Solution...\n'); + + try { + // Test 1: Basic add and get + console.log('Test 1: Add and retrieve messages'); + const history = new ConversationHistory(); + history.add(new SystemMessage("You are helpful")); + history.add(new HumanMessage("Hi")); + history.add(new AIMessage("Hello!")); + + const all = history.getAll(); + console.log(` Added: 3 messages`); + console.log(` Retrieved: ${all.length} messages`); + console.assert(all.length === 3, 'Should have 3 messages'); + console.log('✅ Basic operations work\n'); + + // Test 2: Get last N messages + console.log('Test 2: Get last N messages'); + const last2 = history.getLast(2); + console.log(` Last 2 messages:`); + last2.forEach(msg => console.log(` - ${msg.type}: ${msg.content}`)); + console.assert(last2.length === 2, 'Should return 2 messages'); + console.assert(last2[0].type === 'human', 'First should be human'); + console.log('✅ getLast works\n'); + + // Test 3: Filter by type + console.log('Test 3: Filter by message type'); + const humanMessages = history.getByType('human'); + const aiMessages = history.getByType('ai'); + console.log(` Human messages: ${humanMessages.length}`); + console.log(` AI messages: ${aiMessages.length}`); + console.assert(humanMessages.length === 1, 'Should have 1 human message'); + console.assert(aiMessages.length === 1, 'Should have 1 AI message'); + console.log('✅ Filtering works\n'); + + // Test 4: Sliding window + console.log('Test 4: Sliding window (max messages)'); + const limited = new ConversationHistory({ maxMessages: 5 }); + limited.add(new SystemMessage("You are helpful")); + + // Add 10 messages + for (let i = 0; i < 10; i++) { + limited.add(new HumanMessage(`Message ${i}`)); + } + + const count = limited.count(); + console.log(` Added: 11 messages (1 system + 10 human)`); + console.log(` Kept: ${count} messages (max: 5)`); + console.assert(count === 5, 'Should keep only 5 messages'); + + // System message should be preserved + const hasSystem = limited.getAll().some(m => m.type === 'system'); + console.assert(hasSystem, 'Should preserve system message'); + console.log(` System message preserved: ${hasSystem}`); + console.log('✅ Sliding window works\n'); + + // Test 5: Clear history + console.log('Test 5: Clear history'); + const hist5 = new ConversationHistory(); + hist5.add(new SystemMessage("You are helpful")); + hist5.add(new HumanMessage("Hi")); + hist5.add(new AIMessage("Hello!")); + + console.log(` Before clear: ${hist5.count()} messages`); + hist5.clear(); + console.log(` After clear: ${hist5.count()} messages`); + + const afterClear = hist5.getAll(); + console.assert(afterClear.length === 1, 'Should keep system message'); + console.assert(afterClear[0].type === 'system', 'Should be system message'); + console.log('✅ Clear preserves system message\n'); + + // Test 6: Save and load + console.log('Test 6: Save and load from JSON'); + const hist6 = new ConversationHistory(); + hist6.add(new SystemMessage("You are helpful")); + hist6.add(new HumanMessage("Hi")); + hist6.add(new AIMessage("Hello!")); + + const json = hist6.save(); + console.log(` Saved JSON length: ${json.length} chars`); + + const loaded = ConversationHistory.load(json); + const loadedMessages = loaded.getAll(); + console.log(` Loaded: ${loadedMessages.length} messages`); + console.assert(loadedMessages.length === 3, 'Should load all messages'); + console.assert(loadedMessages[0].type === 'system', 'Should preserve types'); + console.assert(loadedMessages[0].content === 'You are helpful', 'Should preserve content'); + console.log('✅ Persistence works\n'); + + // Test 7: Format for LLM + console.log('Test 7: Format for LLM'); + const hist7 = new ConversationHistory(); + hist7.add(new SystemMessage("You are helpful")); + hist7.add(new HumanMessage("Hi")); + hist7.add(new AIMessage("Hello!")); + + const formatted = hist7.toPromptFormat(); + console.log(` Formatted messages:`); + formatted.forEach(msg => console.log(` ${msg.role}: ${msg.content}`)); + console.assert(formatted[0].role === 'system', 'Should have system role'); + console.assert(formatted[1].role === 'user', 'Should map human to user'); + console.assert(formatted[2].role === 'assistant', 'Should map ai to assistant'); + console.log('✅ LLM formatting works\n'); + + // Test 8: Statistics + console.log('Test 8: Get conversation statistics'); + const hist8 = new ConversationHistory(); + hist8.add(new SystemMessage("You are helpful")); + hist8.add(new HumanMessage("Hi")); + hist8.add(new AIMessage("Hello!")); + hist8.add(new HumanMessage("How are you?")); + hist8.add(new AIMessage("I'm great!")); + + const stats = hist8.getStats(); + console.log(` Statistics:`, stats); + console.assert(stats.total === 5, 'Should count total'); + console.assert(stats.human === 2, 'Should count human messages'); + console.assert(stats.ai === 2, 'Should count AI messages'); + console.assert(stats.system === 1, 'Should count system messages'); + console.log('✅ Statistics work\n'); + + // Test 9: Search functionality + console.log('Test 9: Search messages by content'); + const hist9 = new ConversationHistory(); + hist9.add(new HumanMessage("What's the weather?")); + hist9.add(new AIMessage("It's sunny today")); + hist9.add(new HumanMessage("What about tomorrow?")); + hist9.add(new AIMessage("Tomorrow will be rainy")); + + const weatherResults = hist9.search('weather'); + const tomorrowResults = hist9.search('tomorrow'); + console.log(` 'weather' found in: ${weatherResults.length} messages`); + console.log(` 'tomorrow' found in: ${tomorrowResults.length} messages`); + console.assert(weatherResults.length === 1, 'Should find weather query'); + console.assert(tomorrowResults.length === 2, 'Should find tomorrow in both messages'); + console.log('✅ Search works\n'); + + // Test 10: Time range queries + console.log('Test 10: Get messages in time range'); + const hist10 = new ConversationHistory(); + + hist10.add(new HumanMessage("Message 1")); + await new Promise(resolve => setTimeout(resolve, 10)); + const midTime = Date.now(); + await new Promise(resolve => setTimeout(resolve, 10)); + hist10.add(new HumanMessage("Message 2")); + hist10.add(new HumanMessage("Message 3")); + + const afterMid = hist10.getRange(midTime, Date.now() + 1000); + console.log(` Messages after midpoint: ${afterMid.length}`); + console.assert(afterMid.length === 2, 'Should get messages 2 and 3'); + console.log('✅ Time range queries work\n'); + + // Test 11: Complex conversation + console.log('Test 11: Complex conversation with tools'); + const hist11 = new ConversationHistory(); + hist11.add(new SystemMessage("You are a calculator")); + hist11.add(new HumanMessage("What's 5+3?")); + hist11.add(new AIMessage("Let me calculate", { + toolCalls: [{ id: 'call_1', type: 'function', function: { name: 'add' } }] + })); + hist11.add(new ToolMessage("8", "call_1")); + hist11.add(new AIMessage("5+3 equals 8")); + + const stats11 = hist11.getStats(); + console.log(` Total messages: ${stats11.total}`); + console.log(` Message breakdown:`, stats11); + console.assert(stats11.tool === 1, 'Should count tool message'); + console.log('✅ Complex conversations work\n'); + + console.log('🎉 All tests passed!'); + console.log('\n💡 Features Demonstrated:'); + console.log(' • Message storage and retrieval'); + console.log(' • Sliding window with system preservation'); + console.log(' • Type filtering'); + console.log(' • JSON persistence'); + console.log(' • LLM format conversion'); + console.log(' • Statistics and analytics'); + console.log(' • Search functionality'); + console.log(' • Time-based queries'); + } catch (error) { + console.error('❌ Test failed:', error.message); + console.error(error.stack); + } +} + +// Run tests +if (import.meta.url === `file://${process.argv[1]}`) { + runTests(); +} + +export { ConversationHistory }; \ No newline at end of file diff --git a/tutorial/01-foundation/02-messages/solutions/08-tool-flow-solution.js b/tutorial/01-foundation/02-messages/solutions/08-tool-flow-solution.js new file mode 100644 index 0000000000000000000000000000000000000000..35a200d4ce59c3f4e75b228233b40b509b072260 --- /dev/null +++ b/tutorial/01-foundation/02-messages/solutions/08-tool-flow-solution.js @@ -0,0 +1,456 @@ +/** + * Solution 8: Tool Call Flow + * + * This solution demonstrates: + * - Complete agent conversation flow + * - Tool call creation and execution + * - Linking tool messages to AI tool calls + * - Multi-step reasoning with tools + * - Realistic agent behavior + */ + +import {AIMessage, HumanMessage, SystemMessage, ToolMessage} from '../../../../src/index.js'; + +/** + * Mock Calculator Tool + */ +class Calculator { + constructor() { + this.name = 'calculator'; + } + + execute(operation, a, b) { + const ops = { + 'add': (x, y) => x + y, + 'subtract': (x, y) => x - y, + 'multiply': (x, y) => x * y, + 'divide': (x, y) => x / y + }; + + if (!ops[operation]) { + throw new Error(`Unknown operation: ${operation}`); + } + + return ops[operation](a, b); + } + + getDefinition() { + return { + name: 'calculator', + description: 'Performs basic arithmetic operations', + parameters: { + type: 'object', + properties: { + operation: { + type: 'string', + enum: ['add', 'subtract', 'multiply', 'divide'], + description: 'The operation to perform' + }, + a: { type: 'number', description: 'First number' }, + b: { type: 'number', description: 'Second number' } + }, + required: ['operation', 'a', 'b'] + } + }; + } +} + +/** + * Generate unique tool call ID + */ +function generateToolCallId() { + return `call_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; +} + +/** + * Simulate a complete tool call conversation + */ +function simulateToolCallFlow(userQuery) { + const messages = []; + const calculator = new Calculator(); + + // 1. System message sets context + messages.push(new SystemMessage( + "You are a helpful assistant with access to a calculator tool. " + + "Use the calculator for any arithmetic operations." + )); + + // 2. Human asks a question + messages.push(new HumanMessage(userQuery)); + + // 3. AI decides to use calculator tool + const toolCallId = generateToolCallId(); + + // Parse the query to extract operation (simple parsing for demo) + let operation = 'multiply'; + let a = 5, b = 3; + + if (userQuery.includes('*') || userQuery.toLowerCase().includes('multiply')) { + operation = 'multiply'; + // Extract numbers (simplified) + const numbers = userQuery.match(/\d+/g); + if (numbers && numbers.length >= 2) { + a = parseInt(numbers[0]); + b = parseInt(numbers[1]); + } + } else if (userQuery.includes('+') || userQuery.toLowerCase().includes('add')) { + operation = 'add'; + const numbers = userQuery.match(/\d+/g); + if (numbers && numbers.length >= 2) { + a = parseInt(numbers[0]); + b = parseInt(numbers[1]); + } + } else if (userQuery.includes('-') || userQuery.toLowerCase().includes('subtract')) { + operation = 'subtract'; + const numbers = userQuery.match(/\d+/g); + if (numbers && numbers.length >= 2) { + a = parseInt(numbers[0]); + b = parseInt(numbers[1]); + } + } else if (userQuery.includes('/') || userQuery.toLowerCase().includes('divide')) { + operation = 'divide'; + const numbers = userQuery.match(/\d+/g); + if (numbers && numbers.length >= 2) { + a = parseInt(numbers[0]); + b = parseInt(numbers[1]); + } + } + + messages.push(new AIMessage( + "I'll calculate that for you using the calculator tool.", + { + toolCalls: [{ + id: toolCallId, + type: 'function', + function: { + name: 'calculator', + arguments: JSON.stringify({ operation, a, b }) + } + }] + } + )); + + // 4. Execute the tool + const result = calculator.execute(operation, a, b); + + // 5. Tool returns result + messages.push(new ToolMessage( + JSON.stringify({ result }), + toolCallId + )); + + // 6. AI processes result and responds to user + const opSymbol = { + 'add': '+', + 'subtract': '-', + 'multiply': '*', + 'divide': '/' + }[operation]; + + messages.push(new AIMessage( + `The result of ${a} ${opSymbol} ${b} is ${result}.` + )); + + return messages; +} + +/** + * Simulate multi-step tool calling + */ +function simulateMultiToolFlow(userQuery) { + const messages = []; + const calculator = new Calculator(); + + // 1. System message + messages.push(new SystemMessage( + "You are a helpful assistant with access to a calculator. " + + "You can perform multiple calculations in sequence." + )); + + // 2. Human query + messages.push(new HumanMessage(userQuery)); + + // 3. First calculation: 5 * 3 + const toolCall1Id = generateToolCallId(); + messages.push(new AIMessage( + "I'll solve this step by step. First, I'll calculate 5 * 3.", + { + toolCalls: [{ + id: toolCall1Id, + type: 'function', + function: { + name: 'calculator', + arguments: JSON.stringify({ operation: 'multiply', a: 5, b: 3 }) + } + }] + } + )); + + const result1 = calculator.execute('multiply', 5, 3); + messages.push(new ToolMessage( + JSON.stringify({ result: result1 }), + toolCall1Id + )); + + // 4. Second calculation: result + 10 + const toolCall2Id = generateToolCallId(); + messages.push(new AIMessage( + `Now I'll add 10 to ${result1}.`, + { + toolCalls: [{ + id: toolCall2Id, + type: 'function', + function: { + name: 'calculator', + arguments: JSON.stringify({ operation: 'add', a: result1, b: 10 }) + } + }] + } + )); + + const result2 = calculator.execute('add', result1, 10); + messages.push(new ToolMessage( + JSON.stringify({ result: result2 }), + toolCall2Id + )); + + // 5. Final answer + messages.push(new AIMessage( + `The final result is ${result2}. (5 * 3 = ${result1}, then ${result1} + 10 = ${result2})` + )); + + return messages; +} + +/** + * Display conversation with formatting + */ +function displayConversation(messages) { + const separator = '─'.repeat(70); + + console.log(separator); + + messages.forEach((msg, idx) => { + const time = new Date(msg.timestamp).toLocaleTimeString(); + const type = msg.type.toUpperCase().padEnd(6); + + console.log(`\n[${idx + 1}] [${time}] ${type}`); + + if (msg.type === 'ai' && msg.hasToolCalls && msg.hasToolCalls()) { + // Show tool call details + console.log(` Content: ${msg.content}`); + console.log(` 🛠️ Tool Calls:`); + msg.toolCalls.forEach(tc => { + console.log(` • ${tc.function.name}(${tc.function.arguments})`); + console.log(` ID: ${tc.id}`); + }); + } else if (msg.type === 'tool') { + // Show tool result with link to call + console.log(` 🔧 Result: ${msg.content}`); + console.log(` ↳ Response to: ${msg.toolCallId}`); + } else { + // Regular message + console.log(` ${msg.content}`); + } + }); + + console.log(`\n${separator}`); +} + +/** + * Validate tool call flow + */ +function validateToolFlow(messages) { + const errors = []; + + // Check every tool message has a corresponding AI tool call + const toolCallIds = new Set(); + + messages.forEach((msg, idx) => { + if (msg.type === 'ai' && msg.toolCalls) { + msg.toolCalls.forEach(tc => toolCallIds.add(tc.id)); + } + + if (msg.type === 'tool') { + if (!toolCallIds.has(msg.toolCallId)) { + errors.push( + `Tool message at index ${idx} references unknown call ID: ${msg.toolCallId}` + ); + } + + // Check it follows an AI message + if (idx === 0 || messages[idx - 1].type !== 'ai') { + errors.push(`Tool message at index ${idx} doesn't follow an AI message`); + } + } + }); + + return { + valid: errors.length === 0, + errors + }; +} + +// ============================================================================ +// Tests +// ============================================================================ + +async function runTests() { + console.log('🧪 Testing Tool Call Flow Solution...\n'); + + try { + // Test 1: Simple tool call + console.log('Test 1: Simple calculation with tool'); + const conversation1 = simulateToolCallFlow("What's 5 * 3?"); + + console.log(` Messages created: ${conversation1.length}`); + console.assert(conversation1.length >= 5, 'Should have at least 5 messages'); + + // Validate message types + console.assert(conversation1[0].type === 'system', 'Should start with system'); + console.assert(conversation1[1].type === 'human', 'Should have human query'); + console.assert(conversation1[2].type === 'ai', 'Should have AI decision'); + console.assert(conversation1[3].type === 'tool', 'Should have tool result'); + console.assert(conversation1[4].type === 'ai', 'Should have final AI response'); + + // Check AI message has tool calls + const aiWithTool = conversation1[2]; + console.assert(aiWithTool.hasToolCalls(), 'AI message should have tool calls'); + + displayConversation(conversation1); + console.log('\n✅ Simple tool call works\n'); + + // Test 2: Multi-step tool calls + console.log('Test 2: Multi-step calculation'); + const conversation2 = simulateMultiToolFlow("What's 5*3 and then add 10?"); + + console.log(` Messages created: ${conversation2.length}`); + + // Count tool messages + const toolMessages = conversation2.filter(m => m.type === 'tool'); + console.log(` Tool calls made: ${toolMessages.length}`); + console.assert(toolMessages.length >= 2, 'Should have at least 2 tool calls'); + + displayConversation(conversation2); + console.log('\n✅ Multi-step tool calls work\n'); + + // Test 3: Tool call ID linking + console.log('Test 3: Tool call IDs match'); + const testConv = simulateToolCallFlow("Calculate 10 + 5"); + + const aiMsg = testConv.find(m => m.type === 'ai' && m.hasToolCalls && m.hasToolCalls()); + const toolMsg = testConv.find(m => m.type === 'tool'); + + console.assert(aiMsg, 'Should have AI message with tool call'); + console.assert(toolMsg, 'Should have tool message'); + + const toolCallId = aiMsg.toolCalls[0].id; + console.log(` Tool call ID: ${toolCallId}`); + console.log(` Tool message references: ${toolMsg.toolCallId}`); + console.assert(toolCallId === toolMsg.toolCallId, 'IDs should match'); + console.log('✅ Tool call IDs link correctly\n'); + + // Test 4: Calculator tool + console.log('Test 4: Calculator tool execution'); + const calc = new Calculator(); + + const result1 = calc.execute('multiply', 5, 3); + const result2 = calc.execute('add', 10, 5); + const result3 = calc.execute('divide', 20, 4); + const result4 = calc.execute('subtract', 10, 3); + + console.log(` 5 * 3 = ${result1}`); + console.log(` 10 + 5 = ${result2}`); + console.log(` 20 / 4 = ${result3}`); + console.log(` 10 - 3 = ${result4}`); + + console.assert(result1 === 15, 'Multiplication'); + console.assert(result2 === 15, 'Addition'); + console.assert(result3 === 5, 'Division'); + console.assert(result4 === 7, 'Subtraction'); + console.log('✅ Calculator works\n'); + + // Test 5: Tool definition + console.log('Test 5: Tool definition format'); + const calc5 = new Calculator(); + const definition = calc5.getDefinition(); + + console.log(` Tool name: ${definition.name}`); + console.log(` Description: ${definition.description}`); + console.log(` Parameters: ${Object.keys(definition.parameters.properties).join(', ')}`); + console.assert(definition.name === 'calculator', 'Should have name'); + console.assert(definition.parameters, 'Should have parameters'); + console.assert(definition.parameters.properties.operation, 'Should have operation param'); + console.log('✅ Tool definition is correct\n'); + + // Test 6: Flow validation + console.log('Test 6: Validate tool flow'); + const validFlow = simulateToolCallFlow("What's 2 + 2?"); + const validation = validateToolFlow(validFlow); + + console.log(` Valid: ${validation.valid}`); + console.log(` Errors: ${validation.errors.length}`); + console.assert(validation.valid, 'Flow should be valid'); + console.log('✅ Flow validation works\n'); + + // Test 7: Different operations + console.log('Test 7: Different calculator operations'); + const operations = [ + { query: "What's 100 / 5?", expected: 20 }, + { query: "Calculate 50 - 30", expected: 20 }, + { query: "Add 25 and 25", expected: 50 } + ]; + + operations.forEach(({ query, expected }) => { + const conv = simulateToolCallFlow(query); + const toolResult = conv.find(m => m.type === 'tool'); + const result = JSON.parse(toolResult.content).result; + console.log(` ${query} → ${result}`); + console.assert(result === expected, `Should equal ${expected}`); + }); + console.log('✅ All operations work\n'); + + // Test 8: Complex multi-step + console.log('Test 8: Complex calculation chain'); + const complex = simulateMultiToolFlow("Calculate ((5 * 3) + 10)"); + + // Should have multiple intermediate steps + const aiMessages = complex.filter(m => m.type === 'ai'); + const toolCalls = complex.filter(m => m.type === 'tool'); + + console.log(` AI messages: ${aiMessages.length}`); + console.log(` Tool executions: ${toolCalls.length}`); + console.assert(toolCalls.length >= 2, 'Should have multiple tool calls'); + + // Final answer should be 25 + const finalAI = complex[complex.length - 1]; + console.assert(finalAI.content.includes('25'), 'Final answer should be 25'); + console.log('✅ Complex calculations work\n'); + + console.log('🎉 All tests passed!'); + console.log('\n💡 Key Concepts Demonstrated:'); + console.log(' • Tool call creation with unique IDs'); + console.log(' • Tool message linking to AI calls'); + console.log(' • Multi-step reasoning with tools'); + console.log(' • Tool definition structure'); + console.log(' • Realistic agent conversation flow'); + console.log(' • Intermediate result usage'); + } catch (error) { + console.error('❌ Test failed:', error.message); + console.error(error.stack); + } +} + +// Run tests +if (import.meta.url === `file://${process.argv[1]}`) { + runTests(); +} + +export { + Calculator, + simulateToolCallFlow, + simulateMultiToolFlow, + displayConversation, + validateToolFlow, + generateToolCallId +}; \ No newline at end of file diff --git a/tutorial/01-foundation/03-llm-wrapper/exercises/09-basic-llm-wrapper.js b/tutorial/01-foundation/03-llm-wrapper/exercises/09-basic-llm-wrapper.js new file mode 100644 index 0000000000000000000000000000000000000000..a7824151ca9cdb7b776eb771e54d2f1dacca0b18 --- /dev/null +++ b/tutorial/01-foundation/03-llm-wrapper/exercises/09-basic-llm-wrapper.js @@ -0,0 +1,87 @@ +/** + * Exercise 9: Basic LLM Setup + * + * Goal: Get comfortable with basic LlamaCppLLM usage + * + * In this exercise, you'll: + * 1. Create a LlamaCppLLM instance + * 2. Invoke it with a simple string + * 3. Invoke it with message objects + * 4. Try different temperatures + * + * This is the same as your fundamentals, just wrapped! + */ + +import {HumanMessage, SystemMessage, LlamaCppLLM} from '../../../../src/index.js'; +import {QwenChatWrapper} from "node-llama-cpp"; + +async function exercise1() { + console.log('=== Exercise 1: Basic LLM Setup ===\n'); + + // TODO: Create a LlamaCppLLM instance + // Use a model path that exists on your system + // Set temperature to 0.7 and maxTokens to 100 + // Add a QwenChatWrapper with thoughts: 'discourage' + // Set verbose to true to see model loading + const llm = null; // Replace with your code + + try { + // Part 1: Simple string invocation + console.log('Part 1: Simple string input'); + // TODO: Invoke the LLM with: "What is 2+2? Answer in one sentence" + const response1 = null; // Replace with your code + console.log('Response:', response1.content); + console.log(); + + // Part 2: Using message objects + console.log('Part 2: Using message objects'); + // TODO: Create an array with: + // - A SystemMessage: "You are a patient math tutor teaching a 10-year-old. Always explain the reasoning step-by-step in simple terms." + // - A HumanMessage: "What is 5*5? Answer in one sentence." + const messages = []; // Replace with your code + + // TODO: Invoke the LLM with the messages array + const response2 = null; // Replace with your code + console.log('Response:', response2.content); + console.log(); + + // Part 3: Temperature experimentation + console.log('Part 3: Temperature differences'); + console.log('Temperature controls randomness: 0.0 = deterministic, 1.0 = creative\n'); + const question = "Give me one adjective to describe winter:"; + + // TODO: Invoke with low temperature (0.1) using runtime config + // Remember to clear chat history first: llm._chatSession.setChatHistory([]); + console.log('Low temperature (0.1):'); + const lowTemp = null; // Replace with your code + // Should always return the same word: "cold" + console.log(lowTemp.content); + + // TODO: Invoke with high temperature (0.9) using runtime config + // Remember to clear chat history first: llm._chatSession.setChatHistory([]); + console.log('\nHigh temperature (0.9):'); + const highTemp = null; // Replace with your code + // Different each time: "frosty", "snowy", "chilly", "icy", "freezing" + console.log(highTemp.content); + + } finally { + // TODO: Always dispose of the LLM when done + // Add your cleanup code here + console.log('\n✓ Resources cleaned up'); + } + + console.log('\n✓ Exercise 1 complete!'); +} + +// Run the exercise +exercise1().catch(console.error); + +/** + * Learning Points: + * 1. LlamaCppLLM accepts both strings and message arrays + * 2. QwenChatWrapper with thoughts: 'discourage' prevents thinking tokens + * 3. Temperature affects creativity (low=focused, high=creative) + * 4. Clear chat history between temperature tests to avoid contamination + * 5. Runtime config overrides instance defaults + * 6. Always dispose() when done to free resources + */ \ No newline at end of file diff --git a/tutorial/01-foundation/03-llm-wrapper/exercises/10-batch-processing.js b/tutorial/01-foundation/03-llm-wrapper/exercises/10-batch-processing.js new file mode 100644 index 0000000000000000000000000000000000000000..52848ca5d8ea1b574dfda020a481d84145aafa52 --- /dev/null +++ b/tutorial/01-foundation/03-llm-wrapper/exercises/10-batch-processing.js @@ -0,0 +1,108 @@ +/** + * Exercise 10: Batch Processing + * + * Goal: Learn to process multiple inputs efficiently + * + * In this exercise, you'll: + * 1. Use batch() to process multiple questions at once + * 2. Compare batch vs sequential processing + * 3. Test your agent on multiple test cases + * 4. Understand parallel execution benefits + * + * This is incredibly useful for testing and evaluation! + */ + +import {HumanMessage, SystemMessage, LlamaCppLLM} from '../../../../src/index.js'; + +async function exercise2() { + console.log('=== Exercise 2: Batch Processing ===\n'); + + const llm = new LlamaCppLLM({ + modelPath: './models/Meta-Llama-3.1-8B-Instruct-Q5_K_S.gguf', // Adjust to your model + temperature: 0.3, // Lower for consistent answers + maxTokens: 50 + }); + + try { + // Part 1: Simple batch processing + console.log('Part 1: Batch processing simple questions'); + + // TODO: Create an array of 5 different math questions + const mathQuestions = []; // Replace with your code + + // TODO: Use batch() to process all questions at once + const mathAnswers = null; // Replace with your code + + // TODO: Print each question and answer + // Loop through and console.log them nicely + + console.log(); + + // Part 2: Batch with message arrays + console.log('Part 2: Batch processing with message arrays'); + + // TODO: Create an array of message arrays + // Each should have a SystemMessage defining a role and a HumanMessage with a question + // Roles: ["chef", "scientist", "poet"] + // Question for each: "Describe an apple in one sentence" + const conversationBatch = []; // Replace with your code + + // TODO: Process the batch + const perspectives = null; // Replace with your code + + // TODO: Print each role's perspective + // Format: "Chef: [response]" + + console.log(); + + // Part 3: Performance comparison + console.log('Part 3: Sequential vs Batch performance'); + + const testQuestions = [ + "What is AI?", + "What is ML?", + "What is DL?", + "What is NLP?", + "What is CV?" + ]; + + // TODO: Time sequential processing + console.log('Sequential processing...'); + const startSeq = Date.now(); + // Process each question one by one with a loop + // (code here) + const seqTime = Date.now() - startSeq; + console.log(`Sequential: ${seqTime}ms`); + + // TODO: Time batch processing + console.log('\nBatch processing...'); + const startBatch = Date.now(); + // Process all at once with batch() + // (code here) + const batchTime = Date.now() - startBatch; + console.log(`Batch: ${batchTime}ms`); + + console.log(`\nSpeedup: ${(seqTime / batchTime).toFixed(2)}x faster`); + + } finally { + await llm.dispose(); + } + + console.log('\n✓ Exercise 2 complete!'); +} + +// Run the exercise +exercise2().catch(console.error); + +/** + * Expected Output: + * - Part 1: 5 math questions answered + * - Part 2: Same question from 3 different perspectives + * - Part 3: Batch should be significantly faster than sequential + * + * Learning Points: + * 1. batch() processes inputs in parallel + * 2. Great for testing multiple cases + * 3. Works with both strings and message arrays + * 4. Much faster than sequential processing + */ \ No newline at end of file diff --git a/tutorial/01-foundation/03-llm-wrapper/exercises/11-streaming.js b/tutorial/01-foundation/03-llm-wrapper/exercises/11-streaming.js new file mode 100644 index 0000000000000000000000000000000000000000..9d382211440e60b8109b64ec746a5f901c75601a --- /dev/null +++ b/tutorial/01-foundation/03-llm-wrapper/exercises/11-streaming.js @@ -0,0 +1,117 @@ +/** + * Exercise 11: Streaming Responses + * + * Goal: Learn to stream LLM responses in real-time + * + * In this exercise, you'll: + * 1. Stream a response and print it character by character + * 2. Build a progress indicator while streaming + * 3. Collect chunks into a full response + * 4. Compare streaming vs non-streaming + * + * This creates the "ChatGPT typing effect"! + */ + +import {HumanMessage, SystemMessage, LlamaCppLLM} from '../../../../src/index.js'; + +async function exercise3() { + console.log('=== Exercise 3: Streaming Responses ===\n'); + + const llm = new LlamaCppLLM({ + modelPath: './models/Meta-Llama-3.1-8B-Instruct-Q5_K_S.gguf', + temperature: 0.7, + maxTokens: 200 + }); + + try { + // Part 1: Basic streaming + console.log('Part 1: Basic streaming'); + console.log('Question: Tell me a short fun fact about space.\n'); + console.log('Response: '); + + // TODO: Use llm.stream() to stream the response + // Use a for await loop to iterate through chunks + // Print each chunk without a newline (use process.stdout.write) + // (code here) + + console.log('\n'); + + // Part 2: Streaming with progress indicator + console.log('Part 2: Streaming with progress indicator'); + console.log('Question: Explain what a black hole is in 2-3 sentences.\n'); + + let charCount = 0; + // TODO: Stream the response and count characters + // Every 10 characters, print a dot (.) as progress + // Print the actual response too + // Hint: Use charCount % 10 === 0 to check + console.log('Progress: '); + // (code here) + + console.log(`\n\nTotal characters streamed: ${charCount}`); + console.log(); + + // Part 3: Collecting streamed chunks + console.log('Part 3: Collecting full response from stream'); + + const messages = [ + new SystemMessage("You are a helpful assistant"), + new HumanMessage("What are the three primary colors? Answer briefly.") + ]; + + // TODO: Stream the response and collect all chunks + let fullResponse = ''; + // Use for await loop to collect chunks + // Build up fullResponse by concatenating chunk.content + // (code here) + + console.log('Full response:', fullResponse); + console.log(); + + // Part 4: Compare streaming vs regular invoke + console.log('Part 4: Streaming vs Regular invoke'); + const question = "What is JavaScript? Answer in one sentence."; + + // TODO: Time a streaming response + console.log('Streaming:'); + const streamStart = Date.now(); + let streamedText = ''; + // (code here - stream and collect) + const streamTime = Date.now() - streamStart; + console.log(`Time: ${streamTime}ms`); + console.log(`Response: ${streamedText}`); + console.log(); + + // TODO: Time a regular invoke + console.log('Regular invoke:'); + const invokeStart = Date.now(); + // (code here - use invoke) + const invokeTime = Date.now() - invokeStart; + console.log(`Time: ${invokeTime}ms`); + // console.log response + + console.log(`\nTime difference: ${Math.abs(streamTime - invokeTime)}ms`); + + } finally { + await llm.dispose(); + } + + console.log('\n✓ Exercise 3 complete!'); +} + +// Run the exercise +exercise3().catch(console.error); + +/** + * Expected Output: + * - Part 1: Text appearing character by character + * - Part 2: Progress dots while streaming + * - Part 3: Full collected response + * - Part 4: Similar times for both methods (streaming shows progress) + * + * Learning Points: + * 1. Streaming shows results as they generate (better UX) + * 2. for await...of loop handles async generators + * 3. Each chunk is an AIMessage with partial content + * 4. Total time similar, but perceived as faster + */ \ No newline at end of file diff --git a/tutorial/01-foundation/03-llm-wrapper/exercises/12-composition.js b/tutorial/01-foundation/03-llm-wrapper/exercises/12-composition.js new file mode 100644 index 0000000000000000000000000000000000000000..8133aaa037684436ae50772ce1c8f505a4fa22bd --- /dev/null +++ b/tutorial/01-foundation/03-llm-wrapper/exercises/12-composition.js @@ -0,0 +1,161 @@ +/** + * Exercise 12: Composition and Pipelines + * + * Goal: Learn to compose LLM with other Runnables + * + * In this exercise, you'll: + * 1. Create helper Runnables to work with LLM + * 2. Build a pipeline by chaining operations + * 3. Create a reusable agent pipeline + * 4. See the power of composition + * + * This is where the Runnable pattern really shines! + */ + +import {HumanMessage, SystemMessage, LlamaCppLLM, Runnable} from '../../../../src/index.js'; + +// TODO: Part 1 - Create a PromptFormatter Runnable +// This should: +// - Take a string input (user question) +// - Return an array of messages with a system prompt and the user question +// - System prompt: "You are a helpful assistant. Be concise." +class PromptFormatter extends Runnable { + async _call(input, config) { + // Your code here + return null; + } +} + +// TODO: Part 2 - Create a ResponseParser Runnable +// This should: +// - Take an AIMessage input +// - Extract and return just the content string +// - Trim whitespace +class ResponseParser extends Runnable { + async _call(input, config) { + // Your code here + return null; + } +} + +// TODO: Part 3 - Create an AnswerValidator Runnable +// This should: +// - Take a string input (the parsed response) +// - Check if it's longer than 10 characters +// - If too short, return "Error: Response too short" +// - Otherwise return the original response +class AnswerValidator extends Runnable { + async _call(input, config) { + // Your code here + return null; + } +} + +async function exercise4() { + console.log('=== Exercise 4: Composition and Pipelines ===\n'); + + const llm = new LlamaCppLLM({ + modelPath: './models/Meta-Llama-3.1-8B-Instruct-Q5_K_S.gguf', + temperature: 0.7, + maxTokens: 100 + }); + + try { + // Part 1: Test individual components + console.log('Part 1: Testing individual components'); + + const formatter = new PromptFormatter(); + const parser = new ResponseParser(); + const validator = new AnswerValidator(); + + // TODO: Test the formatter + console.log('Testing formatter:'); + const formatted = null; // await formatter.invoke("What is AI?") + console.log(formatted); + console.log(); + + // TODO: Test with LLM + parser + console.log('Testing LLM + parser:'); + const llmResponse = null; // await llm.invoke(formatted) + const parsed = null; // await parser.invoke(llmResponse) + console.log('Parsed:', parsed); + console.log(); + + // TODO: Test validator with short input + console.log('Testing validator with short input:'); + const shortResult = null; // await validator.invoke("Hi") + console.log(shortResult); + console.log(); + + // Part 2: Build a complete pipeline + console.log('Part 2: Complete pipeline'); + + // TODO: Chain all components together + // formatter -> llm -> parser -> validator + const pipeline = null; // Your code here + + // TODO: Test the pipeline + const result1 = null; // await pipeline.invoke("What is machine learning?") + console.log('Result:', result1); + console.log(); + + // Part 3: Reusable agent pipeline + console.log('Part 3: Reusable agent pipeline'); + + // TODO: Create different pipelines for different tasks + // Creative pipeline: high temperature, no validator + const creativePipeline = null; // Your code here + + // Factual pipeline: low temperature, with validator + const factualPipeline = null; // Your code here + + // TODO: Test both pipelines + console.log('Creative (temp=0.9):'); + const creative = null; // await creativePipeline.invoke("Describe a sunset") + console.log(creative); + console.log(); + + console.log('Factual (temp=0.1):'); + const factual = null; // await factualPipeline.invoke("What is the capital of France?") + console.log(factual); + console.log(); + + // Part 4: Batch processing with pipelines + console.log('Part 4: Batch processing with pipeline'); + + // TODO: Use the pipeline with batch() + const questions = [ + "What is Python?", + "What is JavaScript?", + "What is Rust?" + ]; + + const answers = null; // await pipeline.batch(questions) + + // TODO: Print results + // questions.forEach((q, i) => ...) + + } finally { + await llm.dispose(); + } + + console.log('\n✓ Exercise 4 complete!'); +} + +// Run the exercise +exercise4().catch(console.error); + +/** + * Expected Output: + * - Part 1: Each component works independently + * - Part 2: Full pipeline processes input -> output + * - Part 3: Different pipelines for different tasks + * - Part 4: Pipeline works with batch processing + * + * Learning Points: + * 1. Runnables are composable building blocks + * 2. .pipe() chains operations together + * 3. Pipelines are themselves Runnables + * 4. Easy to create specialized pipelines + * 5. Composition makes testing and reuse easy + */ \ No newline at end of file diff --git a/tutorial/01-foundation/03-llm-wrapper/lesson.md b/tutorial/01-foundation/03-llm-wrapper/lesson.md new file mode 100644 index 0000000000000000000000000000000000000000..788f4d6e7089e8d173a9b444db1c13797857f748 --- /dev/null +++ b/tutorial/01-foundation/03-llm-wrapper/lesson.md @@ -0,0 +1,1042 @@ +# The LLM Wrapper + +**Part 1: Foundation - Lesson 3** + +> Wrapping node-llama-cpp as a Runnable for seamless integration + +## Overview + +In Lesson 1, you learned about Runnables - the composable interface. In Lesson 2, you mastered Messages - the data structures. Now we'll connect these concepts by wrapping **node-llama-cpp** (our local LLM) as a Runnable that understands Messages. + +By the end of this lesson, you'll have a LLM wrapper that can generate text, handle conversations, stream responses, and integrate seamlessly with chains. + +## Why Does This Matter? + +### The Problem: LLMs Don't Compose + +node-llama-cpp is excellent at what it does - running local LLMs efficiently. But when you're building agents, you need more than just an LLM. You need components that work together seamlessly. + +**Without a composable framework:** +```javascript +import { getLlama } from 'node-llama-cpp'; + +// Each component is isolated - they don't know about each other +async function myAgent(userInput) { + // Step 1: Format the prompt + const prompt = myCustomFormatter(userInput); + + // Step 2: Call the LLM + const llama = await getLlama(); + const model = await llama.loadModel({ modelPath: './model.gguf' }); + const response = await model.createCompletion(prompt); + + // Step 3: Parse the response + const parsed = myCustomParser(response); + + // Step 4: Maybe call a tool? + if (parsed.needsTool) { + const toolResult = await myTool(parsed.args); + // Now what? Call the LLM again? How do we loop? + // How do we add logging? Memory? Retries? + } + + return parsed; +} + +// Problems: +// - Can't reuse components +// - Can't chain operations +// - Hard to add logging, metrics, or debugging +// - Complex control flow for agents +// - Every new feature requires changing everything +``` + +**With a composable framework:** +```javascript +// Components that work together +const llm = new LlamaCppLLM({ modelPath: './model.gguf' }); + +// Simple usage +const response = await llm.invoke([ + new SystemMessage("You are helpful"), + new HumanMessage("Hi") +]); +// Returns: AIMessage("Hello! How can I help you?") + +// But the real power is composition +const agent = promptTemplate + .pipe(llm) + .pipe(outputParser) + .pipe(toolExecutor); + +// Now you can: +// ✅ Reuse components in different chains +// ✅ Add logging with callbacks (no code changes) +// ✅ Build complex agents that use tools +// ✅ Test each component independently +// ✅ Swap LLMs without rewriting everything +``` + +### What the Wrapper Provides + +The LLM wrapper isn't about making node-llama-cpp easier - it's about making it **work with everything else**: + +1. **Common Interface**: Same `invoke()` / `stream()` / `batch()` as every other component +2. **Message Support**: Understands HumanMessage, AIMessage, SystemMessage +3. **Composability**: Works with `.pipe()` to chain operations +4. **Observability**: Callbacks work automatically for logging/metrics +5. **Configuration**: Runtime settings pass through cleanly +6. **History Isolation**: Proper batch processing without contamination + +Think of it as an adapter that lets node-llama-cpp play nicely with the rest of your agent system. + +## Learning Objectives + +By the end of this lesson, you will: + +- ✅ Understand how to wrap complex libraries as Runnables +- ✅ Convert Messages to LLM chat history +- ✅ Handle model loading and lifecycle +- ✅ Implement streaming for real-time output +- ✅ Add temperature and other generation parameters +- ✅ Manage context windows and chat history +- ✅ Handle batch processing with history isolation + +## Core Concepts + +### What is an LLM Wrapper? + +An LLM wrapper is an abstraction layer that: +1. **Hides complexity** - No need to manage contexts, sessions, or cleanup +2. **Provides a standard interface** - Same API regardless of underlying model +3. **Handles conversion** - Transforms Messages into model-specific chat history +4. **Manages resources** - Automatic initialization and cleanup +5. **Enables composition** - Works seamlessly in chains +6. **Isolates state** - Prevents history contamination in batch processing + +### The Wrapper's Responsibilities + +``` +Input (Messages) + ↓ +[1. Convert to Chat History] + ↓ +[2. Manage System Prompt] + ↓ +[3. Call LLM] + ↓ +[4. Parse Response] + ↓ +Output (AIMessage) +``` + +### Key Challenges + +1. **Model Loading**: Models are large and slow to load +2. **Chat History Format**: Convert Messages to node-llama-cpp format +3. **System Prompt Management**: Clear and set for each call +4. **Context Management**: Limited context windows +5. **Streaming**: Real-time output is complex +6. **Batch Isolation**: Prevent history contamination +7. **Error Handling**: Models can fail in various ways +8. **Chat Wrappers**: Different models need different formats + +## Implementation Deep Dive + +Let's build the LLM wrapper step by step. + +### Step 1: The Base Structure + +**Location:** `src/llm/llama-cpp-llm.js` +```javascript +import { Runnable } from './runnable.js'; +import { AIMessage } from './message.js'; +import { getLlama, LlamaChatSession } from 'node-llama-cpp'; + +export class LlamaCppLLM extends Runnable { + constructor(options = {}) { + super(); + + // Model configuration + this.modelPath = options.modelPath; + this.temperature = options.temperature ?? 0.7; + this.maxTokens = options.maxTokens ?? 2048; + this.contextSize = options.contextSize ?? 4096; + + // Chat wrapper configuration (auto-detects by default) + this.chatWrapper = options.chatWrapper ?? 'auto'; + + // Internal state + this._llama = null; + this._model = null; + this._context = null; + this._chatSession = null; + this._initialized = false; + } + + async _call(input, config) { + // Will implement next + } +} +``` + +**Key decisions**: +- Stores configuration (temperature, max tokens, etc.) +- Supports custom chat wrappers (e.g., QwenChatWrapper) +- Tracks internal state (model, context, session) +- Lazy initialization (load on first use) + +### Step 2: Model Initialization with Chat Wrapper Support + +```javascript +export class LlamaCppLLM extends Runnable { + // ... constructor ... + + /** + * Initialize the model (lazy loading) + */ + async _initialize() { + if (this._initialized) return; + + if (this.verbose) { + console.log(`Loading model: ${this.modelPath}`); + } + + try { + // Step 1: Get llama instance + this._llama = await getLlama(); + + // Step 2: Load the model + this._model = await this._llama.loadModel({ + modelPath: this.modelPath + }); + + // Step 3: Create context (working memory) + this._context = await this._model.createContext({ + contextSize: this.contextSize, + batchSize: this.batchSize + }); + + // Step 4: Create chat session with optional chat wrapper + const contextSequence = this._context.getSequence(); + const sessionConfig = { contextSequence }; + + // Add custom chat wrapper if specified + if (this.chatWrapper !== 'auto') { + sessionConfig.chatWrapper = this.chatWrapper; + } + + this._chatSession = new LlamaChatSession(sessionConfig); + + this._initialized = true; + + if (this.verbose) { + console.log('✓ Model loaded successfully'); + if (this.chatWrapper !== 'auto') { + console.log(`✓ Using custom chat wrapper: ${this.chatWrapper.constructor.name}`); + } + } + } catch (error) { + throw new Error( + `Failed to initialize model at ${this.modelPath}: ${error.message}` + ); + } + } + + /** + * Cleanup resources + */ + async dispose() { + if (this._context) { + await this._context.dispose(); + this._context = null; + } + if (this._model) { + await this._model.dispose(); + this._model = null; + } + this._chatSession = null; + this._initialized = false; + + if (this.verbose) { + console.log('✓ Model resources disposed'); + } + } +} +``` + +**Why lazy loading?** +- Models take 5-30 seconds to load +- Don't load until actually needed +- Share one loaded model across multiple calls + +**Chat Wrapper Support**: +- Defaults to 'auto' (library auto-detects) +- Supports custom wrappers like QwenChatWrapper for specific models +- Useful for controlling model behavior (e.g., discouraging thoughts) + +### Step 3: Converting Messages to Chat History + +```javascript +export class LlamaCppLLM extends Runnable { + // ... previous code ... + + /** + * Convert our Message objects to node-llama-cpp chat history format + */ + _messagesToChatHistory(messages) { + return messages.map(msg => { + // System messages: instructions for the AI + if (msg._type === 'system') { + return { type: 'system', text: msg.content }; + } + // Human messages: user input + else if (msg._type === 'human') { + return { type: 'user', text: msg.content }; + } + // AI messages: previous AI responses + else if (msg._type === 'ai') { + return { type: 'model', response: msg.content }; + } + // Tool messages: results from tool execution + else if (msg._type === 'tool') { + return { type: 'system', text: `Tool Result: ${msg.content}` }; + } + + // Fallback: treat unknown types as user messages + return { type: 'user', text: msg.content }; + }); + } +} +``` + +**Key insight**: This bridges between your standardized Message types and what node-llama-cpp expects. Different models may need different chat formats, which is why chat wrappers exist. + +### Step 4: The Main Generation Method + +```javascript +export class LlamaCppLLM extends Runnable { + // ... previous code ... + + async _call(input, config = {}) { + // Initialize if needed + await this._initialize(); + + // Clear history if requested (important for batch processing) + if (config.clearHistory) { + this._chatSession.setChatHistory([]); + } + + // Handle different input types + let messages; + if (typeof input === 'string') { + messages = [new HumanMessage(input)]; + } else if (Array.isArray(input)) { + messages = input; + } else { + throw new Error('Input must be string or array of messages'); + } + + // Extract system message if present + const systemMessages = messages.filter(msg => msg._type === 'system'); + const systemPrompt = systemMessages.length > 0 + ? systemMessages[0].content + : ''; + + // Convert our Message objects to llama.cpp format + const chatHistory = this._messagesToChatHistory(messages); + this._chatSession.setChatHistory(chatHistory); + + // ALWAYS set system prompt (either new value or empty string to clear) + this._chatSession.systemPrompt = systemPrompt; + + try { + // Build prompt options + const promptOptions = { + temperature: config.temperature ?? this.temperature, + topP: config.topP ?? this.topP, + topK: config.topK ?? this.topK, + maxTokens: config.maxTokens ?? this.maxTokens, + repeatPenalty: config.repeatPenalty ?? this.repeatPenalty, + customStopTriggers: config.stopStrings ?? this.stopStrings + }; + + // Add random seed if temperature > 0 and no seed specified + // This ensures randomness works properly + if (promptOptions.temperature > 0 && config.seed === undefined) { + promptOptions.seed = Math.floor(Math.random() * 1000000); + } else if (config.seed !== undefined) { + promptOptions.seed = config.seed; + } + + // Generate response using prompt + const response = await this._chatSession.prompt('', promptOptions); + + // Return as AIMessage for consistency + return new AIMessage(response); + } catch (error) { + throw new Error(`Generation failed: ${error.message}`); + } + } +} +``` + +**Critical details**: +- Always clears and sets system prompt (prevents contamination) +- Adds random seed for proper temperature behavior +- Uses `customStopTriggers` (correct parameter name) +- Supports `clearHistory` for batch processing + +### Step 5: Batch Processing with History Isolation + +```javascript +export class LlamaCppLLM extends Runnable { + // ... previous code ... + + /** + * Batch processing with history isolation + * + * Processes multiple inputs sequentially, ensuring each gets + * a clean chat history to prevent contamination. + */ + async batch(inputs, config = {}) { + const results = []; + for (const input of inputs) { + // Clear history before each batch item + const result = await this._call(input, { + ...config, + clearHistory: true + }); + results.push(result); + } + return results; + } +} +``` + +**Why sequential processing?** +- Local models can't run truly in parallel +- Sequential ensures proper history isolation +- Each item gets a clean slate + +### Step 6: Streaming Support + +For real-time output (like ChatGPT's typing effect): + +```javascript +export class LlamaCppLLM extends Runnable { + // ... previous code ... + + async *_stream(input, config = {}) { + await this._initialize(); + + // Clear history if requested + if (config.clearHistory) { + this._chatSession.setChatHistory([]); + } + + // Handle input types (same as _call) + let messages; + if (typeof input === 'string') { + messages = [new HumanMessage(input)]; + } else if (Array.isArray(input)) { + messages = input; + } else { + throw new Error('Input must be string or array of messages'); + } + + // Extract system message + const systemMessages = messages.filter(msg => msg._type === 'system'); + const systemPrompt = systemMessages.length > 0 + ? systemMessages[0].content + : ''; + + // Set up chat history + const chatHistory = this._messagesToChatHistory(messages); + this._chatSession.setChatHistory(chatHistory); + + // ALWAYS set system prompt + this._chatSession.systemPrompt = systemPrompt; + + try { + // Build prompt options + const promptOptions = { + temperature: config.temperature ?? this.temperature, + topP: config.topP ?? this.topP, + topK: config.topK ?? this.topK, + maxTokens: config.maxTokens ?? this.maxTokens, + repeatPenalty: config.repeatPenalty ?? this.repeatPenalty, + customStopTriggers: config.stopStrings ?? this.stopStrings + }; + + // Add random seed + if (promptOptions.temperature > 0 && config.seed === undefined) { + promptOptions.seed = Math.floor(Math.random() * 1000000); + } else if (config.seed !== undefined) { + promptOptions.seed = config.seed; + } + + // Use onTextChunk callback to collect chunks + const self = this; + promptOptions.onTextChunk = (chunk) => { + self._currentStreamChunks = self._currentStreamChunks || []; + self._currentStreamChunks.push(chunk); + }; + + // Initialize chunk collection + this._currentStreamChunks = []; + + // Start generation + const responsePromise = this._chatSession.prompt('', promptOptions); + + // Yield chunks as they become available + let lastYieldedIndex = 0; + + // Poll for new chunks + while (true) { + // Yield any new chunks + while (lastYieldedIndex < this._currentStreamChunks.length) { + yield new AIMessage(this._currentStreamChunks[lastYieldedIndex], { + additionalKwargs: { chunk: true } + }); + lastYieldedIndex++; + } + + // Check if generation is complete + const isDone = await Promise.race([ + responsePromise.then(() => true), + new Promise(resolve => setTimeout(() => resolve(false), 10)) + ]); + + if (isDone) { + // Yield any remaining chunks + while (lastYieldedIndex < this._currentStreamChunks.length) { + yield new AIMessage(this._currentStreamChunks[lastYieldedIndex], { + additionalKwargs: { chunk: true } + }); + lastYieldedIndex++; + } + break; + } + } + + // Wait for completion + await responsePromise; + + // Clean up + delete this._currentStreamChunks; + + } catch (error) { + throw new Error(`Streaming failed: ${error.message}`); + } + } +} +``` + +**Streaming challenges**: +- `onTextChunk` is a synchronous callback +- Can't yield directly from callback +- Use polling mechanism to yield as chunks arrive +- 10ms polling interval balances responsiveness vs CPU usage + +## Real-World Examples + +### Example 1: Simple Text Generation + +```javascript +const llm = new LlamaCppLLM({ + modelPath: './models/Meta-Llama-3.1-8B-Instruct-Q5_K_S.gguf', + temperature: 0.7, + maxTokens: 100 +}); + +// Simple string input +const response = await llm.invoke("What is 2+2?"); +console.log(response.content); // "2+2 equals 4." +``` + +### Example 2: Conversation with System Prompt + +```javascript +const messages = [ + new SystemMessage("You are a helpful math tutor."), + new HumanMessage("What is 5*5?") +]; + +const response = await llm.invoke(messages); +console.log(response.content); +// "5 times 5 is 25. Here's a simple explanation..." +``` + +### Example 3: Using Qwen Chat Wrapper + +```javascript +import { QwenChatWrapper } from 'node-llama-cpp'; + +const llm = new LlamaCppLLM({ + modelPath: './models/Qwen3-1.7B-Q6_K.gguf', + temperature: 0.7, + chatWrapper: new QwenChatWrapper({ + thoughts: 'discourage' // Prevents thinking tokens + }) +}); + +const response = await llm.invoke("What is AI?"); +// Response won't include tokens +``` + +### Example 4: Temperature Comparison + +```javascript +const question = "Give me one adjective to describe winter:"; + +// Low temperature - consistent answers +llm._chatSession.setChatHistory([]); +const lowTemp = await llm.invoke(question, { temperature: 0.1 }); +// Likely: "cold" + +// High temperature - varied answers +llm._chatSession.setChatHistory([]); +const highTemp = await llm.invoke(question, { temperature: 0.9 }); +// Could be: "frosty", "snowy", "icy", "chilly" +``` + +### Example 5: Streaming Output + +```javascript +console.log('Response: '); +for await (const chunk of llm.stream("Tell me a fun fact about space")) { + process.stdout.write(chunk.content); // No newline +} +console.log('\n'); + +// Output streams in real-time as it's generated +``` + +### Example 6: Batch Processing + +```javascript +const questions = [ + "What is Python?", + "What is JavaScript?", + "What is Rust?" +]; + +const answers = await llm.batch(questions); + +questions.forEach((q, i) => { + console.log(`Q: ${q}`); + console.log(`A: ${answers[i].content}`); + console.log(); +}); + +// Each answer is independent - no history contamination! +``` + +### Example 7: Using in a Pipeline + +```javascript +import { PromptTemplate } from '../prompts/prompt-template.js'; + +const prompt = PromptTemplate.fromTemplate( + "Translate the following to {language}: {text}" +); + +const chain = prompt.pipe(llm); + +const result = await chain.invoke({ + language: "Spanish", + text: "Hello, how are you?" +}); + +console.log(result.content); // "Hola, ¿cómo estás?" +``` + +## Advanced Patterns + +### Pattern 1: Model Pool (Reusing Loaded Models) + +```javascript +class LLMPool { + constructor() { + this.models = new Map(); + } + + async get(modelPath, options = {}) { + if (!this.models.has(modelPath)) { + const llm = new LlamaCppLLM({ modelPath, ...options }); + await llm._initialize(); // Pre-load + this.models.set(modelPath, llm); + } + return this.models.get(modelPath); + } + + async disposeAll() { + for (const llm of this.models.values()) { + await llm.dispose(); + } + this.models.clear(); + } +} + +// Usage +const pool = new LLMPool(); +const llm = await pool.get('./models/llama-3.1-8b.gguf'); +``` + +### Pattern 2: Retry on Failure + +```javascript +class ReliableLLM extends LlamaCppLLM { + async _call(input, config = {}) { + const maxRetries = config.maxRetries || 3; + let lastError; + + for (let i = 0; i < maxRetries; i++) { + try { + return await super._call(input, config); + } catch (error) { + lastError = error; + console.warn(`Attempt ${i + 1} failed, retrying...`); + await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1))); + } + } + + throw new Error(`All ${maxRetries} attempts failed: ${lastError.message}`); + } +} +``` + +### Pattern 3: Token Counting + +```javascript +class LlamaCppLLMWithCounting extends LlamaCppLLM { + constructor(options) { + super(options); + this.totalTokens = 0; + } + + async _call(input, config = {}) { + const result = await super._call(input, config); + + // Rough token estimation (4 chars ≈ 1 token) + const promptTokens = Math.ceil(JSON.stringify(input).length / 4); + const completionTokens = Math.ceil(result.content.length / 4); + + this.totalTokens += promptTokens + completionTokens; + + result.additionalKwargs.usage = { + promptTokens, + completionTokens, + totalTokens: promptTokens + completionTokens + }; + + return result; + } + + getUsage() { + return { totalTokens: this.totalTokens }; + } +} +``` + +## Common Patterns and Best Practices + +### ✅ DO: + +```javascript +// Initialize once, use many times +const llm = new LlamaCppLLM({ modelPath: './model.gguf' }); +await llm.invoke("Question 1"); +await llm.invoke("Question 2"); +await llm.dispose(); // Cleanup when done + +// Use Messages for structure +const messages = [ + new SystemMessage("You are helpful"), + new HumanMessage("Hi") +]; + +// Clear history for independent calls +const response = await llm.invoke(messages, { clearHistory: true }); + +// Handle errors gracefully +try { + const result = await llm.invoke(messages); +} catch (error) { + console.error('Generation failed:', error); +} +``` + +### ❌ DON'T: + +```javascript +// Don't create new LLM for each request (slow!) +for (const question of questions) { + const llm = new LlamaCppLLM({ modelPath: './model.gguf' }); + await llm.invoke(question); // Loads model every time! +} + +// Don't forget to clear history in batch processing +// This will cause history contamination! +for (const q of questions) { + await llm.invoke(q); // Sees all previous questions! +} + +// Don't forget cleanup +// Missing: await llm.dispose() +``` + +## Performance Tips + +### Tip 1: Preload Models + +```javascript +// Load during app startup +const llm = new LlamaCppLLM({ modelPath: './model.gguf' }); +await llm._initialize(); // Force load now + +// Later requests are instant +await llm.invoke("Fast response!"); +``` + +### Tip 2: Use Batch Properly + +```javascript +// This correctly isolates each question +const answers = await llm.batch(questions); + +// Not: Sequential with contamination +for (const q of questions) { + await llm.invoke(q); // History builds up! +} +``` + +### Tip 3: Adjust Context Size + +```javascript +// Smaller context = faster, less memory +const fastLLM = new LlamaCppLLM({ + modelPath: './model.gguf', + contextSize: 2048 // vs default 4096 +}); +``` + +### Tip 4: Use Appropriate Temperature + +```javascript +// Factual answers: low temperature +const fact = await llm.invoke(query, { temperature: 0.1 }); + +// Creative writing: high temperature +const story = await llm.invoke(query, { temperature: 0.9 }); +``` + +## Debugging Tips + +### Tip 1: Enable Verbose Mode + +```javascript +const llm = new LlamaCppLLM({ + modelPath: './model.gguf', + verbose: true // Shows loading and generation details +}); +``` + +### Tip 2: Test History Isolation + +```javascript +// Test batch processing +const questions = ["Q1", "Q2", "Q3"]; +const answers = await llm.batch(questions); + +// Each answer should be independent +// If Q2 mentions Q1, history contamination occurred! +``` + +### Tip 3: Verify Streaming + +```javascript +// Verify streaming works +console.log('Testing stream:'); +for await (const chunk of llm.stream("Count to 5")) { + console.log('Chunk:', chunk.content); +} +``` + +## Common Mistakes + +### ❌ Mistake 1: Not Clearing History in Batches + +```javascript +// Bad: History contamination +const pipeline = formatter.pipe(llm).pipe(parser); +const results = await pipeline.batch(inputs); // Q2 sees Q1! +``` + +**Fix**: The LlamaCppLLM.batch() method automatically clears history: +```javascript +// Good: Each input is isolated +const results = await llm.batch(inputs); +``` + +### ❌ Mistake 2: Forgetting Random Seed + +```javascript +// Bad: Temperature doesn't work +const response = await llm.invoke(prompt, { temperature: 0.9 }); +// Without random seed, might get same answer +``` + +**Fix**: Our implementation automatically adds random seed: +```javascript +// Good: Randomness works properly +if (promptOptions.temperature > 0 && config.seed === undefined) { + promptOptions.seed = Math.floor(Math.random() * 1000000); +} +``` + +### ❌ Mistake 3: Not Setting System Prompt Properly + +```javascript +// Bad: System prompt persists between calls +await llm.invoke([new SystemMessage("Be creative"), ...]); +await llm.invoke([new HumanMessage("Hi")]); // Still "creative"! +``` + +**Fix**: Always set system prompt (empty string to clear): +```javascript +// Good: Always explicitly set or clear +this._chatSession.systemPrompt = systemPrompt || ''; +``` + +## Mental Model + +Think of the LLM wrapper as managing a conversation session: + +``` +Call 1: [System: "Be helpful", User: "Hi"] + ↓ + Model generates response + ↓ + Returns: AIMessage("Hello!") + +Call 2: [User: "How are you?"] + ↓ + PROBLEM: Still has "Be helpful" system prompt! + PROBLEM: Might remember "Hi" conversation! + +SOLUTION: Clear history + reset system prompt between calls +``` + +The wrapper handles: +- Loading models once +- Converting Messages to chat history +- Managing system prompts +- Clearing history when needed +- Streaming chunks +- Random seeds for temperature +- Error handling + +## Summary + +Congratulations! You now understand how to wrap a complex LLM library as a clean, composable Runnable with proper state management. + +### Key Takeaways + +1. **Lazy loading saves time**: Load models only when needed +2. **Messages enable structure**: Proper conversation formatting +3. **History isolation prevents bugs**: Critical for batch processing +4. **System prompts must be managed**: Always set or clear explicitly +5. **Streaming improves UX**: Real-time output feels responsive +6. **Random seeds enable temperature**: Required for randomness +7. **Chat wrappers add flexibility**: Support different models +8. **Sequential batch processing**: Local models can't truly parallelize + +### What You Built + +A LLM wrapper that: +- ✅ Loads models lazily +- ✅ Handles Messages properly +- ✅ Manages chat history correctly +- ✅ Isolates batches +- ✅ Supports streaming +- ✅ Handles system prompts +- ✅ Supports chat wrappers +- ✅ Adds random seeds for temperature +- ✅ Provides good error messages + +### Critical Implementation Details + +```javascript +// 1. Always clear and set system prompt +this._chatSession.systemPrompt = systemPrompt || ''; + +// 2. Use clearHistory for batch isolation +async batch(inputs, config = {}) { + const results = []; + for (const input of inputs) { + const result = await this._call(input, { + ...config, + clearHistory: true + }); + results.push(result); + } + return results; +} + +// 3. Add random seed for temperature +if (promptOptions.temperature > 0 && config.seed === undefined) { + promptOptions.seed = Math.floor(Math.random() * 1000000); +} + +// 4. Use correct parameter names +customStopTriggers: config.stopStrings ?? this.stopStrings +``` + +### What's Next + +In the next lesson, we'll explore **Context & Configuration** - how to pass state and settings through chains. + +**Preview**: You'll learn: +- RunnableConfig object +- Callback systems +- Metadata tracking +- Debug modes + +➡️ [Continue to Lesson 4: Context & Configuration](04-context.md) + +## Additional Resources + +- [node-llama-cpp Documentation](https://node-llama-cpp.withcat.ai) +- [Chat Wrappers Guide](https://node-llama-cpp.withcat.ai/guide/chat-wrapper) +- [Temperature Guide](https://node-llama-cpp.withcat.ai/guide/chat-session#temperature) +- [GGUF Model Format](https://huggingface.co/docs/hub/gguf) + +## Questions & Discussion + +**Q: Why do we always set system prompt instead of only when present?** + +A: To prevent contamination. If call 1 sets a system prompt but call 2 doesn't, call 2 would still use call 1's system prompt. Always setting (even to empty string) ensures clean state. + +**Q: Why sequential batch processing instead of parallel?** + +A: Local models (node-llama-cpp) can't run true parallel inference on a single model instance. The library serializes requests internally, so parallel Promise.all() provides no benefit and can cause race conditions on the shared chat session. + +**Q: Why do we need random seeds for temperature?** + +A: The node-llama-cpp library states: "The randomness of the temperature can be controlled by the seed parameter. Setting a specific seed and a specific temperature will yield the same response every time for the same input." Without a random seed, high temperature might still give deterministic results. + +**Q: Can I use multiple models simultaneously?** + +A: Yes! Each LlamaCppLLM instance can have a different model. Just be aware of memory constraints - each model takes several GB of RAM. + +**Q: What's the difference between customStopTriggers and stopStrings?** + +A: `customStopTriggers` is the correct parameter name in node-llama-cpp. We accept `stopStrings` in our config for a more intuitive API, then map it to `customStopTriggers` internally. + +--- + +**Built with ❤️ for learners who want to understand AI agents deeply** + +[← Previous: Messages](02-messages.md) | [Tutorial Index](../README.md) | [Next: Context →](04-context.md) \ No newline at end of file diff --git a/tutorial/01-foundation/03-llm-wrapper/solutions/09-basic-llm-wrapper-solution.js b/tutorial/01-foundation/03-llm-wrapper/solutions/09-basic-llm-wrapper-solution.js new file mode 100644 index 0000000000000000000000000000000000000000..453435a8540256930d46b6a7513c94cce1748be1 --- /dev/null +++ b/tutorial/01-foundation/03-llm-wrapper/solutions/09-basic-llm-wrapper-solution.js @@ -0,0 +1,92 @@ +/** + * Exercise 9 Solution: Basic LLM Setup + */ + +import {HumanMessage, SystemMessage, LlamaCppLLM} from '../../../../src/index.js'; +import {QwenChatWrapper} from "node-llama-cpp"; + +async function exercise1() { + console.log('=== Exercise 1: Basic LLM Setup ===\n'); + + // Solution: Create a LlamaCppLLM instance + const llm = new LlamaCppLLM({ + modelPath: './models/Qwen3-1.7B-Q6_K.gguf', // Adjust to your model path + temperature: 0.7, + maxTokens: 100, + chatWrapper: new QwenChatWrapper({ + thoughts: 'discourage' // Prevents the model from outputting thinking tokens + }), + verbose: true // Enable to see model loading + }); + + try { + // Part 1: Simple string invocation + console.log('Part 1: Simple string input'); + const response1 = await llm.invoke("What is 2+2? Answer in one sentence"); + console.log('Response:', response1.content); + console.log(); + + // Part 2: Using message objects + console.log('Part 2: Using message objects'); + const messages = [ + new SystemMessage("You are a patient math tutor teaching a 10-year-old. Always explain the reasoning step-by-step in simple terms."), + new HumanMessage("What is 5*5? Answer in one sentence.") + ]; + + const response2 = await llm.invoke(messages); + console.log('Response:', response2.content); + console.log(); + + // Part 3: Temperature experimentation + console.log('Part 3: Temperature differences'); + console.log('Temperature controls randomness: 0.0 = deterministic, 1.0 = creative\n'); + const question = "Give me one adjective to describe winter:"; + + console.log('Low temperature (0.1):'); + llm._chatSession.setChatHistory([]); + const lowTemp = await llm.invoke(question, { temperature: 0.1 }); + // Should always return the same word: "cold" + console.log(lowTemp.content); + + console.log('\nHigh temperature (0.9):'); + llm._chatSession.setChatHistory([]); + const highTemp = await llm.invoke(question, { temperature: 0.9 }); + // Different each time: "frosty", "snowy", "chilly", "icy", "freezing" + console.log(highTemp.content); + + } finally { + // Cleanup: Always dispose when done + await llm.dispose(); + console.log('\n✓ Resources cleaned up'); + } + + console.log('\n✓ Exercise 1 complete!'); +} + +// Run the solution +exercise1().catch(console.error); + +/** + * Key Takeaways: + * + * 1. Construction: + * - modelPath is required + * - Other options have sensible defaults + * - Set verbose: true to see what's happening + * + * 2. Input flexibility: + * - Strings work great for simple queries + * - Message arrays give you full control + * - Both approaches return AIMessage objects + * + * 3. Runtime configuration: + * - Pass config as second parameter to invoke() + * - Overrides instance defaults for that call only + * - Useful for different behaviors per query + * + * 4. Resource management: + * - Always call dispose() when done + * - Use try/finally to ensure cleanup + * - Models hold significant memory + */ + diff --git a/tutorial/01-foundation/03-llm-wrapper/solutions/10-batch-processing-solution.js b/tutorial/01-foundation/03-llm-wrapper/solutions/10-batch-processing-solution.js new file mode 100644 index 0000000000000000000000000000000000000000..362636fbdd0c175cab48b844dd7a3da82bed3282 --- /dev/null +++ b/tutorial/01-foundation/03-llm-wrapper/solutions/10-batch-processing-solution.js @@ -0,0 +1,128 @@ +/** + * Exercise 10 Solution: Batch Processing + */ + +import {HumanMessage, SystemMessage, LlamaCppLLM} from '../../../../src/index.js'; + +async function exercise2() { + console.log('=== Exercise 2: Batch Processing ===\n'); + + const llm = new LlamaCppLLM({ + modelPath: './models/Meta-Llama-3.1-8B-Instruct-Q5_K_S.gguf', + temperature: 0.3, + maxTokens: 50 + }); + + try { + // Part 1: Simple batch processing + console.log('Part 1: Batch processing simple questions'); + + const mathQuestions = [ + "What is 10 + 15?", + "What is 20 * 3?", + "What is 100 / 4?", + "What is 50 - 18?", + "What is 7 squared?" + ]; + + const mathAnswers = await llm.batch(mathQuestions); + + mathQuestions.forEach((question, i) => { + console.log(`Q${i + 1}: ${question}`); + console.log(`A${i + 1}: ${mathAnswers[i].content}`); + console.log(); + }); + + // Part 2: Batch with message arrays + console.log('Part 2: Batch processing with message arrays'); + + const conversationBatch = [ + [ + new SystemMessage("You are a professional chef"), + new HumanMessage("Describe an apple in one sentence") + ], + [ + new SystemMessage("You are a research scientist"), + new HumanMessage("Describe an apple in one sentence") + ], + [ + new SystemMessage("You are a romantic poet"), + new HumanMessage("Describe an apple in one sentence") + ] + ]; + + const perspectives = await llm.batch(conversationBatch); + + const roles = ["Chef", "Scientist", "Poet"]; + perspectives.forEach((response, i) => { + console.log(`${roles[i]}: ${response.content}`); + }); + + console.log(); + + // Part 3: Performance comparison + console.log('Part 3: Sequential vs Batch performance'); + + const testQuestions = [ + "What is AI?", + "What is ML?", + "What is DL?", + "What is NLP?", + "What is CV?" + ]; + + // Sequential processing + console.log('Sequential processing...'); + const startSeq = Date.now(); + const seqResults = []; + for (const question of testQuestions) { + const response = await llm.invoke(question); + seqResults.push(response); + } + const seqTime = Date.now() - startSeq; + console.log(`Sequential: ${seqTime}ms for ${testQuestions.length} questions`); + + // Batch processing + console.log('\nBatch processing...'); + const startBatch = Date.now(); + const batchResults = await llm.batch(testQuestions); + const batchTime = Date.now() - startBatch; + console.log(`Batch: ${batchTime}ms for ${testQuestions.length} questions`); + + console.log(`\nSpeedup: ${(seqTime / batchTime).toFixed(2)}x faster`); + console.log(`Time saved: ${seqTime - batchTime}ms`); + + } finally { + await llm.dispose(); + } + + console.log('\n✓ Exercise 2 complete!'); +} + +// Run the solution +exercise2().catch(console.error); + +/** + * Key Takeaways: + * + * 1. Batch API: + * - await llm.batch([input1, input2, ...]) + * - Returns array of AIMessage objects + * - Maintains order (output[i] corresponds to input[i]) + * + * 2. Input flexibility: + * - Can batch strings: ["q1", "q2", "q3"] + * - Can batch message arrays: [[msg1, msg2], [msg3, msg4]] + * - Mix won't work - keep types consistent + * + * 3. Performance: + * - Batch uses Promise.all() for parallel execution + * - Significantly faster than sequential for multiple inputs + * - Great for testing/evaluation scenarios + * + * 4. Use cases: + * - Testing agent on multiple examples + * - Comparing different prompts + * - Evaluating model consistency + * - Processing queued user requests + */ \ No newline at end of file diff --git a/tutorial/01-foundation/03-llm-wrapper/solutions/11-streaming-solution.js b/tutorial/01-foundation/03-llm-wrapper/solutions/11-streaming-solution.js new file mode 100644 index 0000000000000000000000000000000000000000..5bbf89fdda2a018fa6fe3e0934a57275b6dca485 --- /dev/null +++ b/tutorial/01-foundation/03-llm-wrapper/solutions/11-streaming-solution.js @@ -0,0 +1,127 @@ +/** + * Exercise 11 Solution: Streaming Responses + */ + +import {HumanMessage, SystemMessage, LlamaCppLLM} from '../../../../src/index.js'; + +async function exercise3() { + console.log('=== Exercise 3: Streaming Responses ===\n'); + + const llm = new LlamaCppLLM({ + modelPath: './models/Meta-Llama-3.1-8B-Instruct-Q5_K_S.gguf', + temperature: 0.7, + maxTokens: 200 + }); + + try { + // Part 1: Basic streaming + console.log('Part 1: Basic streaming'); + console.log('Question: Tell me a long fun fact about space.\n'); + console.log('Response: '); + + for await (const chunk of llm.stream("Tell me a long fun fact about space.")) { + process.stdout.write(chunk.content); // No newline + } + + console.log('\n'); + + // Part 2: Streaming with progress indicator + console.log('Part 2: Streaming with progress indicator'); + console.log('Question: Explain what a black hole is in 2-3 sentences.\n'); + + let charCount = 0; + console.log('Progress: '); + console.log('Response: '); + + for await (const chunk of llm.stream("Explain what a black hole is in 2-3 sentences.")) { + process.stdout.write(chunk.content); + charCount += chunk.content.length; + } + + console.log(`\n\nTotal characters streamed: ${charCount}`); + console.log(); + + // Part 3: Collecting streamed chunks + console.log('Part 3: Collecting full response from stream'); + + const messages = [ + new SystemMessage("You are a helpful assistant"), + new HumanMessage("What are the three primary colors? Answer briefly.") + ]; + + let fullResponse = ''; + for await (const chunk of llm.stream(messages)) { + fullResponse += chunk.content; + } + + console.log('Full response:', fullResponse); + console.log(); + + // Part 4: Compare streaming vs regular invoke + console.log('Part 4: Streaming vs Regular invoke'); + const question = "What is JavaScript? Answer in one sentence."; + + // Streaming + console.log('Streaming:'); + const streamStart = Date.now(); + let streamedText = ''; + for await (const chunk of llm.stream(question)) { + streamedText += chunk.content; + } + const streamTime = Date.now() - streamStart; + console.log(`Time: ${streamTime}ms`); + console.log(`Response: ${streamedText}`); + console.log(); + + // Regular invoke + console.log('Regular invoke:'); + const invokeStart = Date.now(); + const invokeResponse = await llm.invoke(question); + const invokeTime = Date.now() - invokeStart; + console.log(`Time: ${invokeTime}ms`); + console.log(`Response: ${invokeResponse.content}`); + + console.log(`\nTime difference: ${Math.abs(streamTime - invokeTime)}ms`); + console.log('Note: Streaming feels faster because you see results immediately!'); + + } finally { + await llm.dispose(); + } + + console.log('\n✓ Exercise 3 complete!'); +} + +// Run the solution +exercise3().catch(console.error); + +/** + * Key Takeaways: + * + * 1. Streaming API: + * - for await (const chunk of llm.stream(input)) { } + * - Each chunk is an AIMessage with partial content + * - Use process.stdout.write() to print without newlines + * + * 2. User Experience: + * - Streaming shows immediate feedback + * - Users see progress as it happens + * - Feels faster even if total time is similar + * - Essential for long responses + * + * 3. Collection pattern: + * - Initialize empty string: let full = '' + * - Accumulate: full += chunk.content + * - Use when you need the complete response + * + * 4. When to stream: + * - Long-form content generation + * - Interactive chat interfaces + * - When user experience matters + * - When you want to show progress + * + * 5. When NOT to stream: + * - Need to parse complete response + * - Batch processing + * - Automated testing + * - Response needs to be processed as a whole + */ \ No newline at end of file diff --git a/tutorial/01-foundation/03-llm-wrapper/solutions/12-composition-solution.js b/tutorial/01-foundation/03-llm-wrapper/solutions/12-composition-solution.js new file mode 100644 index 0000000000000000000000000000000000000000..b7ae4b1d3d370328a6df5e76966d952788ac759c --- /dev/null +++ b/tutorial/01-foundation/03-llm-wrapper/solutions/12-composition-solution.js @@ -0,0 +1,236 @@ +/** + * Exercise 12 Solution: Composition and Pipelines + */ + +import {HumanMessage, SystemMessage, LlamaCppLLM, Runnable} from '../../../../src/index.js'; + +// Part 1: PromptFormatter Runnable +class PromptFormatter extends Runnable { + constructor(systemPrompt = "You are a helpful assistant. Be concise.") { + super(); + this.systemPrompt = systemPrompt; + } + + async _call(input, config) { + return [ + new SystemMessage(this.systemPrompt), + new HumanMessage(input) + ]; + } +} + +// Part 2: ResponseParser Runnable +class ResponseParser extends Runnable { + async _call(input, config) { + // Extract content from AIMessage + if (input.content) { + return input.content.trim(); + } + return String(input).trim(); + } +} + +// Part 3: AnswerValidator Runnable +class AnswerValidator extends Runnable { + constructor(minLength = 10) { + super(); + this.minLength = minLength; + } + + async _call(input, config) { + if (input.length < this.minLength) { + return `Error: Response too short (${input.length} chars, min ${this.minLength})`; + } + return input; + } +} + +async function exercise4() { + console.log('=== Exercise 4: Composition and Pipelines ===\n'); + + const llm = new LlamaCppLLM({ + modelPath: './models/Meta-Llama-3.1-8B-Instruct-Q5_K_S.gguf', + temperature: 0.7, + maxTokens: 100 + }); + + try { + // Part 1: Test individual components + console.log('Part 1: Testing individual components'); + + const formatter = new PromptFormatter(); + const parser = new ResponseParser(); + const validator = new AnswerValidator(); + + console.log('Testing formatter:'); + const formatted = await formatter.invoke("What is AI?"); + console.log(formatted); + console.log(); + + console.log('Testing LLM + parser:'); + const llmResponse = await llm.invoke(formatted); + const parsed = await parser.invoke(llmResponse); + console.log('Parsed:', parsed); + console.log(); + + console.log('Testing validator with short input:'); + const shortResult = await validator.invoke("Hi"); + console.log(shortResult); + console.log(); + + // Part 2: Build a complete pipeline + console.log('Part 2: Complete pipeline'); + + // Chain all components together + const pipeline = formatter + .pipe(llm) + .pipe(parser) + .pipe(validator); + + console.log('Pipeline structure:', pipeline.toString()); + + const result1 = await pipeline.invoke("What is machine learning?"); + console.log('Result:', result1); + console.log(); + + // Part 3: Reusable agent pipelines + console.log('Part 3: Reusable agent pipeline'); + + // Creative pipeline: high temperature, no validator + const creativeLLM = new LlamaCppLLM({ + modelPath: './models/Meta-Llama-3.1-8B-Instruct-Q5_K_S.gguf', + temperature: 0.9, + maxTokens: 100 + }); + + const creativeFormatter = new PromptFormatter( + "You are a creative writer. Use vivid imagery." + ); + + const creativePipeline = creativeFormatter + .pipe(creativeLLM) + .pipe(parser); + + // Factual pipeline: low temperature, with validator + const factualLLM = new LlamaCppLLM({ + modelPath: './models/Meta-Llama-3.1-8B-Instruct-Q5_K_S.gguf', + temperature: 0.1, + maxTokens: 100 + }); + + const factualFormatter = new PromptFormatter( + "You are a factual encyclopedia. Be precise and accurate." + ); + + const factualPipeline = factualFormatter + .pipe(factualLLM) + .pipe(parser) + .pipe(validator); + + console.log('Creative (temp=0.9):'); + const creative = await creativePipeline.invoke("Describe a sunset"); + console.log(creative); + console.log(); + + console.log('Factual (temp=0.1):'); + const factual = await factualPipeline.invoke("What is the capital of France?"); + console.log(factual); + console.log(); + + // Part 4: Batch processing with pipelines + console.log('Part 4: Batch processing with pipeline'); + + const questions = [ + "What is Python?", + "What is JavaScript?", + "What is Rust?" + ]; + + const answers = await pipeline.batch(questions); + + questions.forEach((q, i) => { + console.log(`Q: ${q}`); + console.log(`A: ${answers[i]}`); + console.log(); + }); + + // Cleanup additional LLMs + await creativeLLM.dispose(); + await factualLLM.dispose(); + + } finally { + await llm.dispose(); + } + + console.log('\n✓ Exercise 4 complete!'); +} + +// Run the solution +exercise4().catch(console.error); + +/** + * Key Takeaways: + * + * 1. Building Runnables: + * - Extend Runnable base class + * - Override _call() method + * - Each Runnable does one thing well + * - Configure via constructor parameters + * + * 2. Composition with .pipe(): + * - a.pipe(b).pipe(c) chains operations + * - Output of one becomes input of next + * - Creates a new RunnableSequence + * - Result is itself a Runnable + * + * 3. Pipeline benefits: + * - Reusable across different inputs + * - Testable components in isolation + * - Easy to modify (swap components) + * - Clear data flow + * + * 4. Specialized pipelines: + * - Create different LLM instances with different configs + * - Mix and match components + * - Same interface, different behavior + * + * 5. Batch compatibility: + * - Pipelines work with batch() automatically + * - Each input goes through entire pipeline + * - No extra code needed + * + * 6. Real-world pattern: + * - This is exactly how LangChain works + * - Prompt -> Model -> Parser is common pattern + * - You now understand what frameworks do + * + * 7. Design principles: + * - Single Responsibility: Each Runnable does one thing + * - Composition over Inheritance: Build complex from simple + * - Open/Closed: Easy to extend, no need to modify + * + * Example advanced pipeline: + * + * ```javascript + * const agent = inputValidator + * .pipe(promptFormatter) + * .pipe(llm) + * .pipe(responseParser) + * .pipe(errorChecker) + * .pipe(outputFormatter); + * + * // Use it + * const result = await agent.invoke(userInput); + * + * // Test a component + * const parsed = await responseParser.invoke(testData); + * + * // Swap LLM + * const newAgent = inputValidator + * .pipe(promptFormatter) + * .pipe(differentLLM) // Just changed this! + * .pipe(responseParser) + * .pipe(errorChecker) + * .pipe(outputFormatter); + * ``` + */ \ No newline at end of file diff --git a/tutorial/01-foundation/04-context/exercises/13-simple-logger.js b/tutorial/01-foundation/04-context/exercises/13-simple-logger.js new file mode 100644 index 0000000000000000000000000000000000000000..6cdc82e7f9f794eea8092dd25bbe72894fced347 --- /dev/null +++ b/tutorial/01-foundation/04-context/exercises/13-simple-logger.js @@ -0,0 +1,150 @@ +/** + * Exercise 1: Build a Simple Logging Callback + * + * Goal: Understand the basic callback lifecycle + * + * In this exercise, you'll: + * 1. Extend BaseCallback to create a custom logger + * 2. Implement onStart, onEnd, and onError methods + * 3. Format output with emoji indicators + * 4. See how callbacks observe Runnable execution + * + * This is the foundation of observability in your framework! + */ + +import {Runnable} from '../../../../src/index.js'; +import {BaseCallback} from '../../../../src/utils/callbacks.js'; + +// TODO: Create SimpleLoggerCallback that extends BaseCallback +class SimpleLoggerCallback extends BaseCallback { + constructor(options = {}) { + super(); + this.showTimestamp = options.showTimestamp ?? false; + } + + // TODO: Implement onStart - log when runnable starts + // Use emoji: ▶️ + // Log to console: runnable name and input + async onStart(runnable, input, config) { + // Your code here + } + + // TODO: Implement onEnd - log when runnable completes + // Use emoji: ✔️ + // Log to console: runnable name and output + async onEnd(runnable, output, config) { + // Your code here + } + + // TODO: Implement onError - log when runnable errors + // Use emoji: ❌ + // Log to console: runnable name and error message + async onError(runnable, error, config) { + // Your code here + } +} + +// Test Runnables +class GreeterRunnable extends Runnable { + async _call(input, config) { + return `Hello, ${input}!`; + } +} + +class UpperCaseRunnable extends Runnable { + async _call(input, config) { + if (typeof input !== 'string') { + throw new Error('Input must be a string'); + } + return input.toUpperCase(); + } +} + +class ErrorRunnable extends Runnable { + async _call(input, config) { + throw new Error('Intentional error for testing'); + } +} + +// TODO: Test your callback +async function exercise1() { + console.log('=== Exercise 1: Simple Logging Callback ===\n'); + + // TODO: Create an instance of your SimpleLoggerCallback + const logger = null; // Replace with your code + + const config = { + callbacks: [logger] + }; + + // Test 1: Normal execution + console.log('--- Test 1: Normal Execution ---'); + const greeter = new GreeterRunnable(); + // TODO: Invoke greeter with "World" and config + const result1 = null; // Replace with your code + console.log('Final result:', result1); + console.log(); + + // Test 2: Pipeline + console.log('--- Test 2: Pipeline ---'); + const upper = new UpperCaseRunnable(); + const pipeline = greeter.pipe(upper); + // TODO: Invoke pipeline with "claude" and config + const result2 = null; // Replace with your code + console.log('Final result:', result2); + console.log(); + + // Test 3: Error handling + console.log('--- Test 3: Error Handling ---'); + const errorRunnable = new ErrorRunnable(); + try { + // TODO: Invoke errorRunnable with "test" and config + // Replace with your code + } catch (error) { + console.log('Caught error (expected):', error.message); + } + + console.log('\n✓ Exercise 1 complete!'); +} + +// Run the exercise +exercise1().catch(console.error); + +/** + * Expected Output: + * + * --- Test 1: Normal Execution --- + * ▶️ Starting: GreeterRunnable + * Input: World + * ✔️ Completed: GreeterRunnable + * Output: Hello, World! + * Final result: Hello, World! + * + * --- Test 2: Pipeline --- + * ▶️ Starting: RunnableSequence + * Input: claude + * ▶️ Starting: GreeterRunnable + * Input: claude + * ✔️ Completed: GreeterRunnable + * Output: Hello, claude! + * ▶️ Starting: UpperCaseRunnable + * Input: Hello, claude! + * ✔️ Completed: UpperCaseRunnable + * Output: HELLO, CLAUDE! + * ✔️ Completed: RunnableSequence + * Output: HELLO, CLAUDE! + * Final result: HELLO, CLAUDE! + * + * --- Test 3: Error Handling --- + * ▶️ Starting: ErrorRunnable + * Input: test + * ❌ ErrorRunnable: Intentional error for testing + * Caught error (expected): Intentional error for testing + * + * Learning Points: + * 1. Callbacks see every step in execution + * 2. onStart fires before _call() + * 3. onEnd fires after successful _call() + * 4. onError fires when _call() throws error + * 5. Callbacks don't change the output - they just observe + */ \ No newline at end of file diff --git a/tutorial/01-foundation/04-context/exercises/14-metrics-metadata.js b/tutorial/01-foundation/04-context/exercises/14-metrics-metadata.js new file mode 100644 index 0000000000000000000000000000000000000000..b9b3fba11651f915581286a0cf6616e682552188 --- /dev/null +++ b/tutorial/01-foundation/04-context/exercises/14-metrics-metadata.js @@ -0,0 +1,171 @@ +/** + * Exercise 14: Build a Metrics Tracker with Metadata + * + * Goal: Learn to use config metadata and track metrics + * + * In this exercise, you'll: + * 1. Build a stateful callback that tracks metrics + * 2. Use config.metadata to track user information + * 3. Measure execution time for each Runnable + * 4. Generate a summary report + * + * This teaches you how to pass context through your chains! + */ + +import {Runnable} from '../../../../src/index.js'; +import {BaseCallback} from '../../../../src/utils/callbacks.js'; + +// TODO: Create MetricsTrackerCallback +class MetricsTrackerCallback extends BaseCallback { + constructor() { + super(); + + // Track when each call started + this.startTimes = new Map(); + + // Metrics per runnable + this.metrics = {}; + } + + async onStart(runnable, input, config) { + // TODO: Record the start time for this runnable + // Hint: Use a unique key like `${runnable.name}_${Date.now()}` + + // TODO: Extract userId from config.metadata + // Hint: const userId = config.metadata?.userId + + // TODO: Initialize metrics for this runnable if it doesnt exist + // Hint: You'll need to track per-runnable: + // - Total calls + // - Total time + // - Users who called it + } + + async onEnd(runnable, output, config) { + // TODO: Calculate duration + // Hint: Find the matching start time and calculate Date.now() - startTime + + // TODO: Update total time for this runnable + + // TODO: Track which user made this call + // Hint: Get userId from config.metadata + } + + async onError(runnable, error, config) { + // TODO: Track errors (optional) + // Still need to clean up start time! + } + + // TODO: Create getReport() method + getReport() { + const report = {}; + + // TODO: For each runnable in metrics: + // - Calculate total calls + // - Calculate average time + // - List unique users + + return report; + } + + // TODO: Create printReport() method for nice display + printReport() { + console.log('\n📊 Metrics Report:'); + console.log('─'.repeat(60)); + + const report = this.getReport(); + + // TODO: Print nicely formatted report + // for (const [name, data] of Object.entries(report)) { ... } + } +} + +// Test Runnables with different speeds +class FastRunnable extends Runnable { + async _call(input, config) { + await new Promise(resolve => setTimeout(resolve, 100)); + return `Fast: ${input}`; + } +} + +class SlowRunnable extends Runnable { + async _call(input, config) { + await new Promise(resolve => setTimeout(resolve, 500)); + return `Slow: ${input}`; + } +} + +class MediumRunnable extends Runnable { + async _call(input, config) { + await new Promise(resolve => setTimeout(resolve, 250)); + return `Medium: ${input}`; + } +} + +// TODO: Test with different users +async function exercise2() { + console.log('=== Exercise 2: Metrics Tracker with Metadata ===\n'); + + // TODO: Create metrics tracker + const metrics = null; // Replace with: new MetricsTrackerCallback() + + const fast = new FastRunnable(); + const slow = new SlowRunnable(); + const medium = new MediumRunnable(); + + // Test 1: User 1 makes some calls + console.log('--- User 1 making calls ---'); + // TODO: Call fast.invoke with metadata: { userId: "user_123" } + // TODO: Call medium.invoke with metadata: { userId: "user_123" } + // TODO: Call fast.invoke again with metadata: { userId: "user_123" } + + // Test 2: User 2 makes different calls + console.log('--- User 2 making calls ---'); + // TODO: Call slow.invoke with metadata: { userId: "user_456" } + // TODO: Call fast.invoke with metadata: { userId: "user_456" } + + // Test 3: User 3 makes calls + console.log('--- User 3 making calls ---'); + // TODO: Call medium.invoke with metadata: { userId: "user_789" } + // TODO: Call slow.invoke with metadata: { userId: "user_789" } + // TODO: Call medium.invoke with metadata: { userId: "user_789" } + + // TODO: Print the report + // metrics.printReport(); + + console.log('\n✓ Exercise 2 complete!'); +} + +// Run the exercise +exercise2().catch(console.error); + +/** + * Expected Output: + * + * 📊 Metrics Report: + * ──────────────────────────────────────────────────────────── + * FastRunnable: + * Calls: 3 + * Total Time: 305ms + * Avg Time: 102ms + * Users: user_123, user_456 + * + * SlowRunnable: + * Calls: 2 + * Total Time: 1008ms + * Avg Time: 504ms + * Users: user_456, user_789 + * + * MediumRunnable: + * Calls: 3 + * Total Time: 756ms + * Avg Time: 252ms + * Users: user_123, user_789 + * + * Learning Points: + * 1. Stateful callbacks can accumulate data + * 2. config.metadata passes arbitrary context + * 3. Useful for tracking per-user metrics + * 4. Map.set() and Map.get() for tracking start times + * 5. Report generation for observability + */ \ No newline at end of file diff --git a/tutorial/01-foundation/04-context/exercises/15-config-inheritance.js b/tutorial/01-foundation/04-context/exercises/15-config-inheritance.js new file mode 100644 index 0000000000000000000000000000000000000000..81c185af121c708db31494f1724d45136e0f749b --- /dev/null +++ b/tutorial/01-foundation/04-context/exercises/15-config-inheritance.js @@ -0,0 +1,199 @@ +/** + * Exercise 15: Config Merging and Child Configs + * + * Goal: Understand how configs inherit and merge + * + * In this exercise, you'll: + * 1. Create parent and child configs + * 2. See how child configs inherit from parents + * 3. Understand callback accumulation + * 4. Learn when to use config.merge() vs config.child() + * + * This is crucial for nested Runnable calls! + */ + +import {RunnableConfig} from '../../../../src/core/context.js'; +import {Runnable} from '../../../../src/index.js'; +import {BaseCallback} from '../../../../src/utils/callbacks.js'; + +// Simple callback to show when it's called +class TagLoggerCallback extends BaseCallback { + constructor(name) { + super(); + this.name = name; + } + + async onStart(runnable, input, config) { + console.log(`[${this.name}] Starting ${runnable.name}`); + console.log(` Tags: [${config.tags.join(', ')}]`); + console.log(` Callback count: ${config.callbacks.length}`); + } + + async onEnd(runnable, output, config) { + console.log(`[${this.name}] Completed ${runnable.name}`); + } +} + +// Test Runnables that create child configs +class Step1Runnable extends Runnable { + async _call(input, config) { + console.log('\n--- Inside Step1 ---'); + + // TODO: Create a child config with additional tag + const childConfig = null; // Use config.child({ tags: ['step1'] }) + + // TODO: Log how many callbacks childConfig has + console.log(`Step1 child has ___ callbacks`); + + // Simulate nested work + return `Step1(${input})`; + } +} + +class Step2Runnable extends Runnable { + async _call(input, config) { + console.log('\n--- Inside Step2 ---'); + + // TODO: Create a child config with different tag + const childConfig = null; // Use config.child({ tags: ['step2'] }) + + // TODO: Log how many callbacks childConfig has + console.log(`Step2 child has ___ callbacks`); + + return `Step2(${input})`; + } +} + +class Step3Runnable extends Runnable { + async _call(input, config) { + console.log('\n--- Inside Step3 ---'); + + // TODO: Create a child config with: + // - Additional tag: 'step3' + // - Additional metadata: { nested: true } + const childConfig = null; // Use config.child({ ... }) + + // TODO: Log the tags and metadata + console.log(`Step3 tags: [${childConfig.tags.join(', ')}]`); + console.log(`Step3 metadata:`, childConfig.metadata); + + return `Step3(${input})`; + } +} + +async function exercise() { + console.log('=== Exercise 15: Config Merging and Child Configs ===\n'); + + // Part 1: Basic config inheritance + console.log('--- Part 1: Basic Inheritance ---\n'); + + // TODO: Create parent config with one callback + const parentConfig = null; // new RunnableConfig({ ... }) + + // TODO: Create child config with additional callback + const childConfig = null; // parentConfig.child({ ... }) + + // TODO: Check what's in each config + console.log('Parent callbacks:', 0); // parentConfig.callbacks.length + console.log('Child callbacks:', 0); // childConfig.callbacks.length + console.log('Parent tags:', []); // parentConfig.tags + console.log('Child tags:', []); // childConfig.tags + + // Part 2: Config in pipelines + console.log('\n--- Part 2: Config in Pipelines ---\n'); + + // TODO: Create a parent config with callbacks and tags + const pipelineConfig = null; // new RunnableConfig({ ... }) + + // TODO: Create pipeline + const step1 = new Step1Runnable(); + const step2 = new Step2Runnable(); + const pipeline = null; // step1.pipe(step2) + + // TODO: Invoke pipeline with config + // await pipeline.invoke("test", pipelineConfig); + + // Part 3: Multiple levels of nesting + console.log('\n--- Part 3: Multiple Nesting Levels ---\n'); + + const level1Config = new RunnableConfig({ + callbacks: [new TagLoggerCallback('Level1')], + tags: ['level1'], + metadata: {level: 1} + }); + + // TODO: Create level2 config as child of level1 + const level2Config = null; // level1Config.child({ ... }) + + // TODO: Create level3 config as child of level2 + const level3Config = null; // level2Config.child({ ... }) + + // TODO: Check the accumulation + console.log('Level 1 - Callbacks:', 0, 'Tags:', []); // level1Config + console.log('Level 2 - Callbacks:', 0, 'Tags:', []); // level2Config + console.log('Level 3 - Callbacks:', 0, 'Tags:', []); // level3Config + + // Part 4: merge() vs child() + console.log('\n--- Part 4: merge() vs child() ---\n'); + + const configA = new RunnableConfig({ + tags: ['a'], + metadata: {source: 'A'} + }); + + const configB = new RunnableConfig({ + tags: ['b'], + metadata: {source: 'B', extra: 'data'} + }); + + // TODO: Use merge() - combines two configs as equals + const merged = null; // configA.merge(configB) + + // TODO: Use child() - B inherits from A + const child = null; // configA.child({ tags: ['b'], metadata: { extra: 'data' } }) + + // TODO: Compare results + console.log('Merged metadata:', {}); // merged.metadata + console.log('Child metadata:', {}); // child.metadata + + console.log('\n✓ Exercise 3 complete!'); +} + +// Run the exercise +exercise().catch(console.error); + +/** + * Expected Output Snippets: + * + * --- Part 1: Basic Inheritance --- + * Parent callbacks: 1 + * Child callbacks: 2 + * Parent tags: ['base'] + * Child tags: ['base', 'child'] + * + * --- Part 2: Config in Pipelines --- + * [Parent] Starting Step1Runnable + * Tags: [base] + * Callback count: 1 + * + * --- Inside Step1 --- + * Step1 child has 1 callbacks + * [Parent] Completed Step1Runnable + * + * --- Part 3: Multiple Nesting Levels --- + * Level 1 - Callbacks: 1, Tags: ['level1'] + * Level 2 - Callbacks: 2, Tags: ['level1', 'level2'] + * Level 3 - Callbacks: 3, Tags: ['level1', 'level2', 'level3'] + * + * --- Part 4: merge() vs child() --- + * Merged metadata: { source: 'B', extra: 'data' } + * Child metadata: { source: 'A', extra: 'data' } + * + * Learning Points: + * 1. child() creates a new config inheriting from parent + * 2. Callbacks accumulate (child has parent's + its own) + * 3. Tags accumulate (arrays concatenate) + * 4. Metadata merges (child overrides parent keys) + * 5. merge() treats both configs equally + * 6. child() treats parent as base, child as override + */ \ No newline at end of file diff --git a/tutorial/01-foundation/04-context/exercises/16-runtime-config.js b/tutorial/01-foundation/04-context/exercises/16-runtime-config.js new file mode 100644 index 0000000000000000000000000000000000000000..526ceb37f7598733aea8a0e99ad19d8fc781017c --- /dev/null +++ b/tutorial/01-foundation/04-context/exercises/16-runtime-config.js @@ -0,0 +1,223 @@ +/** + * Exercise 16: Runtime Configuration Override + * + * Goal: Learn to override settings at runtime using config.configurable + * + * In this exercise, you'll: + * 1. Build a Runnable that reads from config.configurable + * 2. Override default settings at runtime + * 3. Test different configurations without changing code + * 4. Understand A/B testing with configs + * + * This is how you change LLM temperature, max tokens, etc. at runtime! + */ + +import { RunnableConfig } from '../../../../src//core/context.js'; +import {Runnable} from '../../../../src/index.js'; +import {BaseCallback} from '../../../../src/utils/callbacks.js'; + +// TODO: Create a configurable text processor +class TextProcessorRunnable extends Runnable { + constructor(options = {}) { + super(); + // Set defaults + this.defaultMaxLength = options.maxLength ?? 50; + this.defaultUppercase = options.uppercase ?? false; + this.defaultPrefix = options.prefix ?? ''; + } + + async _call(input, config) { + // TODO: Get maxLength from config.configurable or use default + const maxLength = 0; // config.configurable?.maxLength ?? this.defaultMaxLength + + // TODO: Get uppercase setting from config + const uppercase = false; // config.configurable?.uppercase ?? this.defaultUppercase + + // TODO: Get prefix setting from config + const prefix = ''; // config.configurable?.prefix ?? this.defaultPrefix + + // Process text + let result = input; + + // Apply prefix + if (prefix) { + result = prefix + result; + } + + // Apply uppercase + if (uppercase) { + result = result.toUpperCase(); + } + + // Apply truncation + if (result.length > maxLength) { + result = result.substring(0, maxLength) + '...'; + } + + return result; + } +} + +// Callback to show what config was used +class ConfigLoggerCallback extends BaseCallback { + async onStart(runnable, input, config) { + if (config.configurable && Object.keys(config.configurable).length > 0) { + console.log(`📋 Runtime config:`, config.configurable); + } + } +} + +// TODO: Test with different runtime configs +async function exercise() { + console.log('=== Exercise 16: Runtime Configuration Override ===\n'); + + // TODO: Create processor with defaults + const processor = null; // new TextProcessorRunnable({ maxLength: 50 }) + + const logger = new ConfigLoggerCallback(); + + const longText = "The quick brown fox jumps over the lazy dog. This is a longer sentence to test truncation and various configuration options."; + + // Test 1: Use defaults + console.log('--- Test 1: Using Defaults ---'); + // TODO: Invoke with just callbacks, no configurable + const result1 = null; + console.log('Result:', result1); + console.log(); + + // Test 2: Override maxLength + console.log('--- Test 2: Override maxLength ---'); + // TODO: Invoke with configurable: { maxLength: 20 } + const result2 = null; + console.log('Result:', result2); + console.log(); + + // Test 3: Override multiple settings + console.log('--- Test 3: Override Multiple Settings ---'); + // TODO: Invoke with configurable: { uppercase: true, maxLength: 30 } + const result3 = null; + console.log('Result:', result3); + console.log(); + + // Test 4: Add prefix at runtime + console.log('--- Test 4: Add Prefix at Runtime ---'); + // TODO: Invoke with configurable: { prefix: '[PREFIX] ', maxLength: 40 } + const result4 = null; + console.log('Result:', result4); + console.log(); + + // Test 5: A/B Testing scenario + console.log('--- Test 5: A/B Testing Different Configs ---'); + + // TODO: Create config variant A + const configA = null; // new RunnableConfig({ + // callbacks: [logger], + // configurable: { maxLength: 25, uppercase: false }, + // metadata: { variant: 'A', experiment: 'text-processing' } + // }) + + // TODO: Create config variant B + const configB = null; // new RunnableConfig({ + // callbacks: [logger], + // configurable: { maxLength: 40, uppercase: true }, + // metadata: { variant: 'B', experiment: 'text-processing' } + // }) + + const testText = "Testing A/B configuration variants"; + + // TODO: Test both variants + // const resultA = await processor.invoke(testText, configA); + // const resultB = await processor.invoke(testText, configB); + + console.log('Variant A:', 'result here'); + console.log('Variant B:', 'result here'); + console.log(); + + // Test 6: Simulating LLM-style configuration + console.log('--- Test 6: LLM-Style Temperature Override ---'); + + // Create a mock LLM runnable + class MockLLMRunnable extends Runnable { + constructor(defaultTemp = 0.7) { + super(); + this.defaultTemperature = defaultTemp; + } + + async _call(input, config) { + // TODO: Get temperature from config.configurable + const temperature = 0.7; // config.configurable?.temperature ?? this.defaultTemperature + + // Simulate different outputs based on temperature + if (temperature < 0.3) { + return `[temp=${temperature}] Deterministic response: ${input}`; + } else if (temperature > 0.8) { + return `[temp=${temperature}] Creative response about ${input}!!!`; + } else { + return `[temp=${temperature}] Balanced response: ${input}.`; + } + } + } + + // TODO: Test the mock LLM with different temperatures + const llm = null; // new MockLLMRunnable() + + console.log('Low temp (0.1):'); + // const low = await llm.invoke("AI", { configurable: { temperature: 0.1 } }); + // console.log(low); + + console.log('\nMedium temp (0.7):'); + // const med = await llm.invoke("AI", { configurable: { temperature: 0.7 } }); + // console.log(med); + + console.log('\nHigh temp (1.0):'); + // const high = await llm.invoke("AI", { configurable: { temperature: 1.0 } }); + // console.log(high); + + console.log('\n✓ Exercise 4 complete!'); +} + +// Run the exercise +exercise().catch(console.error); + +/** + * Expected Output: + * + * --- Test 1: Using Defaults --- + * Result: The quick brown fox jumps over the lazy dog. Thi... + * + * --- Test 2: Override maxLength --- + * 📋 Runtime config: { maxLength: 20 } + * Result: The quick brown fox ... + * + * --- Test 3: Override Multiple Settings --- + * 📋 Runtime config: { uppercase: true, maxLength: 30 } + * Result: THE QUICK BROWN FOX JUMPS O... + * + * --- Test 4: Add Prefix at Runtime --- + * 📋 Runtime config: { prefix: '[PREFIX] ', maxLength: 40 } + * Result: [PREFIX] The quick brown fox jumps ove... + * + * --- Test 5: A/B Testing Different Configs --- + * 📋 Runtime config: { maxLength: 25, uppercase: false } + * Variant A: Testing A/B configurati... + * 📋 Runtime config: { maxLength: 40, uppercase: true } + * Variant B: TESTING A/B CONFIGURATION VARIANTS + * + * --- Test 6: LLM-Style Temperature Override --- + * Low temp (0.1): + * [temp=0.1] Deterministic response: AI + * + * Medium temp (0.7): + * [temp=0.7] Balanced response: AI. + * + * High temp (1.0): + * [temp=1.0] Creative response about AI!!! + * + * Learning Points: + * 1. config.configurable holds runtime overrides + * 2. Use ?? operator for defaults: config.configurable?.key ?? default + * 3. Don't modify instance defaults - just use config value + * 4. Perfect for A/B testing different settings + * 5. This is how LLMs change temperature/maxTokens at runtime + * 6. Combine with metadata to track which config was used + */ \ No newline at end of file diff --git a/tutorial/01-foundation/04-context/lesson.md b/tutorial/01-foundation/04-context/lesson.md new file mode 100644 index 0000000000000000000000000000000000000000..f46fec0224fe55630f52c12c46c606273a8ea271 --- /dev/null +++ b/tutorial/01-foundation/04-context/lesson.md @@ -0,0 +1,1346 @@ +# Context & Configuration + +**Part 1: Foundation - Lesson 4** + +> Passing state, callbacks, and metadata through Runnable chains + +## Overview + +You've learned Runnables (Lesson 1), Messages (Lesson 2), and LLM Wrappers (Lesson 3). Now we tackle a critical question: **How do we pass configuration, callbacks, and state through complex chains without cluttering our code?** + +The answer is **RunnableConfig** - a powerful pattern that threads context through every step of a pipeline, enabling logging, debugging, authentication, and more without changing your core logic. + +## Why Does This Matter? + +### The Problem: Configuration Chaos + +Without a proper context system: + +```javascript +// Bad: Configuration everywhere +async function complexPipeline(input, temperature, callbacks, debug, userId) { + const result1 = await step1(input, temperature, debug); + callbacks.onStep('step1', result1); + + const result2 = await step2(result1, userId, debug); + callbacks.onStep('step2', result2); + + const result3 = await step3(result2, temperature, callbacks, debug); + callbacks.onStep('step3', result3); + + return result3; +} + +// Every function needs to know about every configuration option! +``` + +Problems: +- Every function signature becomes huge +- Adding new config requires changing every function +- Hard to add features like logging or metrics +- Impossible to intercept at specific points +- Can't pass user context through chains + +### The Solution: RunnableConfig + +With RunnableConfig: + +```javascript +// Good: Config flows automatically +const config = { + temperature: 0.7, + callbacks: [loggingCallback, metricsCallback], + metadata: { userId: 'user_123', sessionId: 'sess_456' }, + tags: ['production', 'api-v2'] +}; + +const result = await pipeline.invoke(input, config); + +// Every Runnable in the pipeline receives config automatically +// No need to pass it manually at each step! +``` + +Much cleaner! And infinitely extensible. + +## Learning Objectives + +By the end of this lesson, you will: + +- ✅ Understand the RunnableConfig pattern +- ✅ Implement a callback system for monitoring +- ✅ Add metadata and tags for tracking +- ✅ Build configurable Runnables +- ✅ Create custom callbacks for logging and metrics +- ✅ Debug chains with visibility into each step +- ✅ Understand how LangChain's callbacks work + +## Core Concepts + +### What is RunnableConfig? + +RunnableConfig is an object that flows through your entire pipeline, carrying: + +1. **Callbacks** - Functions called at specific points (logging, metrics, debugging) +2. **Metadata** - Arbitrary data (user IDs, session info, request context) +3. **Tags** - Labels for filtering and organization +4. **Recursion Limit** - Prevent infinite loops +5. **Runtime Configuration** - Override default settings (temperature, max tokens) + +**📍 Where This Lives in the Framework:** + +Looking back at our framework structure from the main README, RunnableConfig is part of the **Core** module: + +```javascript +// Core module (what we're building now) +export { + Runnable, // ← Lesson 1 + RunnableSequence, // ← Lesson 1 + BaseMessage, // ← Lesson 2 + HumanMessage, // ← Lesson 2 + AIMessage, // ← Lesson 2 + SystemMessage, // ← Lesson 2 + RunnableConfig // ← THIS LESSON (Lesson 4) +} from './core/index.js'; +``` + +RunnableConfig isn't a separate feature you add later - it's **foundational infrastructure** built into the Core module that every other part of the framework depends on. The callback system we're about to build is how this config becomes useful for observability. + +### The Flow + +``` +User calls: runnable.invoke(input, config) + ↓ + ┌───────────────┴─────────────┐ + │ │ + Config passed to every step │ + │ │ + ┌─────────┼─────────┬──────────┬────────┼────┐ + │ │ │ │ │ │ + Step1 Step2 Step3 Step4 Step5 ... + │ │ │ │ │ + └─────────┴─────────┴──────────┴────────┘ + │ + All use same config + All trigger callbacks + All have access to metadata +``` + +### Key Benefits + +1. **Separation of concerns**: Logic separate from monitoring +2. **Composability**: Add features without changing code +3. **Observability**: See what's happening at every step +4. **Flexibility**: Runtime configuration override +5. **Extensibility**: Easy to add new capabilities + +## Implementation Deep Dive + +### Step 1: The RunnableConfig Object + +**Location:** `src/core/context.js` +```javascript +/** + * RunnableConfig - Configuration passed through chains + */ +export class RunnableConfig { + constructor(options = {}) { + // Callbacks for monitoring + this.callbacks = options.callbacks || []; + + // Metadata (arbitrary data) + this.metadata = options.metadata || {}; + + // Tags for filtering/organization + this.tags = options.tags || []; + + // Recursion limit (prevent infinite loops) + this.recursionLimit = options.recursionLimit ?? 25; + + // Runtime overrides for generation parameters + this.configurable = options.configurable || {}; + } + + /** + * Merge with another config (child inherits from parent) + */ + merge(other) { + return new RunnableConfig({ + callbacks: [...this.callbacks, ...(other.callbacks || [])], + metadata: { ...this.metadata, ...(other.metadata || {}) }, + tags: [...this.tags, ...(other.tags || [])], + recursionLimit: other.recursionLimit ?? this.recursionLimit, + configurable: { ...this.configurable, ...(other.configurable || {}) } + }); + } + + /** + * Create a child config with additional settings + */ + child(options = {}) { + return this.merge(new RunnableConfig(options)); + } +} +``` + +**Why this design?** +- Immutable merging (doesn't modify original) +- Child configs inherit parent settings +- Easy to add new fields without breaking existing code + +### Step 2: The Callback System + +**📍 How Callbacks Relate to the Framework:** + +Callbacks are the **mechanism** that makes RunnableConfig useful. They're not a separate module - they're the "hooks" that get triggered as your Runnables execute. Think of them as event listeners built into the Core module. + +**In the framework structure, callbacks support observability:** +- They live in Core (used by every module) +- Later modules like **Agents** and **Chains** use them for tracing +- The **Utils** module's `CallbackManager` (which we'll see) is a helper for managing them + +Here's the base callback class: + +**Location:** `src/utils/callbacks.js` +```javascript +/** + * BaseCallback - Abstract callback handler + */ +export class BaseCallback { + /** + * Called when a Runnable starts + */ + async onStart(runnable, input, config) { + // Override in subclass + } + + /** + * Called when a Runnable completes successfully + */ + async onEnd(runnable, output, config) { + // Override in subclass + } + + /** + * Called when a Runnable errors + */ + async onError(runnable, error, config) { + // Override in subclass + } + + /** + * Called for LLM token streaming + */ + async onLLMNewToken(token, config) { + // Override in subclass + } + + /** + * Called when a chain step completes + */ + async onChainStep(stepName, output, config) { + // Override in subclass + } +} +``` + +**Callback Lifecycle**: +``` +invoke() called + ↓ + onStart() ← Before execution + ↓ + [execution] ← Your _call() method runs + ↓ + onEnd() ← After success + or + onError() ← After failure +``` + +**Key insight:** Callbacks are optional observers - your code works fine without them, but they let you see what's happening. + +### Step 3: CallbackManager + +Manages multiple callbacks and ensures they all get called: + +**Location:** `src/utils/callback-manager.js` +```javascript +/** + * CallbackManager - Manages multiple callbacks + */ +export class CallbackManager { + constructor(callbacks = []) { + this.callbacks = callbacks; + } + + /** + * Add a callback + */ + add(callback) { + this.callbacks.push(callback); + } + + /** + * Call onStart for all callbacks + */ + async handleStart(runnable, input, config) { + await Promise.all( + this.callbacks.map(cb => + this._safeCall(() => cb.onStart(runnable, input, config)) + ) + ); + } + + /** + * Call onEnd for all callbacks + */ + async handleEnd(runnable, output, config) { + await Promise.all( + this.callbacks.map(cb => + this._safeCall(() => cb.onEnd(runnable, output, config)) + ) + ); + } + + /** + * Call onError for all callbacks + */ + async handleError(runnable, error, config) { + await Promise.all( + this.callbacks.map(cb => + this._safeCall(() => cb.onError(runnable, error, config)) + ) + ); + } + + /** + * Call onLLMNewToken for all callbacks + */ + async handleLLMNewToken(token, config) { + await Promise.all( + this.callbacks.map(cb => + this._safeCall(() => cb.onLLMNewToken(token, config)) + ) + ); + } + + /** + * Call onChainStep for all callbacks + */ + async handleChainStep(stepName, output, config) { + await Promise.all( + this.callbacks.map(cb => + this._safeCall(() => cb.onChainStep(stepName, output, config)) + ) + ); + } + + /** + * Safely call a callback (don't let one callback crash others) + */ + async _safeCall(fn) { + try { + await fn(); + } catch (error) { + console.error('Callback error:', error); + // Don't throw - callbacks shouldn't break the pipeline + } + } +} +``` + +**Key insight**: Callbacks can fail without breaking the pipeline. + +### Step 4: Integrating with Runnable + +Update the Runnable base class to use config: + +**Location:** `src/core/runnable.js` +```javascript +export class Runnable { + constructor() { + this.name = this.constructor.name; + } + + /** + * Execute with config support + */ + async invoke(input, config = {}) { + // Normalize config to RunnableConfig instance + const runnableConfig = config instanceof RunnableConfig + ? config + : new RunnableConfig(config); + + // Create callback manager + const callbackManager = new CallbackManager(runnableConfig.callbacks); + + try { + // Notify callbacks: starting + await callbackManager.handleStart(this, input, runnableConfig); + + // Execute the runnable + const output = await this._call(input, runnableConfig); + + // Notify callbacks: success + await callbackManager.handleEnd(this, output, runnableConfig); + + return output; + } catch (error) { + // Notify callbacks: error + await callbackManager.handleError(this, error, runnableConfig); + throw error; + } + } + + async _call(input, config) { + throw new Error( + `${this.name} must implement _call() method` + ); + } + + // ... stream(), batch(), pipe() methods remain the same ... +} +``` + +Now every Runnable automatically: +- ✅ Receives config +- ✅ Triggers callbacks +- ✅ Handles errors properly +- ✅ Passes config to nested Runnables + +### Step 5: Useful Built-in Callbacks + +```javascript +/** + * ConsoleCallback - Logs to console + */ +export class ConsoleCallback extends Callbacks { + constructor(options = {}) { + super(); + this.verbose = options.verbose ?? true; + this.colors = options.colors ?? true; + } + + async onStart(runnable, input, config) { + if (this.verbose) { + console.log(`\n▶ Starting: ${runnable._name}`); + console.log(` Input:`, this._format(input)); + } + } + + async onEnd(runnable, output, config) { + if (this.verbose) { + console.log(`✓ Completed: ${runnable._name}`); + console.log(` Output:`, this._format(output)); + } + } + + async onError(runnable, error, config) { + console.error(`✗ Error in ${runnable._name}:`, error.message); + } + + async onLLMNewToken(token, config) { + process.stdout.write(token); + } + + _format(value) { + if (typeof value === 'string') { + return value.length > 100 ? value.substring(0, 97) + '...' : value; + } + return JSON.stringify(value, null, 2); + } +} +``` + +```javascript +/** + * MetricsCallback - Tracks timing and counts + */ +export class MetricsCallback extends Callbacks { + constructor() { + super(); + this.metrics = { + calls: {}, + totalTime: {}, + errors: {} + }; + this.startTimes = new Map(); + } + + async onStart(runnable, input, config) { + const name = runnable._name; + this.startTimes.set(name, Date.now()); + + this.metrics.calls[name] = (this.metrics.calls[name] || 0) + 1; + } + + async onEnd(runnable, output, config) { + const name = runnable._name; + const startTime = this.startTimes.get(name); + + if (startTime) { + const duration = Date.now() - startTime; + this.metrics.totalTime[name] = (this.metrics.totalTime[name] || 0) + duration; + this.startTimes.delete(name); + } + } + + async onError(runnable, error, config) { + const name = runnable._name; + this.metrics.errors[name] = (this.metrics.errors[name] || 0) + 1; + } + + getReport() { + const report = []; + + for (const [name, calls] of Object.entries(this.metrics.calls)) { + const totalTime = this.metrics.totalTime[name] || 0; + const avgTime = calls > 0 ? (totalTime / calls).toFixed(2) : 0; + const errors = this.metrics.errors[name] || 0; + + report.push({ + runnable: name, + calls, + avgTime: `${avgTime}ms`, + totalTime: `${totalTime}ms`, + errors + }); + } + + return report; + } + + reset() { + this.metrics = {calls: {}, totalTime: {}, errors: {}}; + this.startTimes.clear(); + } +} +``` + +```javascript +/** + * FileCallback - Logs to file + */ +export class FileCallback extends Callbacks { + constructor(filename) { + super(); + this.filename = filename; + this.logs = []; + } + + async onStart(runnable, input, config) { + this.logs.push({ + timestamp: new Date().toISOString(), + event: 'start', + runnable: runnable._name, + input: this._serialize(input) + }); + } + + async onEnd(runnable, output, config) { + this.logs.push({ + timestamp: new Date().toISOString(), + event: 'end', + runnable: runnable._name, + output: this._serialize(output) + }); + } + + async onError(runnable, error, config) { + this.logs.push({ + timestamp: new Date().toISOString(), + event: 'error', + runnable: runnable._name, + error: error.message + }); + } + + async flush() { + const fs = await import('fs/promises'); + await fs.writeFile( + this.filename, + JSON.stringify(this.logs, null, 2), + 'utf-8' + ); + this.logs = []; + } + + _serialize(value) { + if (typeof value === 'string') return value; + if (value?.content) return value.content; // Message + return JSON.stringify(value); + } +} +``` + +## Complete Implementation + +Here's the full context system: + +```javascript +/** + * Context & Configuration System + * + * @module core/context + */ + +/** + * RunnableConfig - Configuration passed through chains + */ +export class RunnableConfig { + constructor(options = {}) { + this.callbacks = options.callbacks || []; + this.metadata = options.metadata || {}; + this.tags = options.tags || []; + this.recursionLimit = options.recursionLimit ?? 25; + this.configurable = options.configurable || {}; + } + + merge(other) { + return new RunnableConfig({ + callbacks: [...this.callbacks, ...(other.callbacks || [])], + metadata: { ...this.metadata, ...(other.metadata || {}) }, + tags: [...this.tags, ...(other.tags || [])], + recursionLimit: other.recursionLimit ?? this.recursionLimit, + configurable: { ...this.configurable, ...(other.configurable || {}) } + }); + } + + child(options = {}) { + return this.merge(new RunnableConfig(options)); + } +} + +/** + * BaseCallback - Base class for callbacks + */ +export class BaseCallback { + async onStart(runnable, input, config) {} + async onEnd(runnable, output, config) {} + async onError(runnable, error, config) {} + async onLLMNewToken(token, config) {} + async onChainStep(stepName, output, config) {} +} + +/** + * CallbackManager - Manages multiple callbacks + */ +export class CallbackManager { + constructor(callbacks = []) { + this.callbacks = callbacks; + } + + add(callback) { + this.callbacks.push(callback); + } + + async handleStart(runnable, input, config) { + await Promise.all( + this.callbacks.map(cb => + this._safeCall(() => cb.onStart(runnable, input, config)) + ) + ); + } + + async handleEnd(runnable, output, config) { + await Promise.all( + this.callbacks.map(cb => + this._safeCall(() => cb.onEnd(runnable, output, config)) + ) + ); + } + + async handleError(runnable, error, config) { + await Promise.all( + this.callbacks.map(cb => + this._safeCall(() => cb.onError(runnable, error, config)) + ) + ); + } + + async handleLLMNewToken(token, config) { + await Promise.all( + this.callbacks.map(cb => + this._safeCall(() => cb.onLLMNewToken(token, config)) + ) + ); + } + + async handleChainStep(stepName, output, config) { + await Promise.all( + this.callbacks.map(cb => + this._safeCall(() => cb.onChainStep(stepName, output, config)) + ) + ); + } + + async _safeCall(fn) { + try { + await fn(); + } catch (error) { + console.error('Callback error:', error); + } + } +} + +/** + * ConsoleCallback - Logs to console with colors + */ +export class ConsoleCallback extends BaseCallback { + constructor(options = {}) { + super(); + this.verbose = options.verbose ?? true; + } + + async onStart(runnable, input, config) { + if (this.verbose) { + console.log(`\n▶ Starting: ${runnable._name}`); + console.log(` Input:`, this._format(input)); + if (config.metadata && Object.keys(config.metadata).length > 0) { + console.log(` Metadata:`, config.metadata); + } + } + } + + async onEnd(runnable, output, config) { + if (this.verbose) { + console.log(`✓ Completed: ${runnable._name}`); + console.log(` Output:`, this._format(output)); + } + } + + async onError(runnable, error, config) { + console.error(`✗ Error in ${runnable._name}:`, error.message); + } + + async onLLMNewToken(token, config) { + process.stdout.write(token); + } + + _format(value) { + if (typeof value === 'string') { + return value.length > 100 ? value.substring(0, 97) + '...' : value; + } + if (value?.content) { + return value.content.substring(0, 100); + } + return JSON.stringify(value, null, 2); + } +} + +/** + * MetricsCallback - Tracks performance metrics + */ +export class MetricsCallback extends BaseCallback { + constructor() { + super(); + this.metrics = { + calls: {}, + totalTime: {}, + errors: {} + }; + this.startTimes = new Map(); + } + + async onStart(runnable, input, config) { + const key = `${runnable._name}_${Date.now()}_${Math.random()}`; + this.startTimes.set(key, { name: runnable._name, time: Date.now() }); + + const name = runnable._name; + this.metrics.calls[name] = (this.metrics.calls[name] || 0) + 1; + } + + async onEnd(runnable, output, config) { + const name = runnable._name; + + // Find the most recent start time for this runnable + let startTime = null; + for (const [key, value] of this.startTimes.entries()) { + if (value.name === name) { + startTime = value.time; + this.startTimes.delete(key); + break; + } + } + + if (startTime) { + const duration = Date.now() - startTime; + this.metrics.totalTime[name] = (this.metrics.totalTime[name] || 0) + duration; + } + } + + async onError(runnable, error, config) { + const name = runnable._name; + this.metrics.errors[name] = (this.metrics.errors[name] || 0) + 1; + } + + getReport() { + const report = []; + + for (const [name, calls] of Object.entries(this.metrics.calls)) { + const totalTime = this.metrics.totalTime[name] || 0; + const avgTime = calls > 0 ? (totalTime / calls).toFixed(2) : 0; + const errors = this.metrics.errors[name] || 0; + + report.push({ + runnable: name, + calls, + avgTime: `${avgTime}ms`, + totalTime: `${totalTime}ms`, + errors, + successRate: calls > 0 ? `${((calls - errors) / calls * 100).toFixed(1)}%` : '0%' + }); + } + + return report; + } + + printReport() { + console.log('\n📊 Performance Report:'); + console.log('─'.repeat(80)); + console.table(this.getReport()); + } + + reset() { + this.metrics = { calls: {}, totalTime: {}, errors: {} }; + this.startTimes.clear(); + } +} + +export default { + RunnableConfig, + BaseCallback, + CallbackManager, + ConsoleCallback, + MetricsCallback +}; +``` + +## Real-World Examples + +### Example 1: Basic Logging + +```javascript +import { ConsoleCallback } from './context.js'; + +const logger = new ConsoleCallback({ verbose: true }); + +const config = { + callbacks: [logger] +}; + +// Every step will log +const result = await chain.invoke(input, config); +``` + +Output: +``` +▶ Starting: PromptTemplate + Input: "Translate to Spanish: Hello" + +✓ Completed: PromptTemplate + Output: "Translate the following to Spanish: Hello" + +▶ Starting: LlamaCppLLM + Input: "Translate the following to Spanish: Hello" + +✓ Completed: LlamaCppLLM + Output: AIMessage("Hola") +``` + +### Example 2: Performance Monitoring + +```javascript +import { MetricsCallback } from './context.js'; + +const metrics = new MetricsCallback(); + +const config = { + callbacks: [metrics] +}; + +// Run multiple times +for (let i = 0; i < 10; i++) { + await chain.invoke(input, config); +} + +// Get performance report +metrics.printReport(); +``` + +Output: +``` +📊 Performance Report: +┌─────────┬─────────────────┬───────┬──────────┬───────────┬────────┬──────────────┐ +│ (index) │ runnable │ calls │ avgTime │ totalTime │ errors │ successRate │ +├─────────┼─────────────────┼───────┼──────────┼───────────┼────────┼──────────────┤ +│ 0 │ 'PromptTemplate'│ 10 │ '5.20ms' │ '52ms' │ 0 │ '100.0%' │ +│ 1 │ 'LlamaCppLLM' │ 10 │ '243.5ms'│ '2435ms' │ 0 │ '100.0%' │ +└─────────┴─────────────────┴───────┴──────────┴───────────┴────────┴──────────────┘ +``` + +### Example 3: Metadata Tracking + +```javascript +const config = { + metadata: { + userId: 'user_123', + sessionId: 'sess_456', + requestId: 'req_789' + }, + tags: ['production', 'api-v2'] +}; + +await agent.invoke(input, config); + +// Every callback receives this metadata +// Useful for logging, debugging, billing +``` + +### Example 4: Multiple Callbacks + +```javascript +const logger = new ConsoleCallback(); +const metrics = new MetricsCallback(); +const fileLogger = new FileCallback('./logs/agent.json'); + +const config = { + callbacks: [logger, metrics, fileLogger] +}; + +await chain.invoke(input, config); + +// All three callbacks are triggered +await fileLogger.flush(); // Save to file +metrics.printReport(); // Show metrics +``` + +### Example 5: Runtime Configuration Override + +```javascript +const llm = new LlamaCppLLM({ + modelPath: './model.gguf', + temperature: 0.7 // default +}); + +// Override at runtime +const result1 = await llm.invoke(input, { + configurable: { temperature: 0.2 } // more deterministic +}); + +const result2 = await llm.invoke(input, { + configurable: { temperature: 1.2 } // more creative +}); +``` + +### Example 6: Custom Callback for API Logging + +```javascript +class APILoggerCallback extends Callbacks { + constructor(apiKey) { + super(); + this.apiKey = apiKey; + } + + async onEnd(runnable, output, config) { + // Send to logging API + await fetch('https://api.yourservice.com/logs', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + runnable: runnable._name, + output: this._serialize(output), + metadata: config.metadata, + timestamp: new Date().toISOString() + }) + }); + } + + _serialize(output) { + if (output?.content) return output.content; + return String(output); + } +} + +// Usage +const apiLogger = new APILoggerCallback(process.env.API_KEY); +const config = {callbacks: [apiLogger]}; +await chain.invoke(input, config); +``` + +## Advanced Patterns + +### Pattern 1: Conditional Callbacks + +```javascript +class ConditionalCallback extends Callbacks { + constructor(condition, callback) { + super(); + this.condition = condition; + this.callback = callback; + } + + async onEnd(runnable, output, config) { + if (this.condition(runnable, output, config)) { + await this.callback.onEnd(runnable, output, config); + } + } +} + +// Only log slow operations +const slowLogger = new ConditionalCallback( + (runnable, output, config) => { + // Check if operation took > 1 second + return config.executionTime > 1000; + }, + new ConsoleCallback() +); +``` + +### Pattern 2: Callback Composition + +```javascript +class CompositeCallback extends Callbacks { + constructor(callbacks) { + super(); + this.callbacks = callbacks; + } + + async onStart(runnable, input, config) { + for (const cb of this.callbacks) { + await cb.onStart(runnable, input, config); + } + } + + async onEnd(runnable, output, config) { + for (const cb of this.callbacks) { + await cb.onEnd(runnable, output, config); + } + } + + async onError(runnable, error, config) { + for (const cb of this.callbacks) { + await cb.onError(runnable, error, config); + } + } +} + +// Combine multiple callbacks +const composite = new CompositeCallback([ + new ConsoleCallback(), + new MetricsCallback(), + new FileCallback('./logs.json') +]); +``` + +### Pattern 3: Filtered Logging + +```javascript +class FilteredCallback extends Callbacks { + constructor(filter, callback) { + super(); + this.filter = filter; + this.callback = callback; + } + + async onStart(runnable, input, config) { + if (this.filter(runnable._name, 'start')) { + await this.callback.onStart(runnable, input, config); + } + } + + async onEnd(runnable, output, config) { + if (this.filter(runnable._name, 'end')) { + await this.callback.onEnd(runnable, output, config); + } + } +} + +// Only log LLM calls +const llmOnly = new FilteredCallback( + (name, event) => name.includes('LLM'), + new ConsoleCallback() +); +``` + +### Pattern 4: Callback with State + +```javascript +class StatefulCallback extends Callbacks { + constructor() { + super(); + this.state = { + callCount: 0, + totalTokens: 0, + errors: [] + }; + } + + async onEnd(runnable, output, config) { + this.state.callCount++; + + if (output?.additionalKwargs?.usage) { + this.state.totalTokens += output.additionalKwargs.usage.totalTokens; + } + } + + async onError(runnable, error, config) { + this.state.errors.push({ + runnable: runnable._name, + error: error.message, + timestamp: Date.now() + }); + } + + getState() { + return {...this.state}; + } +} +``` + +## Common Use Cases + +### Use Case 1: Debug Mode + +```javascript +const debugConfig = { + callbacks: [new ConsoleCallback({ verbose: true })], + tags: ['debug'] +}; + +// See everything that happens +await agent.invoke(query, debugConfig); +``` + +### Use Case 2: Production Monitoring + +```javascript +const productionConfig = { + callbacks: [ + new MetricsCallback(), + new APILoggerCallback(API_KEY) + ], + metadata: { + environment: 'production', + version: '1.2.3' + }, + tags: ['production'] +}; +``` + +### Use Case 3: A/B Testing + +```javascript +const configA = { + metadata: { variant: 'A' }, + configurable: { temperature: 0.7 } +}; + +const configB = { + metadata: { variant: 'B' }, + configurable: { temperature: 0.9 } +}; + +// Track which performs better +``` + +### Use Case 4: User Context + +```javascript +const userConfig = { + metadata: { + userId: req.userId, + sessionId: req.sessionId, + plan: req.user.plan // 'free', 'pro', 'enterprise' + } +}; + +// Different behavior based on user plan +if (userConfig.metadata.plan === 'free') { + userConfig.configurable = { maxTokens: 100 }; +} else if (userConfig.metadata.plan === 'pro') { + userConfig.configurable = { maxTokens: 500 }; +} +``` + +## Best Practices + +### ✅ DO: + +```javascript +// Use config for cross-cutting concerns +const config = { + callbacks: [logger, metrics], + metadata: { userId, sessionId } +}; + +// Let config flow automatically +await chain.invoke(input, config); + +// Create child configs for nested calls +const childConfig = config.child({ tags: ['nested'] }); + +// Handle callback errors gracefully (already done in CallbackManager) +``` + +### ❌ DON'T: + +```javascript +// Don't pass config manually at each step +const result1 = await step1(input, config); +const result2 = await step2(result1, config); // Unnecessary + +// Don't mutate config +config.metadata.foo = 'bar'; // Bad! Create new config instead + +// Don't put business logic in callbacks +// Callbacks are for observability, not logic +``` + +## Debugging Tips + +### Tip 1: Add Timestamps + +```javascript +class TimestampCallback extends Callbacks { + async onStart(runnable, input, config) { + console.log(`[${new Date().toISOString()}] ${runnable._name} started`); + } +} +``` + +### Tip 2: Stack Traces in Callbacks + +```javascript +class DebugCallback extends Callbacks { + async onError(runnable, error, config) { + console.error('Full stack trace:'); + console.error(error.stack); + console.error('Config:', config); + } +} +``` + +### Tip 3: Callback Filtering + +```javascript +// Only show LLM operations +const config = { + callbacks: [ + new FilteredCallback( + name => name.includes('LLM'), + new ConsoleCallback() + ) + ] +}; +``` + +## Exercises + +Practice building with context and callbacks: + +### Exercise 13: Build a Simple Logging Callback +Understand the basic callback lifecycle. +**Starter code**: [`exercises/13-simple-logger.js`](exercises/13-simple-logger.js) + +### Exercise 14: Build a Metrics Tracker with Metadata +Learn to use config metadata and track metrics. +**Starter code**: [`exercises/14-metrics-metadata.js`](exercises/14-metrics-metadata.js) + +### Exercise 15: Config Merging and Child Configs +Understand how configs inherit and merge. +**Starter code**: [`exercises/15-retry-callback.js`](exercises/15-retry-callback.js) + +### Exercise 16: Runtime Configuration Override +Learn to override LLM settings at runtime. +**Starter code**: [`exercises/16-runtime-config.js`](exercises/16-runtime-config.js) + + +## Summary + +Congratulations! You've mastered the context and configuration system. + +### Key Takeaways + +1. **RunnableConfig flows automatically**: No need to pass manually at each step +2. **Callbacks enable observability**: See what's happening without changing code +3. **Metadata carries context**: User info, session data, request IDs +4. **Tags enable filtering**: Organize and filter operations +5. **Callbacks don't break pipelines**: Errors in callbacks are caught +6. **Configuration is composable**: Child configs inherit from parents +7. **Runtime overrides are powerful**: Change behavior without changing code + +### What You Built + +A context system that: +- ✅ Flows config through chains automatically +- ✅ Supports multiple callbacks +- ✅ Tracks metrics and performance +- ✅ Enables runtime configuration +- ✅ Provides observability +- ✅ Handles errors gracefully +- ✅ Is infinitely extensible + +### Foundation Complete! 🎉 + +You've completed Part 1 (Foundation). You now understand: +1. **Runnable** - The composability pattern +2. **Messages** - Structured conversation data +3. **LLM Wrapper** - Integrating real models +4. **Context** - Passing state and observability + +These four concepts are the foundation of **every** agent framework. + +### What's Next + +In **Part 2: Composition**, you'll learn: +- Prompt templates +- Output parsers +- LLM chains +- The pipe operator +- Memory systems + +➡️ [Continue to Part 2: Composition](../02-composition/01-prompts.md) + +## Additional Resources + +- [LangChain Callbacks](https://python.langchain.com/docs/modules/callbacks/) +- [OpenTelemetry](https://opentelemetry.io/) - Industry standard for observability +- [Structured Logging](https://www.structlog.org/en/stable/) + +## Questions & Discussion + +**Q: Why not just use console.log everywhere?** + +A: Callbacks are: +- Composable (turn on/off easily) +- Non-invasive (don't clutter code) +- Centralized (one place to change logging) +- Production-ready (can send to monitoring services) + +**Q: What's the performance overhead of callbacks?** + +A: Minimal if implemented correctly. The CallbackManager calls them in parallel and catches errors, so one slow callback doesn't slow everything down. + +**Q: Can I modify the input/output in a callback?** + +A: You *can*, but you *shouldn't*. Callbacks are for observation, not transformation. Transformations belong in the Runnable logic. + +**Q: How do I pass authentication through chains?** + +A: Use metadata: +```javascript +const config = { + metadata: { + authToken: req.headers.authorization + } +}; +``` + +--- + +**Built with ❤️ for learners who want to understand AI agents deeply** + +[← Previous: LLM Wrapper](03-llm-wrapper.md) | [Tutorial Index](../README.md) | [Next: Prompts →](../02-composition/01-prompts.md) \ No newline at end of file diff --git a/tutorial/01-foundation/04-context/solutions/13-simple-logger-solution.js b/tutorial/01-foundation/04-context/solutions/13-simple-logger-solution.js new file mode 100644 index 0000000000000000000000000000000000000000..aa43f6eeed45c270c13df1ea3acf96d702d6da0f --- /dev/null +++ b/tutorial/01-foundation/04-context/solutions/13-simple-logger-solution.js @@ -0,0 +1,133 @@ +/** + * Solution 13: Build a Simple Logging Callback + * + * Goal: Understand the basic callback lifecycle + * + * This is the foundation of observability in your framework! + */ + +import {Runnable} from '../../../../src/index.js'; +import {BaseCallback} from '../../../../src/utils/callbacks.js'; + +class SimpleLoggerCallback extends BaseCallback { + constructor(options = {}) { + super(); + this.showTimestamp = options.showTimestamp ?? false; + } + + async onStart(runnable, input, config) { + // Your code here + console.log(`▶️ Starting: ${runnable.name}`) + console.log(` Input: ${input}`) + } + + async onEnd(runnable, output, config) { + // Your code here + console.log(`✔️ Completed: ${runnable.name}`) + console.log(` Output: ${output}`) + } + + async onError(runnable, error, config) { + // Your code here + console.log(` ❌ ${runnable.name}: ${error.message}`) + } +} + +// Test Runnables +class GreeterRunnable extends Runnable { + async _call(input, config) { + return `Hello, ${input}!`; + } +} + +class UpperCaseRunnable extends Runnable { + async _call(input, config) { + if (typeof input !== 'string') { + throw new Error('Input must be a string'); + } + return input.toUpperCase(); + } +} + +class ErrorRunnable extends Runnable { + async _call(input, config) { + throw new Error('Intentional error for testing'); + } +} + +async function exercise1() { + console.log('=== Exercise 1: Simple Logging Callback ===\n'); + + const logger = new SimpleLoggerCallback(); + const config = { + callbacks: [logger] + }; + + // Test 1: Normal execution + console.log('--- Test 1: Normal Execution ---'); + const greeter = new GreeterRunnable(); + const result1 = await greeter.invoke("World", config); + console.log('Final result:', result1); + console.log(); + + // Test 2: Pipeline + console.log('--- Test 2: Pipeline ---'); + const upper = new UpperCaseRunnable(); + const pipeline = greeter.pipe(upper); + const result2 = await pipeline.invoke("claude", config); + console.log('Final result:', result2); + console.log(); + + // Test 3: Error handling + console.log('--- Test 3: Error Handling ---'); + const errorRunnable = new ErrorRunnable(); + try { + await errorRunnable.invoke("test", config); + } catch (error) { + console.log('Caught error (expected):', error.message); + } + + console.log('\n✓ Exercise 1 complete!'); +} + +// Run the exercise +exercise1().catch(console.error); + +/** + * Expected Output: + * + * --- Test 1: Normal Execution --- + * ▶️ Starting: GreeterRunnable + * Input: World + * ✔️ Completed: GreeterRunnable + * Output: Hello, World! + * Final result: Hello, World! + * + * --- Test 2: Pipeline --- + * ▶️ Starting: RunnableSequence + * Input: claude + * ▶️ Starting: GreeterRunnable + * Input: claude + * ✔️ Completed: GreeterRunnable + * Output: Hello, claude! + * ▶️ Starting: UpperCaseRunnable + * Input: Hello, claude! + * ✔️ Completed: UpperCaseRunnable + * Output: HELLO, CLAUDE! + * ✔️ Completed: RunnableSequence + * Output: HELLO, CLAUDE! + * Final result: HELLO, CLAUDE! + * + * --- Test 3: Error Handling --- + * ▶️ Starting: ErrorRunnable + * Input: test + * ❌ ErrorRunnable: Intentional error for testing + * Caught error (expected): Intentional error for testing + * + * Learning Points: + * 1. Callbacks see every step in execution + * 2. onStart fires before _call() + * 3. onEnd fires after successful _call() + * 4. onError fires when _call() throws error + * 5. Callbacks don't change the output - they just observe + */ \ No newline at end of file diff --git a/tutorial/01-foundation/04-context/solutions/14-metrics-metadata-solution.js b/tutorial/01-foundation/04-context/solutions/14-metrics-metadata-solution.js new file mode 100644 index 0000000000000000000000000000000000000000..033642682a1cc4c4f84160195a047cf666208444 --- /dev/null +++ b/tutorial/01-foundation/04-context/solutions/14-metrics-metadata-solution.js @@ -0,0 +1,193 @@ +/** + * Exercise 14: Build a Metrics Tracker with Metadata + * + * Goal: Learn to use config metadata and track metrics + * + * In this exercise, you'll: + * 1. Build a stateful callback that tracks metrics + * 2. Use config.metadata to track user information + * 3. Measure execution time for each Runnable + * 4. Generate a summary report + * + * This teaches you how to pass context through your chains! + */ + +import {Runnable} from '../../../../src/index.js'; +import {BaseCallback} from '../../../../src/utils/callbacks.js'; + +class MetricsTrackerCallback extends BaseCallback { + constructor() { + super(); + + // Track when each call started + this.startTimes = new Map(); + + // Metrics per runnable + this.metrics = {}; + } + + async onStart(runnable, input, config) { + const key = `${runnable.constructor.name}_${Date.now()}_${Math.random()}`; + this.startTimes.set(key, Date.now()); + + const userId = config?.metadata?.userId; + + const runnableName = runnable.constructor.name; + if (!this.metrics[runnableName]) { + this.metrics[runnableName] = { + totalCalls: 0, + totalTime: 0, + users: new Set() + }; + } + + this.metrics[runnableName].totalCalls++; + if (userId) { + this.metrics[runnableName].users.add(userId); + } + + // Store key for matching in onEnd + config._metricsKey = key; + } + + async onEnd(runnable, output, config) { + const key = config._metricsKey; + const startTime = this.startTimes.get(key); + + if (startTime) { + const duration = Date.now() - startTime; + const runnableName = runnable.constructor.name; + + if (this.metrics[runnableName]) { + this.metrics[runnableName].totalTime += duration; + } + + this.startTimes.delete(key); + } + } + + async onError(runnable, error, config) { + const key = config._metricsKey; + if (key) { + this.startTimes.delete(key); + } + } + + getReport() { + const report = {}; + + for (const [name, data] of Object.entries(this.metrics)) { + report[name] = { + calls: data.totalCalls, + totalTime: data.totalTime, + avgTime: data.totalCalls > 0 ? Math.round(data.totalTime / data.totalCalls) : 0, + users: Array.from(data.users) + }; + } + + return report; + } + + printReport() { + console.log('\n📊 Metrics Report:'); + console.log('─'.repeat(60)); + + const report = this.getReport(); + + for (const [name, data] of Object.entries(report)) { + console.log(`${name}:`); + console.log(` Calls: ${data.calls}`); + console.log(` Total Time: ${data.totalTime}ms`); + console.log(` Avg Time: ${data.avgTime}ms`); + console.log(` Users: ${data.users.join(', ')}`); + console.log(''); + } + } +} + +// Test Runnables with different speeds +class FastRunnable extends Runnable { + async _call(input, config) { + await new Promise(resolve => setTimeout(resolve, 100)); + return `Fast: ${input}`; + } +} + +class SlowRunnable extends Runnable { + async _call(input, config) { + await new Promise(resolve => setTimeout(resolve, 500)); + return `Slow: ${input}`; + } +} + +class MediumRunnable extends Runnable { + async _call(input, config) { + await new Promise(resolve => setTimeout(resolve, 250)); + return `Medium: ${input}`; + } +} + +async function exercise() { + console.log('=== Exercise 14: Metrics Tracker with Metadata ===\n'); + + const metrics = new MetricsTrackerCallback(); + + const fast = new FastRunnable(); + const medium = new MediumRunnable(); + const slow = new SlowRunnable(); + + // Test 1: User 1 makes some calls + console.log('--- User 1 making calls ---'); + await fast.invoke('test1', { callbacks: [metrics], metadata: { userId: "user_123" } }); + await medium.invoke('test2', { callbacks: [metrics], metadata: { userId: "user_123" } }); + await fast.invoke('test3', { callbacks: [metrics], metadata: { userId: "user_123" } }); + + // Test 2: User 2 makes different calls + console.log('--- User 2 making calls ---'); + await slow.invoke('test4', { callbacks: [metrics], metadata: { userId: "user_456" } }); + await fast.invoke('test5', { callbacks: [metrics], metadata: { userId: "user_456" } }); + + // Test 3: User 3 makes calls + console.log('--- User 3 making calls ---'); + await medium.invoke('test6', { callbacks: [metrics], metadata: { userId: "user_789" } }); + await slow.invoke('test7', { callbacks: [metrics], metadata: { userId: "user_789" } }); + await medium.invoke('test8', { callbacks: [metrics], metadata: { userId: "user_789" } }); + + metrics.printReport(); + + console.log('\n✓ Exercise 2 complete!'); +} + +// Run the exercise +exercise().catch(console.error); + +/** + * Expected Output: + * + * 📊 Metrics Report: + * ──────────────────────────────────────────────────────────── + * FastRunnable: + * Calls: 3 + * Total Time: 305ms + * Avg Time: 102ms + * Users: user_123, user_456 + * + * SlowRunnable: + * Calls: 2 + * Total Time: 1008ms + * Avg Time: 504ms + * Users: user_456, user_789 + * + * MediumRunnable: + * Calls: 3 + * Total Time: 756ms + * Avg Time: 252ms + * Users: user_123, user_789 + * + * Learning Points: + * 1. Stateful callbacks can accumulate data + * 2. config.metadata passes arbitrary context + * 3. Useful for tracking per-user metrics + * 4. Map.set() and Map.get() for tracking start times + * 5. Report generation for observability + */ \ No newline at end of file diff --git a/tutorial/01-foundation/04-context/solutions/15-config-inheritance-solution.js b/tutorial/01-foundation/04-context/solutions/15-config-inheritance-solution.js new file mode 100644 index 0000000000000000000000000000000000000000..d1d91713f12aa1575de2d0b53b18d67147fe8429 --- /dev/null +++ b/tutorial/01-foundation/04-context/solutions/15-config-inheritance-solution.js @@ -0,0 +1,202 @@ +/** + * Exercise 15: Config Merging and Child Configs + * + * Goal: Understand how configs inherit and merge + * + * In this exercise, you'll: + * 1. Create parent and child configs + * 2. See how child configs inherit from parents + * 3. Understand callback accumulation + * 4. Learn when to use config.merge() vs config.child() + * + * This is crucial for nested Runnable calls! + */ + +import {RunnableConfig} from '../../../../src/core/context.js'; +import {Runnable} from '../../../../src/index.js'; +import {BaseCallback} from '../../../../src/utils/callbacks.js'; + +// Simple callback to show when it's called +class TagLoggerCallback extends BaseCallback { + constructor(name) { + super(); + this.name = name; + } + + async onStart(runnable, input, config) { + console.log(`[${this.name}] Starting ${runnable.constructor.name}`); + console.log(` Tags: [${config.tags.join(', ')}]`); + console.log(` Callback count: ${config.callbacks.length}`); + } + + async onEnd(runnable, output, config) { + console.log(`[${this.name}] Completed ${runnable.constructor.name}`); + } +} + +// Test Runnables that create child configs +class Step1Runnable extends Runnable { + async _call(input, config) { + console.log('\n--- Inside Step1 ---'); + + const childConfig = config.child({ tags: ['step1'] }); + + console.log(`Step1 child has ${childConfig.callbacks.length} callbacks`); + + // Simulate nested work + return `Step1(${input})`; + } +} + +class Step2Runnable extends Runnable { + async _call(input, config) { + console.log('\n--- Inside Step2 ---'); + + const childConfig = config.child({ tags: ['step2'] }); + + console.log(`Step2 child has ${childConfig.callbacks.length} callbacks`); + + return `Step2(${input})`; + } +} + +class Step3Runnable extends Runnable { + async _call(input, config) { + console.log('\n--- Inside Step3 ---'); + + const childConfig = config.child({ + tags: ['step3'], + metadata: { nested: true } + }); + + console.log(`Step3 tags: [${childConfig.tags.join(', ')}]`); + console.log(`Step3 metadata:`, childConfig.metadata); + + return `Step3(${input})`; + } +} + +async function exercise() { + console.log('=== Exercise 15: Config Merging and Child Configs ===\n'); + + // Part 1: Basic config inheritance + console.log('--- Part 1: Basic Inheritance ---\n'); + + const parentConfig = new RunnableConfig({ + callbacks: [new TagLoggerCallback('Parent')], + tags: ['base'] + }); + + const childConfig = parentConfig.child({ + callbacks: [new TagLoggerCallback('Child')], + tags: ['child'] + }); + + console.log('Parent callbacks:', parentConfig.callbacks.length); + console.log('Child callbacks:', childConfig.callbacks.length); + console.log('Parent tags:', parentConfig.tags); + console.log('Child tags:', childConfig.tags); + + // Part 2: Config in pipelines + console.log('\n--- Part 2: Config in Pipelines ---\n'); + + const pipelineConfig = new RunnableConfig({ + callbacks: [new TagLoggerCallback('Pipeline')], + tags: ['base'] + }); + + const step1 = new Step1Runnable(); + const step2 = new Step2Runnable(); + const pipeline = step1.pipe(step2); + + await pipeline.invoke("test", pipelineConfig); + + // Part 3: Multiple levels of nesting + console.log('\n--- Part 3: Multiple Nesting Levels ---\n'); + + const level1Config = new RunnableConfig({ + callbacks: [new TagLoggerCallback('Level1')], + tags: ['level1'], + metadata: {level: 1} + }); + + const level2Config = level1Config.child({ + callbacks: [new TagLoggerCallback('Level2')], + tags: ['level2'], + metadata: {level: 2} + }); + + const level3Config = level2Config.child({ + callbacks: [new TagLoggerCallback('Level3')], + tags: ['level3'], + metadata: {level: 3} + }); + + console.log('Level 1 - Callbacks:', level1Config.callbacks.length, 'Tags:', level1Config.tags); + console.log('Level 2 - Callbacks:', level2Config.callbacks.length, 'Tags:', level2Config.tags); + console.log('Level 3 - Callbacks:', level3Config.callbacks.length, 'Tags:', level3Config.tags); + + // Part 4: merge() vs child() + console.log('\n--- Part 4: merge() vs child() ---\n'); + + const configA = new RunnableConfig({ + tags: ['a'], + metadata: {source: 'A'} + }); + + const configB = new RunnableConfig({ + tags: ['b'], + metadata: {source: 'B', extra: 'data'} + }); + + const merged = configA.merge(configB); + + const child = configA.child({ + tags: ['b'], + metadata: { extra: 'data' } + }); + + console.log('Merged metadata:', merged.metadata); + console.log('Child metadata:', child.metadata); + + console.log('\n✓ Exercise 15 complete!'); +} + +// Run the exercise +exercise().catch(console.error); + +/** + * Expected Output Snippets: + * + * --- Part 1: Basic Inheritance --- + * Parent callbacks: 1 + * Child callbacks: 2 + * Parent tags: ['base'] + * Child tags: ['base', 'child'] + * + * --- Part 2: Config in Pipelines --- + * [Pipeline] Starting Step1Runnable + * Tags: [base] + * Callback count: 1 + * + * --- Inside Step1 --- + * Step1 child has 1 callbacks + * [Pipeline] Completed Step1Runnable + * + * --- Part 3: Multiple Nesting Levels --- + * Level 1 - Callbacks: 1, Tags: ['level1'] + * Level 2 - Callbacks: 2, Tags: ['level1', 'level2'] + * Level 3 - Callbacks: 3, Tags: ['level1', 'level2', 'level3'] + * + * --- Part 4: merge() vs child() --- + * Merged metadata: { source: 'B', extra: 'data' } + * Child metadata: { source: 'A', extra: 'data' } + * + * Learning Points: + * 1. child() creates a new config inheriting from parent + * 2. Callbacks accumulate (child has parent's + its own) + * 3. Tags accumulate (arrays concatenate) + * 4. Metadata merges (child overrides parent keys) + * 5. merge() treats both configs equally + * 6. child() treats parent as base, child as override + */ \ No newline at end of file diff --git a/tutorial/01-foundation/04-context/solutions/16-runtime-config-solution.js b/tutorial/01-foundation/04-context/solutions/16-runtime-config-solution.js new file mode 100644 index 0000000000000000000000000000000000000000..1410b75e76c6188bd19a846bb6e8255d0a15b875 --- /dev/null +++ b/tutorial/01-foundation/04-context/solutions/16-runtime-config-solution.js @@ -0,0 +1,215 @@ +/** + * Exercise 16: Runtime Configuration Override + * + * Goal: Learn to override settings at runtime using config.configurable + * + * In this exercise, you'll: + * 1. Build a Runnable that reads from config.configurable + * 2. Override default settings at runtime + * 3. Test different configurations without changing code + * 4. Understand A/B testing with configs + * + * This is how you change LLM temperature, max tokens, etc. at runtime! + */ + +import { RunnableConfig } from '../../../../src//core/context.js'; +import {Runnable} from '../../../../src/index.js'; +import {BaseCallback} from '../../../../src/utils/callbacks.js'; + +class TextProcessorRunnable extends Runnable { + constructor(options = {}) { + super(); + // Set defaults + this.defaultMaxLength = options.maxLength ?? 50; + this.defaultUppercase = options.uppercase ?? false; + this.defaultPrefix = options.prefix ?? ''; + } + + async _call(input, config) { + const maxLength = config.configurable?.maxLength ?? this.defaultMaxLength; + const uppercase = config.configurable?.uppercase ?? this.defaultUppercase; + const prefix = config.configurable?.prefix ?? this.defaultPrefix; + + // Process text + let result = input; + + // Apply prefix + if (prefix) { + result = prefix + result; + } + + // Apply uppercase + if (uppercase) { + result = result.toUpperCase(); + } + + // Apply truncation + if (result.length > maxLength) { + result = result.substring(0, maxLength) + '...'; + } + + return result; + } +} + +// Callback to show what config was used +class ConfigLoggerCallback extends BaseCallback { + async onStart(runnable, input, config) { + if (config.configurable && Object.keys(config.configurable).length > 0) { + console.log(`📋 Runtime config:`, config.configurable); + } + } +} + +async function exercise() { + console.log('=== Exercise 16: Runtime Configuration Override ===\n'); + + const processor = new TextProcessorRunnable({ maxLength: 50 }); + + const logger = new ConfigLoggerCallback(); + + const longText = "The quick brown fox jumps over the lazy dog. This is a longer sentence to test truncation and various configuration options."; + + // Test 1: Use defaults + console.log('--- Test 1: Using Defaults ---'); + const result1 = await processor.invoke(longText, { callbacks: [logger] }); + console.log('Result:', result1); + console.log(); + + // Test 2: Override maxLength + console.log('--- Test 2: Override maxLength ---'); + const result2 = await processor.invoke(longText, { + callbacks: [logger], + configurable: { maxLength: 20 } + }); + console.log('Result:', result2); + console.log(); + + // Test 3: Override multiple settings + console.log('--- Test 3: Override Multiple Settings ---'); + const result3 = await processor.invoke(longText, { + callbacks: [logger], + configurable: { uppercase: true, maxLength: 30 } + }); + console.log('Result:', result3); + console.log(); + + // Test 4: Add prefix at runtime + console.log('--- Test 4: Add Prefix at Runtime ---'); + const result4 = await processor.invoke(longText, { + callbacks: [logger], + configurable: { prefix: '[PREFIX] ', maxLength: 40 } + }); + console.log('Result:', result4); + console.log(); + + // Test 5: A/B Testing scenario + console.log('--- Test 5: A/B Testing Different Configs ---'); + + const configA = new RunnableConfig({ + callbacks: [logger], + configurable: { maxLength: 25, uppercase: false }, + metadata: { variant: 'A', experiment: 'text-processing' } + }); + + const configB = new RunnableConfig({ + callbacks: [logger], + configurable: { maxLength: 40, uppercase: true }, + metadata: { variant: 'B', experiment: 'text-processing' } + }); + + const testText = "Testing A/B configuration variants"; + + const resultA = await processor.invoke(testText, configA); + const resultB = await processor.invoke(testText, configB); + + console.log('Variant A:', resultA); + console.log('Variant B:', resultB); + console.log(); + + // Test 6: Simulating LLM-style configuration + console.log('--- Test 6: LLM-Style Temperature Override ---'); + + // Create a mock LLM runnable + class MockLLMRunnable extends Runnable { + constructor(defaultTemp = 0.7) { + super(); + this.defaultTemperature = defaultTemp; + } + + async _call(input, config) { + const temperature = config.configurable?.temperature ?? this.defaultTemperature; + + // Simulate different outputs based on temperature + if (temperature < 0.3) { + return `[temp=${temperature}] Deterministic response: ${input}`; + } else if (temperature > 0.8) { + return `[temp=${temperature}] Creative response about ${input}!!!`; + } else { + return `[temp=${temperature}] Balanced response: ${input}.`; + } + } + } + + const llm = new MockLLMRunnable(); + + console.log('Low temp (0.1):'); + const low = await llm.invoke("AI", { configurable: { temperature: 0.1 } }); + console.log(low); + + console.log('\nMedium temp (0.7):'); + const med = await llm.invoke("AI", { configurable: { temperature: 0.7 } }); + console.log(med); + + console.log('\nHigh temp (1.0):'); + const high = await llm.invoke("AI", { configurable: { temperature: 1.0 } }); + console.log(high); + + console.log('\n✓ Exercise 16 complete!'); +} + +// Run the exercise +exercise().catch(console.error); + +/** + * Expected Output: + * + * --- Test 1: Using Defaults --- + * Result: The quick brown fox jumps over the lazy dog. Thi... + * + * --- Test 2: Override maxLength --- + * 📋 Runtime config: { maxLength: 20 } + * Result: The quick brown fox ... + * + * --- Test 3: Override Multiple Settings --- + * 📋 Runtime config: { uppercase: true, maxLength: 30 } + * Result: THE QUICK BROWN FOX JUMPS O... + * + * --- Test 4: Add Prefix at Runtime --- + * 📋 Runtime config: { prefix: '[PREFIX] ', maxLength: 40 } + * Result: [PREFIX] The quick brown fox jumps ove... + * + * --- Test 5: A/B Testing Different Configs --- + * 📋 Runtime config: { maxLength: 25, uppercase: false } + * Variant A: Testing A/B configurati... + * 📋 Runtime config: { maxLength: 40, uppercase: true } + * Variant B: TESTING A/B CONFIGURATION VARIANTS + * + * --- Test 6: LLM-Style Temperature Override --- + * Low temp (0.1): + * [temp=0.1] Deterministic response: AI + * + * Medium temp (0.7): + * [temp=0.7] Balanced response: AI. + * + * High temp (1.0): + * [temp=1.0] Creative response about AI!!! + * + * Learning Points: + * 1. config.configurable holds runtime overrides + * 2. Use ?? operator for defaults: config.configurable?.key ?? default + * 3. Don't modify instance defaults - just use config value + * 4. Perfect for A/B testing different settings + * 5. This is how LLMs change temperature/maxTokens at runtime + * 6. Combine with metadata to track which config was used + */ \ No newline at end of file diff --git a/tutorial/02-composition/01-prompts/exercises/17-prompt-template.js b/tutorial/02-composition/01-prompts/exercises/17-prompt-template.js new file mode 100644 index 0000000000000000000000000000000000000000..63d0a122edf1520a3592d78e66491dd4562fe3c0 --- /dev/null +++ b/tutorial/02-composition/01-prompts/exercises/17-prompt-template.js @@ -0,0 +1,175 @@ +/** + * Exercise 17: Basic PromptTemplate + * + * Goal: Build a PromptTemplate that replaces {variable} placeholders + * + * In this exercise, you'll: + * 1. Implement variable extraction from template strings + * 2. Replace placeholders with actual values + * 3. Validate that all required variables are provided + * 4. Test with various template patterns + * + * This is the foundation of all prompt engineering! + */ + +import {PromptTemplate} from '../../../../src/index.js'; + +// Test cases +async function exercise() { + console.log('=== Exercise 1: Basic PromptTemplate ===\n'); + + // Test 1: Simple translation prompt + console.log('--- Test 1: Simple Translation ---'); + + // TODO: Create a translation prompt template + // Template: "Translate to {language}: {text}" + const translatePrompt = null; // new PromptTemplate({ ... }) + + // TODO: Format with Spanish and "Hello, world!" + // const result1 = await translatePrompt.format({ ... }) + + console.log('Input: "Hello, world!" → Spanish'); + console.log('Result:', 'TODO'); // result1 + console.log(); + + // Test 2: Email template with multiple variables + console.log('--- Test 2: Email Template ---'); + + // TODO: Create an email template + // Template: "Dear {name},\n\nThank you for {action}. Your {item} will arrive on {date}.\n\nBest,\n{sender}" + const emailPrompt = null; // PromptTemplate.fromTemplate(...) + + // TODO: Format with appropriate values + // const result2 = await emailPrompt.format({ ... }) + + console.log('Result:', 'TODO'); // result2 + console.log(); + + // Test 3: Auto-detect variables + console.log('--- Test 3: Auto-Detect Variables ---'); + + // TODO: Create a template WITHOUT specifying inputVariables + // Let the class auto-detect them from the template string + const autoPrompt = null; // new PromptTemplate({ template: "..." }) + + console.log('Detected variables:', []); // autoPrompt.inputVariables + + // TODO: Format it + // const result3 = await autoPrompt.format({ ... }) + console.log('Result:', 'TODO'); // result3 + console.log(); + + // Test 4: Partial variables (defaults) + console.log('--- Test 4: Partial Variables ---'); + + // TODO: Create a prompt with partial variables + // Template: "You are a {role} assistant. User: {input}" + // Partial: { role: "helpful" } - this is pre-filled + const partialPrompt = null; // new PromptTemplate({ + // template: "...", + // partialVariables: { role: "helpful" } + // }) + + // TODO: Format - only need to provide 'input', 'role' uses default + // const result4 = await partialPrompt.format({ input: "What's the weather?" }) + + console.log('Result:', 'TODO'); // result4 + console.log(); + + // Test 5: Validation error + console.log('--- Test 5: Validation Error ---'); + + // TODO: Try to format without providing all variables + const strictPrompt = new PromptTemplate({ + template: "Hello {name}, you are {age} years old", + inputVariables: ["name", "age"] + }); + + try { + // TODO: This should throw an error - only provide 'name' + // await strictPrompt.format({ name: "Alice" }) + console.log('ERROR: Should have thrown validation error!'); + } catch (error) { + console.log('✓ Validation error caught:', error.message); + } + console.log(); + + // Test 6: Use as Runnable (invoke) + console.log('--- Test 6: Use as Runnable ---'); + + // TODO: Create a simple prompt + const runnablePrompt = null; // PromptTemplate.fromTemplate("...") + + // TODO: Use invoke() instead of format() + // Since PromptTemplate extends Runnable, it has invoke() + // const result6 = await runnablePrompt.invoke({ ... }) + + console.log('Invoked result:', 'TODO'); // result6 + console.log(); + + // Test 7: Complex nested replacement + console.log('--- Test 7: Complex Template ---'); + + // TODO: Create a code documentation template + // Template with function, params, returns, description + const docPrompt = null; // PromptTemplate.fromTemplate(` + // Function: {function} + // Parameters: {params} + // Returns: {returns} + // Description: {description} + // `) + + // TODO: Format with actual documentation + // const result7 = await docPrompt.format({ ... }) + + console.log('Result:', 'TODO'); // result7 + console.log(); + + console.log('✓ Exercise 1 complete!'); +} + +// Run the exercise +exercise().catch(console.error); + +/** + * Expected Output: + * + * --- Test 1: Simple Translation --- + * Input: "Hello, world!" → Spanish + * Result: Translate to Spanish: Hello, world! + * + * --- Test 2: Email Template --- + * Result: Dear John, + * + * Thank you for your purchase. Your laptop will arrive on Friday. + * + * Best, + * Customer Service + * + * --- Test 3: Auto-Detect Variables --- + * Detected variables: ['city', 'activity'] + * Result: In Paris, I love to visit museums + * + * --- Test 4: Partial Variables --- + * Result: You are a helpful assistant. User: What's the weather? + * + * --- Test 5: Validation Error --- + * ✓ Validation error caught: Missing required input variables: age + * + * --- Test 6: Use as Runnable --- + * Invoked result: Search for artificial intelligence + * + * --- Test 7: Complex Template --- + * Result: Function: calculateSum + * Parameters: a: number, b: number + * Returns: number + * Description: Adds two numbers together + * + * Learning Points: + * 1. PromptTemplate replaces {variable} placeholders + * 2. Auto-detection extracts variables from template + * 3. Partial variables provide defaults + * 4. Validation ensures all variables are provided + * 5. As a Runnable, prompts work with invoke() + * 6. Regex is key: /\{(\w+)\}/g finds all variables + */ \ No newline at end of file diff --git a/tutorial/02-composition/01-prompts/exercises/18-chat-prompt-template.js b/tutorial/02-composition/01-prompts/exercises/18-chat-prompt-template.js new file mode 100644 index 0000000000000000000000000000000000000000..7855cfd30b7000f37c6878c33e1835af5b34c142 --- /dev/null +++ b/tutorial/02-composition/01-prompts/exercises/18-chat-prompt-template.js @@ -0,0 +1,190 @@ +/** + * Exercise 18: ChatPromptTemplate + * + * Goal: Build structured chat conversations with role-based messages + * + * In this exercise, you'll: + * 1. Support multiple message roles in one template + * 2. Extract variables from all messages + * 3. Create reusable chat patterns + * + * This is how modern LLM chat interfaces work! + */ + +import {ChatPromptTemplate} from '../../../../src/index.js'; + +// Test cases +async function exercise() { + console.log('=== Exercise 2: ChatPromptTemplate ===\n'); + + // Test 1: Simple chat with system and human messages + console.log('--- Test 1: Basic Chat ---'); + + // TODO: Create a chat prompt with system and human messages + // System: "You are a {role} assistant" + // Human: "{question}" + const chatPrompt1 = null; // ChatPromptTemplate.fromMessages([...]) + + // TODO: Format with values + // const messages1 = await chatPrompt1.format({ ... }) + + // TODO: Print each message + console.log('Messages:'); + // messages1.forEach(msg => console.log(` ${msg}`)) + console.log(); + + // Test 2: Multi-turn conversation + console.log('--- Test 2: Multi-Turn Conversation ---'); + + // TODO: Create a conversation with system, human, ai, and human + // System: "You are a {personality} chatbot" + // Human: "Hi, I'm {name}" + // AI: "Nice to meet you, {name}!" + // Human: "Can you help me with {topic}?" + const chatPrompt2 = null; // ChatPromptTemplate.fromMessages([...]) + + console.log('Detected variables:', []); // chatPrompt2.inputVariables + + // TODO: Format with values + // const messages2 = await chatPrompt2.format({ ... }) + + console.log('\nConversation:'); + // messages2.forEach(msg => console.log(` ${msg}`)) + console.log(); + + // Test 3: Translation bot template + console.log('--- Test 3: Translation Bot ---'); + + // TODO: Create a specialized translation chat + // System: "You are a translator. Translate from {source_lang} to {target_lang}." + // Human: "Translate: {text}" + const translateChat = null; // ChatPromptTemplate.fromMessages([...]) + + // TODO: Format with translation request + // const messages3 = await translateChat.format({ ... }) + + console.log('Translation request:'); + // messages3.forEach(msg => console.log(` ${msg}`)) + console.log(); + + // Test 4: Customer service template + console.log('--- Test 4: Customer Service ---'); + + // TODO: Create customer service chat template + // System: "You are a {company} customer service agent. Be {tone}." + // Human: "Order #{order_id}: {issue}" + const serviceChat = null; // ChatPromptTemplate.fromMessages([...]) + + // TODO: Format with customer issue + // const messages4 = await serviceChat.format({ ... }) + + console.log('Service interaction:'); + // messages4.forEach(msg => console.log(` ${msg}`)) + console.log(); + + // Test 5: Use as Runnable + console.log('--- Test 5: Use as Runnable ---'); + + // TODO: Create a simple chat prompt + const runnableChat = null; // ChatPromptTemplate.fromMessages([...]) + + // TODO: Use invoke() instead of format() + // const messages5 = await runnableChat.invoke({ ... }) + + console.log('Invoked messages:'); + // messages5.forEach(msg => console.log(` ${msg}`)) + console.log(); + + // Test 6: Validation + console.log('--- Test 6: Validation ---'); + + const strictChat = ChatPromptTemplate.fromMessages([ + ["system", "You need {var1} and {var2}"], + ["human", "Using {var3}"] + ]); + + console.log('Required variables:', []); // strictChat.inputVariables + + try { + // TODO: Try to format without all variables + // await strictChat.format({ var1: "one" }) + console.log('ERROR: Should have thrown!'); + } catch (error) { + console.log('✓ Validation error:', error.message); + } + console.log(); + + // Test 7: Code review chat + console.log('--- Test 7: Code Review Chat ---'); + + // TODO: Create a code review chat template + // System: "You are a {language} code reviewer. Focus on {focus}." + // Human: "Review this code:\n{code}" + // AI: "I'll review your {language} code for {focus}." + const reviewChat = null; // ChatPromptTemplate.fromMessages([...]) + + // TODO: Format with code review request + // const messages7 = await reviewChat.format({ ... }) + + console.log('Code review chat:'); + // messages7.forEach(msg => console.log(` ${msg}`)) + console.log(); + + console.log('✓ Exercise 2 complete!'); +} + +// Run the exercise +exercise().catch(console.error); + +/** + * Expected Output: + * + * --- Test 1: Basic Chat --- + * Messages: + * [system]: You are a helpful assistant + * [human]: What's the weather? + * + * --- Test 2: Multi-Turn Conversation --- + * Detected variables: ['personality', 'name', 'topic'] + * + * Conversation: + * [system]: You are a friendly chatbot + * [human]: Hi, I'm Alice + * [ai]: Nice to meet you, Alice! + * [human]: Can you help me with JavaScript? + * + * --- Test 3: Translation Bot --- + * Translation request: + * [system]: You are a translator. Translate from English to Spanish. + * [human]: Translate: Hello, world! + * + * --- Test 4: Customer Service --- + * Service interaction: + * [system]: You are a TechCorp customer service agent. Be professional and empathetic. + * [human]: Order #12345: My item hasn't arrived + * + * --- Test 5: Use as Runnable --- + * Invoked messages: + * [system]: You are a math tutor + * [human]: Explain calculus + * + * --- Test 6: Validation --- + * Required variables: ['var1', 'var2', 'var3'] + * ✓ Validation error: Missing required input variables: var2, var3 + * + * --- Test 7: Code Review Chat --- + * Code review chat: + * [system]: You are a Python code reviewer. Focus on performance. + * [human]: Review this code: + * def slow_sum(n): return sum([i for i in range(n)]) + * [ai]: I'll review your Python code for performance. + * + * Learning Points: + * 1. ChatPromptTemplate creates structured conversations + * 2. Each message has a role: system, human, ai + * 3. Variables can span multiple messages + * 4. Auto-extraction finds all variables across messages + * 5. Message classes provide type safety + * 6. Perfect for building chat interfaces + * 7. Reusable patterns for different domains + */ \ No newline at end of file diff --git a/tutorial/02-composition/01-prompts/exercises/19-few-shot-prompt-template.js b/tutorial/02-composition/01-prompts/exercises/19-few-shot-prompt-template.js new file mode 100644 index 0000000000000000000000000000000000000000..a9861aa1c3bb522f0c8d0ed21b8d028401505768 --- /dev/null +++ b/tutorial/02-composition/01-prompts/exercises/19-few-shot-prompt-template.js @@ -0,0 +1,297 @@ +/** + * Exercise 19: FewShotPromptTemplate + * + * Goal: Build prompts with examples (few-shot learning) + * + * In this exercise, you'll: + * 1. Format examples with an example template + * 2. Combine prefix + examples + suffix + * 3. Learn how few-shot learning improves LLM outputs + * 4. Test with various domains (math, translation, classification) + * + * Few-shot prompting is one of the most powerful techniques! + */ + +import {PromptTemplate, FewShotPromptTemplate} from '../../../../src/index.js'; + +// Test cases +async function exercise3() { + console.log('=== Exercise 19: FewShotPromptTemplate ===\n'); + + // Test 1: Antonym generator + console.log('--- Test 1: Antonym Generator ---'); + + // TODO: Create example template for antonyms + // Template: "Input: {input}\nOutput: {output}" + const antonymExamplePrompt = null; // new PromptTemplate({ ... }) + + // TODO: Create few-shot prompt for antonyms + // Examples: [ + // { input: "happy", output: "sad" }, + // { input: "tall", output: "short" }, + // { input: "hot", output: "cold" } + // ] + // Prefix: "Give the antonym of each word." + // Suffix: "Input: {word}\nOutput:" + const antonymPrompt = null; // new FewShotPromptTemplate({ ... }) + + // TODO: Format with a new word + // const result1 = await antonymPrompt.format({ word: "fast" }) + + console.log('Result:'); + console.log('TODO'); // result1 + console.log(); + + // Test 2: Math word problems + console.log('--- Test 2: Math Word Problems ---'); + + // TODO: Create example template + // Template: "Q: {question}\nA: {answer}" + const mathExamplePrompt = null; // new PromptTemplate({ ... }) + + // TODO: Create few-shot prompt for math + // Examples: [ + // { question: "If I have 3 apples and buy 2 more, how many do I have?", answer: "5" }, + // { question: "A train travels 60 mph for 2 hours. How far does it go?", answer: "120 miles" } + // ] + // Prefix: "Solve these word problems:" + // Suffix: "Q: {question}\nA:" + const mathPrompt = null; // new FewShotPromptTemplate({ ... }) + + // TODO: Format with new question + // const result2 = await mathPrompt.format({ + // question: "If a book costs $12 and I buy 3, how much do I spend?" + // }) + + console.log('Result:'); + console.log('TODO'); // result2 + console.log(); + + // Test 3: Sentiment classification + console.log('--- Test 3: Sentiment Classification ---'); + + // TODO: Create example template for sentiment + // Template: "Text: {text}\nSentiment: {sentiment}" + const sentimentExamplePrompt = null; // new PromptTemplate({ ... }) + + // TODO: Create few-shot prompt for sentiment + // Examples with positive, negative, neutral texts + const sentimentPrompt = null; // new FewShotPromptTemplate({ ... }) + + // TODO: Format with new text + // const result3 = await sentimentPrompt.format({ + // text: "The product is okay, nothing special." + // }) + + console.log('Result:'); + console.log('TODO'); // result3 + console.log(); + + // Test 4: Code explanation + console.log('--- Test 4: Code Explanation ---'); + + // TODO: Create example template + // Template: "Code: {code}\nExplanation: {explanation}" + const codeExamplePrompt = null; // PromptTemplate.fromTemplate(...) + + // TODO: Create few-shot prompt for code explanation + // Examples: [ + // { code: "x = x + 1", explanation: "Increment x by 1" }, + // { code: "if x > 0:", explanation: "Check if x is positive" } + // ] + const codePrompt = null; // new FewShotPromptTemplate({ ... }) + + // TODO: Format with new code + // const result4 = await codePrompt.format({ + // code: "for i in range(10):" + // }) + + console.log('Result:'); + console.log('TODO'); // result4 + console.log(); + + // Test 5: Custom separator + console.log('--- Test 5: Custom Separator ---'); + + // TODO: Create few-shot prompt with custom separator + // Use "---" as separator instead of default "\n\n" + const customSepPrompt = null; // new FewShotPromptTemplate({ + // exampleSeparator: "\n---\n", + // ... + // }) + + // TODO: Format and see custom separator + // const result5 = await customSepPrompt.format({ ... }) + + console.log('Result:'); + console.log('TODO'); // result5 + console.log(); + + // Test 6: Translation with context + console.log('--- Test 6: Translation with Context ---'); + + // TODO: Create example template for translation + // Template: "English: {english}\nSpanish: {spanish}" + const translationExamplePrompt = null; // PromptTemplate.fromTemplate(...) + + // TODO: Create few-shot prompt + // Examples with common phrases + // Prefix: "Translate English to Spanish. Context: {context}" + const translationPrompt = null; // new FewShotPromptTemplate({ ... }) + + // TODO: Format with context and new phrase + // const result6 = await translationPrompt.format({ + // context: "casual conversation", + // english: "See you later!" + // }) + + console.log('Result:'); + console.log('TODO'); // result6 + console.log(); + + // Test 7: No examples (just prefix and suffix) + console.log('--- Test 7: No Examples ---'); + + // TODO: Create few-shot prompt with empty examples array + const noExamplesPrompt = null; // new FewShotPromptTemplate({ + // examples: [], + // examplePrompt: new PromptTemplate({ template: "" }), + // prefix: "Answer the following question:", + // suffix: "Question: {question}\nAnswer:", + // inputVariables: ["question"] + // }) + + // TODO: Format - should just show prefix and suffix + // const result7 = await noExamplesPrompt.format({ + // question: "What is AI?" + // }) + + console.log('Result:'); + console.log('TODO'); // result7 + console.log(); + + // Test 8: Use as Runnable + console.log('--- Test 8: Use as Runnable ---'); + + // TODO: Create a few-shot prompt + const runnablePrompt = null; // new FewShotPromptTemplate({ ... }) + + // TODO: Use invoke() instead of format() + // const result8 = await runnablePrompt.invoke({ ... }) + + console.log('Invoked result:'); + console.log('TODO'); // result8 + console.log(); + + console.log('✓ Exercise 3 complete!'); +} + +// Run the exercise +exercise3().catch(console.error); + +/** + * Expected Output: + * + * --- Test 1: Antonym Generator --- + * Result: + * Give the antonym of each word. + * + * Input: happy + * Output: sad + * + * Input: tall + * Output: short + * + * Input: hot + * Output: cold + * + * Input: fast + * Output: + * + * --- Test 2: Math Word Problems --- + * Result: + * Solve these word problems: + * + * Q: If I have 3 apples and buy 2 more, how many do I have? + * A: 5 + * + * Q: A train travels 60 mph for 2 hours. How far does it go? + * A: 120 miles + * + * Q: If a book costs $12 and I buy 3, how much do I spend? + * A: + * + * --- Test 3: Sentiment Classification --- + * Result: + * Classify the sentiment of each text as Positive, Negative, or Neutral: + * + * Text: I love this product! + * Sentiment: Positive + * + * Text: This is terrible. + * Sentiment: Negative + * + * Text: It's okay. + * Sentiment: Neutral + * + * Text: The product is okay, nothing special. + * Sentiment: + * + * --- Test 4: Code Explanation --- + * Result: + * Explain what each line of code does: + * + * Code: x = x + 1 + * Explanation: Increment x by 1 + * + * Code: if x > 0: + * Explanation: Check if x is positive + * + * Code: for i in range(10): + * Explanation: + * + * --- Test 5: Custom Separator --- + * Result: + * Examples: + * Example 1 + * --- + * Example 2 + * --- + * Example 3 + * + * Your turn: ... + * + * --- Test 6: Translation with Context --- + * Result: + * Translate English to Spanish. Context: casual conversation + * + * English: Hello + * Spanish: Hola + * + * English: How are you? + * Spanish: ¿Cómo estás? + * + * English: See you later! + * Spanish: + * + * --- Test 7: No Examples --- + * Result: + * Answer the following question: + * + * Question: What is AI? + * Answer: + * + * --- Test 8: Use as Runnable --- + * Invoked result: + * [Similar to previous outputs] + * + * Learning Points: + * 1. Few-shot learning provides examples to guide LLMs + * 2. Structure: prefix + examples + suffix + * 3. Examples are formatted with examplePrompt + * 4. Custom separators control formatting + * 5. Can work with or without examples + * 6. Dramatically improves LLM output quality + * 7. Essential for classification, generation, and transformation tasks + * 8. Variable substitution works in prefix, suffix, and examples + */ \ No newline at end of file diff --git a/tutorial/02-composition/01-prompts/exercises/20-pipeline-prompt-template.js b/tutorial/02-composition/01-prompts/exercises/20-pipeline-prompt-template.js new file mode 100644 index 0000000000000000000000000000000000000000..389cf6142d5be9658344cf75642201f821c5fe40 --- /dev/null +++ b/tutorial/02-composition/01-prompts/exercises/20-pipeline-prompt-template.js @@ -0,0 +1,324 @@ +/** + * Exercise 20: PipelinePromptTemplate + * + * Goal: Compose multiple prompts into modular pipelines + * + * In this exercise, you'll: + * 1. Build modular prompt components + * 2. Pipe outputs of one template into another + * 3. Collect and merge input variables + * 4. Create reusable prompt building blocks + * + * This is advanced prompt engineering - building complex prompts from simple parts! + */ + +import {PromptTemplate, PipelinePromptTemplate} from '../../../../src/index.js'; + +// Test cases +async function exercise() { + console.log('=== Exercise 20: PipelinePromptTemplate ===\n'); + + // Test 1: Context + Question pattern + console.log('--- Test 1: Context + Question ---'); + + // TODO: Create a context prompt + // Template: "Context: {topic} is important because {reason}." + const contextPrompt = null; // PromptTemplate.fromTemplate(...) + + // TODO: Create a main prompt that uses the context + // Template: "{context}\n\nQuestion: {question}\nAnswer:" + const mainPrompt = null; // PromptTemplate.fromTemplate(...) + + // TODO: Create pipeline that combines them + const pipeline1 = null; // new PipelinePromptTemplate({ + // finalPrompt: mainPrompt, + // pipelinePrompts: [ + // { name: "context", prompt: contextPrompt } + // ] + // }) + + console.log('Input variables:', []); // pipeline1.inputVariables + + // TODO: Format with values + // const result1 = await pipeline1.format({ + // topic: "AI safety", + // reason: "it affects humanity's future", + // question: "What are the main concerns?" + // }) + + console.log('\nResult:'); + console.log('TODO'); // result1 + console.log(); + + // Test 2: Instructions + Examples + Query + console.log('--- Test 2: Instructions + Examples + Query ---'); + + // TODO: Create instructions prompt + const instructionsPrompt = null; // PromptTemplate.fromTemplate( + // "Instructions: {instructions}" + // ) + + // TODO: Create examples prompt + const examplesPrompt = null; // PromptTemplate.fromTemplate( + // "Examples:\n{example1}\n{example2}" + // ) + + // TODO: Create final prompt that uses both + const taskPrompt = null; // PromptTemplate.fromTemplate( + // "{instructions}\n\n{examples}\n\nNow you try:\n{query}" + // ) + + // TODO: Create pipeline + const pipeline2 = null; // new PipelinePromptTemplate({ ... }) + + // TODO: Format with values + // const result2 = await pipeline2.format({ ... }) + + console.log('Result:'); + console.log('TODO'); // result2 + console.log(); + + // Test 3: Multi-stage prompt composition + console.log('--- Test 3: Multi-Stage Composition ---'); + + // TODO: Create domain context prompt + const domainPrompt = null; // PromptTemplate.fromTemplate( + // "Domain: {domain}\nExpertise Level: {level}" + // ) + + // TODO: Create constraints prompt + const constraintsPrompt = null; // PromptTemplate.fromTemplate( + // "Constraints:\n- Max length: {max_length} words\n- Tone: {tone}" + // ) + + // TODO: Create task prompt + const taskPrompt3 = null; // PromptTemplate.fromTemplate( + // "Task: {task}" + // ) + + // TODO: Create final prompt combining all three + const finalPrompt3 = null; // PromptTemplate.fromTemplate( + // "{domain_context}\n\n{constraints}\n\n{task_description}\n\nResponse:" + // ) + + // TODO: Create pipeline with multiple stages + const pipeline3 = null; // new PipelinePromptTemplate({ + // finalPrompt: finalPrompt3, + // pipelinePrompts: [ + // { name: "domain_context", prompt: domainPrompt }, + // { name: "constraints", prompt: constraintsPrompt }, + // { name: "task_description", prompt: taskPrompt3 } + // ] + // }) + + // TODO: Format with all values + // const result3 = await pipeline3.format({ ... }) + + console.log('Result:'); + console.log('TODO'); // result3 + console.log(); + + // Test 4: Reusable components + console.log('--- Test 4: Reusable Components ---'); + + // TODO: Create reusable system context + const systemContext = null; // PromptTemplate.fromTemplate( + // "You are a {role} with expertise in {expertise}." + // ) + + // TODO: Use same component in different pipelines + const pipeline4a = null; // new PipelinePromptTemplate({ + // finalPrompt: PromptTemplate.fromTemplate("{system}\n\nTask: {task}"), + // pipelinePrompts: [{ name: "system", prompt: systemContext }] + // }) + + const pipeline4b = null; // new PipelinePromptTemplate({ + // finalPrompt: PromptTemplate.fromTemplate("{system}\n\nQuestion: {question}"), + // pipelinePrompts: [{ name: "system", prompt: systemContext }] + // }) + + // TODO: Format both with different purposes + // const result4a = await pipeline4a.format({ ... }) + // const result4b = await pipeline4b.format({ ... }) + + console.log('Pipeline A (Task):'); + console.log('TODO'); // result4a + console.log('\nPipeline B (Question):'); + console.log('TODO'); // result4b + console.log(); + + // Test 5: Dynamic content injection + console.log('--- Test 5: Dynamic Content Injection ---'); + + // TODO: Create date prompt + const datePrompt = null; // PromptTemplate.fromTemplate( + // "Current Date: {date}" + // ) + + // TODO: Create user info prompt + const userPrompt = null; // PromptTemplate.fromTemplate( + // "User: {username} (Preference: {preference})" + // ) + + // TODO: Create main prompt + const mainPrompt5 = null; // PromptTemplate.fromTemplate( + // "{date_info}\n{user_info}\n\nRequest: {request}\nResponse:" + // ) + + // TODO: Create pipeline + const pipeline5 = null; // new PipelinePromptTemplate({ ... }) + + // TODO: Format with dynamic content + // const result5 = await pipeline5.format({ + // date: new Date().toLocaleDateString(), + // username: "Alice", + // preference: "concise answers", + // request: "Explain quantum computing" + // }) + + console.log('Result:'); + console.log('TODO'); // result5 + console.log(); + + // Test 6: Nested complexity + console.log('--- Test 6: Complex Nested Pipeline ---'); + + // TODO: Create background prompt + const backgroundPrompt = null; // PromptTemplate.fromTemplate( + // "Background:\n{background}" + // ) + + // TODO: Create methodology prompt + const methodologyPrompt = null; // PromptTemplate.fromTemplate( + // "Methodology:\n{methodology}" + // ) + + // TODO: Create expected outcome prompt + const outcomePrompt = null; // PromptTemplate.fromTemplate( + // "Expected Outcome:\n{outcome}" + // ) + + // TODO: Create research proposal template + const proposalPrompt = null; // PromptTemplate.fromTemplate( + // "Research Proposal: {title}\n\n{background_section}\n\n{methodology_section}\n\n{outcome_section}\n\nSubmitted by: {author}" + // ) + + // TODO: Create complex pipeline + const pipeline6 = null; // new PipelinePromptTemplate({ ... }) + + console.log('Input variables needed:', []); // pipeline6.inputVariables + + // TODO: Format complex proposal + // const result6 = await pipeline6.format({ ... }) + + console.log('\nResult:'); + console.log('TODO'); // result6 + console.log(); + + // Test 7: Use as Runnable + console.log('--- Test 7: Use as Runnable ---'); + + // TODO: Create a simple pipeline + const runnablePipeline = null; // new PipelinePromptTemplate({ ... }) + + // TODO: Use invoke() instead of format() + // const result7 = await runnablePipeline.invoke({ ... }) + + console.log('Invoked result:'); + console.log('TODO'); // result7 + console.log(); + + console.log('✓ Exercise 4 complete!'); +} + +// Run the exercise +exercise().catch(console.error); + +/** + * Expected Output: + * + * --- Test 1: Context + Question --- + * Input variables: ['topic', 'reason', 'question'] + * + * Result: + * Context: AI safety is important because it affects humanity's future. + * + * Question: What are the main concerns? + * Answer: + * + * --- Test 2: Instructions + Examples + Query --- + * Result: + * Instructions: Translate words to Spanish + * + * Examples: + * hello → hola + * goodbye → adiós + * + * Now you try: + * cat + * + * --- Test 3: Multi-Stage Composition --- + * Result: + * Domain: Machine Learning + * Expertise Level: Advanced + * + * Constraints: + * - Max length: 100 words + * - Tone: technical + * + * Task: Explain gradient descent + * + * Response: + * + * --- Test 4: Reusable Components --- + * Pipeline A (Task): + * You are a software engineer with expertise in Python. + * + * Task: Debug this code + * + * Pipeline B (Question): + * You are a software engineer with expertise in Python. + * + * Question: What are decorators? + * + * --- Test 5: Dynamic Content Injection --- + * Result: + * Current Date: 11/20/2025 + * User: Alice (Preference: concise answers) + * + * Request: Explain quantum computing + * Response: + * + * --- Test 6: Complex Nested Pipeline --- + * Input variables: ['title', 'background', 'methodology', 'outcome', 'author'] + * + * Result: + * Research Proposal: AI in Healthcare + * + * Background: + * Current healthcare systems face challenges... + * + * Methodology: + * We will use deep learning models... + * + * Expected Outcome: + * Improved diagnosis accuracy by 20% + * + * Submitted by: Dr. Smith + * + * --- Test 7: Use as Runnable --- + * Invoked result: + * [Similar formatted output] + * + * Learning Points: + * 1. PipelinePromptTemplate composes prompts modularly + * 2. Each pipeline prompt generates a named output + * 3. Final prompt uses pipeline outputs as variables + * 4. Input variables are collected from all prompts + * 5. Pipeline outputs are NOT input variables + * 6. Enables reusable prompt components + * 7. Perfect for complex, structured prompts + * 8. Makes prompt engineering more maintainable + * 9. Can nest arbitrary levels of complexity + * 10. Separates concerns in prompt construction + */ \ No newline at end of file diff --git a/tutorial/02-composition/01-prompts/lesson.md b/tutorial/02-composition/01-prompts/lesson.md new file mode 100644 index 0000000000000000000000000000000000000000..7d2395a404fe24b6ef8fe3b94aab4e9ae4025acc --- /dev/null +++ b/tutorial/02-composition/01-prompts/lesson.md @@ -0,0 +1,1498 @@ +# Prompts: Template-Driven LLM Inputs + +**Part 2: Composition - Lesson 1** + +> Stop hardcoding prompts. Start composing them. + +## Overview + +You've been writing prompts like this: + +```javascript +const prompt = `You are a helpful assistant. The user asked: ${userInput}`; +const response = await llm.invoke(prompt); +``` + +This works, but it's fragile. What if you need: +- Different system messages for different use cases? +- To inject multiple variables? +- To reuse prompt patterns across your app? +- To validate inputs before sending to the LLM? +- To compose prompts from smaller pieces? + +**PromptTemplates** solve all of these problems. + +## Why This Matters + +### The Problem: Prompt Chaos + +Without templates, your code becomes a mess: + +```javascript +// Scattered throughout your codebase: +const prompt1 = `Translate to ${lang}: ${text}`; +const prompt2 = "Translate to " + language + ": " + input; +const prompt3 = `Translate to ${target_language}: ${user_text}`; + +// Same logic, different implementations everywhere! +``` + +Problems: +- No consistency in prompt format +- Hard to test prompts in isolation +- Can't reuse prompt patterns +- Difficult to track what prompts are being used +- No validation of variables + +### The Solution: PromptTemplate + +```javascript +const translatePrompt = new PromptTemplate({ + template: "Translate to {language}: {text}", + inputVariables: ["language", "text"] +}); + +const prompt = await translatePrompt.format({ + language: "Spanish", + text: "Hello, world!" +}); +// "Translate to Spanish: Hello, world!" +``` + +Benefits: +- ✅ Reusable prompt patterns +- ✅ Variable validation +- ✅ Testable in isolation +- ✅ Composable with other Runnables +- ✅ Type-safe variable injection + +## Learning Objectives + +By the end of this lesson, you will: + +- ✅ Build a PromptTemplate class that replaces variables +- ✅ Create ChatPromptTemplate for structured messages +- ✅ Implement Few-Shot prompts for examples +- ✅ Build Pipeline prompts for composition +- ✅ Use prompts as Runnables in chains +- ✅ Understand LangChain's prompting patterns + +## Core Concepts + +### What is a PromptTemplate? + +A PromptTemplate is a **reusable prompt pattern** with placeholders for variables. + +**Structure:** +``` +Template String: "Translate to {language}: {text}" + ↓ +Variables Injected: { language: "Spanish", text: "Hello" } + ↓ +Output: "Translate to Spanish: Hello" +``` + +### The Prompt Hierarchy + +``` +BasePromptTemplate (abstract) + ├── PromptTemplate (string templates) + ├── ChatPromptTemplate (message templates) + ├── FewShotPromptTemplate (examples + template) + ├── PipelinePromptTemplate (compose templates) + └── SystemMessagePromptTemplate (system message helper) +``` + +Each type serves a specific use case. + +### Key Operations + +1. **Format**: Replace variables with values +2. **Validate**: Check required variables are provided +3. **Compose**: Combine templates together +4. **Invoke**: Use as a Runnable (returns formatted prompt) + +## Implementation Guide + +### Step 1: Base Prompt Template + +**Location:** [`src/prompts/base-prompt-template.js`](../../../src/prompts/base-prompt-template.js) + +This is the abstract base class all prompts inherit from. + +**What it does:** +- Defines the interface for all prompt templates +- Extends Runnable (so prompts work in chains) +- Provides validation logic + +**Key methods:** +- `format(values)` - Replace variables and return string/messages +- `formatPromptValue(values)` - Return as PromptValue (for messages) +- `_validateInput(values)` - Check all required variables present + +**Implementation:** + +```javascript +import { Runnable } from '../core/runnable.js'; + +/** + * Base class for all prompt templates + */ +export class BasePromptTemplate extends Runnable { + constructor(options = {}) { + super(); + this.inputVariables = options.inputVariables || []; + this.partialVariables = options.partialVariables || {}; + } + + /** + * Format the prompt with given values + * @abstract + */ + async format(values) { + throw new Error('Subclasses must implement format()'); + } + + /** + * Runnable interface: invoke returns formatted prompt + */ + async _call(input, config) { + return await this.format(input); + } + + /** + * Validate that all required variables are provided + */ + _validateInput(values) { + const provided = { ...this.partialVariables, ...values }; + const missing = this.inputVariables.filter( + key => !(key in provided) + ); + + if (missing.length > 0) { + throw new Error( + `Missing required input variables: ${missing.join(', ')}` + ); + } + } + + /** + * Merge partial variables with provided values + */ + _mergePartialAndUserVariables(values) { + return { ...this.partialVariables, ...values }; + } +} +``` + +**Key insights:** +- Extends `Runnable` so prompts can be chained +- `_call` invokes `format` - this makes prompts work in pipelines +- Validation ensures all variables are provided +- Partial variables are pre-filled defaults + +--- + +### Step 2: PromptTemplate + +**Location:** [`src/prompts/prompt-template.js`](../../../src/prompts/prompt-template.js) + +The most common prompt template - replaces `{variable}` placeholders. + +**What it does:** +- Takes a template string with `{placeholders}` +- Replaces placeholders with actual values +- Validates all variables are provided + +**Template syntax:** +```javascript +"Hello {name}, you are {age} years old." +``` + +**Implementation:** + +```javascript +import { BasePromptTemplate } from './base-prompt-template.js'; + +/** + * Simple string template with {variable} placeholders + * + * Example: + * const prompt = new PromptTemplate({ + * template: "Translate to {language}: {text}", + * inputVariables: ["language", "text"] + * }); + * + * await prompt.format({ language: "Spanish", text: "Hello" }); + * // "Translate to Spanish: Hello" + */ +export class PromptTemplate extends BasePromptTemplate { + constructor(options = {}) { + super(options); + this.template = options.template; + + // Auto-detect input variables if not provided + if (!options.inputVariables) { + this.inputVariables = this._extractInputVariables(this.template); + } + } + + /** + * Format the template with provided values + */ + async format(values) { + this._validateInput(values); + const allValues = this._mergePartialAndUserVariables(values); + + let result = this.template; + for (const [key, value] of Object.entries(allValues)) { + const regex = new RegExp(`\\{${key}\\}`, 'g'); + result = result.replace(regex, String(value)); + } + + return result; + } + + /** + * Extract variable names from template string + * Finds all {variable} patterns + */ + _extractInputVariables(template) { + const matches = template.match(/\{(\w+)\}/g) || []; + return matches.map(match => match.slice(1, -1)); + } + + /** + * Static helper to create from template string + */ + static fromTemplate(template, options = {}) { + return new PromptTemplate({ + template, + ...options + }); + } +} +``` + +**Usage example:** + +```javascript +const prompt = new PromptTemplate({ + template: "Translate to {language}: {text}", + inputVariables: ["language", "text"] +}); + +const formatted = await prompt.format({ + language: "Spanish", + text: "Hello, world!" +}); +// "Translate to Spanish: Hello, world!" +``` + +--- + +### Step 3: ChatPromptTemplate + +**Location:** [`src/prompts/chat-prompt-template.js`](../../../src/prompts/chat-prompt-template.js) + +For structured conversations with system/user/assistant messages. + +**What it does:** +- Creates arrays of Message objects +- Supports system, human, and AI message templates +- Returns properly formatted conversation structure + +**Message template syntax:** +```javascript +[ + ["system", "You are a {role}"], + ["human", "My question: {question}"], + ["ai", "Let me help with {topic}"] +] +``` + +**Implementation:** + +```javascript +import { BasePromptTemplate } from './base-prompt-template.js'; +import { SystemMessage, HumanMessage, AIMessage } from '../core/message.js'; +import { PromptTemplate } from './prompt-template.js'; + +/** + * Template for chat-based conversations + * Returns an array of Message objects + * + * Example: + * const prompt = ChatPromptTemplate.fromMessages([ + * ["system", "You are a {role}"], + * ["human", "{input}"] + * ]); + * + * const messages = await prompt.format({ + * role: "translator", + * input: "Hello" + * }); + * // [SystemMessage(...), HumanMessage(...)] + */ +export class ChatPromptTemplate extends BasePromptTemplate { + constructor(options = {}) { + super(options); + this.promptMessages = options.promptMessages || []; + } + + /** + * Format into array of Message objects + */ + async format(values) { + this._validateInput(values); + const allValues = this._mergePartialAndUserVariables(values); + + const messages = []; + for (const [role, template] of this.promptMessages) { + const content = await this._formatMessageTemplate(template, allValues); + messages.push(this._createMessage(role, content)); + } + + return messages; + } + + /** + * Format a single message template + */ + async _formatMessageTemplate(template, values) { + if (typeof template === 'string') { + const promptTemplate = new PromptTemplate({ template }); + return await promptTemplate.format(values); + } + return template; + } + + /** + * Create appropriate Message object for role + */ + _createMessage(role, content) { + switch (role.toLowerCase()) { + case 'system': + return new SystemMessage(content); + case 'human': + case 'user': + return new HumanMessage(content); + case 'ai': + case 'assistant': + return new AIMessage(content); + default: + throw new Error(`Unknown message role: ${role}`); + } + } + + /** + * Static helper to create from message list + */ + static fromMessages(messages, options = {}) { + const promptMessages = messages.map(msg => { + if (Array.isArray(msg)) { + return msg; // [role, template] + } + throw new Error('Each message must be [role, template] array'); + }); + + // Extract all input variables from all templates + const inputVariables = new Set(); + for (const [, template] of promptMessages) { + if (typeof template === 'string') { + const matches = template.match(/\{(\w+)\}/g) || []; + matches.forEach(m => inputVariables.add(m.slice(1, -1))); + } + } + + return new ChatPromptTemplate({ + promptMessages, + inputVariables: Array.from(inputVariables), + ...options + }); + } +} +``` + +**Usage example:** + +```javascript +const chatPrompt = ChatPromptTemplate.fromMessages([ + ["system", "You are a {role} assistant"], + ["human", "My question: {question}"] +]); + +const messages = await chatPrompt.format({ + role: "helpful", + question: "What's the weather?" +}); + +// Returns: +// [ +// SystemMessage("You are a helpful assistant"), +// HumanMessage("My question: What's the weather?") +// ] +``` + +--- + +### Step 4: Few-Shot Prompt Template + +**Location:** [`src/prompts/few-shot-prompt.js`](../../../src/prompts/few-shot-prompt.js) + +For including examples in your prompts (few-shot learning). + +**What it does:** +- Takes a list of examples +- Formats each example with a template +- Combines examples with the main prompt + +**Structure:** +``` +Examples: + Input: 2+2 Output: 4 + Input: 3+5 Output: 8 + +Prompt: Input: {input} Output: +``` + +**Implementation:** + +```javascript +import { BasePromptTemplate } from './base-prompt-template.js'; +import { PromptTemplate } from './prompt-template.js'; + +/** + * Prompt template that includes examples (few-shot learning) + * + * Example: + * const fewShot = new FewShotPromptTemplate({ + * examples: [ + * { input: "2+2", output: "4" }, + * { input: "3+5", output: "8" } + * ], + * examplePrompt: new PromptTemplate({ + * template: "Input: {input}\nOutput: {output}", + * inputVariables: ["input", "output"] + * }), + * prefix: "Solve these math problems:", + * suffix: "Input: {input}\nOutput:", + * inputVariables: ["input"] + * }); + */ +export class FewShotPromptTemplate extends BasePromptTemplate { + constructor(options = {}) { + super(options); + this.examples = options.examples || []; + this.examplePrompt = options.examplePrompt; + this.prefix = options.prefix || ''; + this.suffix = options.suffix || ''; + this.exampleSeparator = options.exampleSeparator || '\n\n'; + } + + /** + * Format the few-shot prompt + */ + async format(values) { + this._validateInput(values); + const allValues = this._mergePartialAndUserVariables(values); + + const parts = []; + + // Add prefix + if (this.prefix) { + const prefixTemplate = new PromptTemplate({ template: this.prefix }); + parts.push(await prefixTemplate.format(allValues)); + } + + // Add formatted examples + if (this.examples.length > 0) { + const exampleStrings = await Promise.all( + this.examples.map(ex => this.examplePrompt.format(ex)) + ); + parts.push(exampleStrings.join(this.exampleSeparator)); + } + + // Add suffix + if (this.suffix) { + const suffixTemplate = new PromptTemplate({ template: this.suffix }); + parts.push(await suffixTemplate.format(allValues)); + } + + return parts.join('\n\n'); + } +} +``` + +**Usage example:** + +```javascript +const fewShotPrompt = new FewShotPromptTemplate({ + examples: [ + { input: "happy", output: "sad" }, + { input: "tall", output: "short" } + ], + examplePrompt: new PromptTemplate({ + template: "Input: {input}\nOutput: {output}", + inputVariables: ["input", "output"] + }), + prefix: "Give the antonym of each word:", + suffix: "Input: {word}\nOutput:", + inputVariables: ["word"] +}); + +const prompt = await fewShotPrompt.format({ word: "hot" }); + +// Output: +// Give the antonym of each word: +// +// Input: happy +// Output: sad +// +// Input: tall +// Output: short +// +// Input: hot +// Output: +``` + +--- + +### Step 5: Pipeline Prompt Template + +**Location:** [`src/prompts/pipeline-prompt.js`](../../../src/prompts/pipeline-prompt.js) + +For composing multiple prompts together. + +**What it does:** +- Combines multiple prompt templates +- Pipes output of one template to input of another +- Enables modular prompt construction + +**Implementation:** + +```javascript +import { BasePromptTemplate } from './base-prompt-template.js'; + +/** + * Compose multiple prompts into a pipeline + * + * Example: + * const pipeline = new PipelinePromptTemplate({ + * finalPrompt: mainPrompt, + * pipelinePrompts: [ + * { name: "context", prompt: contextPrompt }, + * { name: "instructions", prompt: instructionPrompt } + * ] + * }); + */ +export class PipelinePromptTemplate extends BasePromptTemplate { + constructor(options = {}) { + super(options); + this.finalPrompt = options.finalPrompt; + this.pipelinePrompts = options.pipelinePrompts || []; + + // Collect all input variables + this.inputVariables = this._collectInputVariables(); + } + + /** + * Format by running pipeline prompts first, then final prompt + */ + async format(values) { + this._validateInput(values); + const allValues = this._mergePartialAndUserVariables(values); + + // Format each pipeline prompt and collect results + const pipelineResults = {}; + for (const { name, prompt } of this.pipelinePrompts) { + pipelineResults[name] = await prompt.format(allValues); + } + + // Merge with original values and format final prompt + const finalValues = { ...allValues, ...pipelineResults }; + return await this.finalPrompt.format(finalValues); + } + + /** + * Collect input variables from all prompts + */ + _collectInputVariables() { + const vars = new Set(this.finalPrompt.inputVariables); + + for (const { prompt } of this.pipelinePrompts) { + prompt.inputVariables.forEach(v => vars.add(v)); + } + + // Remove pipeline output names (they're generated) + this.pipelinePrompts.forEach(({ name }) => vars.delete(name)); + + return Array.from(vars); + } +} +``` + +**Usage example:** + +```javascript +const contextPrompt = new PromptTemplate({ + template: "Context: {topic} is important because {reason}", + inputVariables: ["topic", "reason"] +}); + +const mainPrompt = new PromptTemplate({ + template: "{context}\n\nQuestion: {question}", + inputVariables: ["context", "question"] +}); + +const pipeline = new PipelinePromptTemplate({ + finalPrompt: mainPrompt, + pipelinePrompts: [ + { name: "context", prompt: contextPrompt } + ] +}); + +const result = await pipeline.format({ + topic: "AI", + reason: "it transforms industries", + question: "What are the risks?" +}); + +// Output: +// Context: AI is important because it transforms industries +// +// Question: What are the risks? +``` + +### Step 6: System Message Prompt Template + +**Location:** [`src/prompts/system-message-prompt.js`](../../../src/prompts/system-message-prompt.js) + +A specialized template for creating system messages with consistent formatting. + +**What it does:** +- Creates SystemMessage objects with variable substitution +- Simplifies creating reusable system prompts +- Provides a cleaner API than ChatPromptTemplate for system-only messages + +**Why this matters:** +System messages set the behavior and context for LLMs. Having a dedicated template makes it easier to: +- Manage different system prompts for different use cases +- A/B test system message variations +- Inject context dynamically (user preferences, current date, etc.) +- Maintain consistent system message formatting + +**Implementation:** + +```javascript +import { BasePromptTemplate } from './base-prompt-template.js'; +import { SystemMessage } from '../core/message.js'; +import { PromptTemplate } from './prompt-template.js'; + +/** + * Template specifically for system messages + * Returns a single SystemMessage object + * + * Example: + * const systemPrompt = new SystemMessagePromptTemplate({ + * template: "You are a {role} assistant specialized in {domain}. {instructions}", + * inputVariables: ["role", "domain", "instructions"] + * }); + * + * const message = await systemPrompt.format({ + * role: "helpful", + * domain: "cooking", + * instructions: "Always provide recipe alternatives." + * }); + * // SystemMessage("You are a helpful assistant specialized in cooking. Always provide recipe alternatives.") + */ +export class SystemMessagePromptTemplate extends BasePromptTemplate { + constructor(options = {}) { + super(options); + this.prompt = options.prompt || new PromptTemplate({ + template: options.template, + inputVariables: options.inputVariables, + partialVariables: options.partialVariables + }); + + // Inherit input variables from inner prompt + if (!options.inputVariables) { + this.inputVariables = this.prompt.inputVariables; + } + } + + /** + * Format into a SystemMessage object + */ + async format(values) { + this._validateInput(values); + const allValues = this._mergePartialAndUserVariables(values); + + const content = await this.prompt.format(allValues); + return new SystemMessage(content); + } + + /** + * Static helper to create from template string + */ + static fromTemplate(template, options = {}) { + return new SystemMessagePromptTemplate({ + template, + ...options + }); + } + + /** + * Create with partial variables pre-filled + * Useful for setting default context that can be overridden + */ + static fromTemplateWithPartials(template, partialVariables = {}, options = {}) { + const promptTemplate = new PromptTemplate({ template }); + const inputVariables = promptTemplate.inputVariables.filter( + v => !(v in partialVariables) + ); + + return new SystemMessagePromptTemplate({ + template, + inputVariables, + partialVariables, + ...options + }); + } +} +``` + +**Usage Examples:** + +**Example 1: Basic system message** +```javascript +const systemPrompt = SystemMessagePromptTemplate.fromTemplate( + "You are a {role} assistant. Always be {tone}." +); + +const message = await systemPrompt.format({ + role: "helpful", + tone: "professional" +}); +// SystemMessage("You are a helpful assistant. Always be professional.") +``` + +**Example 2: System message with partial variables (defaults)** +```javascript +const systemPrompt = SystemMessagePromptTemplate.fromTemplateWithPartials( + "You are a {role} assistant. Today is {date}. User preference: {preference}", + { + date: new Date().toLocaleDateString(), // Pre-filled default + preference: "concise responses" // Pre-filled default + } +); + +// Only need to provide 'role' - others use defaults +const message1 = await systemPrompt.format({ + role: "coding" +}); +// SystemMessage("You are a coding assistant. Today is 11/20/2025. User preference: concise responses") + +// Can override defaults if needed +const message2 = await systemPrompt.format({ + role: "coding", + preference: "detailed explanations" +}); +// SystemMessage("You are a coding assistant. Today is 11/20/2025. User preference: detailed explanations") +``` + +**Key Benefits:** + +1. **Separation of Concerns**: System prompts are separate from conversation flow +2. **Reusability**: Create once, use in multiple chat templates +3. **A/B Testing**: Easily swap system prompts to test effectiveness +4. **Dynamic Context**: Inject runtime information (date, user prefs, etc.) +5. **Type Safety**: Always returns SystemMessage (not just strings) +6. **Partial Variables**: Set defaults that can be overridden + +**Common Use Cases:** + +- **Role-based prompting**: Different system messages for different assistant personalities +- **Context injection**: Add current date, user preferences, session info +- **A/B testing**: Test different instruction phrasings +- **Domain switching**: Same app, different domains (customer service vs technical support) +- **Compliance**: Ensure required disclaimers/policies in every system message + +--- + +# Prompts: Real-World Patterns + +## Real-World Examples + +### Example 1: Translation Service + +```javascript +import { PromptTemplate } from './prompts/prompt-template.js'; +import { LlamaCppLLM } from './llm/llama-cpp-llm.js'; + +// Reusable translation prompt +const translationPrompt = new PromptTemplate({ + template: `Translate the following text from {source_lang} to {target_lang}. + +Text: {text} + +Translation:`, + inputVariables: ["source_lang", "target_lang", "text"] +}); + +// Use in your app +const llm = new LlamaCppLLM({ modelPath: './model.gguf' }); + +// Build a reusable translation chain +const translationChain = translationPrompt.pipe(llm); + +// Now you can easily translate +const spanish = await translationChain.invoke({ + source_lang: "English", + target_lang: "Spanish", + text: "Hello, how are you?" +}); + +const french = await translationChain.invoke({ + source_lang: "English", + target_lang: "French", + text: "Hello, how are you?" +}); +``` + +**Why this is better:** +- ✅ Prompt pattern defined once, used everywhere +- ✅ Consistent formatting across translations +- ✅ Easy to test prompt in isolation +- ✅ Can swap LLM without changing prompt + +--- + +### Example 2: Customer Support Bot + +```javascript +import { ChatPromptTemplate } from './prompts/chat-prompt-template.js'; + +const supportPrompt = ChatPromptTemplate.fromMessages([ + ["system", `You are a customer support agent for {company}. + +Company details: +- Product: {product} +- Return policy: {return_policy} +- Support hours: {support_hours} + +Be helpful, professional, and concise.`], + ["human", "{customer_message}"] +]); + +// Use with partial variables for company info +const myCompanySupportPrompt = supportPrompt.partial({ + company: "TechCorp", + product: "Cloud Storage", + return_policy: "30 days", + support_hours: "9am-5pm EST" +}); + +// Now only need customer message +const messages = await myCompanySupportPrompt.format({ + customer_message: "How do I cancel my subscription?" +}); + +const response = await llm.invoke(messages); +``` + +**Why this is better:** +- ✅ Company info defined once, reused everywhere +- ✅ Partial variables reduce repetition +- ✅ Easy to update company policy in one place +- ✅ Structured conversation format + +--- + +### Example 3: Few-Shot Classification + +```javascript +import { FewShotPromptTemplate } from './prompts/few-shot-prompt.js'; +import { PromptTemplate } from './prompts/prompt-template.js'; + +// Example formatter +const examplePrompt = new PromptTemplate({ + template: "Text: {text}\nSentiment: {sentiment}", + inputVariables: ["text", "sentiment"] +}); + +// Few-shot sentiment classifier +const sentimentPrompt = new FewShotPromptTemplate({ + examples: [ + { text: "I love this product!", sentiment: "positive" }, + { text: "This is terrible.", sentiment: "negative" }, + { text: "It's okay, I guess.", sentiment: "neutral" } + ], + examplePrompt: examplePrompt, + prefix: "Classify the sentiment of the following texts:", + suffix: "Text: {input}\nSentiment:", + inputVariables: ["input"], + exampleSeparator: "\n\n" +}); + +const prompt = await sentimentPrompt.format({ + input: "This exceeded my expectations!" +}); + +// The LLM now has examples to learn from! +const sentiment = await llm.invoke(prompt); +``` + +**Why this is better:** +- ✅ Examples teach the LLM the task +- ✅ More consistent classifications +- ✅ Easy to add/remove examples +- ✅ Can dynamically select relevant examples + +--- + +### Example 4: Modular Prompt Composition + +```javascript +import { PipelinePromptTemplate } from './prompts/pipeline-prompt.js'; +import { PromptTemplate } from './prompts/prompt-template.js'; + +// Reusable context builder +const contextPrompt = new PromptTemplate({ + template: `Domain: {domain} +Key concepts: {concepts} +Current date: {date}`, + inputVariables: ["domain", "concepts", "date"] +}); + +// Reusable instruction builder +const instructionPrompt = new PromptTemplate({ + template: `Task: {task} +Format: {format} +Constraints: {constraints}`, + inputVariables: ["task", "format", "constraints"] +}); + +// Main prompt uses outputs from sub-prompts +const mainPrompt = new PromptTemplate({ + template: `{context} + +{instructions} + +Input: {input} + +Output:`, + inputVariables: ["context", "instructions", "input"] +}); + +// Compose them together +const composedPrompt = new PipelinePromptTemplate({ + finalPrompt: mainPrompt, + pipelinePrompts: [ + { name: "context", prompt: contextPrompt }, + { name: "instructions", prompt: instructionPrompt } + ] +}); + +// Use the composed prompt +const result = await composedPrompt.format({ + domain: "Healthcare", + concepts: "diagnosis, treatment, patient care", + date: new Date().toISOString().split('T')[0], + task: "Analyze symptoms", + format: "JSON with confidence scores", + constraints: "HIPAA compliant, evidence-based", + input: "Patient reports fatigue and headaches" +}); +``` + +**Why this is better:** +- ✅ Modular prompt components +- ✅ Reuse context/instructions across prompts +- ✅ Easy to update individual sections +- ✅ Cleaner prompt management + +--- + +## Advanced Patterns + +### Pattern 1: Conditional Prompts + +```javascript +class ConditionalPromptTemplate extends BasePromptTemplate { + constructor(options = {}) { + super(options); + this.condition = options.condition; + this.truePrompt = options.truePrompt; + this.falsePrompt = options.falsePrompt; + } + + async format(values) { + const useTrue = this.condition(values); + const selectedPrompt = useTrue ? this.truePrompt : this.falsePrompt; + return await selectedPrompt.format(values); + } +} + +// Usage +const prompt = new ConditionalPromptTemplate({ + condition: (values) => values.userType === 'expert', + truePrompt: new PromptTemplate({ + template: "Technical analysis: {query}" + }), + falsePrompt: new PromptTemplate({ + template: "Explain in simple terms: {query}" + }), + inputVariables: ["userType", "query"] +}); +``` + +--- + +### Pattern 2: Dynamic Examples (Select Best Examples) + +```javascript +class DynamicFewShotPromptTemplate extends FewShotPromptTemplate { + constructor(options = {}) { + super(options); + this.exampleSelector = options.exampleSelector; + this.maxExamples = options.maxExamples || 3; + } + + async format(values) { + // Select most relevant examples dynamically + const selectedExamples = await this.exampleSelector.select( + values, + this.maxExamples + ); + + // Temporarily replace examples + const originalExamples = this.examples; + this.examples = selectedExamples; + + const result = await super.format(values); + + // Restore original examples + this.examples = originalExamples; + + return result; + } +} + +// Example selector based on similarity +class SimilarityExampleSelector { + constructor(examples) { + this.examples = examples; + } + + async select(input, k) { + // In real implementation, use embeddings for similarity + // For now, simple keyword matching + const scores = this.examples.map(ex => ({ + example: ex, + score: this._similarity(ex.input, input.input) + })); + + scores.sort((a, b) => b.score - a.score); + return scores.slice(0, k).map(s => s.example); + } + + _similarity(a, b) { + // Simple word overlap + const wordsA = new Set(a.toLowerCase().split(' ')); + const wordsB = new Set(b.toLowerCase().split(' ')); + const intersection = new Set([...wordsA].filter(x => wordsB.has(x))); + return intersection.size / Math.max(wordsA.size, wordsB.size); + } +} +``` + +--- + +### Pattern 3: Prompt with Validation + +```javascript +class ValidatedPromptTemplate extends PromptTemplate { + constructor(options = {}) { + super(options); + this.validators = options.validators || {}; + } + + async format(values) { + // Validate each input + for (const [key, validator] of Object.entries(this.validators)) { + if (key in values) { + const isValid = validator(values[key]); + if (!isValid) { + throw new Error(`Invalid value for ${key}: ${values[key]}`); + } + } + } + + return await super.format(values); + } +} + +// Usage +const emailPrompt = new ValidatedPromptTemplate({ + template: "Send email to {email} about {subject}", + inputVariables: ["email", "subject"], + validators: { + email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value), + subject: (value) => value.length > 0 && value.length <= 100 + } +}); + +// This will throw an error +await emailPrompt.format({ + email: "invalid-email", + subject: "Hi" +}); +``` + +--- + +### Pattern 4: Prompt Chaining + +```javascript +// Chain multiple prompts together +const summaryPrompt = new PromptTemplate({ + template: "Summarize this text: {text}" +}); + +const bulletPrompt = new PromptTemplate({ + template: "Convert this summary to bullet points:\n{summary}" +}); + +// Create a chain +const summaryChain = summaryPrompt.pipe(llm); +const bulletChain = bulletPrompt.pipe(llm); + +// Use them together +async function summarizeAsBullets(text) { + const summary = await summaryChain.invoke({ text }); + const bullets = await bulletChain.invoke({ summary }); + return bullets; +} +``` + +--- + +## Common Use Cases + +### Use Case 1: Multi-Language Support + +```javascript +const prompts = { + en: new PromptTemplate({ + template: "Answer in English: {query}" + }), + es: new PromptTemplate({ + template: "Responde en español: {query}" + }), + fr: new PromptTemplate({ + template: "Répondre en français: {query}" + }) +}; + +function getPrompt(language) { + return prompts[language] || prompts.en; +} + +const prompt = getPrompt(userLanguage); +const response = await prompt.pipe(llm).invoke({ query: "Hello" }); +``` + +--- + +### Use Case 2: A/B Testing Prompts + +```javascript +const variantA = new PromptTemplate({ + template: "Explain {concept} simply" +}); + +const variantB = new PromptTemplate({ + template: "Teach me about {concept} like I'm 5 years old" +}); + +// Randomly select variant +const prompt = Math.random() < 0.5 ? variantA : variantB; + +// Track which variant was used +const config = { + metadata: { variant: prompt === variantA ? 'A' : 'B' } +}; + +const response = await prompt.pipe(llm).invoke({ concept: "gravity" }, config); +``` + +--- + +### Use Case 3: Prompt Versioning + +```javascript +const prompts = { + v1: new PromptTemplate({ + template: "Classify: {text}" + }), + v2: new PromptTemplate({ + template: "Classify the sentiment of: {text}\nOptions: positive, negative, neutral" + }), + v3: ChatPromptTemplate.fromMessages([ + ["system", "You are a sentiment classifier. Only respond with: positive, negative, or neutral"], + ["human", "{text}"] + ]) +}; + +// Use specific version +const currentPrompt = prompts.v3; + +// Easy to roll back if needed +const response = await currentPrompt.pipe(llm).invoke({ text: "I love this!" }); +``` + +--- + +## Best Practices + +### ✅ DO: + +**1. Keep prompts in separate files** +```javascript +// prompts/translation.js +export const translationPrompt = new PromptTemplate({ + template: "Translate to {language}: {text}", + inputVariables: ["language", "text"] +}); + +// app.js +import { translationPrompt } from './prompts/translation.js'; +``` + +**2. Use partial variables for constants** +```javascript +const prompt = new PromptTemplate({ + template: "Company: {company}\nQuery: {query}", + partialVariables: { + company: "MyCompany" + } +}); +``` + +**3. Test prompts in isolation** +```javascript +const formatted = await prompt.format({ input: "test" }); +console.log(formatted); +// Verify output before sending to LLM +``` + +**4. Version your prompts** +```javascript +// prompts/classifier/v2.js +export const classifierPromptV2 = ... +``` + +**5. Document your templates** +```javascript +/** + * Email classification prompt + * + * Variables: + * - from: Email sender + * - subject: Email subject + * - body: Email body + * + * Returns: Category name + */ +export const emailPrompt = ... +``` + +--- + +### ❌ DON'T: + +**1. Hardcode prompts throughout codebase** +```javascript +// Bad +const result = await llm.invoke(`Translate to ${lang}: ${text}`); +``` + +**2. Skip input validation** +```javascript +// Bad +const prompt = new PromptTemplate({ template: "..." }); +await prompt.format({}); // Missing required variables! +``` + +**3. Make prompts too complex** +```javascript +// Bad - too many nested conditions +const prompt = condition1 ? (condition2 ? prompt1 : prompt2) : ... +``` + +**4. Forget to handle formatting errors** +```javascript +// Bad +const formatted = await prompt.format(values); // What if this throws? + +// Good +try { + const formatted = await prompt.format(values); +} catch (error) { + console.error('Prompt formatting failed:', error); + // Handle gracefully +} +``` + +--- + +## Integration with Framework + +### Using Prompts in Chains + +```javascript +import { PromptTemplate } from './prompts/prompt-template.js'; +import { LlamaCppLLM } from './llm/llama-cpp-llm.js'; +import { StringOutputParser } from './output-parsers/string-parser.js'; + +const prompt = new PromptTemplate({ + template: "Summarize: {text}" +}); + +const llm = new LlamaCppLLM({ modelPath: './model.gguf' }); +const parser = new StringOutputParser(); + +// Build chain: prompt -> llm -> parser +const chain = prompt.pipe(llm).pipe(parser); + +// Use it +const summary = await chain.invoke({ text: "Long article..." }); +``` + +--- + +### Using Prompts with Memory + +```javascript +import { ChatPromptTemplate } from './prompts/chat-prompt-template.js'; +import { BufferMemory } from './memory/buffer-memory.js'; + +const memory = new BufferMemory(); + +const prompt = ChatPromptTemplate.fromMessages([ + ["system", "You are a helpful assistant"], + ["placeholder", "{chat_history}"], + ["human", "{input}"] +]); + +// Load history from memory +const chatHistory = await memory.loadMemoryVariables(); + +const messages = await prompt.format({ + chat_history: chatHistory.history, + input: "What did we discuss?" +}); +``` + +--- + +## Testing Prompts + +### Unit Testing + +```javascript +import { describe, it, expect } from 'your-test-framework'; +import { PromptTemplate } from './prompts/prompt-template.js'; + +describe('PromptTemplate', () => { + it('should format template with variables', async () => { + const prompt = new PromptTemplate({ + template: "Hello {name}", + inputVariables: ["name"] + }); + + const result = await prompt.format({ name: "Alice" }); + expect(result).toBe("Hello Alice"); + }); + + it('should throw on missing variables', async () => { + const prompt = new PromptTemplate({ + template: "Hello {name}", + inputVariables: ["name"] + }); + + await expect(prompt.format({})).rejects.toThrow(); + }); + + it('should handle partial variables', async () => { + const prompt = new PromptTemplate({ + template: "{greeting} {name}", + inputVariables: ["greeting", "name"], + partialVariables: { greeting: "Hello" } + }); + + const result = await prompt.format({ name: "Bob" }); + expect(result).toBe("Hello Bob"); + }); +}); +``` +--- + +## Exercises + +Practice using prompt templates from simple to complex: + +### Exercise 17: Using a Basic PromptTemplate +Master variable replacement and template formatting fundamentals. +**Starter code**: [`exercises/17-prompt-template.js`](exercises/17-prompt-template.js) + +### Exercise 18: Using the ChatPromptTemplate +Create structured conversations with role-based messages. +**Starter code**: [`exercises/18-chat-prompt-template.js`](exercises/18-chat-prompt-template.js) + +### Exercise 19: Using the FewShotPromptTemplate +Implement few-shot learning with examples for better LLM outputs. +**Starter code**: [`exercises/19-few-shot-prompt-template.js`](exercises/19-few-shot-prompt-template.js) + +### Exercise 20: Using the PipelinePromptTemplate +Compose modular prompts by connecting multiple templates. +**Starter code**: [`exercises/20-pipeline-prompt-template.js`](exercises/20-pipeline-prompt-template.js) + +--- + +## Summary + +You've learned how to build a complete prompting system! + +### Key Takeaways + +1. **PromptTemplate**: Basic string templates with variable replacement +2. **ChatPromptTemplate**: Structured conversations with role-based messages +3. **FewShotPromptTemplate**: Include examples for better LLM performance +4. **PipelinePromptTemplate**: Compose prompts modularly +5. **BasePromptTemplate**: Foundation that makes prompts Runnables + +### What You Built + +A production-ready prompt system that: +- ✅ Replaces variables safely +- ✅ Validates inputs +- ✅ Composes with other Runnables +- ✅ Supports chat and completion formats +- ✅ Enables few-shot learning +- ✅ Allows prompt composition + +### Next Steps + +Now that you have reusable prompts, you need to parse LLM outputs into structured data. + +➡️ **Next: [Output Parsers](../02-parsers/lesson.md)** + +Learn how to extract structured data from LLM responses reliably. + +--- + +**Built with ❤️ for learners who want to understand AI frameworks deeply** + +[← Previous: Context](../../01-foundation/04-context/lesson.md) | [Tutorial Index](../../README.md) | [Next: Output Parsers →](../02-parsers/lesson.md) \ No newline at end of file diff --git a/tutorial/02-composition/01-prompts/solutions/17-prompt-template-solution.js b/tutorial/02-composition/01-prompts/solutions/17-prompt-template-solution.js new file mode 100644 index 0000000000000000000000000000000000000000..736267971fd4eb86becab16758b2bb0671935a48 --- /dev/null +++ b/tutorial/02-composition/01-prompts/solutions/17-prompt-template-solution.js @@ -0,0 +1,172 @@ +/** + * Solution 17: Basic PromptTemplate + * + * Goal: Build a PromptTemplate that replaces {variable} placeholders + */ + +import {PromptTemplate} from '../../../../src/index.js'; + + +// Test cases +async function exercise() { + console.log('=== Exercise 17: Basic PromptTemplate ===\n'); + + // Test 1: Simple translation prompt + console.log('--- Test 1: Simple Translation ---'); + + const translatePrompt = new PromptTemplate({ + template: "Translate to {language}: {text}", + }); + const result1 = await translatePrompt.format({ + language: "Spanish", + text: "Hello, world!" + }) + + console.log('Input: "Hello, world!" → Spanish'); + console.log('Result:', result1); // result1 + console.log(); + + // Test 2: Email template with multiple variables + console.log('--- Test 2: Email Template ---'); + + const emailPrompt = new PromptTemplate({ + template: `Dear {name},\n\nThank you for {action}. Your {item} will arrive on {date}.\n\nBest,\n{sender}` + }); + + const result2 = await emailPrompt.format({ + name: "Patric Gutersohn", + action: "your order", + item: "Mac Studio M3 Ultra", + date: "24.12.2026", + sender: "Apple Inc." + }) + + console.log('Result:', result2); + console.log(); + + // Test 3: Auto-detect variables + console.log('--- Test 3: Auto-Detect Variables ---'); + + const autoPrompt = new PromptTemplate({ + template: "In {city}, I love to visit {activity}" + }); + + console.log('Detected variables:', autoPrompt.inputVariables); + + const result3 = await autoPrompt.format({ + city: "Paris", + activity: "museums" + }) + console.log('Result:', result3); + console.log(); + + // Test 4: Partial variables (defaults) + console.log('--- Test 4: Partial Variables ---'); + + const partialPrompt = new PromptTemplate({ + template: "You are a {role} assistant. User: {input}", + partialVariables: { role: "helpful" } + }); + + const result4 = await partialPrompt.format({ input: "What's the weather?" }) + + console.log('Result:', result4); + console.log(); + + // Test 5: Validation error + console.log('--- Test 5: Validation Error ---'); + + const strictPrompt = new PromptTemplate({ + template: "Hello {name}, you are {age} years old", + inputVariables: ["name", "age"] + }); + + try { + await strictPrompt.format({ name: "Alice" }) + console.log('ERROR: Should have thrown validation error!'); + } catch (error) { + console.log('✓ Validation error caught:', error.message); + } + console.log(); + + // Test 6: Use as Runnable (invoke) + console.log('--- Test 6: Use as Runnable ---'); + + const runnablePrompt = PromptTemplate.fromTemplate("Search for {topic}") + + const result6 = await runnablePrompt.invoke({ + topic: "artificial intelligence" + }) + + console.log('Invoked result:', result6); + console.log(); + + // Test 7: Complex nested replacement + console.log('--- Test 7: Complex Template ---'); + + const docPrompt = PromptTemplate.fromTemplate(` + Function: {function} + Parameters: {params} + Returns: {returns} + Description: {description} + `); + + const result7 = await docPrompt.format({ + function: "calculateSum", + params: "a: number, b: number", + returns: "number", + description: "Adds two numbers together" + }) + + console.log('Result:', result7); // result7 + console.log(); + + console.log('✓ Exercise 1 complete!'); +} + +// Run the exercise +exercise().catch(console.error); + +/** + * Expected Output: + * + * --- Test 1: Simple Translation --- + * Input: "Hello, world!" → Spanish + * Result: Translate to Spanish: Hello, world! + * + * --- Test 2: Email Template --- + * Result: Dear Patric Gutersohn, + * + * Thank you for your order. Your Mac Studio M3 Ultra will arrive on 24.12.2026. + * + * Best, + * Apple Inc. + * + * --- Test 3: Auto-Detect Variables --- + * Detected variables: ['city', 'activity'] + * Result: In Paris, I love to visit museums + * + * --- Test 4: Partial Variables --- + * Result: You are a helpful assistant. User: What's the weather? + * + * --- Test 5: Validation Error --- + * ✓ Validation error caught: Missing required input variables: age + * + * --- Test 6: Use as Runnable --- + * Invoked result: Search for artificial intelligence + * + * --- Test 7: Complex Template --- + * Result: + * Function: calculateSum + * Parameters: a: number, b: number + * Returns: number + * Description: Adds two numbers together + * + * Learning Points: + * 1. PromptTemplate replaces {variable} placeholders + * 2. Auto-detection extracts variables from template + * 3. Partial variables provide defaults + * 4. Validation ensures all variables are provided + * 5. As a Runnable, prompts work with invoke() + * 6. Regex is key: /\{(\w+)\}/g finds all variables + */ \ No newline at end of file diff --git a/tutorial/02-composition/01-prompts/solutions/18-chat-prompt-template-solution.js b/tutorial/02-composition/01-prompts/solutions/18-chat-prompt-template-solution.js new file mode 100644 index 0000000000000000000000000000000000000000..93628c593c1a88aeaac3e494d94a524a11ab7b7e --- /dev/null +++ b/tutorial/02-composition/01-prompts/solutions/18-chat-prompt-template-solution.js @@ -0,0 +1,198 @@ +/** + * Solution 18: ChatPromptTemplate + */ + +import {ChatPromptTemplate} from '../../../../src/index.js'; + +// Test cases +async function exercise() { + console.log('=== Exercise 18: ChatPromptTemplate ===\n'); + + // Test 1: Simple chat with system and human messages + console.log('--- Test 1: Basic Chat ---'); + + const chatPrompt1 = ChatPromptTemplate.fromMessages([ + ["system", "You are a {role} assistant"], + ["human", "{question}"] + ]); + + const messages1 = await chatPrompt1.format({ + role: "helpful", + question: " What's the weather?" + }) + + console.log('Messages:'); + messages1.forEach(msg => console.log(` ${msg}`)) + console.log(); + + // Test 2: Multi-turn conversation + console.log('--- Test 2: Multi-Turn Conversation ---'); + + const chatPrompt2 = ChatPromptTemplate.fromMessages([ + ["system", "You are a {personality} chatbot"], + ["human", "Hi, I'm {name}"], + ["ai", "Nice to meet you, {name}!"], + ["human", "Can you help me with {topic}?"] + ]); + + console.log('Detected variables:', chatPrompt2.inputVariables); + + const messages2 = await chatPrompt2.format({ + personality: "friendly", + name: "Alice", + topic: "JavaScript" + }); + + console.log('\nConversation:'); + messages2.forEach(msg => console.log(` ${msg}`)) + console.log(); + + // Test 3: Translation bot template + console.log('--- Test 3: Translation Bot ---'); + + const translateChat = ChatPromptTemplate.fromMessages([ + ["system", "You are a translator. Translate from {source_lang} to {target_lang}."], + ["human", "Translate: {text}"] + ]); + + const messages3 = await translateChat.format({ + source_lang: "English", + target_lang: "Spanish", + text: "Hello, world!" + }); + + console.log('Translation request:'); + messages3.forEach(msg => console.log(` ${msg}`)) + console.log(); + + // Test 4: Customer service template + console.log('--- Test 4: Customer Service ---'); + + const serviceChat = ChatPromptTemplate.fromMessages([ + ["system", "You are a {company} customer service agent. Be {tone}."], + ["human", "Order #{order_id}: {issue}"] + ]); + + const messages4 = await serviceChat.format({ + company: "TechCorp", + tone: "professional and empathetic", + order_id: "12345", + issue: "My item hasn't arrived" + }); + + console.log('Service interaction:'); + messages4.forEach(msg => console.log(` ${msg}`)) + console.log(); + + // Test 5: Use as Runnable + console.log('--- Test 5: Use as Runnable ---'); + + const runnableChat = ChatPromptTemplate.fromMessages([ + ["system", "You are a {role}"], + ["human", "{query}"] + ]); + + const messages5 = await runnableChat.invoke({ + role: "math tutor", + query: "Explain calculus" + }); + + console.log('Invoked messages:'); + messages5.forEach(msg => console.log(` ${msg}`)) + console.log(); + + // Test 6: Validation + console.log('--- Test 6: Validation ---'); + + const strictChat = ChatPromptTemplate.fromMessages([ + ["system", "You need {var1} and {var2}"], + ["human", "Using {var3}"] + ]); + + console.log('Required variables:', strictChat.inputVariables); + + try { + await strictChat.format({ var1: "one" }) + console.log('ERROR: Should have thrown!'); + } catch (error) { + console.log('✓ Validation error:', error.message); + } + console.log(); + + // Test 7: Code review chat + console.log('--- Test 7: Code Review Chat ---'); + + const reviewChat = ChatPromptTemplate.fromMessages([ + ["system", "You are a {language} code reviewer. Focus on {focus}."], + ["human", "Review this code:\n{code}"], + ["ai", "I'll review your {language} code for {focus}."] + ]); + + const messages7 = await reviewChat.format({ + language: "Python", + focus: "performance", + code: "def slow_sum(n): return sum([i for i in range(n)])" + }); + + console.log('Code review chat:'); + messages7.forEach(msg => console.log(` ${msg}`)) + console.log(); + + console.log('✓ Exercise 2 complete!'); +} + +// Run the exercise +exercise().catch(console.error); + +/** + * Expected Output: + * + * --- Test 1: Basic Chat --- + * Messages: + * [1:27:22 PM] system: You are a helpful assistant + * [1:27:22 PM] human: What's the weather? + * + * --- Test 2: Multi-Turn Conversation --- + * Detected variables: [ 'personality', 'name', 'topic' ] + * + * Conversation: + * [1:31:36 PM] system: You are a friendly chatbot + * [1:31:36 PM] human: Hi, I'm Alice + * [1:31:36 PM] ai: Nice to meet you, Alice! + * [1:31:36 PM] human: Can you help me with JavaScript? + * + * --- Test 3: Translation Bot --- + * Translation request: + * [1:31:36 PM] system: You are a translator. Translate from English to Spanish. + * [1:31:36 PM] human: Translate: Hello, world! + * + * --- Test 4: Customer Service --- + * Service interaction: + * [1:31:36 PM] system: You are a TechCorp customer service agent. Be professional and empathetic. + * [1:31:36 PM] human: Order #12345: My item hasn't arrived + * + * --- Test 5: Use as Runnable --- + * Invoked messages: + * [1:31:36 PM] system: You are a math tutor + * [1:31:36 PM] human: Explain calculus + * + * --- Test 6: Validation --- + * Required variables: ['var1', 'var2', 'var3'] + * ✓ Validation error: Missing required input variables: var2, var3 + * + * --- Test 7: Code Review Chat --- + * Code review chat: + * [1:31:36 PM] system: You are a Python code reviewer. Focus on performance. + * [1:31:36 PM] human: Review this code: + * def slow_sum(n): return sum([i for i in range(n)]) + * [1:31:36 PM] ai: I'll review your Python code for performance. + * + * Learning Points: + * 1. ChatPromptTemplate creates structured conversations + * 2. Each message has a role: system, human, ai + * 3. Variables can span multiple messages + * 4. Auto-extraction finds all variables across messages + * 5. Message classes provide type safety + * 6. Perfect for building chat interfaces + * 7. Reusable patterns for different domains + */ \ No newline at end of file diff --git a/tutorial/02-composition/01-prompts/solutions/19-few-shot-prompt-template-solution.js b/tutorial/02-composition/01-prompts/solutions/19-few-shot-prompt-template-solution.js new file mode 100644 index 0000000000000000000000000000000000000000..3bc41acd216630d1ae381dcbfd5c583b274a153e --- /dev/null +++ b/tutorial/02-composition/01-prompts/solutions/19-few-shot-prompt-template-solution.js @@ -0,0 +1,328 @@ +/** + * Solution 19: FewShotPromptTemplate + */ + +import {PromptTemplate, FewShotPromptTemplate} from '../../../../src/index.js'; + +// Test cases +async function exercise3() { + console.log('=== Exercise 19: FewShotPromptTemplate ===\n'); + + // Test 1: Antonym generator + console.log('--- Test 1: Antonym Generator ---'); + + const antonymExamplePrompt = new PromptTemplate({ + template: "Input: {input}\nOutput: {output}", + inputVariables: ["input", "output"] + }); + + const antonymPrompt = new FewShotPromptTemplate({ + examples: [ + { input: "happy", output: "sad" }, + { input: "tall", output: "short" }, + { input: "hot", output: "cold" } + ], + examplePrompt: antonymExamplePrompt, + prefix: "Give the antonym of each word.", + suffix: "Input: {word}\nOutput:", + inputVariables: ["word"] + }); + + const result1 = await antonymPrompt.format({ word: "fast" }); + + console.log('Result:'); + console.log(result1); + console.log(); + + // Test 2: Math word problems + console.log('--- Test 2: Math Word Problems ---'); + + const mathExamplePrompt = new PromptTemplate({ + template: "Q: {question}\nA: {answer}", + inputVariables: ["question", "answer"] + }); + + const mathPrompt = new FewShotPromptTemplate({ + examples: [ + { question: "If I have 3 apples and buy 2 more, how many do I have?", answer: "5" }, + { question: "A train travels 60 mph for 2 hours. How far does it go?", answer: "120 miles" } + ], + examplePrompt: mathExamplePrompt, + prefix: "Solve these word problems:", + suffix: "Q: {question}\nA:", + inputVariables: ["question"] + }); + + const result2 = await mathPrompt.format({ + question: "If a book costs $12 and I buy 3, how much do I spend?" + }); + + console.log('Result:'); + console.log(result2); + console.log(); + + // Test 3: Sentiment classification + console.log('--- Test 3: Sentiment Classification ---'); + + const sentimentExamplePrompt = new PromptTemplate({ + template: "Text: {text}\nSentiment: {sentiment}", + inputVariables: ["text", "sentiment"] + }); + + const sentimentPrompt = new FewShotPromptTemplate({ + examples: [ + { text: "I love this product!", sentiment: "Positive" }, + { text: "This is terrible.", sentiment: "Negative" }, + { text: "It's okay.", sentiment: "Neutral" } + ], + examplePrompt: sentimentExamplePrompt, + prefix: "Classify the sentiment of each text as Positive, Negative, or Neutral:", + suffix: "Text: {text}\nSentiment:", + inputVariables: ["text"] + }); + + const result3 = await sentimentPrompt.format({ + text: "The product is okay, nothing special." + }); + + console.log('Result:'); + console.log(result3); + console.log(); + + // Test 4: Code explanation + console.log('--- Test 4: Code Explanation ---'); + + const codeExamplePrompt = PromptTemplate.fromTemplate("Code: {code}\nExplanation: {explanation}"); + + const codePrompt = new FewShotPromptTemplate({ + examples: [ + { code: "x = x + 1", explanation: "Increment x by 1" }, + { code: "if x > 0:", explanation: "Check if x is positive" } + ], + examplePrompt: codeExamplePrompt, + prefix: "Explain what each line of code does:", + suffix: "Code: {code}\nExplanation:", + inputVariables: ["code"] + }); + + const result4 = await codePrompt.format({ + code: "for i in range(10):" + }); + + console.log('Result:'); + console.log(result4); + console.log(); + + // Test 5: Custom separator + console.log('--- Test 5: Custom Separator ---'); + + const customSepPrompt = new FewShotPromptTemplate({ + examples: [ + { example: "Example 1" }, + { example: "Example 2" }, + { example: "Example 3" } + ], + examplePrompt: new PromptTemplate({ + template: "{example}", + inputVariables: ["example"] + }), + prefix: "Examples:", + suffix: "Your turn: ...", + inputVariables: [], + exampleSeparator: "\n---\n" + }); + + const result5 = await customSepPrompt.format({}); + + console.log('Result:'); + console.log(result5); + console.log(); + + // Test 6: Translation with context + console.log('--- Test 6: Translation with Context ---'); + + const translationExamplePrompt = PromptTemplate.fromTemplate("English: {english}\nSpanish: {spanish}"); + + const translationPrompt = new FewShotPromptTemplate({ + examples: [ + { english: "Hello", spanish: "Hola" }, + { english: "How are you?", spanish: "¿Cómo estás?" } + ], + examplePrompt: translationExamplePrompt, + prefix: "Translate English to Spanish. Context: {context}", + suffix: "English: {english}\nSpanish:", + inputVariables: ["context", "english"] + }); + + const result6 = await translationPrompt.format({ + context: "casual conversation", + english: "See you later!" + }); + + console.log('Result:'); + console.log(result6); + console.log(); + + // Test 7: No examples (just prefix and suffix) + console.log('--- Test 7: No Examples ---'); + + const noExamplesPrompt = new FewShotPromptTemplate({ + examples: [], + examplePrompt: new PromptTemplate({ + template: "", + inputVariables: [] + }), + prefix: "Answer the following question:", + suffix: "Question: {question}\nAnswer:", + inputVariables: ["question"] + }); + + const result7 = await noExamplesPrompt.format({ + question: "What is AI?" + }); + + console.log('Result:'); + console.log(result7); + console.log(); + + // Test 8: Use as Runnable + console.log('--- Test 8: Use as Runnable ---'); + + const runnablePrompt = new FewShotPromptTemplate({ + examples: [ + { word: "big", antonym: "small" }, + { word: "fast", antonym: "slow" } + ], + examplePrompt: new PromptTemplate({ + template: "{word} -> {antonym}", + inputVariables: ["word", "antonym"] + }), + prefix: "Find antonyms:", + suffix: "{word} ->", + inputVariables: ["word"] + }); + + const result8 = await runnablePrompt.invoke({ word: "light" }); + + console.log('Invoked result:'); + console.log(result8); + console.log(); + + console.log('✓ Exercise 3 complete!'); +} + +// Run the exercise +exercise3().catch(console.error); + +/** + * Expected Output: + * + * --- Test 1: Antonym Generator --- + * Result: + * Give the antonym of each word. + * + * Input: happy + * Output: sad + * + * Input: tall + * Output: short + * + * Input: hot + * Output: cold + * + * Input: fast + * Output: + * + * --- Test 2: Math Word Problems --- + * Result: + * Solve these word problems: + * + * Q: If I have 3 apples and buy 2 more, how many do I have? + * A: 5 + * + * Q: A train travels 60 mph for 2 hours. How far does it go? + * A: 120 miles + * + * Q: If a book costs $12 and I buy 3, how much do I spend? + * A: + * + * --- Test 3: Sentiment Classification --- + * Result: + * Classify the sentiment of each text as Positive, Negative, or Neutral: + * + * Text: I love this product! + * Sentiment: Positive + * + * Text: This is terrible. + * Sentiment: Negative + * + * Text: It's okay. + * Sentiment: Neutral + * + * Text: The product is okay, nothing special. + * Sentiment: + * + * --- Test 4: Code Explanation --- + * Result: + * Explain what each line of code does: + * + * Code: x = x + 1 + * Explanation: Increment x by 1 + * + * Code: if x > 0: + * Explanation: Check if x is positive + * + * Code: for i in range(10): + * Explanation: + * + * --- Test 5: Custom Separator --- + * Result: + * Examples: + * Example 1 + * --- + * Example 2 + * --- + * Example 3 + * + * Your turn: ... + * + * --- Test 6: Translation with Context --- + * Result: + * Translate English to Spanish. Context: casual conversation + * + * English: Hello + * Spanish: Hola + * + * English: How are you? + * Spanish: ¿Cómo estás? + * + * English: See you later! + * Spanish: + * + * --- Test 7: No Examples --- + * Result: + * Answer the following question: + * + * Question: What is AI? + * Answer: + * + * --- Test 8: Use as Runnable --- + * Invoked result: + * Find antonyms: + * + * big -> small + * + * fast -> slow + * + * light -> + * + * Learning Points: + * 1. Few-shot learning provides examples to guide LLMs + * 2. Structure: prefix + examples + suffix + * 3. Examples are formatted with examplePrompt + * 4. Custom separators control formatting + * 5. Can work with or without examples + * 6. Dramatically improves LLM output quality + * 7. Essential for classification, generation, and transformation tasks + * 8. Variable substitution works in prefix, suffix, and examples + */ \ No newline at end of file diff --git a/tutorial/02-composition/01-prompts/solutions/20-pipeline-prompt-template-solution.js b/tutorial/02-composition/01-prompts/solutions/20-pipeline-prompt-template-solution.js new file mode 100644 index 0000000000000000000000000000000000000000..ae9340011676812cb2cdfd48663d8e41844dfa1f --- /dev/null +++ b/tutorial/02-composition/01-prompts/solutions/20-pipeline-prompt-template-solution.js @@ -0,0 +1,343 @@ +/** + * Solution 20: PipelinePromptTemplate + */ + +import {PromptTemplate, PipelinePromptTemplate} from '../../../../src/index.js'; + +// Test cases +async function exercise() { + console.log('=== Exercise 20: PipelinePromptTemplate ===\n'); + + // Test 1: Context + Question pattern + console.log('--- Test 1: Context + Question ---'); + + const mainPrompt = PromptTemplate.fromTemplate( + "{context}\n\nQuestion: {question}\nAnswer:" + ); + const contextPrompt = PromptTemplate.fromTemplate( + "Context: {topic} is important because {reason}" + ); + + const pipeline1 = new PipelinePromptTemplate({ + finalPrompt: mainPrompt, + pipelinePrompts: [ + {name: "context", prompt: contextPrompt} + ] + }) + + console.log('Input variables:', pipeline1.inputVariables); + + const result1 = await pipeline1.format({ + topic: "AI safety", + reason: "it affects humanity's future", + question: "What are the main concerns?" + }) + + console.log('\nResult:'); + console.log(result1); + console.log(); + + // Test 2: Instructions + Examples + Query + console.log('--- Test 2: Instructions + Examples + Query ---'); + + const instructionsPrompt = PromptTemplate.fromTemplate( + "Instructions: {instructions}" + ); + + const examplesPrompt = PromptTemplate.fromTemplate( + "Examples:\n{example1}\n{example2}" + ); + + const taskPrompt = PromptTemplate.fromTemplate( + "{instructions}\n\n{examples}\n\nNow you try:\n{query}" + ); + + const pipeline2 = new PipelinePromptTemplate({ + finalPrompt: taskPrompt, + pipelinePrompts: [ + { name: "instructions", prompt: instructionsPrompt }, + { name: "examples", prompt: examplesPrompt } + ] + }); + + const result2 = await pipeline2.format({ + instructions: "Translate words to Spanish", + example1: "hello → hola", + example2: "goodbye → adiós", + query: "cat" + }); + + console.log('Result:'); + console.log(result2); + console.log(); + + // Test 3: Multi-stage prompt composition + console.log('--- Test 3: Multi-Stage Composition ---'); + + const domainPrompt = PromptTemplate.fromTemplate( + "Domain: {domain}\nExpertise Level: {level}" + ); + + const constraintsPrompt = PromptTemplate.fromTemplate( + "Constraints:\n- Max length: {max_length} words\n- Tone: {tone}" + ); + + const taskPrompt3 = PromptTemplate.fromTemplate( + "Task: {task}" + ); + + const finalPrompt3 = PromptTemplate.fromTemplate( + "{domain_context}\n\n{constraints}\n\n{task_description}\n\nResponse:" + ); + + const pipeline3 = new PipelinePromptTemplate({ + finalPrompt: finalPrompt3, + pipelinePrompts: [ + { name: "domain_context", prompt: domainPrompt }, + { name: "constraints", prompt: constraintsPrompt }, + { name: "task_description", prompt: taskPrompt3 } + ] + }); + + const result3 = await pipeline3.format({ + domain: "Machine Learning", + level: "Advanced", + max_length: "100", + tone: "technical", + task: "Explain gradient descent" + }); + + console.log('Result:'); + console.log(result3); + console.log(); + + // Test 4: Reusable components + console.log('--- Test 4: Reusable Components ---'); + + const systemContext = PromptTemplate.fromTemplate( + "You are a {role} with expertise in {expertise}." + ); + + const pipeline4a = new PipelinePromptTemplate({ + finalPrompt: PromptTemplate.fromTemplate("{system}\n\nTask: {task}"), + pipelinePrompts: [{ name: "system", prompt: systemContext }] + }); + + const pipeline4b = new PipelinePromptTemplate({ + finalPrompt: PromptTemplate.fromTemplate("{system}\n\nQuestion: {question}"), + pipelinePrompts: [{ name: "system", prompt: systemContext }] + }); + + const result4a = await pipeline4a.format({ + role: "software engineer", + expertise: "Python", + task: "Debug this code" + }); + + const result4b = await pipeline4b.format({ + role: "software engineer", + expertise: "Python", + question: "What are decorators?" + }); + + console.log('Pipeline A (Task):'); + console.log(result4a); + console.log('\nPipeline B (Question):'); + console.log(result4b); + console.log(); + + // Test 5: Dynamic content injection + console.log('--- Test 5: Dynamic Content Injection ---'); + + const datePrompt = PromptTemplate.fromTemplate( + "Current Date: {date}" + ); + + const userPrompt = PromptTemplate.fromTemplate( + "User: {username} (Preference: {preference})" + ); + + const mainPrompt5 = PromptTemplate.fromTemplate( + "{date_info}\n{user_info}\n\nRequest: {request}\nResponse:" + ); + + const pipeline5 = new PipelinePromptTemplate({ + finalPrompt: mainPrompt5, + pipelinePrompts: [ + { name: "date_info", prompt: datePrompt }, + { name: "user_info", prompt: userPrompt } + ] + }); + + const result5 = await pipeline5.format({ + date: new Date().toLocaleDateString(), + username: "Alice", + preference: "concise answers", + request: "Explain quantum computing" + }); + + console.log('Result:'); + console.log(result5); + console.log(); + + // Test 6: Nested complexity + console.log('--- Test 6: Complex Nested Pipeline ---'); + + const backgroundPrompt = PromptTemplate.fromTemplate( + "Background:\n{background}" + ); + + const methodologyPrompt = PromptTemplate.fromTemplate( + "Methodology:\n{methodology}" + ); + + const outcomePrompt = PromptTemplate.fromTemplate( + "Expected Outcome:\n{outcome}" + ); + + const proposalPrompt = PromptTemplate.fromTemplate( + "Research Proposal: {title}\n\n{background_section}\n\n{methodology_section}\n\n{outcome_section}\n\nSubmitted by: {author}" + ); + + const pipeline6 = new PipelinePromptTemplate({ + finalPrompt: proposalPrompt, + pipelinePrompts: [ + { name: "background_section", prompt: backgroundPrompt }, + { name: "methodology_section", prompt: methodologyPrompt }, + { name: "outcome_section", prompt: outcomePrompt } + ] + }); + + console.log('Input variables needed:', pipeline6.inputVariables); + + const result6 = await pipeline6.format({ + title: "AI in Healthcare", + background: "Current healthcare systems face challenges...", + methodology: "We will use deep learning models...", + outcome: "Improved diagnosis accuracy by 20%", + author: "Dr. Smith" + }); + + console.log('\nResult:'); + console.log(result6); + console.log(); + + // Test 7: Use as Runnable + console.log('--- Test 7: Use as Runnable ---'); + + const runnablePipeline = new PipelinePromptTemplate({ + finalPrompt: PromptTemplate.fromTemplate("{intro}\n\nQuery: {query}\nAnswer:"), + pipelinePrompts: [ + { + name: "intro", + prompt: PromptTemplate.fromTemplate("You are a {role} assistant.") + } + ] + }); + + const result7 = await runnablePipeline.invoke({ + role: "helpful", + query: "What is machine learning?" + }); + + console.log('Invoked result:'); + console.log(result7); + console.log(); + + console.log('✓ Exercise 4 complete!'); +} + +// Run the exercise +exercise().catch(console.error); + +/** + * Expected Output: + * + * --- Test 1: Context + Question --- + * Input variables: [ 'question', 'topic', 'reason' ] + * + * Result: + * Context: AI safety is important because it affects humanity's future + * + * Question: What are the main concerns? + * Answer: + * + * --- Test 2: Instructions + Examples + Query --- + * Result: + * Instructions: Translate words to Spanish + * + * Examples: + * hello → hola + * goodbye → adiós + * + * Now you try: + * cat + * + * --- Test 3: Multi-Stage Composition --- + * Result: + * Domain: Machine Learning + * Expertise Level: Advanced + * + * Constraints: + * - Max length: 100 words + * - Tone: technical + * + * Task: Explain gradient descent + * + * Response: + * + * --- Test 4: Reusable Components --- + * Pipeline A (Task): + * You are a software engineer with expertise in Python. + * + * Task: Debug this code + * + * Pipeline B (Question): + * You are a software engineer with expertise in Python. + * + * Question: What are decorators? + * + * --- Test 5: Dynamic Content Injection --- + * Result: + * Current Date: 11/20/2025 + * User: Alice (Preference: concise answers) + * + * Request: Explain quantum computing + * Response: + * + * --- Test 6: Complex Nested Pipeline --- + * Input variables: [ 'title', 'author', 'background', 'methodology', 'outcome' ] + * + * Result: + * Research Proposal: AI in Healthcare + * + * Background: + * Current healthcare systems face challenges... + * + * Methodology: + * We will use deep learning models... + * + * Expected Outcome: + * Improved diagnosis accuracy by 20% + * + * Submitted by: Dr. Smith + * + * --- Test 7: Use as Runnable --- + * Invoked result: + * You are a helpful assistant. + * + * Query: What is machine learning? + * Answer: + * + * Learning Points: + * 1. PipelinePromptTemplate composes prompts modularly + * 2. Each pipeline prompt generates a named output + * 3. Final prompt uses pipeline outputs as variables + * 4. Input variables are collected from all prompts + * 5. Pipeline outputs are NOT input variables + * 6. Enables reusable prompt components + * 7. Perfect for complex, structured prompts + * 8. Makes prompt engineering more maintainable + * 9. Can nest arbitrary levels of complexity + * 10. Separates concerns in prompt construction + */ \ No newline at end of file diff --git a/tutorial/02-composition/02-parsers/exercises/21-review-analyzer.js b/tutorial/02-composition/02-parsers/exercises/21-review-analyzer.js new file mode 100644 index 0000000000000000000000000000000000000000..8526aeef94482afadde7ce26f116ee312e35ceef --- /dev/null +++ b/tutorial/02-composition/02-parsers/exercises/21-review-analyzer.js @@ -0,0 +1,294 @@ +/** + * Exercise 21: Product Review Analyzer + * + * Difficulty: ⭐☆☆☆ (Beginner) + * + * Goal: Learn to use StringOutputParser for text cleanup and basic chain building + * + * In this exercise, you'll: + * 1. Use StringOutputParser to clean LLM outputs + * 2. Build a simple prompt → LLM → parser chain + * 3. Process multiple reviews + * 4. See why parsers matter for text cleanup + * + * Skills practiced: + * - Using parsers in chains + * - Text cleaning with StringOutputParser + * - Basic chain composition + * + * You can find a few HINTS at the end of this file + */ + +import {Runnable, PromptTemplate, StringOutputParser} from '../../../../src/index.js'; +import {LlamaCppLLM} from '../../../../src/llm/llama-cpp-llm.js'; +import {QwenChatWrapper} from "node-llama-cpp"; + +// Sample product reviews to analyze +const REVIEWS = [ + "This product is amazing! Best purchase ever. 5 stars!", + "Terrible quality. Broke after one week. Very disappointed.", + "It's okay. Does the job but nothing special.", + "Love it! Exactly what I needed. Highly recommend!", + "Not worth the price. Expected better quality." +]; + +// ============================================================================ +// TODO 1: Create a Review Summarizer +// ============================================================================ + +/** + * Build a chain that: + * 1. Takes a review as input + * 2. Asks LLM to summarize in one sentence + * 3. Uses StringOutputParser to clean the output + */ +async function createReviewSummarizer() { + // TODO: Create a prompt template + // Ask LLM to summarize the review in one short sentence + // Template should have {review} variable + const prompt = null; + + // TODO: Create LLM instance + // Use low temperature for consistent summaries + const llm = null; + + // TODO: Create StringOutputParser + // This will clean whitespace and remove markdown + const parser = null; + + // TODO: Build the chain: prompt → llm → parser + const chain = null; + + return chain; +} + +// ============================================================================ +// TODO 2: Create a Sentiment Extractor +// ============================================================================ + +/** + * Build a chain that: + * 1. Takes a review + * 2. Asks LLM to respond with ONLY one word: positive, negative, or neutral + * 3. Uses StringOutputParser to clean the response + */ +async function createSentimentExtractor() { + // TODO: Create a prompt template + // Ask LLM to respond with ONLY: positive, negative, or neutral + // Be very explicit in the prompt about the format + const prompt = null; + + // TODO: Create LLM with low temperature + const llm = null; + + // TODO: Create StringOutputParser + const parser = null; + + // TODO: Build the chain + const chain = null; + + return chain; +} + +// ============================================================================ +// TODO 3: Process All Reviews +// ============================================================================ + +async function analyzeReviews() { + console.log('=== Exercise 21: Product Review Analyzer ===\n'); + + // TODO: Create both chains + const summarizerChain = null; + const sentimentChain = null; + + console.log('Processing reviews...\n'); + + // TODO: Process each review + for (let i = 0; i < REVIEWS.length; i++) { + const review = REVIEWS[i]; + + console.log(`Review ${i + 1}: "${review}"`); + + // TODO: Get summary using summarizerChain + const summary = null; + + // TODO: Get sentiment using sentimentChain + const sentiment = null; + + console.log(`Summary: ${summary}`); + console.log(`Sentiment: ${sentiment}`); + console.log(); + } + + console.log('✓ Exercise 1 Complete!'); + + return {summarizerChain, sentimentChain}; +} + +// Run the exercise +analyzeReviews() + .then(runTests) + .catch(console.error); + +// ============================================================================ +// AUTOMATED TESTS +// ============================================================================ + +async function runTests(results) { + const {summarizerChain, sentimentChain} = results; + + console.log('\n' + '='.repeat(60)); + console.log('RUNNING AUTOMATED TESTS'); + console.log('='.repeat(60) + '\n'); + + const assert = (await import('assert')).default; + let passed = 0; + let failed = 0; + + async function test(name, fn) { + try { + await fn(); + passed++; + console.log(`✅ ${name}`); + } catch (error) { + failed++; + console.error(`❌ ${name}`); + console.error(` ${error.message}\n`); + } + } + + // Test 1: Chains created + await test('Summarizer chain created', async () => { + assert(summarizerChain !== null && summarizerChain !== undefined, 'Create summarizerChain'); + assert(summarizerChain instanceof Runnable, 'Chain should be Runnable'); + }); + + await test('Sentiment chain created', async () => { + assert(sentimentChain !== null && sentimentChain !== undefined, 'Create sentimentChain'); + assert(sentimentChain instanceof Runnable, 'Chain should be Runnable'); + }); + + // Test 2: Chains work (only run if chains exist) + if (summarizerChain !== null && summarizerChain !== undefined) { + await test('Summarizer chain produces output', async () => { + const result = await summarizerChain.invoke({ + review: "Great product! Love it!" + }); + assert(typeof result === 'string', 'Should return string'); + assert(result.length > 0, 'Should not be empty'); + assert(result.length < 200, 'Should be concise (< 200 chars)'); + }); + } else { + failed++; + console.error(`❌ Summarizer chain produces output`); + console.error(` Cannot test - summarizerChain is not created\n`); + } + + if (sentimentChain !== null && sentimentChain !== undefined) { + await test('Sentiment chain produces valid sentiment', async () => { + const result = await sentimentChain.invoke({ + review: "Terrible product. Very bad." + }); + const cleaned = result.toLowerCase().trim(); + const validSentiments = ['positive', 'negative', 'neutral']; + assert( + validSentiments.includes(cleaned), + `Should be one of: ${validSentiments.join(', ')}. Got: ${cleaned}` + ); + }); + } else { + failed++; + console.error(`❌ Sentiment chain produces valid sentiment`); + console.error(` Cannot test - sentimentChain is not created\n`); + } + + // Test 3: Parser cleans output (only if chain exists) + if (sentimentChain !== null && sentimentChain !== undefined) { + await test('StringOutputParser removes extra whitespace', async () => { + const result = await sentimentChain.invoke({ + review: "It's okay" + }); + assert(result === result.trim(), 'Should have no leading/trailing whitespace'); + assert(!result.includes(' '), 'Should have no double spaces'); + }); + } else { + failed++; + console.error(`❌ StringOutputParser removes extra whitespace`); + console.error(` Cannot test - sentimentChain is not created\n`); + } + + // Test 4: Consistent results (only if chain exists) + if (sentimentChain !== null && sentimentChain !== undefined) { + await test('Chains produce consistent sentiment', async () => { + const positive = await sentimentChain.invoke({ + review: "Amazing! Best ever! 5 stars!" + }); + const negative = await sentimentChain.invoke({ + review: "Horrible! Worst purchase ever! 0 stars!" + }); + + assert( + positive.toLowerCase().includes('positive'), + 'Clearly positive review should be classified as positive' + ); + assert( + negative.toLowerCase().includes('negative'), + 'Clearly negative review should be classified as negative' + ); + }); + } else { + failed++; + console.error(`❌ Chains produce consistent sentiment`); + console.error(` Cannot test - sentimentChain is not created\n`); + } + + // Summary + console.log('\n' + '='.repeat(60)); + console.log('TEST SUMMARY'); + console.log('='.repeat(60)); + console.log(`Total: ${passed + failed}`); + console.log(`✅ Passed: ${passed}`); + console.log(`❌ Failed: ${failed}`); + console.log('='.repeat(60)); + + if (failed === 0) { + console.log('\n🎉 All tests passed!\n'); + console.log('📚 What you learned:'); + console.log(' • StringOutputParser cleans text automatically'); + console.log(' • Parsers work seamlessly in chains with .pipe()'); + console.log(' • Low temperature gives consistent outputs'); + console.log(' • Clear prompts help parsers succeed'); + console.log(' • Chains are reusable across multiple inputs\n'); + } else { + console.log('\n⚠️ Some tests failed. Check your implementation.\n'); + } +} + +/** + * HINTS: + * + * 1. PromptTemplate syntax: + * new PromptTemplate({ + * template: "Your prompt with {variable}", + * inputVariables: ["variable"] + * }) + * + * 2. LlamaCppLLM setup: + * new LlamaCppLLM({ + * modelPath: './models/your-model.gguf', + * temperature: 0.1 // Low for consistency + * }) + * + * 3. StringOutputParser: + * new StringOutputParser() + * // That's it! No config needed + * + * 4. Building chains: + * const chain = prompt.pipe(llm).pipe(parser); + * + * 5. Using chains: + * const result = await chain.invoke({ variable: "value" }); + * + * 6. For sentiment, be VERY explicit in prompt: + * "Respond with ONLY one word: positive, negative, or neutral" + */ \ No newline at end of file diff --git a/tutorial/02-composition/02-parsers/exercises/22-contact-extractor.js b/tutorial/02-composition/02-parsers/exercises/22-contact-extractor.js new file mode 100644 index 0000000000000000000000000000000000000000..01f9cad3dad4e7500b6c348fa036477397687dff --- /dev/null +++ b/tutorial/02-composition/02-parsers/exercises/22-contact-extractor.js @@ -0,0 +1,379 @@ +/** + * Exercise 22: Contact Information Extractor + * + * Difficulty: ⭐⭐☆☆ (Intermediate) + * + * Goal: Learn to use JsonOutputParser and ListOutputParser for structured data + * + * In this exercise, you'll: + * 1. Use JsonOutputParser to extract contact info from text + * 2. Use ListOutputParser to extract lists of items + * 3. Handle format instructions in prompts + * 4. Validate extracted data + * + * Skills practiced: + * - JSON extraction from unstructured text + * - List parsing from various formats + * - Including format instructions + * - Schema validation + */ + +import {Runnable, PromptTemplate, JsonOutputParser, ListOutputParser} from '../../../../src/index.js'; +import {LlamaCppLLM} from '../../../../src/llm/llama-cpp-llm.js'; +import {QwenChatWrapper} from "node-llama-cpp"; + +// Sample text snippets with contact information +const TEXT_SAMPLES = [ + "Contact John Smith at john.smith@email.com or call 555-0123. He's based in New York.", + "For inquiries, reach out to Sarah Johnson (sarah.j@company.com), phone: 555-9876, located in San Francisco.", + "Please contact Dr. Michael Chen at m.chen@hospital.org or 555-4567. Office in Boston." +]; + +// ============================================================================ +// TODO 1: Create Contact Info Extractor (JSON) +// ============================================================================ + +/** + * Build a chain that extracts structured contact information: + * - name + * - email + * - phone + * - location + */ +async function createContactExtractor() { + // TODO: Create JsonOutputParser with schema + // Schema should validate: name, email, phone, location (all strings) + const parser = null; + + // TODO: Create prompt template + // Include parser.getFormatInstructions() in the template! + // This tells the LLM what JSON format you expect + const prompt = null; + + // TODO: Create LLM + const llm = null; + + // TODO: Build chain + const chain = null; + + return chain; +} + +// ============================================================================ +// TODO 2: Create Skills Extractor (List) +// ============================================================================ + +/** + * Build a chain that extracts a list of skills from a job description + * Should return array of strings + */ +async function createSkillsExtractor() { + // TODO: Create ListOutputParser + // No special config needed - it handles various list formats + const parser = null; + + // TODO: Create prompt template + // Ask LLM to list skills, one per line or numbered + // Include parser.getFormatInstructions() + const prompt = null; + + // TODO: Create LLM + const llm = null; + + // TODO: Build chain + const chain = null; + + return chain; +} + +// ============================================================================ +// TODO 3: Create Company Info Extractor (JSON with nested data) +// ============================================================================ + +/** + * Build a chain that extracts company info including multiple contacts + */ +async function createCompanyExtractor() { + // TODO: Create JsonOutputParser + // No schema validation needed, just extract JSON + const parser = null; + + // TODO: Create prompt template + // Ask for: company name, industry, year founded, employee count + // Include format instructions + const prompt = null; + + // TODO: Create LLM + const llm = null; + + // TODO: Build chain + const chain = null; + + return chain; +} + +// ============================================================================ +// TODO 4: Process Examples and Validate +// ============================================================================ + +async function extractContactInfo() { + console.log('=== Exercise 22: Contact Information Extractor ===\n'); + + // TODO: Create all chains + const contactChain = null; + const skillsChain = null; + const companyChain = null; + + // Test 1: Extract contact info + console.log('--- Test 1: Extracting Contact Information ---\n'); + + for (let i = 0; i < TEXT_SAMPLES.length; i++) { + const text = TEXT_SAMPLES[i]; + console.log(`Text ${i + 1}: "${text}"`); + + // TODO: Extract contact info + const contact = null; + + console.log('Extracted:', contact); + console.log(); + } + + // Test 2: Extract skills from job description + console.log('--- Test 2: Extracting Skills List ---\n'); + + const description = `We're looking for a Full Stack Developer with experience in: +JavaScript, Python, React, Node.js, PostgreSQL, Docker, AWS, and Git. +Strong communication and problem-solving skills required.`; + + console.log(`Job Description: "${description}"\n`); + + // TODO: Extract skills + const skills = null; + + console.log('Extracted Skills:', skills); + console.log(); + + // Test 3: Extract company info + console.log('--- Test 3: Extracting Company Information ---\n'); + + const companyText = `TechCorp is a leading software company in the cloud computing industry. +Founded in 2010, the company now employs over 500 people across three continents.`; + + console.log(`Company Text: "${companyText}"\n`); + + // TODO: Extract company info + const companyInfo = null; + + console.log('Extracted Info:', companyInfo); + console.log(); + + console.log('✓ Exercise 2 Complete!'); + + return { contactChain, skillsChain, companyChain }; +} + +// Run the exercise +extractContactInfo() + .then(runTests) + .catch(console.error); + +// ============================================================================ +// AUTOMATED TESTS +// ============================================================================ + +async function runTests(results) { + const { contactChain, skillsChain, companyChain } = results; + + console.log('\n' + '='.repeat(60)); + console.log('RUNNING AUTOMATED TESTS'); + console.log('='.repeat(60) + '\n'); + + const assert = (await import('assert')).default; + let passed = 0; + let failed = 0; + + async function test(name, fn) { + try { + await fn(); + passed++; + console.log(`✅ ${name}`); + } catch (error) { + failed++; + console.error(`❌ ${name}`); + console.error(` ${error.message}\n`); + } + } + + // Test 1: Chains created + await test('Contact extractor chain created', async () => { + assert(contactChain !== null && contactChain !== undefined, 'Create contactChain'); + assert(contactChain instanceof Runnable, 'Should be Runnable'); + }); + + await test('Skills extractor chain created', async () => { + assert(skillsChain !== null && skillsChain !== undefined, 'Create skillsChain'); + assert(skillsChain instanceof Runnable, 'Should be Runnable'); + }); + + await test('Company extractor chain created', async () => { + assert(companyChain !== null && companyChain !== undefined, 'Create companyChain'); + assert(companyChain instanceof Runnable, 'Should be Runnable'); + }); + + // Test 2: Contact extraction (only run if chain exists) + if (contactChain !== null && contactChain !== undefined) { + await test('Contact extractor returns object', async () => { + const result = await contactChain.invoke({ + text: "Contact Alice at alice@email.com, phone 555-1234, in Seattle" + }); + assert(typeof result === 'object', 'Should return object'); + assert(!Array.isArray(result), 'Should not be array'); + }); + + await test('Contact object has required fields', async () => { + const result = await contactChain.invoke({ + text: "Contact Bob at bob@email.com, phone 555-5678, in Portland" + }); + assert('name' in result, 'Should have name field'); + assert('email' in result, 'Should have email field'); + assert('phone' in result, 'Should have phone field'); + }); + + await test('Contact fields are strings', async () => { + const result = await contactChain.invoke({ + text: "Contact Carol at carol@email.com" + }); + if (result.name) assert(typeof result.name === 'string', 'name should be string'); + if (result.email) assert(typeof result.email === 'string', 'email should be string'); + }); + } else { + failed += 3; + console.error(`❌ Contact extractor returns object`); + console.error(` Cannot test - contactChain is not created\n`); + console.error(`❌ Contact object has required fields`); + console.error(` Cannot test - contactChain is not created\n`); + console.error(`❌ Contact fields are strings`); + console.error(` Cannot test - contactChain is not created\n`); + } + + // Test 3: Skills extraction (only run if chain exists) + if (skillsChain !== null && skillsChain !== undefined) { + await test('Skills extractor returns array', async () => { + const result = await skillsChain.invoke({ + description: "Looking for: JavaScript, Python, SQL" + }); + assert(Array.isArray(result), 'Should return array'); + }); + + await test('Skills array contains strings', async () => { + const result = await skillsChain.invoke({ + description: "Requirements: Java, C++, Git, Docker" + }); + assert(result.length > 0, 'Should extract at least one skill'); + assert( + result.every(skill => typeof skill === 'string'), + 'All skills should be strings' + ); + }); + + await test('Skills array has no empty strings', async () => { + const result = await skillsChain.invoke({ + description: "Skills: React, Node.js, MongoDB" + }); + assert( + result.every(skill => skill.trim().length > 0), + 'Should have no empty strings' + ); + }); + } else { + failed += 3; + console.error(`❌ Skills extractor returns array`); + console.error(` Cannot test - skillsChain is not created\n`); + console.error(`❌ Skills array contains strings`); + console.error(` Cannot test - skillsChain is not created\n`); + console.error(`❌ Skills array has no empty strings`); + console.error(` Cannot test - skillsChain is not created\n`); + } + + // Test 4: Company extraction (only run if chain exists) + if (companyChain !== null && companyChain !== undefined) { + await test('Company extractor returns object', async () => { + const result = await companyChain.invoke({ + text: "CloudTech was founded in 2015 in the SaaS industry with 100 employees" + }); + assert(typeof result === 'object', 'Should return object'); + }); + } else { + failed++; + console.error(`❌ Company extractor returns object`); + console.error(` Cannot test - companyChain is not created\n`); + } + + // Test 5: JSON parsing robustness (always run - tests parser capability) + await test('JsonParser handles markdown code blocks', async () => { + // The parser should extract JSON even if LLM wraps it in ```json + // This test verifies the parser class exists and has the capability + const parser = new JsonOutputParser(); + assert(parser !== null, 'JsonOutputParser should be instantiable'); + assert(typeof parser.parse === 'function', 'Parser should have parse method'); + }); + + // Summary + console.log('\n' + '='.repeat(60)); + console.log('TEST SUMMARY'); + console.log('='.repeat(60)); + console.log(`Total: ${passed + failed}`); + console.log(`✅ Passed: ${passed}`); + console.log(`❌ Failed: ${failed}`); + console.log('='.repeat(60)); + + if (failed === 0) { + console.log('\n🎉 All tests passed!\n'); + console.log('📚 What you learned:'); + console.log(' • JsonOutputParser extracts structured data reliably'); + console.log(' • ListOutputParser handles multiple list formats'); + console.log(' • getFormatInstructions() tells LLM what you expect'); + console.log(' • Schema validation ensures data quality'); + console.log(' • Parsers handle markdown and extra text gracefully\n'); + } else { + console.log('\n⚠️ Some tests failed. Check your implementation.\n'); + } +} + +/** + * HINTS: + * + * 1. JsonOutputParser with schema: + * new JsonOutputParser({ + * schema: { + * name: 'string', + * email: 'string', + * phone: 'string' + * } + * }) + * + * 2. Including format instructions: + * const prompt = new PromptTemplate({ + * template: `Extract info from: {text} + * + * {format_instructions}`, + * inputVariables: ["text"], + * partialVariables: { + * format_instructions: parser.getFormatInstructions() + * } + * }) + * + * 3. ListOutputParser: + * new ListOutputParser() + * // Handles: "1. Item", "- Item", "Item1, Item2" + * + * 4. JSON extraction handles: + * - Plain JSON: {"key": "value"} + * - Markdown: ```json\n{...}\n``` + * - Extra text: "Here's the data: {...}" + * + * 5. For better results: + * - Use clear, explicit prompts + * - Include format instructions + * - Use low temperature (0.1-0.3) + */ \ No newline at end of file diff --git a/tutorial/02-composition/02-parsers/exercises/23-article-metadata.js b/tutorial/02-composition/02-parsers/exercises/23-article-metadata.js new file mode 100644 index 0000000000000000000000000000000000000000..8fd438f173faa3d5b62c92a29bb2d819275ecf2e --- /dev/null +++ b/tutorial/02-composition/02-parsers/exercises/23-article-metadata.js @@ -0,0 +1,447 @@ +/** + * Exercise 23: Article Metadata Extractor + * + * Difficulty: ⭐⭐⭐☆ (Advanced) + * + * Goal: Master StructuredOutputParser with complex schemas and validation + * + * In this exercise, you'll: + * 1. Use StructuredOutputParser with detailed schemas + * 2. Define fields with types, descriptions, and enums + * 3. Handle optional vs required fields + * 4. Build a complete metadata extraction system + * + * Skills practiced: + * - Complex schema definition + * - Type validation (string, number, boolean, array) + * - Enum constraints + * - Required vs optional fields + * - Error handling and validation + */ + +import {Runnable, PromptTemplate, StructuredOutputParser} from '../../../../src/index.js'; +import {LlamaCppLLM} from '../../../../src/llm/llama-cpp-llm.js'; +import {QwenChatWrapper} from "node-llama-cpp"; + +// Sample articles to extract metadata from +const ARTICLES = [ + { + title: "The Future of AI in Healthcare", + content: `Artificial intelligence is revolutionizing healthcare. From diagnostic tools to +personalized treatment plans, AI is improving patient outcomes. Recent studies show 85% accuracy +in detecting certain cancers. However, challenges remain around data privacy and ethical concerns. +This technology will continue to transform medicine in the coming decade.`, + author: "Dr. Sarah Johnson" + }, + { + title: "Climate Change: A Global Challenge", + content: `Climate change poses an existential threat to humanity. Rising temperatures, +extreme weather events, and sea level rise are already impacting millions. The latest IPCC report +warns we have less than 10 years to act. Renewable energy and carbon reduction are critical. +International cooperation is essential to address this crisis.`, + author: "Michael Chen" + }, + { + title: "The Rise of Remote Work", + content: `The pandemic accelerated the shift to remote work. Many companies now offer +hybrid or fully remote options. Productivity studies show mixed results - some teams thrive, +others struggle. Work-life balance improves for many, but isolation is a concern. The future +of work will likely be flexible, with employees choosing their preferred setup.`, + author: "Emma Williams" + } +]; + +// ============================================================================ +// TODO 1: Create Article Metadata Extractor +// ============================================================================ + +/** + * Build a chain that extracts comprehensive article metadata with validation + */ +async function createArticleMetadataExtractor() { + // TODO: Create StructuredOutputParser with detailed schema + // + // Schema should include: + // - category: string, enum: ["technology", "health", "environment", "business", "other"] + // - sentiment: string, enum: ["positive", "negative", "neutral", "mixed"] + // - readingLevel: string, enum: ["beginner", "intermediate", "advanced"] + // - mainTopics: array (required) + // - hasCitations: boolean + // - estimatedReadTime: number (in minutes) + // - keyTakeaway: string + // - targetAudience: string + const parser = null; + + // TODO: Create prompt template + // Should include: + // - Article title, content, author + // - Clear instructions for each field + // - Format instructions from parser + const prompt = null; + + // TODO: Create LLM + const llm = null; + + // TODO: Build chain + const chain = null; + + return chain; +} + +// ============================================================================ +// TODO 2: Create Content Quality Analyzer +// ============================================================================ + +/** + * Build a chain that analyzes content quality with scores + */ +async function createQualityAnalyzer() { + // TODO: Create StructuredOutputParser + // + // Schema: + // - clarity: number (1-10) + // - depth: number (1-10) + // - accuracy: number (1-10) + // - engagement: number (1-10) + // - overallScore: number (1-10) + // - strengths: array + // - improvements: array + // - recommendation: string, enum: ["publish", "revise", "reject"] + const parser = null; + + // TODO: Create prompt template + const prompt = null; + + // TODO: Create LLM + const llm = null; + + // TODO: Build chain + const chain = null; + + return chain; +} + +// ============================================================================ +// TODO 3: Create SEO Optimizer +// ============================================================================ + +/** + * Build a chain that provides SEO recommendations + */ +async function createSEOOptimizer() { + // TODO: Create StructuredOutputParser + // + // Schema: + // - suggestedKeywords: array + // - metaDescription: string (max ~160 chars) + // - hasGoodTitle: boolean + // - readabilityScore: number (1-100) + // - seoScore: number (1-100) + // - recommendations: array + const parser = null; + + // TODO: Create prompt template + const prompt = null; + + // TODO: Create LLM + const llm = null; + + // TODO: Build chain + const chain = null; + + return chain; +} + +// ============================================================================ +// TODO 4: Process Articles and Validate All Metadata +// ============================================================================ + +async function analyzeArticles() { + console.log('=== Exercise 23: Article Metadata Extractor ===\n'); + + // TODO: Create all chains + const metadataChain = null; + const qualityChain = null; + const seoChain = null; + + // Process each article + for (let i = 0; i < ARTICLES.length; i++) { + const article = ARTICLES[i]; + + console.log('='.repeat(70)); + console.log(`ARTICLE ${i + 1}: ${article.title}`); + console.log('='.repeat(70)); + console.log(`Author: ${article.author}`); + console.log(`Content: ${article.content.substring(0, 100)}...`); + console.log(); + + try { + // TODO: Extract metadata + console.log('--- Metadata ---'); + const metadata = null; + console.log(JSON.stringify(metadata, null, 2)); + console.log(); + + // TODO: Analyze quality + console.log('--- Quality Analysis ---'); + const quality = null; + console.log(JSON.stringify(quality, null, 2)); + console.log(); + + // TODO: Get SEO recommendations + console.log('--- SEO Recommendations ---'); + const seo = null; + console.log(JSON.stringify(seo, null, 2)); + console.log(); + + } catch (error) { + console.error(`Error processing article: ${error.message}`); + console.log(); + } + } + + console.log('✓ Exercise 23 Complete!'); + + return { metadataChain, qualityChain, seoChain }; +} + +// Run the exercise +analyzeArticles() + .then(runTests) + .catch(console.error); + +// ============================================================================ +// AUTOMATED TESTS +// ============================================================================ + +async function runTests(results) { + const { metadataChain, qualityChain, seoChain } = results; + + console.log('\n' + '='.repeat(60)); + console.log('RUNNING AUTOMATED TESTS'); + console.log('='.repeat(60) + '\n'); + + const assert = (await import('assert')).default; + let passed = 0; + let failed = 0; + + async function test(name, fn) { + try { + await fn(); + passed++; + console.log(`✅ ${name}`); + } catch (error) { + failed++; + console.error(`❌ ${name}`); + console.error(` ${error.message}\n`); + } + } + + const testArticle = { + title: "Test Article", + content: "This is test content about artificial intelligence in healthcare.", + author: "Test Author" + }; + + // Test 1: Chains created + test('Metadata chain created', async () => { + assert(metadataChain !== null, 'Create metadataChain'); + }); + + test('Quality chain created', async () => { + assert(qualityChain !== null, 'Create qualityChain'); + }); + + test('SEO chain created', async () => { + assert(seoChain !== null, 'Create seoChain'); + }); + + // Test 2: Metadata extraction + test('Metadata has required fields', async () => { + const result = await metadataChain.invoke({ + title: testArticle.title, + author: testArticle.author, + content: testArticle.content + }); + + assert('category' in result, 'Should have category'); + assert('sentiment' in result, 'Should have sentiment'); + assert('mainTopics' in result, 'Should have mainTopics'); + }); + + test('Metadata category is valid enum', async () => { + const result = await metadataChain.invoke({ + title: testArticle.title, + author: testArticle.author, + content: testArticle.content + }); + + const validCategories = ["technology", "health", "environment", "business", "other"]; + assert( + validCategories.includes(result.category), + `Category should be one of: ${validCategories.join(', ')}` + ); + }); + + test('Metadata sentiment is valid enum', async () => { + const result = await metadataChain.invoke({ + title: testArticle.title, + author: testArticle.author, + content: testArticle.content + }); + + const validSentiments = ["positive", "negative", "neutral", "mixed"]; + assert( + validSentiments.includes(result.sentiment), + `Sentiment should be one of: ${validSentiments.join(', ')}` + ); + }); + + test('Metadata mainTopics is array', async () => { + const result = await metadataChain.invoke({ + title: testArticle.title, + author: testArticle.author, + content: testArticle.content + }); + + assert(Array.isArray(result.mainTopics), 'mainTopics should be array'); + assert(result.mainTopics.length > 0, 'mainTopics should not be empty'); + }); + + test('Metadata estimatedReadTime is number', async () => { + const result = await metadataChain.invoke({ + title: testArticle.title, + author: testArticle.author, + content: testArticle.content + }); + + assert(typeof result.estimatedReadTime === 'number', 'estimatedReadTime should be number'); + assert(result.estimatedReadTime > 0, 'estimatedReadTime should be positive'); + }); + + test('Metadata hasCitations is boolean', async () => { + const result = await metadataChain.invoke({ + title: testArticle.title, + author: testArticle.author, + content: testArticle.content + }); + + assert(typeof result.hasCitations === 'boolean', 'hasCitations should be boolean'); + }); + + // Test 3: Quality analysis + test('Quality scores are numbers', async () => { + const result = await qualityChain.invoke({ article: testArticle }); + + assert(typeof result.clarity === 'number', 'clarity should be number'); + assert(typeof result.depth === 'number', 'depth should be number'); + assert(typeof result.overallScore === 'number', 'overallScore should be number'); + }); + + test('Quality scores are in valid range', async () => { + const result = await qualityChain.invoke({ article: testArticle }); + + assert(result.clarity >= 1 && result.clarity <= 10, 'clarity should be 1-10'); + assert(result.overallScore >= 1 && result.overallScore <= 10, 'overallScore should be 1-10'); + }); + + test('Quality has array fields', async () => { + const result = await qualityChain.invoke({ article: testArticle }); + + assert(Array.isArray(result.strengths), 'strengths should be array'); + assert(Array.isArray(result.improvements), 'improvements should be array'); + }); + + test('Quality recommendation is valid', async () => { + const result = await qualityChain.invoke({ article: testArticle }); + + const validRecommendations = ["publish", "revise", "reject"]; + assert( + validRecommendations.includes(result.recommendation), + `recommendation should be one of: ${validRecommendations.join(', ')}` + ); + }); + + // Test 4: SEO optimization + test('SEO has keyword suggestions', async () => { + const result = await seoChain.invoke({ article: testArticle }); + + assert(Array.isArray(result.suggestedKeywords), 'suggestedKeywords should be array'); + assert(result.suggestedKeywords.length > 0, 'Should suggest at least one keyword'); + }); + + test('SEO metaDescription is appropriate length', async () => { + const result = await seoChain.invoke({ article: testArticle }); + + assert(typeof result.metaDescription === 'string', 'metaDescription should be string'); + assert(result.metaDescription.length <= 200, 'metaDescription should be concise'); + }); + + test('SEO scores are in valid range', async () => { + const result = await seoChain.invoke({ article: testArticle }); + + assert(result.readabilityScore >= 1 && result.readabilityScore <= 100); + assert(result.seoScore >= 1 && result.seoScore <= 100); + }); + + // Summary + console.log('\n' + '='.repeat(60)); + console.log('TEST SUMMARY'); + console.log('='.repeat(60)); + console.log(`Total: ${passed + failed}`); + console.log(`✅ Passed: ${passed}`); + console.log(`❌ Failed: ${failed}`); + console.log('='.repeat(60)); + + if (failed === 0) { + console.log('\n🎉 All tests passed!\n'); + } else { + console.log('\n⚠️ Some tests failed. Check your implementation.\n'); + } +} + +/** + * HINTS: + * + * 1. StructuredOutputParser with full schema: + * new StructuredOutputParser({ + * responseSchemas: [ + * { + * name: "category", + * type: "string", + * description: "Article category", + * enum: ["tech", "health", "business"], + * required: true + * }, + * { + * name: "score", + * type: "number", + * description: "Quality score 1-10" + * } + * ] + * }) + * + * 2. Always include format instructions: + * partialVariables: { + * format_instructions: parser.getFormatInstructions() + * } + * + * 3. Types supported: + * - "string" + * - "number" + * - "boolean" + * - "array" + * - "object" + * + * 4. The parser will: + * - Validate all required fields exist + * - Check type of each field + * - Verify enum values if specified + * - Throw detailed errors on validation failure + * + * 5. For better LLM compliance: + * - Use low temperature (0.1-0.2) + * - Be explicit in prompts + * - Include examples if needed + * - Reference the format instructions clearly + */ \ No newline at end of file diff --git a/tutorial/02-composition/02-parsers/exercises/24-multi-parser-pipeline.js b/tutorial/02-composition/02-parsers/exercises/24-multi-parser-pipeline.js new file mode 100644 index 0000000000000000000000000000000000000000..f561d5701b920ca131cecc6872e4e041615eab60 --- /dev/null +++ b/tutorial/02-composition/02-parsers/exercises/24-multi-parser-pipeline.js @@ -0,0 +1,493 @@ +/** + * Exercise 24: Multi-Parser Content Pipeline + * + * Difficulty: ⭐⭐⭐⭐ (Expert) + * + * Goal: Build a robust content processing pipeline using multiple parsers with fallbacks + * + * In this exercise, you'll: + * 1. Combine multiple parser types in one system + * 2. Implement fallback strategies when parsing fails + * 3. Use RegexOutputParser for custom extraction + * 4. Build a production-ready error handling system + * 5. Create a complete content analysis pipeline + * + * Skills practiced: + * - Multi-parser orchestration + * - Fallback parsing strategies + * - Regex-based extraction + * - Error handling and recovery + * - Building robust production pipelines + */ + +import { + Runnable, + PromptTemplate, + StructuredOutputParser, + ListOutputParser, + RegexOutputParser +} from '../../../../src/index.js'; +import {LlamaCppLLM} from '../../../../src/llm/llama-cpp-llm.js'; +import {QwenChatWrapper} from "node-llama-cpp"; + +// Sample content to process +const CONTENT_SAMPLES = [ + { + text: "Breaking: Stock market hits record high! NASDAQ up 2.5%, S&P 500 gains 1.8%. Tech sector leads with Apple +3.2%, Microsoft +2.9%. Analysts predict continued growth.", + type: "news" + }, + { + text: "Recipe: Chocolate Chip Cookies. Ingredients: 2 cups flour, 1 cup butter, 1 cup sugar, 2 eggs, 1 tsp vanilla, 2 cups chocolate chips. Bake at 350°F for 12 minutes.", + type: "recipe" + }, + { + text: "Product Review: The XPhone 15 Pro (Score: 8.5/10) - Great camera, long battery life, but expensive at $1,199. Pros: Display, Performance. Cons: Price, Weight.", + type: "review" + } +]; + +// ============================================================================ +// TODO 1: Create News Article Parser (Structured) +// ============================================================================ + +/** + * Extract structured data from news articles + */ +async function createNewsParser() { + // TODO: Create StructuredOutputParser + // Schema: + // - headline: string + // - category: string, enum: ["business", "technology", "politics", "sports", "other"] + // - sentiment: string, enum: ["positive", "negative", "neutral"] + // - entities: array (companies, people, places mentioned) + // - marketData: array (any numbers with context like "NASDAQ up 2.5%") + const parser = null; + + // TODO: Create prompt + const prompt = null; + + const llm = new LlamaCppLLM({ + modelPath: './models/your-model.gguf', + temperature: 0.1 + }); + + const chain = prompt.pipe(llm).pipe(parser); + + return chain; +} + +// ============================================================================ +// TODO 2: Create Recipe Parser (Regex + List) +// ============================================================================ + +/** + * Extract recipe components using regex and list parsers + */ +async function createRecipeParser() { + // TODO: Create a Runnable that: + // 1. Extracts recipe name using RegexOutputParser + // 2. Extracts ingredients list using ListOutputParser + // 3. Extracts temperature and time using RegexOutputParser + // 4. Returns combined object + + // Hint: You'll need to create multiple chains and combine their results + + // RegexOutputParser for name: + // Pattern: /Recipe:\s*(.+?)\./ + const nameParser = null; + + // RegexOutputParser for temp/time: + // Pattern: /(\d+)°F.*?(\d+)\s*minutes/ + const cookingParser = null; + + // ListOutputParser for ingredients + const ingredientsParser = null; + + // TODO: Create a custom Runnable that orchestrates all parsers + class RecipeParserRunnable extends Runnable { + async _call(input, config) { + const text = input.text; + + // TODO: Extract name + // TODO: Extract ingredients + // TODO: Extract cooking details + + // Return combined result + return { + name: null, + ingredients: null, + temperature: null, + time: null + }; + } + } + + return new RecipeParserRunnable(); +} + +// ============================================================================ +// TODO 3: Create Review Parser with Fallback +// ============================================================================ + +/** + * Parse product reviews with fallback strategy + * Try structured parser first, fall back to regex if it fails + */ +async function createReviewParser() { + const llm = new LlamaCppLLM({ + modelPath: './models/your-model.gguf', + temperature: 0.1 + }); + + // TODO: Primary parser - StructuredOutputParser + const structuredParser = null; + // Schema: productName, score (number), pros (array), cons (array), price + + // TODO: Fallback parser - RegexOutputParser + const regexParser = null; + // Pattern to extract: Product name, Score, Price + // Example: /(\w+.*?)\s*\(Score:\s*([\d.]+).*?\$(\d+)/ + + // TODO: Create a Runnable with fallback logic + class ReviewParserWithFallback extends Runnable { + async _call(input, config) { + const text = input.text; + + try { + // TODO: Try structured parser first + const prompt = new PromptTemplate({ + template: `Extract review data: {text}\n\n{format_instructions}`, + inputVariables: ["text"], + partialVariables: { + format_instructions: structuredParser.getFormatInstructions() + } + }); + + const chain = prompt.pipe(llm).pipe(structuredParser); + const result = await chain.invoke({text}); + + return { + method: 'structured', + data: result + }; + } catch (error) { + console.warn('Structured parsing failed, using regex fallback'); + + try { + // TODO: Fall back to regex parser + const result = await regexParser.parse(text); + + return { + method: 'regex', + data: result + }; + } catch (regexError) { + // TODO: Final fallback - return basic string parsing + console.warn('Regex parsing failed, using basic extraction'); + + return { + method: 'basic', + data: { + text: text, + error: 'Could not parse structured data' + } + }; + } + } + } + } + + return new ReviewParserWithFallback(); +} + +// ============================================================================ +// TODO 4: Create Content Router +// ============================================================================ + +/** + * Route content to appropriate parser based on content type + */ +class ContentRouter extends Runnable { + constructor(parsers) { + super(); + this.parsers = parsers; // { news: parser, recipe: parser, review: parser } + } + + async _call(input, config) { + const {text, type} = input; + + // TODO: Route to appropriate parser based on type + const parser = this.parsers[type]; + + if (!parser) { + throw new Error(`No parser for content type: ${type}`); + } + + // TODO: Parse content + const result = await parser.invoke({text}, config); + + return { + type: type, + parsed: result, + originalText: text + }; + } +} + +// ============================================================================ +// TODO 5: Build Complete Pipeline with Error Handling +// ============================================================================ + +async function buildContentPipeline() { + console.log('=== Exercise 24: Multi-Parser Content Pipeline ===\n'); + + // TODO: Create all parsers + const newsParser = null; + const recipeParser = null; + const reviewParser = null; + + // TODO: Create content router + const router = null; // new ContentRouter({ news: newsParser, recipe: recipeParser, review: reviewParser }) + + console.log('Processing content samples...\n'); + + const results = []; + + // TODO: Process each content sample + for (let i = 0; i < CONTENT_SAMPLES.length; i++) { + const sample = CONTENT_SAMPLES[i]; + + console.log('='.repeat(70)); + console.log(`SAMPLE ${i + 1}: ${sample.type.toUpperCase()}`); + console.log('='.repeat(70)); + console.log(`Text: ${sample.text}\n`); + + try { + // TODO: Route and parse + const result = null; + + console.log('Parsing Result:'); + console.log(JSON.stringify(result, null, 2)); + + results.push({ + success: true, + data: result + }); + } catch (error) { + console.error(`Error: ${error.message}`); + + results.push({ + success: false, + error: error.message, + sample: sample + }); + } + + console.log(); + } + + // TODO: Generate summary report + console.log('='.repeat(70)); + console.log('PROCESSING SUMMARY'); + console.log('='.repeat(70)); + + const successful = results.filter(r => r.success).length; + const failed = results.filter(r => !r.success).length; + + console.log(`Total Samples: ${results.length}`); + console.log(`Successful: ${successful}`); + console.log(`Failed: ${failed}`); + console.log(`Success Rate: ${((successful / results.length) * 100).toFixed(1)}%`); + + console.log('\n✓ Exercise 4 Complete!'); + + return {newsParser, recipeParser, reviewParser, router, results}; +} + +// Run the exercise +buildContentPipeline() + .then(runTests) + .catch(console.error); + +// ============================================================================ +// AUTOMATED TESTS +// ============================================================================ + +async function runTests(context) { + const {newsParser, recipeParser, reviewParser, router, results} = context; + + console.log('\n' + '='.repeat(60)); + console.log('RUNNING AUTOMATED TESTS'); + console.log('='.repeat(60) + '\n'); + + const assert = (await import('assert')).default; + let passed = 0; + let failed = 0; + + function test(name, fn) { + try { + fn(); + passed++; + console.log(`✅ ${name}`); + } catch (error) { + failed++; + console.error(`❌ ${name}`); + console.error(` ${error.message}\n`); + } + } + + // Test 1: All parsers created + test('News parser created', () => { + assert(newsParser !== null, 'Create newsParser'); + assert(newsParser instanceof Runnable, 'Should be Runnable'); + }); + + test('Recipe parser created', () => { + assert(recipeParser !== null, 'Create recipeParser'); + }); + + test('Review parser created', () => { + assert(reviewParser !== null, 'Create reviewParser'); + }); + + test('Router created', () => { + assert(router !== null, 'Create ContentRouter'); + assert(router instanceof ContentRouter, 'Should be ContentRouter instance'); + }); + + // Test 2: News parser validation + test('News parser extracts structured data', async () => { + const result = await newsParser.invoke({ + text: "Tech stocks surge: Apple up 5%, Google gains 3%" + }); + assert(typeof result === 'object', 'Should return object'); + assert('headline' in result || 'category' in result, 'Should have headline or category'); + }); + + // Test 3: Recipe parser validation + test('Recipe parser extracts components', async () => { + const result = await recipeParser.invoke({ + text: "Recipe: Pasta. Ingredients: noodles, sauce. Bake at 400°F for 20 minutes." + }); + assert(typeof result === 'object', 'Should return object'); + assert('name' in result || 'ingredients' in result, 'Should have recipe components'); + }); + + // Test 4: Review parser with fallback + test('Review parser handles well-formed input', async () => { + const result = await reviewParser.invoke({ + text: "Product XYZ (Score: 8/10) costs $99. Pros: Good. Cons: Expensive." + }); + assert(result.method, 'Should indicate parsing method used'); + assert(result.data, 'Should have data'); + }); + + test('Review parser falls back gracefully', async () => { + const result = await reviewParser.invoke({ + text: "This is malformed data that won't parse well" + }); + // Should not throw, should fall back + assert(result !== null, 'Should return something even on bad input'); + assert(result.method, 'Should indicate which method was used'); + }); + + // Test 5: Router functionality + test('Router routes to correct parser', async () => { + const newsResult = await router.invoke({ + text: "Breaking news story", + type: "news" + }); + assert(newsResult.type === 'news', 'Should preserve content type'); + assert(newsResult.parsed, 'Should have parsed data'); + }); + + // Test 6: End-to-end pipeline + test('Pipeline processed all samples', () => { + assert(results.length === CONTENT_SAMPLES.length, 'Should process all samples'); + }); + + test('Pipeline has reasonable success rate', () => { + const successRate = results.filter(r => r.success).length / results.length; + assert(successRate >= 0.5, 'Should successfully parse at least 50% of samples'); + }); + + // Test 7: Error handling + test('Pipeline handles invalid content type', async () => { + try { + await router.invoke({ + text: "Some text", + type: "invalid_type" + }); + assert(false, 'Should throw error for invalid type'); + } catch (error) { + assert(true, 'Correctly throws error for invalid type'); + } + }); + + // Summary + console.log('\n' + '='.repeat(60)); + console.log('TEST SUMMARY'); + console.log('='.repeat(60)); + console.log(`Total: ${passed + failed}`); + console.log(`✅ Passed: ${passed}`); + console.log(`❌ Failed: ${failed}`); + console.log('='.repeat(60)); + + if (failed === 0) { + console.log('\n🎉 All tests passed! You are a parser master!\n'); + console.log('📚 What you mastered:'); + console.log(' • Orchestrating multiple parser types'); + console.log(' • Implementing fallback strategies'); + console.log(' • Using RegexOutputParser for custom patterns'); + console.log(' • Building robust error handling'); + console.log(' • Creating production-ready pipelines'); + console.log(' • Routing content to appropriate parsers'); + console.log(' • Combining structured and pattern-based extraction\n'); + console.log('🚀 You are ready for production parser systems!'); + } else { + console.log('\n⚠️ Some tests failed. Review the advanced patterns.\n'); + } +} + +/** + * HINTS: + * + * 1. RegexOutputParser usage: + * new RegexOutputParser({ + * regex: /Pattern: (.+), Value: (\d+)/, + * outputKeys: ["pattern", "value"] + * }) + * + * 2. Fallback strategy: + * try { + * return await primaryParser.parse(text); + * } catch (error) { + * return await fallbackParser.parse(text); + * } + * + * 3. Combining multiple parsers: + * - Create separate chains for each + * - Call them in sequence or parallel + * - Combine results into single object + * + * 4. Custom Runnable for orchestration: + * class MyParser extends Runnable { + * async _call(input, config) { + * const result1 = await parser1.parse(...); + * const result2 = await parser2.parse(...); + * return { result1, result2 }; + * } + * } + * + * 5. Regex tips: + * - Use () groups to capture data + * - Test patterns at regex101.com + * - Use \s for whitespace, \d for digits + * - Make patterns flexible with .*? + * + * 6. Production patterns: + * - Always have fallbacks + * - Log which method succeeded + * - Handle partial failures gracefully + * - Return metadata about parsing method + */ \ No newline at end of file diff --git a/tutorial/02-composition/02-parsers/lesson.md b/tutorial/02-composition/02-parsers/lesson.md new file mode 100644 index 0000000000000000000000000000000000000000..70436cadf5653e2b674de6f636b3fd0e5fd5da87 --- /dev/null +++ b/tutorial/02-composition/02-parsers/lesson.md @@ -0,0 +1,1554 @@ +# Output Parsers: Structured Output Extraction + +**Part 2: Composition - Lesson 2** + +> LLMs return text. You need data. + +## Overview + +You've learned to create great prompts. LLMs return unstructured text, and in some cases you might need structured data: + +```javascript +// LLM returns this: +"The sentiment is positive with a confidence of 0.92" + +// You need this: +{ + sentiment: "positive", + confidence: 0.92 +} +``` + +**Output parsers** transform LLM text into structured data you can use in your applications. + +## Why This Matters + +### The Problem: Parsing Chaos + +Without parsers, your code is full of brittle string manipulation: + +```javascript +const response = await llm.invoke("Classify: I love this product!"); + +// Fragile parsing code everywhere +if (response.includes("positive")) { + sentiment = "positive"; +} else if (response.includes("negative")) { + sentiment = "negative"; +} + +// What if format changes? +// What if LLM adds extra text? +// How do you handle errors? +``` + +Problems: +- Brittle regex and string matching +- No validation of output format +- Hard to test parsing logic +- Inconsistent error handling +- Parser code duplicated everywhere + +### The Solution: Output Parsers + +```javascript +const parser = new JsonOutputParser(); + +const prompt = new PromptTemplate({ + template: `Classify the sentiment. Respond in JSON: +{{"sentiment": "positive/negative/neutral", "confidence": 0.0-1.0}} + +Text: {text}`, + inputVariables: ["text"] +}); + +const chain = prompt.pipe(llm).pipe(parser); + +const result = await chain.invoke({ text: "I love this!" }); +// { sentiment: "positive", confidence: 0.95 } +``` + +Benefits: +- ✅ Reliable structured extraction +- ✅ Format validation +- ✅ Error handling built-in +- ✅ Reusable parsing logic +- ✅ Type-safe outputs + +## Learning Objectives + +By the end of this lesson, you will: + +- ✅ Build a BaseOutputParser abstraction +- ✅ Create a StringOutputParser for text cleanup +- ✅ Implement JsonOutputParser for JSON extraction +- ✅ Build ListOutputParser for arrays +- ✅ Create StructuredOutputParser with schemas +- ✅ Use parsers in chains with prompts +- ✅ Handle parsing errors gracefully + +## Core Concepts + +### What is an Output Parser? + +An output parser **transforms LLM text output into structured data**. + +**Flow:** +``` +LLM Output (text) → Parser → Structured Data + ↓ ↓ ↓ +"positive: 0.95" parse() {sentiment: "positive", confidence: 0.95} +``` + +### The Parser Hierarchy + +``` +BaseOutputParser (abstract) + ├── StringOutputParser (clean text) + ├── JsonOutputParser (extract JSON) + ├── ListOutputParser (extract lists) + ├── RegexOutputParser (regex patterns) + └── StructuredOutputParser (schema validation) +``` + +Each parser handles a specific output format. + +### Key Operations + +1. **Parse**: Extract structured data from text +2. **Get Format Instructions**: Tell LLM how to format response +3. **Validate**: Check output matches expected structure +4. **Handle Errors**: Gracefully handle malformed outputs + +## Implementation Guide + +### Step 1: Base Output Parser + +**Location:** `src/output-parsers/base-parser.js` + +This is the abstract base class all parsers inherit from. + +**What it does:** +- Defines the interface for all parsers +- Extends Runnable (so parsers work in chains) +- Provides format instruction generation +- Handles parsing errors + +**Implementation:** + +```javascript +import { Runnable } from '../core/runnable.js'; + +/** + * Base class for all output parsers + * Transforms LLM text output into structured data + */ +export class BaseOutputParser extends Runnable { + constructor() { + super(); + this.name = this.constructor.name; + } + + /** + * Parse the LLM output into structured data + * @abstract + * @param {string} text - Raw LLM output + * @returns {Promise} Parsed data + */ + async parse(text) { + throw new Error(`${this.name} must implement parse()`); + } + + /** + * Get instructions for the LLM on how to format output + * @returns {string} Format instructions + */ + getFormatInstructions() { + return ''; + } + + /** + * Runnable interface: parse the output + */ + async _call(input, config) { + // Input can be a string or a Message + const text = typeof input === 'string' + ? input + : input.content; + + return await this.parse(text); + } + + /** + * Parse with error handling + */ + async parseWithPrompt(text, prompt) { + try { + return await this.parse(text); + } catch (error) { + throw new OutputParserException( + `Failed to parse output from prompt: ${error.message}`, + text, + error + ); + } + } +} + +/** + * Exception thrown when parsing fails + */ +export class OutputParserException extends Error { + constructor(message, llmOutput, originalError) { + super(message); + this.name = 'OutputParserException'; + this.llmOutput = llmOutput; + this.originalError = originalError; + } +} +``` + +**Key insights:** +- Extends `Runnable` so parsers can be piped in chains +- `_call` extracts text from strings or Messages +- `getFormatInstructions()` helps prompt the LLM +- Error handling wraps parse failures with context + +--- + +### Step 2: String Output Parser + +**Location:** `src/output-parsers/string-parser.js` + +The simplest parser - cleans up text output. + +**What it does:** +- Strips leading/trailing whitespace +- Optionally removes markdown code blocks +- Returns clean string + +**Use when:** +- You just need clean text +- No structure needed +- Want to remove formatting artifacts + +**Implementation:** + +```javascript +import { BaseOutputParser } from './base-parser.js'; + +/** + * Parser that returns cleaned string output + * Strips whitespace and optionally removes markdown + * + * Example: + * const parser = new StringOutputParser(); + * const result = await parser.parse(" Hello World "); + * // "Hello World" + */ +export class StringOutputParser extends BaseOutputParser { + constructor(options = {}) { + super(); + this.stripMarkdown = options.stripMarkdown ?? true; + } + + /** + * Parse: clean the text + */ + async parse(text) { + let cleaned = text.trim(); + + if (this.stripMarkdown) { + cleaned = this._stripMarkdownCodeBlocks(cleaned); + } + + return cleaned; + } + + /** + * Remove markdown code blocks (```code```) + */ + _stripMarkdownCodeBlocks(text) { + // Remove ```language\ncode\n``` + return text.replace(/```[\w]*\n([\s\S]*?)\n```/g, '$1').trim(); + } + + getFormatInstructions() { + return 'Respond with plain text. No markdown formatting.'; + } +} +``` + +**Usage:** + +```javascript +const parser = new StringOutputParser(); + +// Handles various formats +await parser.parse(" Hello "); // "Hello" +await parser.parse("```\ncode\n```"); // "code" +await parser.parse(" \n Text \n "); // "Text" +``` + +--- + +### Step 3: JSON Output Parser + +**Location:** `src/output-parsers/json-parser.js` + +Extracts and validates JSON from LLM output. + +**What it does:** +- Finds JSON in text (handles markdown, extra text) +- Parses and validates JSON +- Optionally validates against a schema + +**Use when:** +- Need structured objects +- Want type-safe data +- Need validation + +**Implementation:** + +```javascript +import { BaseOutputParser, OutputParserException } from './base-parser.js'; + +/** + * Parser that extracts JSON from LLM output + * Handles markdown code blocks and extra text + * + * Example: + * const parser = new JsonOutputParser(); + * const result = await parser.parse('```json\n{"name": "Alice"}\n```'); + * // { name: "Alice" } + */ +export class JsonOutputParser extends BaseOutputParser { + constructor(options = {}) { + super(); + this.schema = options.schema; + } + + /** + * Parse JSON from text + */ + async parse(text) { + try { + // Try to extract JSON from the text + const jsonText = this._extractJson(text); + const parsed = JSON.parse(jsonText); + + // Validate against schema if provided + if (this.schema) { + this._validateSchema(parsed); + } + + return parsed; + } catch (error) { + throw new OutputParserException( + `Failed to parse JSON: ${error.message}`, + text, + error + ); + } + } + + /** + * Extract JSON from text (handles markdown, extra text) + */ + _extractJson(text) { + // Try direct parse first + try { + JSON.parse(text.trim()); + return text.trim(); + } catch { + // Not direct JSON, try to find it + } + + // Look for JSON in markdown code blocks + const markdownMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/); + if (markdownMatch) { + return markdownMatch[1].trim(); + } + + // Look for JSON object/array patterns + const jsonObjectMatch = text.match(/\{[\s\S]*\}/); + if (jsonObjectMatch) { + return jsonObjectMatch[0]; + } + + const jsonArrayMatch = text.match(/\[[\s\S]*\]/); + if (jsonArrayMatch) { + return jsonArrayMatch[0]; + } + + // Give up, return original + return text.trim(); + } + + /** + * Validate parsed JSON against schema + */ + _validateSchema(parsed) { + if (!this.schema) return; + + for (const [key, type] of Object.entries(this.schema)) { + if (!(key in parsed)) { + throw new Error(`Missing required field: ${key}`); + } + + const actualType = typeof parsed[key]; + if (actualType !== type) { + throw new Error( + `Field ${key} should be ${type}, got ${actualType}` + ); + } + } + } + + getFormatInstructions() { + let instructions = 'Respond with valid JSON.'; + + if (this.schema) { + const schemaDesc = Object.entries(this.schema) + .map(([key, type]) => `"${key}": ${type}`) + .join(', '); + instructions += ` Schema: { ${schemaDesc} }`; + } + + return instructions; + } +} +``` + +**Usage:** + +```javascript +const parser = new JsonOutputParser({ + schema: { + name: 'string', + age: 'number', + active: 'boolean' + } +}); + +// Handles various JSON formats +await parser.parse('{"name": "Alice", "age": 30, "active": true}'); +await parser.parse('```json\n{"name": "Bob", "age": 25, "active": false}\n```'); +await parser.parse('Sure! Here\'s the data: {"name": "Charlie", "age": 35, "active": true}'); +``` + +--- + +### Step 4: List Output Parser + +**Location:** `src/output-parsers/list-parser.js` + +Extracts lists/arrays from text. + +**What it does:** +- Parses numbered lists, bullet points, comma-separated +- Returns array of items +- Cleans each item + +**Use when:** +- Need arrays of strings +- LLM outputs lists +- Want simple arrays + +**Implementation:** + +```javascript +import { BaseOutputParser } from './base-parser.js'; + +/** + * Parser that extracts lists from text + * Handles: numbered lists, bullets, comma-separated + * + * Example: + * const parser = new ListOutputParser(); + * const result = await parser.parse("1. Apple\n2. Banana\n3. Orange"); + * // ["Apple", "Banana", "Orange"] + */ +export class ListOutputParser extends BaseOutputParser { + constructor(options = {}) { + super(); + this.separator = options.separator; + } + + /** + * Parse list from text + */ + async parse(text) { + const cleaned = text.trim(); + + // If separator specified, use it + if (this.separator) { + return cleaned + .split(this.separator) + .map(item => item.trim()) + .filter(item => item.length > 0); + } + + // Try to detect format + if (this._isNumberedList(cleaned)) { + return this._parseNumberedList(cleaned); + } + + if (this._isBulletList(cleaned)) { + return this._parseBulletList(cleaned); + } + + // Try comma-separated + if (cleaned.includes(',')) { + return cleaned + .split(',') + .map(item => item.trim()) + .filter(item => item.length > 0); + } + + // Try newline-separated + return cleaned + .split('\n') + .map(item => item.trim()) + .filter(item => item.length > 0); + } + + /** + * Check if text is numbered list (1. Item\n2. Item) + */ + _isNumberedList(text) { + return /^\d+\./.test(text); + } + + /** + * Check if text is bullet list (- Item\n- Item or * Item) + */ + _isBulletList(text) { + return /^[-*•]/.test(text); + } + + /** + * Parse numbered list + */ + _parseNumberedList(text) { + return text + .split('\n') + .map(line => line.replace(/^\d+\.\s*/, '').trim()) + .filter(item => item.length > 0); + } + + /** + * Parse bullet list + */ + _parseBulletList(text) { + return text + .split('\n') + .map(line => line.replace(/^[-*•]\s*/, '').trim()) + .filter(item => item.length > 0); + } + + getFormatInstructions() { + if (this.separator) { + return `Respond with items separated by "${this.separator}".`; + } + return 'Respond with a numbered list (1. Item) or bullet list (- Item).'; + } +} +``` + +**Usage:** + +```javascript +const parser = new ListOutputParser(); + +// Handles various list formats +await parser.parse("1. Apple\n2. Banana\n3. Orange"); +// ["Apple", "Banana", "Orange"] + +await parser.parse("- Red\n- Green\n- Blue"); +// ["Red", "Green", "Blue"] + +await parser.parse("cat, dog, bird"); +// ["cat", "dog", "bird"] + +// Custom separator +const csvParser = new ListOutputParser({ separator: ',' }); +await csvParser.parse("apple,banana,orange"); +// ["apple", "banana", "orange"] +``` + +--- + +### Step 5: Regex Output Parser + +**Location:** `src/output-parsers/regex-parser.js` + +Uses regex patterns to extract structured data. + +**What it does:** +- Applies regex to extract groups +- Maps groups to field names +- Returns structured object + +**Use when:** +- Output has predictable patterns +- Need custom extraction logic +- Regex is simplest solution + +**Implementation:** + +```javascript +import { BaseOutputParser, OutputParserException } from './base-parser.js'; + +/** + * Parser that uses regex to extract structured data + * + * Example: + * const parser = new RegexOutputParser({ + * regex: /Sentiment: (\w+), Confidence: ([\d.]+)/, + * outputKeys: ["sentiment", "confidence"] + * }); + * + * const result = await parser.parse("Sentiment: positive, Confidence: 0.92"); + * // { sentiment: "positive", confidence: "0.92" } + */ +export class RegexOutputParser extends BaseOutputParser { + constructor(options = {}) { + super(); + this.regex = options.regex; + this.outputKeys = options.outputKeys || []; + this.dotAll = options.dotAll ?? false; + + if (this.dotAll) { + // Add 's' flag for dotAll if not present + const flags = this.regex.flags.includes('s') + ? this.regex.flags + : this.regex.flags + 's'; + this.regex = new RegExp(this.regex.source, flags); + } + } + + /** + * Parse using regex + */ + async parse(text) { + const match = text.match(this.regex); + + if (!match) { + throw new OutputParserException( + `Text does not match regex pattern: ${this.regex}`, + text + ); + } + + // If no output keys, return the groups as array + if (this.outputKeys.length === 0) { + return match.slice(1); // Exclude full match + } + + // Map groups to keys + const result = {}; + for (let i = 0; i < this.outputKeys.length; i++) { + result[this.outputKeys[i]] = match[i + 1]; // +1 to skip full match + } + + return result; + } + + getFormatInstructions() { + if (this.outputKeys.length > 0) { + return `Format your response to match: ${this.outputKeys.join(', ')}`; + } + return 'Follow the specified format exactly.'; + } +} +``` + +**Usage:** + +```javascript +const parser = new RegexOutputParser({ + regex: /Sentiment: (\w+), Confidence: ([\d.]+)/, + outputKeys: ["sentiment", "confidence"] +}); + +const result = await parser.parse("Sentiment: positive, Confidence: 0.92"); +// { sentiment: "positive", confidence: "0.92" } +``` + +--- +# Output Parsers: Advanced Patterns & Integration + +## Advanced Parser: Structured Output Parser + +### Step 6: Structured Output Parser + +**Location:** `src/output-parsers/structured-parser.js` + +The most powerful parser - validates against a full schema with types and descriptions. + +**What it does:** +- Defines expected schema with types +- Generates format instructions for LLM +- Validates all fields and types +- Provides detailed error messages + +**Use when:** +- Need complex structured data +- Want strong type validation +- Need to generate format instructions automatically + +**Implementation:** + +```javascript +import { BaseOutputParser, OutputParserException } from './base-parser.js'; + +/** + * Parser with full schema validation + * + * Example: + * const parser = new StructuredOutputParser({ + * responseSchemas: [ + * { + * name: "sentiment", + * type: "string", + * description: "The sentiment (positive/negative/neutral)", + * enum: ["positive", "negative", "neutral"] + * }, + * { + * name: "confidence", + * type: "number", + * description: "Confidence score between 0 and 1" + * } + * ] + * }); + */ +export class StructuredOutputParser extends BaseOutputParser { + constructor(options = {}) { + super(); + this.responseSchemas = options.responseSchemas || []; + } + + /** + * Parse and validate against schema + */ + async parse(text) { + try { + // Extract JSON + const jsonText = this._extractJson(text); + const parsed = JSON.parse(jsonText); + + // Validate against schema + this._validateAgainstSchema(parsed); + + return parsed; + } catch (error) { + throw new OutputParserException( + `Failed to parse structured output: ${error.message}`, + text, + error + ); + } + } + + /** + * Extract JSON from text (same as JsonOutputParser) + */ + _extractJson(text) { + try { + JSON.parse(text.trim()); + return text.trim(); + } catch {} + + const markdownMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/); + if (markdownMatch) return markdownMatch[1].trim(); + + const jsonMatch = text.match(/\{[\s\S]*\}/); + if (jsonMatch) return jsonMatch[0]; + + return text.trim(); + } + + /** + * Validate parsed data against schema + */ + _validateAgainstSchema(parsed) { + for (const schema of this.responseSchemas) { + const { name, type, enum: enumValues, required = true } = schema; + + // Check required fields + if (required && !(name in parsed)) { + throw new Error(`Missing required field: ${name}`); + } + + if (name in parsed) { + const value = parsed[name]; + + // Check type + if (!this._checkType(value, type)) { + throw new Error( + `Field ${name} should be ${type}, got ${typeof value}` + ); + } + + // Check enum values + if (enumValues && !enumValues.includes(value)) { + throw new Error( + `Field ${name} must be one of: ${enumValues.join(', ')}` + ); + } + } + } + } + + /** + * Check if value matches expected type + */ + _checkType(value, type) { + switch (type) { + case 'string': + return typeof value === 'string'; + case 'number': + return typeof value === 'number' && !isNaN(value); + case 'boolean': + return typeof value === 'boolean'; + case 'array': + return Array.isArray(value); + case 'object': + return typeof value === 'object' && value !== null && !Array.isArray(value); + default: + return true; + } + } + + /** + * Generate format instructions for LLM + */ + getFormatInstructions() { + const schemaDescriptions = this.responseSchemas.map(schema => { + let desc = `"${schema.name}": ${schema.type}`; + if (schema.description) { + desc += ` // ${schema.description}`; + } + if (schema.enum) { + desc += ` (one of: ${schema.enum.join(', ')})`; + } + return desc; + }); + + return `Respond with valid JSON matching this schema: +{ +${schemaDescriptions.map(d => ' ' + d).join(',\n')} +}`; + } + + /** + * Static helper to create from simple schema + */ + static fromNamesAndDescriptions(schemas) { + const responseSchemas = Object.entries(schemas).map(([name, description]) => ({ + name, + description, + type: 'string' // Default type + })); + + return new StructuredOutputParser({ responseSchemas }); + } +} +``` + +**Usage:** + +```javascript +const parser = new StructuredOutputParser({ + responseSchemas: [ + { + name: "sentiment", + type: "string", + description: "The sentiment of the text", + enum: ["positive", "negative", "neutral"], + required: true + }, + { + name: "confidence", + type: "number", + description: "Confidence score from 0 to 1", + required: true + }, + { + name: "keywords", + type: "array", + description: "Key themes in the text", + required: false + } + ] +}); + +// Get format instructions to add to prompt +const instructions = parser.getFormatInstructions(); +console.log(instructions); + +// Parse and validate +const result = await parser.parse(`{ + "sentiment": "positive", + "confidence": 0.92, + "keywords": ["great", "love", "excellent"] +}`); +``` + +--- + +## Real-World Examples + +### Example 1: Email Classification with Structured Parser + +```javascript +import { StructuredOutputParser } from './output-parsers/structured-parser.js'; +import { PromptTemplate } from './prompts/prompt-template.js'; +import { LlamaCppLLM } from './llm/llama-cpp-llm.js'; + +// Define the output structure +const parser = new StructuredOutputParser({ + responseSchemas: [ + { + name: "category", + type: "string", + description: "Email category", + enum: ["spam", "invoice", "meeting", "urgent", "personal", "other"] + }, + { + name: "confidence", + type: "number", + description: "Confidence score (0-1)" + }, + { + name: "reason", + type: "string", + description: "Brief explanation for classification" + }, + { + name: "actionRequired", + type: "boolean", + description: "Does email require action?" + } + ] +}); + +// Build prompt with format instructions +const prompt = new PromptTemplate({ + template: `Classify this email. + +Email: +From: {from} +Subject: {subject} +Body: {body} + +{format_instructions}`, + inputVariables: ["from", "subject", "body"], + partialVariables: { + format_instructions: parser.getFormatInstructions() + } +}); + +// Create chain +const llm = new LlamaCppLLM({ modelPath: './model.gguf' }); +const chain = prompt.pipe(llm).pipe(parser); + +// Use it +const result = await chain.invoke({ + from: "billing@company.com", + subject: "Invoice #12345", + body: "Payment due by March 15th" +}); + +console.log(result); +// { +// category: "invoice", +// confidence: 0.98, +// reason: "Email contains invoice number and payment deadline", +// actionRequired: true +// } +``` + +--- + +### Example 2: Content Extraction with JSON Parser + +```javascript +import { JsonOutputParser } from './output-parsers/json-parser.js'; +import { ChatPromptTemplate } from './prompts/chat-prompt-template.js'; + +const parser = new JsonOutputParser({ + schema: { + title: 'string', + summary: 'string', + tags: 'object', // Will be array + author: 'string' + } +}); + +const prompt = ChatPromptTemplate.fromMessages([ + ["system", "Extract article metadata. Respond with JSON."], + ["human", "Article: {article}"] +]); + +const chain = prompt.pipe(llm).pipe(parser); + +const result = await chain.invoke({ + article: "Title: AI Revolution\nBy: John Doe\n\nAI is transforming..." +}); + +// { +// title: "AI Revolution", +// summary: "Article discusses AI's transformative impact", +// tags: ["AI", "technology", "future"], +// author: "John Doe" +// } +``` + +--- + +### Example 3: List Extraction for Recommendations + +```javascript +import { ListOutputParser } from './output-parsers/list-parser.js'; +import { PromptTemplate } from './prompts/prompt-template.js'; + +const parser = new ListOutputParser(); + +const prompt = new PromptTemplate({ + template: `Recommend 5 {category} for someone interested in {interest}. + +{format_instructions} + +List:`, + inputVariables: ["category", "interest"], + partialVariables: { + format_instructions: parser.getFormatInstructions() + } +}); + +const chain = prompt.pipe(llm).pipe(parser); + +const books = await chain.invoke({ + category: "books", + interest: "machine learning" +}); + +console.log(books); +// [ +// "Pattern Recognition and Machine Learning", +// "Deep Learning by Goodfellow", +// "Hands-On Machine Learning", +// "The Hundred-Page Machine Learning Book", +// "Machine Learning Yearning" +// ] +``` + +--- + +### Example 4: Sentiment Analysis with Retry + +```javascript +import { JsonOutputParser } from './output-parsers/json-parser.js'; +import { PromptTemplate } from './prompts/prompt-template.js'; + +const parser = new JsonOutputParser(); + +// If parsing fails, retry with clearer instructions +async function robustSentimentAnalysis(text) { + const prompt = new PromptTemplate({ + template: `Analyze sentiment of: "{text}" + +Respond with ONLY valid JSON: +{{"sentiment": "positive/negative/neutral", "score": 0.0-1.0}}` + }); + + const chain = prompt.pipe(llm).pipe(parser); + + try { + return await chain.invoke({ text }); + } catch (error) { + console.log('Parse failed, retrying with stricter prompt...'); + + // Retry with more explicit prompt + const strictPrompt = new PromptTemplate({ + template: `Analyze: "{text}" + +IMPORTANT: Respond with ONLY this JSON structure, nothing else: +{{"sentiment": "positive", "score": 0.9}} + +Your response:` + }); + + const retryChain = strictPrompt.pipe(llm).pipe(parser); + return await retryChain.invoke({ text }); + } +} +``` + +--- + +## Advanced Patterns + +### Pattern 1: Fallback Parsing + +```javascript +class FallbackOutputParser extends BaseOutputParser { + constructor(parsers) { + super(); + this.parsers = parsers; + } + + async parse(text) { + const errors = []; + + for (const parser of this.parsers) { + try { + return await parser.parse(text); + } catch (error) { + errors.push({ parser: parser.name, error }); + } + } + + throw new OutputParserException( + `All parsers failed. Errors: ${JSON.stringify(errors)}`, + text + ); + } +} + +// Usage +const parser = new FallbackOutputParser([ + new JsonOutputParser(), // Try JSON first + new RegexOutputParser({...}), // Try regex second + new StringOutputParser() // Fallback to string +]); +``` + +--- + +### Pattern 2: Transform After Parse + +```javascript +class TransformOutputParser extends BaseOutputParser { + constructor(parser, transform) { + super(); + this.parser = parser; + this.transform = transform; + } + + async parse(text) { + const parsed = await this.parser.parse(text); + return this.transform(parsed); + } +} + +// Usage: parse JSON then transform values +const parser = new TransformOutputParser( + new JsonOutputParser(), + (data) => ({ + ...data, + confidence: parseFloat(data.confidence), + timestamp: new Date().toISOString() + }) +); +``` + +--- + +### Pattern 3: Conditional Parsing + +```javascript +class ConditionalOutputParser extends BaseOutputParser { + constructor(condition, trueParser, falseParser) { + super(); + this.condition = condition; + this.trueParser = trueParser; + this.falseParser = falseParser; + } + + async parse(text) { + const useTrue = this.condition(text); + const parser = useTrue ? this.trueParser : this.falseParser; + return await parser.parse(text); + } +} + +// Usage: different parsers based on content +const parser = new ConditionalOutputParser( + (text) => text.includes('{'), // Has JSON? + new JsonOutputParser(), + new ListOutputParser() +); +``` + +--- + +### Pattern 4: Validated Output + +```javascript +class ValidatedOutputParser extends BaseOutputParser { + constructor(parser, validator) { + super(); + this.parser = parser; + this.validator = validator; + } + + async parse(text) { + const parsed = await this.parser.parse(text); + + const isValid = this.validator(parsed); + if (!isValid) { + throw new OutputParserException( + 'Parsed output failed validation', + text + ); + } + + return parsed; + } +} + +// Usage: ensure confidence is in range +const parser = new ValidatedOutputParser( + new JsonOutputParser(), + (data) => data.confidence >= 0 && data.confidence <= 1 +); +``` + +--- + +## Integration with Full Chain + +### Complete Example: Sentiment Analysis API + +```javascript +import { PromptTemplate } from './prompts/prompt-template.js'; +import { LlamaCppLLM } from './llm/llama-cpp-llm.js'; +import { StructuredOutputParser } from './output-parsers/structured-parser.js'; +import { ConsoleCallback } from './utils/callbacks.js'; + +// Define output structure +const parser = new StructuredOutputParser({ + responseSchemas: [ + { + name: "sentiment", + type: "string", + enum: ["positive", "negative", "neutral"] + }, + { + name: "confidence", + type: "number" + }, + { + name: "emotions", + type: "array", + description: "List of detected emotions" + } + ] +}); + +// Build prompt +const prompt = new PromptTemplate({ + template: `Analyze the sentiment of this text: + +"{text}" + +{format_instructions}`, + inputVariables: ["text"], + partialVariables: { + format_instructions: parser.getFormatInstructions() + } +}); + +// Create LLM +const llm = new LlamaCppLLM({ + modelPath: './model.gguf', + temperature: 0.1 // Low temp for consistent classification +}); + +// Build chain with logging +const chain = prompt.pipe(llm).pipe(parser); + +const logger = new ConsoleCallback(); + +// Analyze sentiment +async function analyzeSentiment(text) { + try { + const result = await chain.invoke( + { text }, + { callbacks: [logger] } + ); + + return { + success: true, + data: result + }; + } catch (error) { + return { + success: false, + error: error.message, + rawOutput: error.llmOutput + }; + } +} + +// Use it +const result = await analyzeSentiment("I absolutely love this product! It's amazing!"); +console.log(result); +// { +// success: true, +// data: { +// sentiment: "positive", +// confidence: 0.95, +// emotions: ["joy", "excitement", "satisfaction"] +// } +// } +``` + +--- + +## Error Handling + +### Pattern: Graceful Degradation + +```javascript +async function parseWithFallback(text, primaryParser, fallbackValue) { + try { + return await primaryParser.parse(text); + } catch (error) { + console.warn('Primary parser failed:', error.message); + console.warn('Using fallback value:', fallbackValue); + return fallbackValue; + } +} + +// Usage +const result = await parseWithFallback( + llmOutput, + new JsonOutputParser(), + { error: true, message: "Failed to parse", raw: llmOutput } +); +``` + +--- + +### Pattern: Retry with Fix Instructions + +```javascript +async function parseWithRetry(text, parser, llm, maxRetries = 2) { + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + return await parser.parse(text); + } catch (error) { + if (attempt === maxRetries - 1) throw error; + + // Ask LLM to fix the output + const fixPrompt = `The following output is malformed: +${text} + +Error: ${error.message} + +Please provide the output in correct format: +${parser.getFormatInstructions()}`; + + text = await llm.invoke(fixPrompt); + } + } +} +``` + +--- + +## Testing Parsers + +### Unit Tests + +```javascript +import { describe, it, expect } from 'your-test-framework'; +import { JsonOutputParser } from './output-parsers/json-parser.js'; + +describe('JsonOutputParser', () => { + it('should parse plain JSON', async () => { + const parser = new JsonOutputParser(); + const result = await parser.parse('{"name": "Alice", "age": 30}'); + + expect(result.name).toBe('Alice'); + expect(result.age).toBe(30); + }); + + it('should extract JSON from markdown', async () => { + const parser = new JsonOutputParser(); + const text = '```json\n{"key": "value"}\n```'; + const result = await parser.parse(text); + + expect(result.key).toBe('value'); + }); + + it('should validate against schema', async () => { + const parser = new JsonOutputParser({ + schema: { name: 'string', age: 'number' } + }); + + await expect( + parser.parse('{"name": "Bob", "age": "invalid"}') + ).rejects.toThrow(); + }); + + it('should throw on invalid JSON', async () => { + const parser = new JsonOutputParser(); + await expect(parser.parse('not json')).rejects.toThrow(); + }); +}); +``` + +--- + +## Best Practices + +### ✅ DO: + +**1. Include format instructions in prompts** +```javascript +const prompt = new PromptTemplate({ + template: `{task} + +{format_instructions}`, + partialVariables: { + format_instructions: parser.getFormatInstructions() + } +}); +``` + +**2. Use schema validation for complex outputs** +```javascript +const parser = new StructuredOutputParser({ + responseSchemas: [ + { name: "field1", type: "string", required: true }, + { name: "field2", type: "number", required: true } + ] +}); +``` + +**3. Handle parsing errors gracefully** +```javascript +try { + const parsed = await parser.parse(text); +} catch (error) { + console.error('Parsing failed:', error.message); + // Fallback or retry logic +} +``` + +**4. Test parsers independently** +```javascript +// Test without LLM +const result = await parser.parse(mockLLMOutput); +expect(result).toMatchSchema(); +``` + +**5. Use low temperature for structured outputs** +```javascript +const llm = new LlamaCppLLM({ + temperature: 0.1 // More consistent formatting +}); +``` + +--- + +### ❌ DON'T: + +**1. Don't assume perfect LLM formatting** +```javascript +// Bad +const data = JSON.parse(llmOutput); // Will fail often + +// Good +const data = await jsonParser.parse(llmOutput); // Handles variations +``` + +**2. Don't skip validation** +```javascript +// Bad +const result = await parser.parse(text); +// Use result.field without checking + +// Good +const result = await parser.parse(text); +if (result.field && typeof result.field === 'string') { + // Use result.field +} +``` + +**3. Don't use parsers for simple text** +```javascript +// Bad +const parser = new JsonOutputParser(); +const result = await parser.parse(simpleText); + +// Good +const parser = new StringOutputParser(); +const result = await parser.parse(simpleText); +``` + +--- + +## Exercises + +Practice using output parsers in real-world scenarios from simple to complex: + +### Exercise 21: Product Review Analyzer +Extract clean summaries and sentiment from product reviews using StringOutputParser. +**Starter code**: [`exercises/21-review-analyzer.js`](exercises/21-review-analyzer.js) + +### Exercise 22: Contact Information Extractor +Parse structured contact details and skills from unstructured text using JSON and List parsers. +**Starter code**: [`exercises/22-contact-extractor.js`](exercises/22-contact-extractor.js) + +### Exercise 23: Article Metadata Extractor +Extract complex metadata with schema validation using StructuredOutputParser. +**Starter code**: [`exercises/23-article-metadata.js`](exercises/23-article-metadata.js) + +### Exercise 24: Multi-Parser Content Pipeline +Build production-ready pipelines with multiple parsers, fallback strategies, and content routing. +**Starter code**: [`exercises/24-multi-parser-pipeline.js`](exercises/24-multi-parser-pipeline.js) + +--- + +## Summary + +You've built a complete output parsing system! + +### Key Takeaways + +1. **BaseOutputParser**: Foundation for all parsers +2. **StringOutputParser**: Clean text output +3. **JsonOutputParser**: Extract and validate JSON +4. **ListOutputParser**: Parse lists/arrays +5. **RegexOutputParser**: Pattern-based extraction +6. **StructuredOutputParser**: Full schema validation + +### What You Built + +A parsing system that: +- ✅ Extracts structured data reliably +- ✅ Validates output formats +- ✅ Handles errors gracefully +- ✅ Generates format instructions +- ✅ Works in chains with prompts +- ✅ Is testable in isolation + +### Next Steps + +Now you can combine prompts + LLMs + parsers into complete chains. + +➡️ **Next: [LLM Chains](./03-llm-chain.md)** + +Learn how to build complete prompt → LLM → parser pipelines. + +--- + +**Built with ❤️ for learners who want to understand AI frameworks deeply** + +[← Previous: Prompts](./01-prompts.md) | [Tutorial Index](../README.md) | [Next: LLM Chains →](./03-llm-chain.md) \ No newline at end of file diff --git a/tutorial/02-composition/02-parsers/solutions/21-review-analyzer-solution.js b/tutorial/02-composition/02-parsers/solutions/21-review-analyzer-solution.js new file mode 100644 index 0000000000000000000000000000000000000000..ef0716a727aa0024f7adc1d62266e3d630043b4c --- /dev/null +++ b/tutorial/02-composition/02-parsers/solutions/21-review-analyzer-solution.js @@ -0,0 +1,231 @@ +/** + * Solution 21: Product Review Analyzer + * + * Difficulty: ⭐☆☆☆ (Beginner) + * + * Skills gained: + * - Using parsers in chains + * - Text cleaning with StringOutputParser + * - Basic chain composition + */ + +import {Runnable, PromptTemplate, StringOutputParser} from '../../../../src/index.js'; +import {LlamaCppLLM} from '../../../../src/llm/llama-cpp-llm.js'; +import {QwenChatWrapper} from "node-llama-cpp"; + +// Sample product reviews to analyze +const REVIEWS = [ + "This product is amazing! Best purchase ever. 5 stars!", + "Terrible quality. Broke after one week. Very disappointed.", + "It's okay. Does the job but nothing special.", + "Love it! Exactly what I needed. Highly recommend!", + "Not worth the price. Expected better quality." +]; + +async function createReviewSummarizer() { + const prompt = new PromptTemplate({ + template: "Summarize the following review in ONE sentence:\n\n{review}", + inputVariables: ["review"] + }); + const llm = new LlamaCppLLM({ + modelPath: './models/Qwen3-1.7B-Q6_K.gguf', + chatWrapper: new QwenChatWrapper({ + thoughts: 'discourage' // Prevents the model from outputting thinking tokens + }), + }); + + const parser = new StringOutputParser(); + const chain = prompt.pipe(llm).pipe(parser); + + return chain; +} + +async function createSentimentExtractor() { + const prompt = new PromptTemplate({ + template: `Classify the sentiment of this review: + +{review} + +Respond with ONLY ONE WORD: positive, negative, or neutral.`, + inputVariables: ["review"] + }); + + const llm = new LlamaCppLLM({ + modelPath: './models/Qwen3-1.7B-Q6_K.gguf', + chatWrapper: new QwenChatWrapper({ + thoughts: 'discourage' // Prevents the model from outputting thinking tokens + }), + temperature: 0.1 + }); + ; + + const parser = new StringOutputParser(); + const chain = prompt.pipe(llm).pipe(parser); + + return chain; +} + +async function analyzeReviews() { + console.log('=== Exercise 21: Product Review Analyzer ===\n'); + + const summarizerChain = await createReviewSummarizer(); + const sentimentChain = await createSentimentExtractor(); + + console.log('Processing reviews...\n'); + + for (let i = 0; i < REVIEWS.length; i++) { + const review = REVIEWS[i]; + + console.log(`Review ${i + 1}: "${review}"`); + + const summary = await summarizerChain.invoke({review}); + const sentiment = await sentimentChain.invoke({review}); + + console.log(`Summary: ${summary}`); + console.log(`Sentiment: ${sentiment}`); + console.log(); + } + + console.log('✓ Exercise 1 Complete!'); + + return {summarizerChain, sentimentChain}; +} + +// Run the exercise +analyzeReviews() + .then(runTests) + .catch(console.error); + +// ============================================================================ +// AUTOMATED TESTS +// ============================================================================ + +async function runTests(results) { + const {summarizerChain, sentimentChain} = results; + + console.log('\n' + '='.repeat(60)); + console.log('RUNNING AUTOMATED TESTS'); + console.log('='.repeat(60) + '\n'); + + const assert = (await import('assert')).default; + let passed = 0; + let failed = 0; + + async function test(name, fn) { + try { + await fn(); + passed++; + console.log(`✅ ${name}`); + } catch (error) { + failed++; + console.error(`❌ ${name}`); + console.error(` ${error.message}\n`); + } + } + + // Test 1: Chains created + await test('Summarizer chain created', async () => { + assert(summarizerChain !== null && summarizerChain !== undefined, 'Create summarizerChain'); + assert(summarizerChain instanceof Runnable, 'Chain should be Runnable'); + }); + + await test('Sentiment chain created', async () => { + assert(sentimentChain !== null && sentimentChain !== undefined, 'Create sentimentChain'); + assert(sentimentChain instanceof Runnable, 'Chain should be Runnable'); + }); + + // Test 2: Chains work (only run if chains exist) + if (summarizerChain !== null && summarizerChain !== undefined) { + await test('Summarizer chain produces output', async () => { + const result = await summarizerChain.invoke({ + review: "Great product! Love it!" + }); + assert(typeof result === 'string', 'Should return string'); + assert(result.length > 0, 'Should not be empty'); + assert(result.length < 200, 'Should be concise (< 200 chars)'); + }); + } else { + failed++; + console.error(`❌ Summarizer chain produces output`); + console.error(` Cannot test - summarizerChain is not created\n`); + } + + if (sentimentChain !== null && sentimentChain !== undefined) { + await test('Sentiment chain produces valid sentiment', async () => { + const result = await sentimentChain.invoke({ + review: "Terrible product. Very bad." + }); + const cleaned = result.toLowerCase().trim(); + const validSentiments = ['positive', 'negative', 'neutral']; + assert( + validSentiments.includes(cleaned), + `Should be one of: ${validSentiments.join(', ')}. Got: ${cleaned}` + ); + }); + } else { + failed++; + console.error(`❌ Sentiment chain produces valid sentiment`); + console.error(` Cannot test - sentimentChain is not created\n`); + } + + // Test 3: Parser cleans output (only if chain exists) + if (sentimentChain !== null && sentimentChain !== undefined) { + await test('StringOutputParser removes extra whitespace', async () => { + const result = await sentimentChain.invoke({ + review: "It's okay" + }); + assert(result === result.trim(), 'Should have no leading/trailing whitespace'); + assert(!result.includes(' '), 'Should have no double spaces'); + }); + } else { + failed++; + console.error(`❌ StringOutputParser removes extra whitespace`); + console.error(` Cannot test - sentimentChain is not created\n`); + } + + // Test 4: Consistent results (only if chain exists) + if (sentimentChain !== null && sentimentChain !== undefined) { + await test('Chains produce consistent sentiment', async () => { + const positive = await sentimentChain.invoke({ + review: "Amazing! Best ever! 5 stars!" + }); + const negative = await sentimentChain.invoke({ + review: "Horrible! Worst purchase ever! 0 stars!" + }); + + assert( + positive.toLowerCase().includes('positive'), + 'Clearly positive review should be classified as positive' + ); + assert( + negative.toLowerCase().includes('negative'), + 'Clearly negative review should be classified as negative' + ); + }); + } else { + failed++; + console.error(`❌ Chains produce consistent sentiment`); + console.error(` Cannot test - sentimentChain is not created\n`); + } + + // Summary + console.log('\n' + '='.repeat(60)); + console.log('TEST SUMMARY'); + console.log('='.repeat(60)); + console.log(`Total: ${passed + failed}`); + console.log(`✅ Passed: ${passed}`); + console.log(`❌ Failed: ${failed}`); + console.log('='.repeat(60)); + + if (failed === 0) { + console.log('\n🎉 All tests passed!\n'); + console.log('📚 What you learned:'); + console.log(' • StringOutputParser cleans text automatically'); + console.log(' • Parsers work seamlessly in chains with .pipe()'); + console.log(' • Low temperature gives consistent outputs'); + console.log(' • Clear prompts help parsers succeed'); + console.log(' • Chains are reusable across multiple inputs\n'); + } else { + console.log('\n⚠️ Some tests failed. Check your implementation.\n'); + } +} \ No newline at end of file diff --git a/tutorial/02-composition/02-parsers/solutions/22-contact-extractor-solution.js b/tutorial/02-composition/02-parsers/solutions/22-contact-extractor-solution.js new file mode 100644 index 0000000000000000000000000000000000000000..64e5c9d51d5e4bc99a5e7ba1e1b7f34d866cce82 --- /dev/null +++ b/tutorial/02-composition/02-parsers/solutions/22-contact-extractor-solution.js @@ -0,0 +1,336 @@ +/** + * Solution 22: Contact Information Extractor + * + * Difficulty: ⭐⭐☆☆ (Intermediate) + * + * Skills gained: + * - JSON extraction from unstructured text + * - List parsing from various formats + * - Including format instructions + * - Schema validation + */ + +import {Runnable, PromptTemplate, JsonOutputParser, ListOutputParser} from '../../../../src/index.js'; +import {LlamaCppLLM} from '../../../../src/llm/llama-cpp-llm.js'; +import {QwenChatWrapper} from "node-llama-cpp"; + +// Sample text snippets with contact information +const TEXT_SAMPLES = [ + "Contact John Smith at john.smith@email.com or call 555-0123. He's based in New York.", + "For inquiries, reach out to Sarah Johnson (sarah.j@company.com), phone: 555-9876, located in San Francisco.", + "Please contact Dr. Michael Chen at m.chen@hospital.org or 555-4567. Office in Boston." +]; + +/** + * Build a chain that extracts structured contact information: + * - name + * - email + * - phone + * - location + */ +async function createContactExtractor() { + const parser = new JsonOutputParser({ + schema: { + name: 'string', + email: 'string', + phone: 'number', + location: 'string' + } + }); + + const prompt = new PromptTemplate({ + template: `Extract info from: {text} + +{format_instructions}`, + inputVariables: ["text"], + partialVariables: { + format_instructions: parser.getFormatInstructions() + } + }); + + const llm = new LlamaCppLLM({ + modelPath: './models/Qwen3-1.7B-Q6_K.gguf', + chatWrapper: new QwenChatWrapper({ + thoughts: 'discourage' // Prevents the model from outputting thinking tokens + }), + }); + + const chain = prompt.pipe(llm).pipe(parser); + + return chain; +} + +/** + * Build a chain that extracts a list of skills from a job description + * Should return array of strings + */ +async function createSkillsExtractor() { + const parser = new ListOutputParser(); + + const prompt = new PromptTemplate({ + template: `List skill found in this text numbered: {description} + +{format_instructions}`, + inputVariables: ["description"], + partialVariables: { + format_instructions: parser.getFormatInstructions() + } + }); + + const llm = new LlamaCppLLM({ + modelPath: './models/Qwen3-1.7B-Q6_K.gguf', + chatWrapper: new QwenChatWrapper({ + thoughts: 'discourage' // Prevents the model from outputting thinking tokens + }), + }); + + const chain = prompt.pipe(llm).pipe(parser); + + return chain; +} + +/** + * Build a chain that extracts company info including multiple contacts + */ +async function createCompanyExtractor() { + const parser = new JsonOutputParser(); + + const prompt = new PromptTemplate({ + template: `From this text: {text} i need following information extracted: company name, industry, year founded, employee count. {format_instructions}`, + inputVariables: ["text"], + partialVariables: { + format_instructions: parser.getFormatInstructions() + } + }); + + const llm = new LlamaCppLLM({ + modelPath: './models/Qwen3-1.7B-Q6_K.gguf', + chatWrapper: new QwenChatWrapper({ + thoughts: 'discourage' // Prevents the model from outputting thinking tokens + }), + }); + + const chain = prompt.pipe(llm).pipe(parser); + + return chain; +} + +async function extractContactInfo() { + console.log('=== Exercise 22: Contact Information Extractor ===\n'); + + const contactChain = await createContactExtractor(); + const skillsChain = await createSkillsExtractor(); + const companyChain = await createCompanyExtractor(); + + // Test 1: Extract contact info + console.log('--- Test 1: Extracting Contact Information ---\n'); + + for (let i = 0; i < TEXT_SAMPLES.length; i++) { + const text = TEXT_SAMPLES[i]; + console.log(`Text ${i + 1}: "${text}"`); + + const contact = await contactChain.invoke({text}); + + console.log('Extracted:', contact); + console.log(); + } + + // Test 2: Extract skills from job description + console.log('--- Test 2: Extracting Skills List ---\n'); + + const description = `We're looking for a Full Stack Developer with experience in: +JavaScript, Python, React, Node.js, PostgreSQL, Docker, AWS, and Git. +Strong communication and problem-solving skills required.`; + + console.log(`Job Description: "${description}"\n`); + + const skills = await skillsChain.invoke({description}); + + console.log('Extracted Skills:', skills); + console.log(); + + // Test 3: Extract company info + console.log('--- Test 3: Extracting Company Information ---\n'); + + const companyText = `TechCorp is a leading software company in the cloud computing industry. +Founded in 2010, the company now employs over 500 people across three continents.`; + + console.log(`Company Text: "${companyText}"\n`); + + const companyInfo = await companyChain.invoke({text: companyText}); + + console.log('Extracted Info:', companyInfo); + console.log(); + + console.log('✓ Exercise 2 Complete!'); + + return {contactChain, skillsChain, companyChain}; +} + +// Run the exercise +extractContactInfo() + .then(runTests) + .catch(console.error); + +// ============================================================================ +// AUTOMATED TESTS +// ============================================================================ + +async function runTests(results) { + const {contactChain, skillsChain, companyChain} = results; + + console.log('\n' + '='.repeat(60)); + console.log('RUNNING AUTOMATED TESTS'); + console.log('='.repeat(60) + '\n'); + + const assert = (await import('assert')).default; + let passed = 0; + let failed = 0; + + async function test(name, fn) { + try { + await fn(); + passed++; + console.log(`✅ ${name}`); + } catch (error) { + failed++; + console.error(`❌ ${name}`); + console.error(` ${error.message}\n`); + } + } + + // Test 1: Chains created + await test('Contact extractor chain created', async () => { + assert(contactChain !== null && contactChain !== undefined, 'Create contactChain'); + assert(contactChain instanceof Runnable, 'Should be Runnable'); + }); + + await test('Skills extractor chain created', async () => { + assert(skillsChain !== null && skillsChain !== undefined, 'Create skillsChain'); + assert(skillsChain instanceof Runnable, 'Should be Runnable'); + }); + + await test('Company extractor chain created', async () => { + assert(companyChain !== null && companyChain !== undefined, 'Create companyChain'); + assert(companyChain instanceof Runnable, 'Should be Runnable'); + }); + + // Test 2: Contact extraction (only run if chain exists) + if (contactChain !== null && contactChain !== undefined) { + await test('Contact extractor returns object', async () => { + const result = await contactChain.invoke({ + text: "Contact Alice at alice@email.com, phone 555-1234, in Seattle" + }); + assert(typeof result === 'object', 'Should return object'); + assert(!Array.isArray(result), 'Should not be array'); + }); + + await test('Contact object has required fields', async () => { + const result = await contactChain.invoke({ + text: "Contact Bob at bob@email.com, phone 555-5678, in Portland" + }); + assert('name' in result, 'Should have name field'); + assert('email' in result, 'Should have email field'); + assert('phone' in result, 'Should have phone field'); + }); + + await test('Contact fields are strings', async () => { + const result = await contactChain.invoke({ + text: "Contact Carol at carol@email.com" + }); + if (result.name) assert(typeof result.name === 'string', 'name should be string'); + if (result.email) assert(typeof result.email === 'string', 'email should be string'); + }); + } else { + failed += 3; + console.error(`❌ Contact extractor returns object`); + console.error(` Cannot test - contactChain is not created\n`); + console.error(`❌ Contact object has required fields`); + console.error(` Cannot test - contactChain is not created\n`); + console.error(`❌ Contact fields are strings`); + console.error(` Cannot test - contactChain is not created\n`); + } + + // Test 3: Skills extraction (only run if chain exists) + if (skillsChain !== null && skillsChain !== undefined) { + await test('Skills extractor returns array', async () => { + const result = await skillsChain.invoke({ + description: "Looking for: JavaScript, Python, SQL" + }); + assert(Array.isArray(result), 'Should return array'); + }); + + await test('Skills array contains strings', async () => { + const result = await skillsChain.invoke({ + description: "Requirements: Java, C++, Git, Docker" + }); + assert(result.length > 0, 'Should extract at least one skill'); + assert( + result.every(skill => typeof skill === 'string'), + 'All skills should be strings' + ); + }); + + await test('Skills array has no empty strings', async () => { + const result = await skillsChain.invoke({ + description: "Skills: React, Node.js, MongoDB" + }); + assert( + result.every(skill => skill.trim().length > 0), + 'Should have no empty strings' + ); + }); + } else { + failed += 3; + console.error(`❌ Skills extractor returns array`); + console.error(` Cannot test - skillsChain is not created\n`); + console.error(`❌ Skills array contains strings`); + console.error(` Cannot test - skillsChain is not created\n`); + console.error(`❌ Skills array has no empty strings`); + console.error(` Cannot test - skillsChain is not created\n`); + } + + // Test 4: Company extraction (only run if chain exists) + if (companyChain !== null && companyChain !== undefined) { + await test('Company extractor returns object', async () => { + const result = await companyChain.invoke({ + text: "CloudTech was founded in 2015 in the SaaS industry with 100 employees" + }); + assert(typeof result === 'object', 'Should return object'); + }); + } else { + failed++; + console.error(`❌ Company extractor returns object`); + console.error(` Cannot test - companyChain is not created\n`); + } + + // Test 5: JSON parsing robustness (always run - tests parser capability) + await test('JsonParser handles markdown code blocks', async () => { + // The parser should extract JSON even if LLM wraps it in ```json + // This test verifies the parser class exists and has the capability + const parser = new JsonOutputParser(); + assert(parser !== null, 'JsonOutputParser should be instantiable'); + assert(typeof parser.parse === 'function', 'Parser should have parse method'); + }); + + // Summary + console.log('\n' + '='.repeat(60)); + console.log('TEST SUMMARY'); + console.log('='.repeat(60)); + console.log(`Total: ${passed + failed}`); + console.log(`✅ Passed: ${passed}`); + console.log(`❌ Failed: ${failed}`); + console.log('='.repeat(60)); + + if (failed === 0) { + console.log('\n🎉 All tests passed!\n'); + console.log('📚 What you learned:'); + console.log(' • JsonOutputParser extracts structured data reliably'); + console.log(' • ListOutputParser handles multiple list formats'); + console.log(' • getFormatInstructions() tells LLM what you expect'); + console.log(' • Schema validation ensures data quality'); + console.log(' • Parsers handle markdown and extra text gracefully\n'); + } else { + console.log('\n⚠️ Some tests failed. Check your implementation.\n'); + } +} \ No newline at end of file diff --git a/tutorial/02-composition/02-parsers/solutions/23-article-metadata-solution.js b/tutorial/02-composition/02-parsers/solutions/23-article-metadata-solution.js new file mode 100644 index 0000000000000000000000000000000000000000..419be03b61a3c8cb95b34b5488001c6d17660bf2 --- /dev/null +++ b/tutorial/02-composition/02-parsers/solutions/23-article-metadata-solution.js @@ -0,0 +1,562 @@ +/** + * Exercise 23: Article Metadata Extractor + * + * Difficulty: ⭐⭐⭐☆ (Advanced) + * + * Goal: Master StructuredOutputParser with complex schemas and validation + * + * In this exercise, you'll: + * 1. Use StructuredOutputParser with detailed schemas + * 2. Define fields with types, descriptions, and enums + * 3. Handle optional vs required fields + * 4. Build a complete metadata extraction system + * + * Skills practiced: + * - Complex schema definition + * - Type validation (string, number, boolean, array) + * - Enum constraints + * - Required vs optional fields + * - Error handling and validation + */ + +import {Runnable, PromptTemplate, StructuredOutputParser} from '../../../../src/index.js'; +import {LlamaCppLLM} from '../../../../src/llm/llama-cpp-llm.js'; +import {QwenChatWrapper} from "node-llama-cpp"; + +// Sample articles to extract metadata from +const ARTICLES = [ + { + title: "The Future of AI in Healthcare", + content: `Artificial intelligence is revolutionizing healthcare. From diagnostic tools to +personalized treatment plans, AI is improving patient outcomes. Recent studies show 85% accuracy +in detecting certain cancers. However, challenges remain around data privacy and ethical concerns. +This technology will continue to transform medicine in the coming decade.`, + author: "Dr. Sarah Johnson" + }, + { + title: "Climate Change: A Global Challenge", + content: `Climate change poses an existential threat to humanity. Rising temperatures, +extreme weather events, and sea level rise are already impacting millions. The latest IPCC report +warns we have less than 10 years to act. Renewable energy and carbon reduction are critical. +International cooperation is essential to address this crisis.`, + author: "Michael Chen" + }, + { + title: "The Rise of Remote Work", + content: `The pandemic accelerated the shift to remote work. Many companies now offer +hybrid or fully remote options. Productivity studies show mixed results - some teams thrive, +others struggle. Work-life balance improves for many, but isolation is a concern. The future +of work will likely be flexible, with employees choosing their preferred setup.`, + author: "Emma Williams" + } +]; + +/** + * Build a chain that extracts comprehensive article metadata with validation + */ +async function createArticleMetadataExtractor() { + const parser = new StructuredOutputParser({ + responseSchemas: [ + { + name: "category", + type: "string", + enum: ["technology", "health", "environment", "business", "other"], + required: true + }, + { + name: "sentiment", + type: "string", + enum: ["positive", "negative", "neutral", "mixed"], + required: true + }, + { + name: "readingLevel", + type: "string", + enum: ["beginner", "intermediate", "advanced"], + required: true + }, + { + name: "mainTopics", + type: "array", + required: true + }, + { + name: "hasCitations", + type: "boolean", + required: false + }, + { + name: "estimatedReadTime", + type: "number", + required: false + }, + { + name: "keyTakeaway", + type: "string", + required: false + }, + { + name: "targetAudience", + type: "string", + required: false + } + ] + }); + + const prompt = new PromptTemplate({ + template: `You are an advanced content-analysis system. +Analyze the following article and extract the required structured metadata. + +ARTICLE DATA: +Title: {title} +Author: {author} +Content: +{content} + +{format_instructions}`, + inputVariables: ["title", "author", "content"], + partialVariables: { + format_instructions: parser.getFormatInstructions() + } + }); + + const llm = new LlamaCppLLM({ + modelPath: './models/Qwen3-1.7B-Q6_K.gguf', + chatWrapper: new QwenChatWrapper({ + thoughts: 'discourage' // Prevents the model from outputting thinking tokens + }), + }); + + const chain = prompt.pipe(llm).pipe(parser); + + return chain; +} + +/** + * Build a chain that analyzes content quality with scores + */ +async function createQualityAnalyzer() { + const parser = new StructuredOutputParser({ + responseSchemas: [ + { + name: "clarity", + type: "number", + required: true + }, + { + name: "depth", + type: "number", + required: true + }, + { + name: "accuracy", + type: "number", + required: true + }, + { + name: "engagement", + type: "number", + required: true + }, + { + name: "overallScore", + type: "number", + required: true + }, + { + name: "strengths", + type: "array", + required: true + }, + { + name: "improvements", + type: "array", + required: true + }, + { + name: "recommendation", + type: "string", + enum: ["publish", "revise", "reject"], + required: true + } + ] + }); + + const prompt = new PromptTemplate({ + template: `Analyze the quality of this {article} {format_instructions}`, + inputVariables: ["article"], + partialVariables: { + format_instructions: parser.getFormatInstructions() + } + }); + + const llm = new LlamaCppLLM({ + modelPath: './models/Qwen3-1.7B-Q6_K.gguf', + chatWrapper: new QwenChatWrapper({ + thoughts: 'discourage' // Prevents the model from outputting thinking tokens + }), + }); + + const chain = prompt.pipe(llm).pipe(parser); + + return chain; +} + +// ============================================================================ +// TODO 3: Create SEO Optimizer +// ============================================================================ + +/** + * Build a chain that provides SEO recommendations + */ +async function createSEOOptimizer() { + const parser = new StructuredOutputParser({ + responseSchemas: [ + { + name: "suggestedKeywords", + type: "array", + required: true + }, + { + name: "metaDescription", + type: "string", + required: true + }, + { + name: "hasGoodTitle", + type: "boolean", + required: true + }, + { + name: "readabilityScore", + type: "number", + required: true + }, + { + name: "seoScore", + type: "number", + required: true + }, + { + name: "recommendations", + type: "array", + required: true + } + ] + }); + + const prompt = new PromptTemplate({ + template: `Optimize this article for seo {article} {format_instructions}`, + inputVariables: ["article"], + partialVariables: { + format_instructions: parser.getFormatInstructions() + } + }); + + const llm = new LlamaCppLLM({ + modelPath: './models/Qwen3-1.7B-Q6_K.gguf', + chatWrapper: new QwenChatWrapper({ + thoughts: 'discourage' // Prevents the model from outputting thinking tokens + }), + }); + + // TODO: Build chain + const chain = prompt.pipe(llm).pipe(parser); + + return chain; +} + +// ============================================================================ +// TODO 4: Process Articles and Validate All Metadata +// ============================================================================ + +async function analyzeArticles() { + console.log('=== Exercise 23: Article Metadata Extractor ===\n'); + + // TODO: Create all chains + const metadataChain = await createArticleMetadataExtractor(); + const qualityChain = await createQualityAnalyzer(); + const seoChain = await createSEOOptimizer(); + + // Process each article + for (let i = 0; i < ARTICLES.length; i++) { + const article = ARTICLES[i]; + + console.log('='.repeat(70)); + console.log(`ARTICLE ${i + 1}: ${article.title}`); + console.log('='.repeat(70)); + console.log(`Author: ${article.author}`); + console.log(`Content: ${article.content.substring(0, 100)}...`); + console.log(); + + try { + console.log('--- Metadata ---'); + const metadata = await metadataChain.invoke({ + title: article.title, + author: article.author, + content: article.content + }); + console.log(JSON.stringify(metadata, null, 2)); + console.log(); + + console.log('--- Quality Analysis ---'); + const quality = await qualityChain.invoke({article}); + console.log(JSON.stringify(quality, null, 2)); + console.log(); + + console.log('--- SEO Recommendations ---'); + const seo = await seoChain.invoke({article}); + console.log(JSON.stringify(seo, null, 2)); + console.log(); + + } catch (error) { + console.error(`Error processing article: ${error.message}`); + console.log(); + } + } + + console.log('✓ Exercise 23 Complete!'); + + return { metadataChain, qualityChain, seoChain }; +} + +// Run the exercise +analyzeArticles() + .then(runTests) + .catch(console.error); + +// ============================================================================ +// AUTOMATED TESTS +// ============================================================================ + +async function runTests(results) { + const { metadataChain, qualityChain, seoChain } = results; + + console.log('\n' + '='.repeat(60)); + console.log('RUNNING AUTOMATED TESTS'); + console.log('='.repeat(60) + '\n'); + + const assert = (await import('assert')).default; + let passed = 0; + let failed = 0; + + async function test(name, fn) { + try { + await fn(); + passed++; + console.log(`✅ ${name}`); + } catch (error) { + failed++; + console.error(`❌ ${name}`); + console.error(` ${error.message}\n`); + } + } + + const testArticle = { + title: "Test Article", + content: "This is test content about artificial intelligence in healthcare.", + author: "Test Author" + }; + + // Test 1: Chains created + test('Metadata chain created', async () => { + assert(metadataChain !== null, 'Create metadataChain'); + }); + + test('Quality chain created', async () => { + assert(qualityChain !== null, 'Create qualityChain'); + }); + + test('SEO chain created', async () => { + assert(seoChain !== null, 'Create seoChain'); + }); + + // Test 2: Metadata extraction + test('Metadata has required fields', async () => { + const result = await metadataChain.invoke({ + title: testArticle.title, + author: testArticle.author, + content: testArticle.content + }); + + assert('category' in result, 'Should have category'); + assert('sentiment' in result, 'Should have sentiment'); + assert('mainTopics' in result, 'Should have mainTopics'); + }); + + test('Metadata category is valid enum', async () => { + const result = await metadataChain.invoke({ + title: testArticle.title, + author: testArticle.author, + content: testArticle.content + }); + + const validCategories = ["technology", "health", "environment", "business", "other"]; + assert( + validCategories.includes(result.category), + `Category should be one of: ${validCategories.join(', ')}` + ); + }); + + test('Metadata sentiment is valid enum', async () => { + const result = await metadataChain.invoke({ + title: testArticle.title, + author: testArticle.author, + content: testArticle.content + }); + + const validSentiments = ["positive", "negative", "neutral", "mixed"]; + assert( + validSentiments.includes(result.sentiment), + `Sentiment should be one of: ${validSentiments.join(', ')}` + ); + }); + + test('Metadata mainTopics is array', async () => { + const result = await metadataChain.invoke({ + title: testArticle.title, + author: testArticle.author, + content: testArticle.content + }); + + assert(Array.isArray(result.mainTopics), 'mainTopics should be array'); + assert(result.mainTopics.length > 0, 'mainTopics should not be empty'); + }); + + test('Metadata estimatedReadTime is number', async () => { + const result = await metadataChain.invoke({ + title: testArticle.title, + author: testArticle.author, + content: testArticle.content + }); + + assert(typeof result.estimatedReadTime === 'number', 'estimatedReadTime should be number'); + assert(result.estimatedReadTime > 0, 'estimatedReadTime should be positive'); + }); + + test('Metadata hasCitations is boolean', async () => { + const result = await metadataChain.invoke({ + title: testArticle.title, + author: testArticle.author, + content: testArticle.content + }); + + assert(typeof result.hasCitations === 'boolean', 'hasCitations should be boolean'); + }); + + // Test 3: Quality analysis + test('Quality scores are numbers', async () => { + const result = await qualityChain.invoke({ article: testArticle }); + + assert(typeof result.clarity === 'number', 'clarity should be number'); + assert(typeof result.depth === 'number', 'depth should be number'); + assert(typeof result.overallScore === 'number', 'overallScore should be number'); + }); + + test('Quality scores are in valid range', async () => { + const result = await qualityChain.invoke({ article: testArticle }); + + assert(result.clarity >= 1 && result.clarity <= 10, 'clarity should be 1-10'); + assert(result.overallScore >= 1 && result.overallScore <= 10, 'overallScore should be 1-10'); + }); + + test('Quality has array fields', async () => { + const result = await qualityChain.invoke({ article: testArticle }); + + assert(Array.isArray(result.strengths), 'strengths should be array'); + assert(Array.isArray(result.improvements), 'improvements should be array'); + }); + + test('Quality recommendation is valid', async () => { + const result = await qualityChain.invoke({ article: testArticle }); + + const validRecommendations = ["publish", "revise", "reject"]; + assert( + validRecommendations.includes(result.recommendation), + `recommendation should be one of: ${validRecommendations.join(', ')}` + ); + }); + + // Test 4: SEO optimization + test('SEO has keyword suggestions', async () => { + const result = await seoChain.invoke({ article: testArticle }); + + assert(Array.isArray(result.suggestedKeywords), 'suggestedKeywords should be array'); + assert(result.suggestedKeywords.length > 0, 'Should suggest at least one keyword'); + }); + + test('SEO metaDescription is appropriate length', async () => { + const result = await seoChain.invoke({ article: testArticle }); + + assert(typeof result.metaDescription === 'string', 'metaDescription should be string'); + assert(result.metaDescription.length <= 200, 'metaDescription should be concise'); + }); + + test('SEO scores are in valid range', async () => { + const result = await seoChain.invoke({ article: testArticle }); + + assert(result.readabilityScore >= 1 && result.readabilityScore <= 100); + assert(result.seoScore >= 1 && result.seoScore <= 100); + }); + + // Summary + console.log('\n' + '='.repeat(60)); + console.log('TEST SUMMARY'); + console.log('='.repeat(60)); + console.log(`Total: ${passed + failed}`); + console.log(`✅ Passed: ${passed}`); + console.log(`❌ Failed: ${failed}`); + console.log('='.repeat(60)); + + if (failed === 0) { + console.log('\n🎉 All tests passed!\n'); + } else { + console.log('\n⚠️ Some tests failed. Check your implementation.\n'); + } +} + +/** + * HINTS: + * + * 1. StructuredOutputParser with full schema: + * new StructuredOutputParser({ + * responseSchemas: [ + * { + * name: "category", + * type: "string", + * description: "Article category", + * enum: ["tech", "health", "business"], + * required: true + * }, + * { + * name: "score", + * type: "number", + * description: "Quality score 1-10" + * } + * ] + * }) + * + * 2. Always include format instructions: + * partialVariables: { + * format_instructions: parser.getFormatInstructions() + * } + * + * 3. Types supported: + * - "string" + * - "number" + * - "boolean" + * - "array" + * - "object" + * + * 4. The parser will: + * - Validate all required fields exist + * - Check type of each field + * - Verify enum values if specified + * - Throw detailed errors on validation failure + * + * 5. For better LLM compliance: + * - Use low temperature (0.1-0.2) + * - Be explicit in prompts + * - Include examples if needed + * - Reference the format instructions clearly + */ \ No newline at end of file diff --git a/tutorial/02-composition/02-parsers/solutions/24-multi-parser-pipeline-solution.js b/tutorial/02-composition/02-parsers/solutions/24-multi-parser-pipeline-solution.js new file mode 100644 index 0000000000000000000000000000000000000000..d26816823b1293070ce347e06ddca04718ce0e19 --- /dev/null +++ b/tutorial/02-composition/02-parsers/solutions/24-multi-parser-pipeline-solution.js @@ -0,0 +1,505 @@ +/** + * Exercise 24: Multi-Parser Content Pipeline + * + * Difficulty: ⭐⭐⭐⭐ (Expert) + * + * Goal: Build a robust content processing pipeline using multiple parsers with fallbacks + * + * Skills gained: + * - Multi-parser orchestration + * - Fallback parsing strategies + * - Regex-based extraction + * - Error handling and recovery + * - Building robust production pipelines + */ + +import { + Runnable, + PromptTemplate, + StructuredOutputParser, + ListOutputParser, + RegexOutputParser +} from '../../../../src/index.js'; +import {LlamaCppLLM} from '../../../../src/llm/llama-cpp-llm.js'; +import {QwenChatWrapper} from "node-llama-cpp"; + +const CONTENT_SAMPLES = [ + { + text: "Breaking: Stock market hits record high! NASDAQ up 2.5%, S&P 500 gains 1.8%. Tech sector leads with Apple +3.2%, Microsoft +2.9%. Analysts predict continued growth.", + type: "news" + }, + { + text: "Recipe: Chocolate Chip Cookies. Ingredients: 2 cups flour, 1 cup butter, 1 cup sugar, 2 eggs, 1 tsp vanilla, 2 cups chocolate chips. Bake at 350°F for 12 minutes.", + type: "recipe" + }, + { + text: "Product Review: The XPhone 15 Pro (Score: 8.5/10) - Great camera, long battery life, but expensive at $1,199. Pros: Display, Performance. Cons: Price, Weight.", + type: "review" + } +]; + +/** + * Extract structured data from news articles + */ +async function createNewsParser() { + const parser = new StructuredOutputParser({ + responseSchemas: [ + { + name: "headline", + description: "A brief, catchy headline summarizing the article", + type: "string", + required: true + }, + { + name: "category", + description: "The primary category of the article", + type: "string", + enum: ["business", "technology", "politics", "sports", "other"], + required: true + }, + { + name: "sentiment", + description: "The overall sentiment or tone of the article", + type: "string", + enum: ["positive", "negative", "neutral"], + required: true + }, + { + name: "entities", + description: "List of notable entities mentioned in the article such as companies, people, and places", + type: "array" + }, + { + name: "marketData", + description: "Any numerical market data or statistics mentioned with their context (e.g., 'NASDAQ up 2.5%', 'unemployment at 3.8%')", + type: "array" + } + ] + }); + + const prompt = new PromptTemplate({ + template: `Analyze this news article and extract structured information. + +Article: {text} + +{format_instructions} + +Be precise and extract all relevant entities and market data mentioned.`, + inputVariables: ["text"], + partialVariables: { + format_instructions: parser.getFormatInstructions() + } + }); + + const llm = new LlamaCppLLM({ + modelPath: './models/Qwen3-1.7B-Q6_K.gguf', + chatWrapper: new QwenChatWrapper({ + thoughts: 'discourage' + }), + }); + + const chain = prompt.pipe(llm).pipe(parser); + + return chain; +} + +/** + * Extract recipe components using regex and list parsers + */ +async function createRecipeParser() { + const nameParser = new RegexOutputParser({ + regex: /Recipe:\s*(.+?)\./ + }); + + const cookingParser = new RegexOutputParser({ + regex: /(\d+)°F.*?(\d+)\s*minutes/ + }); + + const ingredientsPrompt = new PromptTemplate({ + template: `Extract only the ingredients from this recipe as a comma-separated list. Only list the ingredients, nothing else. + +Recipe: {text} + +Return only the ingredients as a comma-separated list.`, + inputVariables: ["text"] + }); + + const llm = new LlamaCppLLM({ + modelPath: './models/Qwen3-1.7B-Q6_K.gguf', + chatWrapper: new QwenChatWrapper({ + thoughts: 'discourage' + }), + temperature: 0.1 + }); + + const ingredientsParser = new ListOutputParser(); + + class RecipeParserRunnable extends Runnable { + async _call(input, config) { + const text = input.text; + + try { + const nameResult = await nameParser.parse(text); + const name = Array.isArray(nameResult) ? nameResult[0] : nameResult; + + const ingredientsChain = ingredientsPrompt.pipe(llm).pipe(ingredientsParser); + const ingredients = await ingredientsChain.invoke({text}, config); + + const cookingResult = await cookingParser.parse(text); + const temperature = Array.isArray(cookingResult) && cookingResult.length >= 1 ? cookingResult[0] : null; + const time = Array.isArray(cookingResult) && cookingResult.length >= 2 ? cookingResult[1] : null; + + return { + name, + ingredients, + temperature: temperature ? `${temperature}°F` : null, + time: time ? `${time} minutes` : null + }; + } catch (error) { + return { + name: text.match(/Recipe:\s*(.+?)\./)?.[1] || "Unknown", + ingredients: [], + temperature: text.match(/(\d+)°F/)?.[1] || null, + time: text.match(/(\d+)\s*minutes/)?.[1] || null, + error: error.message + }; + } + } + } + + return new RecipeParserRunnable(); +} + +/** + * Parse product reviews with fallback strategy + * Try structured parser first, fall back to regex if it fails + */ +async function createReviewParser() { + const llm = new LlamaCppLLM({ + modelPath: './models/Qwen3-1.7B-Q6_K.gguf', + chatWrapper: new QwenChatWrapper({ + thoughts: 'discourage' + }), + temperature: 0.1 + }); + + const structuredParser = new StructuredOutputParser({ + responseSchemas: [ + { + name: "productName", + description: "The name or model of the product being reviewed", + type: "string", + required: true + }, + { + name: "score", + description: "The numerical rating or score given to the product (e.g., 8.5, 7/10)", + type: "number", + required: true + }, + { + name: "pros", + description: "List of positive aspects or advantages of the product", + type: "array", + required: false + }, + { + name: "cons", + description: "List of negative aspects or disadvantages of the product", + type: "array", + required: false + }, + { + name: "price", + description: "The price of the product if mentioned (as a string with currency symbol, e.g., '$1,199')", + type: "string", + required: false + } + ] + }); + + const regexParser = new RegexOutputParser({ + regex: /(?:Product Review:|The)?\s*(.+?)\s*\(Score:\s*([\d.]+)(?:\/10)?\).*?\$([0-9,]+)/ + }); + + class ReviewParserWithFallback extends Runnable { + async _call(input, config) { + const text = input.text; + + try { + const prompt = new PromptTemplate({ + template: `Extract review data: {text}\n\n{format_instructions}`, + inputVariables: ["text"], + partialVariables: { + format_instructions: structuredParser.getFormatInstructions() + } + }); + + const chain = prompt.pipe(llm).pipe(structuredParser); + const result = await chain.invoke({text}); + + return { + method: 'structured', + data: result + }; + } catch (error) { + console.warn('Structured parsing failed, using regex fallback'); + + try { + const result = await regexParser.parse(text); + + return { + method: 'regex', + data: { + productName: Array.isArray(result) && result.length > 0 ? result[0] : null, + score: Array.isArray(result) && result.length > 1 ? parseFloat(result[1]) : null, + price: Array.isArray(result) && result.length > 2 ? `$${result[2]}` : null + } + }; + } catch (regexError) { + console.warn('Regex parsing failed, using basic extraction'); + + const productMatch = text.match(/(?:Product Review:|The)?\s*(.+?)\s*\(/); + const scoreMatch = text.match(/Score:\s*([\d.]+)/); + const priceMatch = text.match(/\$([0-9,]+)/); + + return { + method: 'basic', + data: { + productName: productMatch ? productMatch[1].trim() : null, + score: scoreMatch ? parseFloat(scoreMatch[1]) : null, + price: priceMatch ? `$${priceMatch[1]}` : null, + text: text + } + }; + } + } + } + } + + return new ReviewParserWithFallback(); +} + +/** + * Route content to appropriate parser based on content type + */ +class ContentRouter extends Runnable { + constructor(parsers) { + super(); + this.parsers = parsers; + } + + async _call(input, config) { + const {text, type} = input; + + const parser = this.parsers[type]; + + if (!parser) { + throw new Error(`No parser for content type: ${type}`); + } + + const result = await parser.invoke({text}, config); + + return { + type: type, + parsed: result, + originalText: text + }; + } +} + +async function buildContentPipeline() { + console.log('=== Exercise 24: Multi-Parser Content Pipeline ===\n'); + + const newsParser = await createNewsParser(); + const recipeParser = await createRecipeParser(); + const reviewParser = await createReviewParser(); + + const router = new ContentRouter({ + news: newsParser, + recipe: recipeParser, + review: reviewParser + }); + + console.log('Processing content samples...\n'); + + const results = []; + + for (let i = 0; i < CONTENT_SAMPLES.length; i++) { + const sample = CONTENT_SAMPLES[i]; + + console.log('='.repeat(70)); + console.log(`SAMPLE ${i + 1}: ${sample.type.toUpperCase()}`); + console.log('='.repeat(70)); + console.log(`Text: ${sample.text}\n`); + + try { + const result = await router.invoke({text: sample.text, type: sample.type}); + + console.log('Parsing Result:'); + console.log(JSON.stringify(result, null, 2)); + + results.push({ + success: true, + data: result + }); + } catch (error) { + console.error(`Error: ${error.message}`); + + results.push({ + success: false, + error: error.message, + sample: sample + }); + } + + console.log(); + } + + console.log('='.repeat(70)); + console.log('PROCESSING SUMMARY'); + console.log('='.repeat(70)); + + const successful = results.filter(r => r.success).length; + const failed = results.filter(r => !r.success).length; + + console.log(`Total Samples: ${results.length}`); + console.log(`Successful: ${successful}`); + console.log(`Failed: ${failed}`); + console.log(`Success Rate: ${((successful / results.length) * 100).toFixed(1)}%`); + + console.log('\n✓ Exercise 24 Complete!'); + + return {newsParser, recipeParser, reviewParser, router, results}; +} + +buildContentPipeline() + .then(runTests) + .catch(console.error); + +async function runTests(context) { + const {newsParser, recipeParser, reviewParser, router, results} = context; + + console.log('\n' + '='.repeat(60)); + console.log('RUNNING AUTOMATED TESTS'); + console.log('='.repeat(60) + '\n'); + + const assert = (await import('assert')).default; + let passed = 0; + let failed = 0; + + function test(name, fn) { + try { + fn(); + passed++; + console.log(`✅ ${name}`); + } catch (error) { + failed++; + console.error(`❌ ${name}`); + console.error(` ${error.message}\n`); + } + } + + test('News parser created', () => { + assert(newsParser !== null, 'Create newsParser'); + assert(newsParser instanceof Runnable, 'Should be Runnable'); + }); + + test('Recipe parser created', () => { + assert(recipeParser !== null, 'Create recipeParser'); + }); + + test('Review parser created', () => { + assert(reviewParser !== null, 'Create reviewParser'); + }); + + test('Router created', () => { + assert(router !== null, 'Create ContentRouter'); + assert(router instanceof ContentRouter, 'Should be ContentRouter instance'); + }); + + test('News parser extracts structured data', async () => { + const result = await newsParser.invoke({ + text: "Tech stocks surge: Apple up 5%, Google gains 3%" + }); + assert(typeof result === 'object', 'Should return object'); + assert('headline' in result || 'category' in result, 'Should have headline or category'); + }); + + test('Recipe parser extracts components', async () => { + const result = await recipeParser.invoke({ + text: "Recipe: Pasta. Ingredients: noodles, sauce. Bake at 400°F for 20 minutes." + }); + assert(typeof result === 'object', 'Should return object'); + assert('name' in result || 'ingredients' in result, 'Should have recipe components'); + }); + + test('Review parser handles well-formed input', async () => { + const result = await reviewParser.invoke({ + text: "Product XYZ (Score: 8/10) costs $99. Pros: Good. Cons: Expensive." + }); + assert(result.method, 'Should indicate parsing method used'); + assert(result.data, 'Should have data'); + }); + + test('Review parser falls back gracefully', async () => { + const result = await reviewParser.invoke({ + text: "This is malformed data that won't parse well" + }); + assert(result !== null, 'Should return something even on bad input'); + assert(result.method, 'Should indicate which method was used'); + }); + + test('Router routes to correct parser', async () => { + const newsResult = await router.invoke({ + text: "Breaking news story", + type: "news" + }); + assert(newsResult.type === 'news', 'Should preserve content type'); + assert(newsResult.parsed, 'Should have parsed data'); + }); + + test('Pipeline processed all samples', () => { + assert(results.length === CONTENT_SAMPLES.length, 'Should process all samples'); + }); + + test('Pipeline has reasonable success rate', () => { + const successRate = results.filter(r => r.success).length / results.length; + assert(successRate >= 0.5, 'Should successfully parse at least 50% of samples'); + }); + + test('Pipeline handles invalid content type', async () => { + try { + await router.invoke({ + text: "Some text", + type: "invalid_type" + }); + assert(false, 'Should throw error for invalid type'); + } catch (error) { + assert(true, 'Correctly throws error for invalid type'); + } + }); + + console.log('\n' + '='.repeat(60)); + console.log('TEST SUMMARY'); + console.log('='.repeat(60)); + console.log(`Total: ${passed + failed}`); + console.log(`✅ Passed: ${passed}`); + console.log(`❌ Failed: ${failed}`); + console.log('='.repeat(60)); + + if (failed === 0) { + console.log('\n🎉 All tests passed! You are a parser master!\n'); + console.log('📚 What you mastered:'); + console.log(' • Orchestrating multiple parser types'); + console.log(' • Implementing fallback strategies'); + console.log(' • Using RegexOutputParser for custom patterns'); + console.log(' • Building robust error handling'); + console.log(' • Creating production-ready pipelines'); + console.log(' • Routing content to appropriate parsers'); + console.log(' • Combining structured and pattern-based extraction\n'); + console.log('🚀 You are ready for production parser systems!'); + } else { + console.log('\n⚠️ Some tests failed. Review the advanced patterns.\n'); + } +} \ No newline at end of file diff --git a/tutorial/README.md b/tutorial/README.md new file mode 100644 index 0000000000000000000000000000000000000000..972ca18b4cb03694ff387d9928e7bc467b5c1b26 --- /dev/null +++ b/tutorial/README.md @@ -0,0 +1,116 @@ +# AI Agents Framework Tutorial + +Welcome to the step-by-step tutorial for building your own AI agent framework! + +This tutorial teaches you to build a **lightweight, educational version of LangChain.js** - +with the same core concepts and API, but simpler implementations designed for learning. + +Instead of diving into LangChain's complex codebase, you'll rebuild its key patterns +yourself with clear, educational code. By the end, you'll understand what frameworks +are actually doing, making you far more effective at using them. + +**What you'll implement:** +- Runnable interface (LangChain's composability pattern) +- Message types (structured conversations) +- LLM wrappers (model integration) +- Chains (composing operations) +- Agents (decision-making loops) +- Graphs (LangGraph state machines) + +**What makes this different:** +- LangChain-compatible API (what you learn transfers directly) +- Simpler implementations (much less code, same concepts) +- Educational focus (understanding over completeness) +- Real, working code (not pseudocode or toys) + +Build it yourself. Understand it deeply. Use LangChain confidently. + +## Learning Paths + +## Before You Start: Why This Tutorial Exists + +**You've just built AI agents with node-llama-cpp.** You know how to call LLMs, format prompts, parse responses, and create agent loops. That's awesome—you understand the fundamentals! + +**But you probably noticed some friction:** +- Copy-pasting prompt formatting everywhere +- Manually building message arrays each time +- Hard to test individual components +- Difficult to swap out models or reuse patterns +- Agent code that works but feels messy + +**This tutorial fixes those problems.** Instead of jumping straight into LangChain's complex codebase, you'll rebuild its core patterns yourself with clear, educational code. You'll transform the script-style code you wrote into clean, composable abstractions. + +**The approach:** +1. Start with problems you've already encountered +2. Build the abstraction that solves each problem +3. See how it connects to LangChain's API +4. Understand frameworks deeply, use them confidently + +### Part 1: From Scripts to Abstractions + +Transform the patterns you already use into reusable components. + +**What you'll solve:** +- **Agent code getting messy?** → Build the Runnable pattern for composability +- **Message formatting tedious?** → Create Message types for structure +- **Model switching hard?** → Design LLM wrappers for flexibility +- **Managing conversation state?** → Implement Context for memory + +**Lessons:** +- [01-runnable](01-foundation/01-runnable/lesson.md) - The composability pattern ← Start here +- [02-messages](01-foundation/02-messages/lesson.md) - Structured conversation data +- [03-llm-wrapper](01-foundation/03-llm-wrapper/lesson.md) - Model abstraction layer +- [04-context](01-foundation/04-context/lesson.md) - Conversation state management + +### Part 2: Composition +Dive deeper into prompt engineering and chain complex operations together. + +**What you'll solve:** +- **Copy-pasting prompts everywhere?** → Build reusable PromptTemplates with variables +- **Need structured LLM outputs?** → Create OutputParsers for reliable data extraction +- **Repeating prompt + LLM patterns?** → Design LLMChain to compose operations +- **Want to chain operations together?** → Use piping to connect Runnables +- **LLM forgets conversation history?** → Implement Memory for context persistence + +**Lessons:** +- [01-prompts](02-composition/01-prompts/lesson.md) - Template-based prompt engineering +- [02-parsers](02-composition/02-parsers/lesson.md) - Structured output extraction +- [03-llm-chain](02-composition/03-llm-chain/lesson.md) - Composing prompts with models - Coming soon +- [04-piping](02-composition/04-piping/lesson.md) - Building data transformation pipelines - Coming soon +- [05-memory](02-composition/05-memory/lesson.md) - Persistent conversation history - Coming soon + +### Part 3: Agents [Coming Soon] +Agents and tools +- [01-tools](03-agency/01-tools/lesson.md) - Coming soon +- [02-tool-executor](03-agency/02-tool-executor/lesson.md) - Coming soon +- [03-simple-agent](03-agency/03-simple-agent/lesson.md) - Coming soon +- [04-react-agent](03-agency/04-react-agent/lesson.md) - Coming soon +- [05-structured-agent](03-agency/05-structured-agent/lesson.md) - Coming soon + +### Part 4: Graphs [Coming Soon] +State machines and workflows +- [01-state-basics](04-graphs/01-state-basics/lesson.md) - Coming soon +- [02-channels](04-graphs/02-channels/lesson.md) - Coming soon +- [03-conditional-edges](04-graphs/03-conditional-edges/lesson.md) - Coming soon +- [04-executor](04-graphs/04-executor/lesson.md) - Coming soon +- [05-checkpointing](04-graphs/05-checkpointing/lesson.md) - Coming soon +- [06-agent-graph](04-graphs/06-agent-graph/lesson.md) - Coming soon + +## Capstone Projects + +Complete these capstone projects to solidify your learning: + +- [Smart Email Classifier](projects/01-smart-email-classifier) - After Part 1 +- [Research Agent](projects/research-agent/) - Coming soon +- [Task Automation](projects/task-automation/) - Coming soon +- [Approval Workflow](projects/approval-workflow/) - Coming soon + +## How to Use This Tutorial + +1. Start with Part 1 and work sequentially +2. Read the markdown lessons +3. Complete the exercises +4. Check solutions when stuck +5. Build the projects + +Happy learning! 🚀 diff --git a/tutorial/projects/01-smart-email-classifier/solution.js b/tutorial/projects/01-smart-email-classifier/solution.js new file mode 100644 index 0000000000000000000000000000000000000000..31dd943134f0f401520b5bb6461334641bfb578e --- /dev/null +++ b/tutorial/projects/01-smart-email-classifier/solution.js @@ -0,0 +1,349 @@ +/** + * Part 1 Capstone Solution: Smart Email Classifier + * + * Build an AI system that organizes your inbox by classifying emails into categories. + * + * Skills Used: + * - Runnables for processing pipeline + * - Messages for structured classification + * - LLM wrapper for flexible model switching + * - Context for classification history + * + * Difficulty: ⭐⭐☆☆☆ + */ + +import { SystemMessage, HumanMessage, Runnable, LlamaCppLLM } from '../../../src/index.js'; +import { BaseCallback } from '../../../src/utils/callbacks.js'; +import { readFileSync } from 'fs'; + +// ============================================================================ +// EMAIL CLASSIFICATION CATEGORIES +// ============================================================================ + +const CATEGORIES = { + SPAM: 'Spam', + INVOICE: 'Invoice', + MEETING: 'Meeting Request', + URGENT: 'Urgent', + PERSONAL: 'Personal', + OTHER: 'Other' +}; + +// ============================================================================ +// Email Parser Runnable +// ============================================================================ + +/** + * Parses raw email text into structured format + * + * Input: { subject: string, body: string, from: string } + * Output: { subject, body, from, timestamp } + */ +class EmailParserRunnable extends Runnable { + async _call(input, config) { + // Validate required fields + if (!input.subject || !input.body || !input.from) { + throw new Error('Email must have subject, body, and from fields'); + } + + // Parse and structure the email + return { + subject: input.subject.trim(), + body: input.body.trim(), + from: input.from.trim(), + timestamp: new Date().toISOString() + }; + } +} + +// ============================================================================ +// Email Classifier Runnable +// ============================================================================ + +/** + * Classifies email using LLM + * + * Input: { subject, body, from, timestamp } + * Output: { ...email, category, confidence, reason } + */ +class EmailClassifierRunnable extends Runnable { + constructor(llm) { + super(); + this.llm = llm; + } + + async _call(input, config) { + // Build the classification prompt + const messages = this._buildPrompt(input); + + // Call the LLM + const response = await this.llm.invoke(messages, config); + + // Parse the LLM response + const classification = this._parseClassification(response.content); + + // Return email with classification + return { + ...input, + category: classification.category, + confidence: classification.confidence, + reason: classification.reason + }; + } + + _buildPrompt(email) { + const systemPrompt = new SystemMessage(`You are an email classification assistant. Your task is to classify emails into one of these categories: + +Categories: +- Spam: Unsolicited promotional emails, advertisements with excessive punctuation/caps, phishing attempts, scams +- Invoice: Bills, payment requests, financial documents, receipts +- Meeting Request: Meeting invitations, calendar requests, scheduling, availability inquiries +- Urgent: Time-sensitive matters requiring immediate attention, security alerts, critical notifications +- Personal: Personal correspondence from friends/family (look for personal tone and familiar email addresses) +- Other: Legitimate newsletters, updates, informational content, everything else that doesn't fit above + +Important distinctions: +- Legitimate newsletters (tech updates, subscriptions) should be "Other", not Spam +- Spam has excessive punctuation (!!!, ALL CAPS), pushy language, or suspicious intent +- Personal emails have familiar sender addresses and casual tone + +Respond in this exact JSON format: +{ + "category": "Category Name", + "confidence": 0.95, + "reason": "Brief explanation" +} + +Confidence should be between 0 and 1.`); + + const userPrompt = new HumanMessage(`Classify this email: + +From: ${email.from} +Subject: ${email.subject} +Body: ${email.body} + +Provide your classification in JSON format.`); + + return [systemPrompt, userPrompt]; + } + + _parseClassification(response) { + try { + // Try to find JSON in the response + const jsonMatch = response.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + throw new Error('No JSON found in response'); + } + + const parsed = JSON.parse(jsonMatch[0]); + + // Validate the parsed response + if (!parsed.category || parsed.confidence === undefined || !parsed.reason) { + throw new Error('Invalid classification format'); + } + + // Ensure confidence is a number between 0 and 1 + const confidence = Math.max(0, Math.min(1, parseFloat(parsed.confidence))); + + return { + category: parsed.category, + confidence: confidence, + reason: parsed.reason + }; + } catch (error) { + // Fallback classification if parsing fails + console.warn('Failed to parse LLM response, using fallback:', error.message); + return { + category: CATEGORIES.OTHER, + confidence: 0.5, + reason: 'Failed to parse classification' + }; + } + } +} + +// ============================================================================ +// Classification History Callback +// ============================================================================ + +/** + * Tracks classification history using callbacks + */ +class ClassificationHistoryCallback extends BaseCallback { + constructor() { + super(); + this.history = []; + } + + async onEnd(runnable, output, config) { + // Only track EmailClassifierRunnable results + if (runnable.name === 'EmailClassifierRunnable' && output.category) { + this.history.push({ + timestamp: output.timestamp, + from: output.from, + subject: output.subject, + category: output.category, + confidence: output.confidence, + reason: output.reason + }); + } + } + + getHistory() { + return this.history; + } + + getStatistics() { + if (this.history.length === 0) { + return { + total: 0, + byCategory: {}, + averageConfidence: 0 + }; + } + + // Count by category + const byCategory = {}; + let totalConfidence = 0; + + for (const entry of this.history) { + byCategory[entry.category] = (byCategory[entry.category] || 0) + 1; + totalConfidence += entry.confidence; + } + + return { + total: this.history.length, + byCategory: byCategory, + averageConfidence: totalConfidence / this.history.length + }; + } + + printHistory() { + console.log('\n📧 Classification History:'); + console.log('─'.repeat(70)); + + for (const entry of this.history) { + console.log(`\n✉️ From: ${entry.from}`); + console.log(` Subject: ${entry.subject}`); + console.log(` Category: ${entry.category}`); + console.log(` Confidence: ${(entry.confidence * 100).toFixed(1)}%`); + console.log(` Reason: ${entry.reason}`); + } + } + + printStatistics() { + const stats = this.getStatistics(); + + console.log('\n📊 Classification Statistics:'); + console.log('─'.repeat(70)); + console.log(`Total Emails: ${stats.total}\n`); + + if (stats.total > 0) { + console.log('By Category:'); + for (const [category, count] of Object.entries(stats.byCategory)) { + const percentage = ((count / stats.total) * 100).toFixed(1); + console.log(` ${category}: ${count} (${percentage}%)`); + } + + console.log(`\nAverage Confidence: ${(stats.averageConfidence * 100).toFixed(1)}%`); + } + } +} + +// ============================================================================ +// Email Classification Pipeline +// ============================================================================ + +/** + * Complete pipeline: Parse → Classify → Store + */ +class EmailClassificationPipeline { + constructor(llm) { + this.parser = new EmailParserRunnable(); + this.classifier = new EmailClassifierRunnable(llm); + this.historyCallback = new ClassificationHistoryCallback(); + + // Build the pipeline: parser -> classifier + this.pipeline = this.parser.pipe(this.classifier); + } + + async classify(email) { + // Run the email through the pipeline with history callback + const config = { + callbacks: [this.historyCallback] + }; + + return await this.pipeline.invoke(email, config); + } + + getHistory() { + return this.historyCallback.getHistory(); + } + + getStatistics() { + return this.historyCallback.getStatistics(); + } + + printHistory() { + this.historyCallback.printHistory(); + } + + printStatistics() { + this.historyCallback.printStatistics(); + } +} + +// ============================================================================ +// TEST DATA +// ============================================================================ + +const TEST_EMAILS = JSON.parse( + readFileSync(new URL('./test-emails.json', import.meta.url), 'utf-8') +); + +// ============================================================================ +// MAIN FUNCTION +// ============================================================================ + +async function main() { + console.log('=== Part 1 Capstone: Smart Email Classifier ===\n'); + + // Initialize the LLM + const llm = new LlamaCppLLM({ + modelPath: './models/Meta-Llama-3.1-8B-Instruct-Q5_K_S.gguf', // Adjust to your model + temperature: 0.1, // Low temperature for consistent classification + maxTokens: 200 + }); + + // Create the classification pipeline + const pipeline = new EmailClassificationPipeline(llm); + + console.log('📬 Processing emails...\n'); + + // Classify each test email + for (const email of TEST_EMAILS) { + try { + const result = await pipeline.classify(email); + + console.log(`✉️ Email from: ${result.from}`); + console.log(` Subject: ${result.subject}`); + console.log(` Category: ${result.category}`); + console.log(` Confidence: ${(result.confidence * 100).toFixed(1)}%`); + console.log(` Reason: ${result.reason}\n`); + } catch (error) { + console.error(`❌ Error classifying email from ${email.from}:`, error.message); + } + } + + // Print history and statistics + pipeline.printHistory(); + pipeline.printStatistics(); + + // Cleanup + await llm.dispose(); + + console.log('\n✓ Capstone Project Complete!'); +} + +// Run the project +main().catch(console.error); \ No newline at end of file diff --git a/tutorial/projects/01-smart-email-classifier/starter.js b/tutorial/projects/01-smart-email-classifier/starter.js new file mode 100644 index 0000000000000000000000000000000000000000..0a36de687317e9821bc9802665edd608cd67cb17 --- /dev/null +++ b/tutorial/projects/01-smart-email-classifier/starter.js @@ -0,0 +1,293 @@ +/** + * Part 1 Capstone: Smart Email Classifier + * + * Build an AI system that organizes your inbox by classifying emails into categories. + * + * Skills Used: + * - Runnables for processing pipeline + * - Messages for structured classification + * - LLM wrapper for flexible model switching + * - Context for classification history + * + * Difficulty: ⭐⭐☆☆☆ + */ + +import {SystemMessage, HumanMessage, Runnable, LlamaCppLLM} from '../../../src/index.js'; +import {BaseCallback} from '../../../src/utils/callbacks.js'; +import { readFileSync } from 'fs'; + +// ============================================================================ +// EMAIL CLASSIFICATION CATEGORIES +// ============================================================================ + +const CATEGORIES = { + SPAM: 'Spam', + INVOICE: 'Invoice', + MEETING: 'Meeting Request', + URGENT: 'Urgent', + PERSONAL: 'Personal', + OTHER: 'Other' +}; + +// ============================================================================ +// TODO 1: Email Parser Runnable +// ============================================================================ + +/** + * Parses raw email text into structured format + * + * Input: { subject: string, body: string, from: string } + * Output: { subject, body, from, timestamp } + */ +class EmailParserRunnable extends Runnable { + async _call(input, config) { + // TODO: Parse and structure the email + // Validate required fields (subject, body, from) + // Add timestamp + // Return structured email object + + return null; // Replace with your implementation + } +} + +// ============================================================================ +// TODO 2: Email Classifier Runnable +// ============================================================================ + +/** + * Classifies email using LLM + * + * Input: { subject, body, from, timestamp } + * Output: { ...email, category, confidence, reason } + */ +class EmailClassifierRunnable extends Runnable { + constructor(llm) { + super(); + this.llm = llm; + } + + async _call(input, config) { + // TODO: Create a prompt for the LLM + // Ask it to classify the email into one of the categories + // Request: category, confidence (0-1), and reason + + // TODO: Parse the LLM response + // Extract category, confidence, reason + + // TODO: Return email with classification + + return null; // Replace with your implementation + } + + _buildPrompt(email) { + // TODO: Build a good classification prompt + // Include: categories, email details, instructions + + return null; // Replace with your implementation + } + + _parseClassification(response) { + // TODO: Parse LLM response into structured format + // Extract: category, confidence, reason + + return null; // Replace with your implementation + } +} + +// ============================================================================ +// TODO 3: Classification History Callback +// ============================================================================ + +/** + * Tracks classification history using callbacks + */ +class ClassificationHistoryCallback extends BaseCallback { + constructor() { + super(); + this.history = []; + } + + async onEnd(runnable, output, config) { + // TODO: If this is an EmailClassifierRunnable, save the classification + // Store: timestamp, email subject, category, confidence + + } + + getHistory() { + return this.history; + } + + getStatistics() { + // TODO: Calculate statistics + // - Total emails classified + // - Count per category + // - Average confidence + + return null; // Replace with your implementation + } + + printHistory() { + console.log('\n📧 Classification History:'); + console.log('─'.repeat(70)); + + // TODO: Print each classification nicely + + } + + printStatistics() { + console.log('\n📊 Classification Statistics:'); + console.log('─'.repeat(70)); + + // TODO: Print statistics + + } +} + +// ============================================================================ +// TODO 4: Email Classification Pipeline +// ============================================================================ + +/** + * Complete pipeline: Parse → Classify → Store + */ +class EmailClassificationPipeline { + constructor(llm) { + // TODO: Create the pipeline + // parser -> classifier + // Add history callback + + this.parser = null; // new EmailParserRunnable() + this.classifier = null; // new EmailClassifierRunnable(llm) + this.historyCallback = null; // new ClassificationHistoryCallback() + this.pipeline = null; // Build the pipeline + } + + async classify(email) { + // TODO: Run the email through the pipeline + // Pass the history callback in config + + return null; // Replace with your implementation + } + + getHistory() { + return this.historyCallback.getHistory(); + } + + getStatistics() { + return this.historyCallback.getStatistics(); + } + + printHistory() { + this.historyCallback.printHistory(); + } + + printStatistics() { + this.historyCallback.printStatistics(); + } +} + +// ============================================================================ +// TEST DATA +// ============================================================================ + +const TEST_EMAILS = JSON.parse( + readFileSync(new URL('./test-emails.json', import.meta.url), 'utf-8') +); + +// ============================================================================ +// MAIN FUNCTION +// ============================================================================ + +async function main() { + console.log('=== Part 1 Capstone: Smart Email Classifier ===\n'); + + // TODO: Initialize the LLM + // Adjust modelPath to your model + const llm = null; // new LlamaCppLLM({ ... }) + + // TODO: Create the classification pipeline + const pipeline = null; // new EmailClassificationPipeline(llm) + + console.log('📬 Processing emails...\n'); + + // TODO: Classify each test email + for (const email of TEST_EMAILS) { + // Classify + // Print result + } + + // TODO: Print history and statistics + // pipeline.printHistory() + // pipeline.printStatistics() + + // TODO: Cleanup + // await llm.dispose() + + console.log('\n✓ Capstone Project Complete!'); +} + +// Run the project +main().catch(console.error); + +/** + * TODO CHECKLIST: + * + * [ ] 1. EmailParserRunnable + * - Parse email structure + * - Add timestamp + * - Validate fields + * + * [ ] 2. EmailClassifierRunnable + * - Build classification prompt + * - Call LLM + * - Parse response + * - Return classified email + * + * [ ] 3. ClassificationHistoryCallback + * - Track classifications in onEnd + * - Calculate statistics + * - Print history and stats + * + * [ ] 4. EmailClassificationPipeline + * - Build parser -> classifier pipeline + * - Add history callback + * - Implement classify method + * + * [ ] 5. Main function + * - Initialize LLM + * - Create pipeline + * - Process test emails + * - Print results + * + * EXPECTED OUTPUT: + * + * 📬 Processing emails... + * + * ✉️ Email from: promotions@shop.com + * Subject: 🎉 70% OFF SALE! Limited Time Only!!! + * Category: Spam + * Confidence: 99.0% + * Reason: Promotional content with excessive punctuation + * + * ✉️ Email from: billing@company.com + * Subject: Invoice #12345 - Payment Due + * Category: Invoice + * Confidence: 98.0% + * Reason: Contains invoice number and payment information + * + * ... (more emails) + * + * 📊 Classification Statistics: + * ────────────────────────────────────────────────────────────────────── + * Total Emails: 24 + * + * By Category: + * Spam: 3 (12.5%) + * Invoice: 4 (16.7%) + * Meeting Request: 5 (20.8%) + * Urgent: 3 (12.5%) + * Personal: 4 (16.7%) + * Other: 5 (20.8%) + * + * Average Confidence: 96.7% + */ \ No newline at end of file diff --git a/tutorial/projects/01-smart-email-classifier/test-emails.json b/tutorial/projects/01-smart-email-classifier/test-emails.json new file mode 100644 index 0000000000000000000000000000000000000000..1db58835325028467df594bd7db55e84cf9f5ca6 --- /dev/null +++ b/tutorial/projects/01-smart-email-classifier/test-emails.json @@ -0,0 +1,122 @@ +[ + { + "from": "promotions@shop.com", + "subject": "🎉 70% OFF SALE! Limited Time Only!!!", + "body": "Click here now to get amazing deals! Free shipping! Act fast!" + }, + { + "from": "billing@company.com", + "subject": "Invoice #12345 - Payment Due", + "body": "Your invoice for $1,250.00 is attached. Payment is due by March 15th." + }, + { + "from": "sarah@company.com", + "subject": "Can we schedule a meeting next week?", + "body": "Hi! I wanted to discuss the Q2 planning. Are you available Tuesday or Wednesday?" + }, + { + "from": "security@bank.com", + "subject": "URGENT: Suspicious Activity Detected", + "body": "We detected unusual login attempts on your account. Please verify immediately." + }, + { + "from": "mom@family.com", + "subject": "How are you doing?", + "body": "Hi honey, just checking in. Hope you're having a great week! Love, Mom" + }, + { + "from": "newsletter@tech.com", + "subject": "Weekly Tech News Digest", + "body": "Here are this week's top technology stories and updates..." + }, + { + "from": "accounting@acmecorp.com", + "subject": "Monthly Statement - January 2024", + "body": "Please find attached your monthly statement for January 2024. Total amount due: $3,450.75. Payment terms: Net 30." + }, + { + "from": "noreply@phishing-site.com", + "subject": "Your Amazon account has been locked!!!", + "body": "URGENT!!! Click here NOW to verify your account or it will be PERMANENTLY DELETED!!! Act immediately!!!" + }, + { + "from": "boss@company.com", + "subject": "Need your input by EOD", + "body": "Hi, I need your analysis on the Q4 projections before end of day today. This is blocking the board meeting tomorrow morning." + }, + { + "from": "hr@company.com", + "subject": "Team Lunch - Friday 12pm", + "body": "Hi everyone, we're having a team lunch this Friday at noon in the conference room. Please let me know if you can attend." + }, + { + "from": "dad@family.com", + "subject": "Re: Weekend plans", + "body": "Sounds great! We'll see you Saturday around 3pm. Your mom is making her famous lasagna. Looking forward to it!" + }, + { + "from": "updates@github.com", + "subject": "Your weekly GitHub activity summary", + "body": "Here's your activity from the past week: 15 commits, 3 pull requests merged, 8 issues closed." + }, + { + "from": "WINNER@lottery-scam.com", + "subject": "YOU WON $1,000,000 USD!!!", + "body": "CONGRATULATIONS!!! You have been selected as the WINNER of our international lottery!!! Send your bank details NOW to claim your prize!!!" + }, + { + "from": "it-support@company.com", + "subject": "CRITICAL: Server maintenance tonight at 11pm", + "body": "Critical maintenance alert: We will be performing emergency server maintenance tonight at 11pm. Expected downtime: 2 hours. Please save all work." + }, + { + "from": "john@company.com", + "subject": "Quick sync on project timeline", + "body": "Hey, do you have 15 minutes today to discuss the project timeline? I want to make sure we're aligned before the client call tomorrow." + }, + { + "from": "receipts@stripe.com", + "subject": "Receipt for your payment to CloudService", + "body": "Thank you for your payment. Receipt #RCP-2024-001. Amount: $49.99. Service: Cloud Storage Pro Plan." + }, + { + "from": "friend@personal.com", + "subject": "Coffee next week?", + "body": "Hey! It's been too long. Want to grab coffee next week? I'm free Tuesday afternoon or Thursday morning. Let me know what works!" + }, + { + "from": "notifications@linkedin.com", + "subject": "You have 5 new connection requests", + "body": "5 people want to connect with you on LinkedIn. View your pending invitations and grow your network." + }, + { + "from": "ceo@company.com", + "subject": "All-hands meeting moved to 2pm TODAY", + "body": "Important: The all-hands meeting has been moved from 3pm to 2pm today due to a schedule conflict. Please adjust your calendars accordingly." + }, + { + "from": "support@legitservice.com", + "subject": "Your subscription renewal", + "body": "Your annual subscription will renew on March 30th. Amount: $99.99. If you need to update your payment method, please visit your account settings." + }, + { + "from": "calendar@company.com", + "subject": "Reminder: Performance review meeting tomorrow at 10am", + "body": "This is a reminder about your performance review meeting scheduled for tomorrow (March 15) at 10:00 AM with your manager in Conference Room B." + }, + { + "from": "sister@family.com", + "subject": "Kids' birthday party next month", + "body": "Hi! Just wanted to give you a heads up that we're planning Emma's birthday party for April 20th. Would love for you to be there! More details soon." + }, + { + "from": "webinar@training.com", + "subject": "Webinar Registration Confirmed: AI Best Practices", + "body": "Thank you for registering for our webinar 'AI Best Practices for Developers' on March 25th at 2pm EST. You will receive a reminder email with the joining link." + }, + { + "from": "no-reply@bank.com", + "subject": "Your bank statement is now available", + "body": "Your monthly bank statement for February 2024 is now available. Login to your account to view and download your statement." + } +] \ No newline at end of file