diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fca2aa1e..d2077e65 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -87,9 +87,14 @@ jobs: services: redis: - image: redis:8.0-M03 + image: redis:8.2 ports: - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 steps: - uses: actions/checkout@v3 @@ -99,6 +104,40 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} + # Start Agent Memory Server + - name: Start Agent Memory Server + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + run: | + # Start the Agent Memory Server + docker run -d \ + --name agent-memory-server \ + --network host \ + -e REDIS_URL=redis://localhost:6379 \ + -e OPENAI_API_KEY=$OPENAI_API_KEY \ + -e LOG_LEVEL=INFO \ + ghcr.io/redis/agent-memory-server:latest + + # Wait for memory server to be ready + echo "Waiting for Agent Memory Server to be ready..." + for i in {1..30}; do + if curl -f http://localhost:8000/health 2>/dev/null; then + echo "✅ Agent Memory Server is ready!" + break + fi + echo "Waiting... ($i/30)" + sleep 2 + done + + # Show status but don't fail if server isn't ready + if curl -f http://localhost:8000/health 2>/dev/null; then + echo "✅ Agent Memory Server is healthy" + else + echo "⚠️ WARNING: Agent Memory Server may not be ready" + echo "Docker logs:" + docker logs agent-memory-server || true + fi + - name: Create and activate venv run: | python -m venv venv @@ -106,11 +145,22 @@ jobs: pip install --upgrade pip setuptools wheel pip install pytest nbval + # Install the redis-context-course package and its dependencies + cd python-recipes/context-engineering/reference-agent + pip install -e . + - name: Test notebook env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }} + AGENT_MEMORY_URL: http://localhost:8000 + REDIS_URL: redis://localhost:6379 run: | echo "Testing notebook: ${{ matrix.notebook }}" source venv/bin/activate pytest --nbval-lax --disable-warnings "${{ matrix.notebook }}" + + - name: Show Agent Memory Server logs on failure + if: failure() + run: | + docker logs agent-memory-server diff --git a/python-recipes/context-engineering/.env.example b/python-recipes/context-engineering/.env.example new file mode 100644 index 00000000..a75ab0a0 --- /dev/null +++ b/python-recipes/context-engineering/.env.example @@ -0,0 +1,2 @@ +# OpenAI API Key (required to pass to the API container) +OPENAI_API_KEY=your-openai-api-key-here diff --git a/python-recipes/context-engineering/.gitignore b/python-recipes/context-engineering/.gitignore new file mode 100644 index 00000000..03300719 --- /dev/null +++ b/python-recipes/context-engineering/.gitignore @@ -0,0 +1,2 @@ +venv +.env diff --git a/python-recipes/context-engineering/COURSE_SUMMARY.md b/python-recipes/context-engineering/COURSE_SUMMARY.md new file mode 100644 index 00000000..cc3cc4fc --- /dev/null +++ b/python-recipes/context-engineering/COURSE_SUMMARY.md @@ -0,0 +1,286 @@ +# Context Engineering Course - Complete Summary + +## Overview + +This course teaches production-ready context engineering for AI agents using Redis and the Agent Memory Server. It covers everything from fundamentals to advanced optimization techniques. + +## Course Structure + +### Section 1: Introduction (3 notebooks) +1. **What is Context Engineering?** - Core concepts and importance +2. **Setting Up Your Environment** - Installation and configuration +3. **Project Overview** - Understanding the reference agent + +### Section 2: System Context (3 notebooks) +1. **System Instructions** - Crafting effective system prompts +2. **Defining Tools** - Giving agents capabilities +3. **Tool Selection Strategies** (Advanced) - Improving tool choice + +**Key Patterns:** +- Progressive system prompt building +- Tool schema design with examples +- Clear naming conventions +- Detailed descriptions with when/when-not guidance + +### Section 3: Memory (4 notebooks) +1. **Working Memory with Extraction Strategies** - Session-scoped context +2. **Long-term Memory** - Cross-session knowledge +3. **Memory Integration** - Combining working and long-term memory +4. **Memory Tools** (Advanced) - LLM control over memory + +**Key Patterns:** +- Automatic memory extraction +- Semantic search for retrieval +- Memory type selection (semantic vs episodic) +- Tool-based memory management + +### Section 4: Optimizations (5 notebooks) +1. **Context Window Management** - Handling token limits +2. **Retrieval Strategies** - RAG, summaries, and hybrid approaches +3. **Grounding with Memory** - Using memory to resolve references +4. **Tool Optimization** (Advanced) - Selective tool exposure +5. **Crafting Data for LLMs** (Advanced) - Creating structured views + +**Key Patterns:** +- Token budget estimation +- Hybrid retrieval (summary + RAG) +- Tool filtering by intent +- Retrieve → Summarize → Stitch → Save pattern +- Structured view creation + +## Reference Agent Components + +### Core Modules + +**`course_manager.py`** +- Course catalog management +- Vector search for courses +- Course data models + +**`memory_client.py`** +- Working memory operations +- Long-term memory operations +- Integration with Agent Memory Server + +**`agent.py`** +- Main agent implementation +- LangGraph workflow +- State management + +### New Modules (From Course Content) + +**`tools.py`** (Section 2) +- `create_course_tools()` - Search, get details, check prerequisites +- `create_memory_tools()` - Store and search memories +- `select_tools_by_keywords()` - Simple tool filtering + +**`optimization_helpers.py`** (Section 4) +- `count_tokens()` - Token counting for any model +- `estimate_token_budget()` - Budget breakdown +- `hybrid_retrieval()` - Combine summary + search +- `create_summary_view()` - Structured summaries +- `create_user_profile_view()` - User profile generation +- `filter_tools_by_intent()` - Keyword-based filtering +- `classify_intent_with_llm()` - LLM-based classification +- `extract_references()` - Find grounding needs +- `format_context_for_llm()` - Combine context sources + +### Examples + +**`examples/advanced_agent_example.py`** +- Complete agent using all patterns +- Tool filtering enabled +- Token budget tracking +- Memory integration +- Production-ready structure + +## Key Concepts by Section + +### Section 2: System Context +- **System vs Retrieved Context**: Static instructions vs dynamic data +- **Tool Schemas**: Name, description, parameters +- **Tool Selection**: How LLMs choose tools +- **Best Practices**: Clear names, detailed descriptions, examples + +### Section 3: Memory +- **Working Memory**: Session-scoped, conversation history +- **Long-term Memory**: User-scoped, persistent facts +- **Memory Types**: Semantic (facts), Episodic (events), Message (conversations) +- **Automatic Extraction**: Agent Memory Server extracts important facts +- **Memory Flow**: Load → Search → Process → Save → Extract + +### Section 4: Optimizations +- **Token Budgets**: Allocating context window space +- **Retrieval Strategies**: Full context (bad), RAG (good), Summaries (compact), Hybrid (best) +- **Grounding**: Resolving references (pronouns, descriptions, implicit) +- **Tool Filtering**: Show only relevant tools based on intent +- **Structured Views**: Pre-computed summaries for LLM consumption + +## Production Patterns + +### 1. Complete Memory Flow +```python +# Load working memory +working_memory = await memory_client.get_working_memory(session_id, model_name) + +# Search long-term memory +memories = await memory_client.search_memories(query, limit=5) + +# Build context +system_prompt = build_prompt(instructions, memories) + +# Process with LLM +response = llm.invoke(messages) + +# Save working memory (triggers extraction) +await memory_client.save_working_memory(session_id, messages) +``` + +### 2. Hybrid Retrieval +```python +# Pre-computed summary +summary = load_catalog_summary() + +# Targeted search +specific_items = await search_courses(query, limit=3) + +# Combine +context = f"{summary}\n\nRelevant items:\n{specific_items}" +``` + +### 3. Tool Filtering +```python +# Filter tools by intent +relevant_tools = filter_tools_by_intent(query, tool_groups) + +# Bind only relevant tools +llm_with_tools = llm.bind_tools(relevant_tools) +``` + +### 4. Token Budget Management +```python +# Estimate budget +budget = estimate_token_budget( + system_prompt=prompt, + working_memory_messages=10, + long_term_memories=5, + retrieved_context_items=3 +) + +# Check if within limits +if budget['total_with_response'] > 128000: + # Trigger summarization or reduce context +``` + +### 5. Structured Views +```python +# Retrieve data +items = await get_all_items() + +# Summarize +summary = await create_summary_view(items, group_by="category") + +# Save for reuse +redis_client.set("summary_view", summary) + +# Use in prompts +system_prompt = f"Overview:\n{summary}\n\nInstructions:..." +``` + +## Usage in Notebooks + +All patterns are demonstrated in notebooks with: +- ✅ Conceptual explanations +- ✅ Bad examples (what not to do) +- ✅ Good examples (best practices) +- ✅ Runnable code +- ✅ Testing and verification +- ✅ Exercises for practice + +## Importing in Your Code + +```python +from redis_context_course import ( + # Core + CourseManager, + MemoryClient, + + # Tools (Section 2) + create_course_tools, + create_memory_tools, + select_tools_by_keywords, + + # Optimizations (Section 4) + count_tokens, + estimate_token_budget, + hybrid_retrieval, + create_summary_view, + create_user_profile_view, + filter_tools_by_intent, + classify_intent_with_llm, + extract_references, + format_context_for_llm, +) +``` + +## Learning Path + +1. **Start with Section 1** - Understand fundamentals +2. **Work through Section 2** - Build system context and tools +3. **Master Section 3** - Implement memory management +4. **Optimize with Section 4** - Apply production patterns +5. **Study advanced_agent_example.py** - See it all together +6. **Build your own agent** - Apply to your use case + +## Key Takeaways + +### What Makes a Production-Ready Agent? + +1. **Clear System Instructions** - Tell the agent what to do +2. **Well-Designed Tools** - Give it capabilities with clear descriptions +3. **Memory Integration** - Remember context across sessions +4. **Token Management** - Stay within limits efficiently +5. **Smart Retrieval** - Hybrid approach (summary + RAG) +6. **Tool Filtering** - Show only relevant tools +7. **Structured Views** - Pre-compute summaries for efficiency + +### Common Pitfalls to Avoid + +❌ **Don't:** +- Include all tools on every request +- Use vague tool descriptions +- Ignore token budgets +- Use only full context or only RAG +- Forget to save working memory +- Store everything in long-term memory + +✅ **Do:** +- Filter tools by intent +- Write detailed tool descriptions with examples +- Estimate and monitor token usage +- Use hybrid retrieval (summary + targeted search) +- Save working memory to trigger extraction +- Store only important facts in long-term memory + +## Next Steps + +After completing this course, you can: + +1. **Extend the reference agent** - Add new tools and capabilities +2. **Apply to your domain** - Adapt patterns to your use case +3. **Optimize further** - Experiment with different strategies +4. **Share your learnings** - Contribute back to the community + +## Resources + +- **Agent Memory Server Docs**: [Link to docs] +- **Redis Documentation**: https://redis.io/docs +- **LangChain Documentation**: https://python.langchain.com +- **Course Repository**: [Link to repo] + +--- + +**Course Version**: 1.0 +**Last Updated**: 2024-09-30 +**Total Notebooks**: 15 (3 intro + 3 system + 4 memory + 5 optimizations) + diff --git a/python-recipes/context-engineering/README.md b/python-recipes/context-engineering/README.md new file mode 100644 index 00000000..4085f01e --- /dev/null +++ b/python-recipes/context-engineering/README.md @@ -0,0 +1,176 @@ +# Context Engineering Recipes + +This section contains comprehensive recipes and tutorials for **Context Engineering** - the practice of designing, implementing, and optimizing context management systems for AI agents and applications. + +## What is Context Engineering? + +Context Engineering is the discipline of building systems that help AI agents understand, maintain, and utilize context effectively. This includes: + +- **System Context**: What the AI should know about its role, capabilities, and environment +- **Memory Management**: How to store, retrieve, and manage working memory (task-focused) and long-term memory (cross-session knowledge) +- **Tool Integration**: How to define and manage available tools and their usage +- **Context Optimization**: Techniques for managing context window limits and improving relevance + +## Repository Structure + +``` +context-engineering/ +├── README.md # This file +├── reference-agent/ # Complete reference implementation +│ ├── src/ # Source code for the Redis University Class Agent +│ ├── scripts/ # Data generation and ingestion scripts +│ ├── data/ # Generated course catalogs and sample data +│ └── tests/ # Test suite +├── notebooks/ # Educational notebooks organized by section +│ ├── section-1-introduction/ # What is Context Engineering? +│ ├── section-2-system-context/# Setting up system context and tools +│ ├── section-3-memory/ # Memory management concepts +│ └── section-4-optimizations/ # Advanced optimization techniques +└── resources/ # Shared resources, diagrams, and assets +``` + +## Course Structure + +This repository supports a comprehensive web course on Context Engineering with the following sections: + +### Section 1: Introduction +- **What is Context Engineering?** - Core concepts and principles +- **The Role of a Context Engine** - How context engines work in AI systems +- **Project Overview: Redis University Class Agent** - Hands-on project introduction + +### Section 2: Setting up System Context +- **Prepping the System Context** - Defining what the AI should know +- **Defining Available Tools** - Tool integration and management + +### Section 3: Memory +- **Memory Overview** - Concepts and architecture +- **Working Memory** - Managing task-focused context (conversation, task data) +- **Long-term Memory** - Cross-session knowledge storage and retrieval +- **Memory Integration** - Combining working and long-term memory +- **Memory Tools** - Giving the LLM control over memory operations + +### Section 4: Optimizations +- **Context Window Management** - Handling token limits and summarization +- **Retrieval Strategies** - RAG, summaries, and hybrid approaches +- **Grounding with Memory** - Using memory to resolve references +- **Tool Optimization** - Selective tool exposure and filtering +- **Crafting Data for LLMs** - Creating structured views and dashboards + +## Reference Agent: Redis University Class Agent + +The reference implementation is a complete **Redis University Class Agent** that demonstrates all context engineering concepts in practice. This agent can: + +- Help students find courses based on their interests and requirements +- Maintain conversation context across sessions +- Remember student preferences and academic history +- Provide personalized course recommendations +- Answer questions about course prerequisites, schedules, and content + +### Key Technologies + +- **LangGraph**: Agent workflow orchestration +- **Redis Agent Memory Server**: Long-term memory management +- **langgraph-redis-checkpointer**: Short-term memory and state persistence +- **RedisVL**: Vector storage for course catalog and semantic search +- **OpenAI GPT**: Language model for natural conversation + +### Code Organization + +The reference agent includes reusable modules that implement patterns from the notebooks: + +- **`tools.py`** - Tool definitions used throughout the course (Section 2) +- **`optimization_helpers.py`** - Production-ready optimization patterns (Section 4) +- **`examples/advanced_agent_example.py`** - Complete example combining all techniques + +These modules are designed to be imported in notebooks and used as building blocks for your own agents. + +## Getting Started + +### Prerequisites + +- Python 3.10+ +- Docker and Docker Compose (for running Redis and Agent Memory Server) +- OpenAI API key +- Basic understanding of AI agents and vector databases + +### Quick Start + +#### 1. Start Required Services + +The notebooks and reference agent require Redis and the Agent Memory Server to be running: + +```bash +# Navigate to the context-engineering directory +cd python-recipes/context-engineering + +# Copy the example environment file +cp .env.example .env + +# Edit .env and add your OpenAI API key +# OPENAI_API_KEY=your-key-here + +# Start Redis and Agent Memory Server +docker-compose up -d + +# Verify services are running +docker-compose ps + +# Check Agent Memory Server health +curl http://localhost:8000/health +``` + +#### 2. Set Up the Reference Agent + +```bash +# Navigate to the reference agent directory +cd reference-agent + +# Install dependencies +pip install -e . + +# Generate sample course data +python -m redis_context_course.scripts.generate_courses + +# Ingest data into Redis +python -m redis_context_course.scripts.ingest_courses + +# Start the CLI agent +python -m redis_context_course.cli +``` + +#### 3. Run the Notebooks + +```bash +# Install Jupyter +pip install jupyter + +# Start Jupyter +jupyter notebook notebooks/ + +# Open any notebook and run the cells +``` + +### Stopping Services + +```bash +# Stop services but keep data +docker-compose stop + +# Stop and remove services (keeps volumes) +docker-compose down + +# Stop and remove everything including data +docker-compose down -v +``` + +## Learning Path + +1. Start with **Section 1** notebooks to understand core concepts +2. Explore the **reference agent** codebase to see concepts in practice +3. Work through **Section 2** to learn system context setup +4. Complete **Section 3** to master memory management +5. Experiment with extending the agent for your own use cases + +## Contributing + +This is an educational resource. Contributions that improve clarity, add examples, or extend the reference implementation are welcome. diff --git a/python-recipes/context-engineering/SETUP.md b/python-recipes/context-engineering/SETUP.md new file mode 100644 index 00000000..20b568b0 --- /dev/null +++ b/python-recipes/context-engineering/SETUP.md @@ -0,0 +1,205 @@ +# Setup Guide for Context Engineering Course + +This guide will help you set up everything you need to run the Context Engineering notebooks and reference agent. + +## Prerequisites + +- **Python 3.10+** installed +- **Docker and Docker Compose** installed +- **OpenAI API key** (get one at https://platform.openai.com/api-keys) + +## Quick Setup (5 minutes) + +### Step 1: Set Your OpenAI API Key + +The OpenAI API key is needed by both the Jupyter notebooks AND the Agent Memory Server. The easiest way to set it up is to use a `.env` file. + +```bash +# Navigate to the context-engineering directory +cd python-recipes/context-engineering + +# Copy the example environment file +cp .env.example .env + +# Edit .env and add your OpenAI API key +# Replace 'your-openai-api-key-here' with your actual key +``` + +Your `.env` file should look like this: +```bash +OPENAI_API_KEY=sk-proj-xxxxxxxxxxxxxxxxxxxxx +REDIS_URL=redis://localhost:6379 +AGENT_MEMORY_URL=http://localhost:8000 +``` + +**Important:** The `.env` file is already in `.gitignore` so your API key won't be committed to git. + +### Step 2: Start Required Services + +Start Redis and the Agent Memory Server using Docker Compose: + +```bash +# Start services in the background +docker-compose up -d + +# Verify services are running +docker-compose ps + +# Check that the Agent Memory Server is healthy +curl http://localhost:8000/health +``` + +You should see: +- `redis-context-engineering` running on ports 6379 (Redis) and 8001 (RedisInsight) +- `agent-memory-server` running on port 8000 + +### Step 3: Install Python Dependencies + +```bash +# Create a virtual environment +python -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate + +# Install notebook dependencies (Jupyter, python-dotenv, etc.) +pip install -r requirements.txt + +# Install the reference agent package +cd reference-agent +pip install -e . +cd .. +``` + +### Step 4: Run the Notebooks + +```bash +# Start Jupyter from the context-engineering directory +jupyter notebook notebooks/ + +# Open any notebook and run the cells +``` + +The notebooks will automatically load your `.env` file using `python-dotenv`, so your `OPENAI_API_KEY` will be available. + +## Verifying Your Setup + +### Check Redis +```bash +# Test Redis connection +docker exec redis-context-engineering redis-cli ping +# Should return: PONG +``` + +### Check Agent Memory Server +```bash +# Test health endpoint +curl http://localhost:8000/health +# Should return: {"status":"healthy"} + +# Test that it can connect to Redis and has your API key +curl http://localhost:8000/api/v1/namespaces +# Should return a list of namespaces (may be empty initially) +``` + +### Check Python Environment +```bash +# Verify the reference agent package is installed +python -c "import redis_context_course; print('✅ Package installed')" + +# Verify OpenAI key is set +python -c "import os; print('✅ OpenAI key set' if os.getenv('OPENAI_API_KEY') else '❌ OpenAI key not set')" +``` + +## Troubleshooting + +### "OPENAI_API_KEY not found" + +**In Notebooks:** The notebooks will prompt you for your API key if it's not set. However, it's better to set it in the `.env` file so you don't have to enter it repeatedly. + +**In Docker:** Make sure: +1. Your `.env` file exists and contains `OPENAI_API_KEY=your-key` +2. You've restarted the services: `docker-compose down && docker-compose up -d` +3. Check the logs: `docker-compose logs agent-memory-server` + +### "Connection refused" to Agent Memory Server + +Make sure the services are running: +```bash +docker-compose ps +``` + +If they're not running, start them: +```bash +docker-compose up -d +``` + +Check the logs for errors: +```bash +docker-compose logs agent-memory-server +``` + +### "Connection refused" to Redis + +Make sure Redis is running: +```bash +docker-compose ps redis +``` + +Test the connection: +```bash +docker exec redis-context-engineering redis-cli ping +``` + +### Port Already in Use + +If you get errors about ports already in use (6379, 8000, or 8001), you can either: + +1. Stop the conflicting service +2. Change the ports in `docker-compose.yml`: + ```yaml + ports: + - "6380:6379" # Use 6380 instead of 6379 + ``` + Then update `REDIS_URL` in your `.env` file accordingly. + +## Stopping Services + +```bash +# Stop services but keep data +docker-compose stop + +# Stop and remove services (keeps volumes/data) +docker-compose down + +# Stop and remove everything including data +docker-compose down -v +``` + +## Alternative: Using Existing Redis or Cloud Redis + +If you already have Redis running or want to use Redis Cloud: + +1. Update `REDIS_URL` in your `.env` file: + ```bash + REDIS_URL=redis://default:password@your-redis-cloud-url:port + ``` + +2. You still need to run the Agent Memory Server locally: + ```bash + docker-compose up -d agent-memory-server + ``` + +## Next Steps + +Once setup is complete: + +1. Start with **Section 1** notebooks to understand core concepts +2. Work through **Section 2** to learn system context setup +3. Complete **Section 3** to master memory management (requires Agent Memory Server) +4. Explore **Section 4** for advanced optimization techniques + +## Getting Help + +- Check the main [README.md](README.md) for course structure and learning path +- Review [COURSE_SUMMARY.md](COURSE_SUMMARY.md) for an overview of all topics +- Open an issue if you encounter problems with the setup + diff --git a/python-recipes/context-engineering/docker-compose.yml b/python-recipes/context-engineering/docker-compose.yml new file mode 100644 index 00000000..80494948 --- /dev/null +++ b/python-recipes/context-engineering/docker-compose.yml @@ -0,0 +1,40 @@ +services: + redis: + image: redis/redis-stack:latest + container_name: redis-context-engineering + ports: + - "6379:6379" + - "8001:8001" # RedisInsight + environment: + - REDIS_ARGS=--save 60 1 --loglevel warning + volumes: + - redis-data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 + + agent-memory-server: + image: ghcr.io/redis/agent-memory-server:0.12.3 + container_name: agent-memory-server + command: ["agent-memory", "api", "--host", "0.0.0.0", "--port", "8000", "--no-worker"] + ports: + - "8000:8000" + environment: + - REDIS_URL=redis://redis:6379 + - OPENAI_API_KEY=${OPENAI_API_KEY} + - LOG_LEVEL=INFO + depends_on: + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/v1/health"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + +volumes: + redis-data: + diff --git a/python-recipes/context-engineering/notebooks/common_setup.py b/python-recipes/context-engineering/notebooks/common_setup.py new file mode 100644 index 00000000..65a9977d --- /dev/null +++ b/python-recipes/context-engineering/notebooks/common_setup.py @@ -0,0 +1,172 @@ +""" +Common setup code for Context Engineering notebooks. + +This module provides a standard setup function that: +1. Installs the redis_context_course package if needed +2. Loads environment variables from .env file +3. Verifies required environment variables are set +4. Provides helpful error messages if setup is incomplete + +Usage in notebooks: + #%% + # Run common setup + import sys + sys.path.insert(0, '..') + from common_setup import setup_notebook + + setup_notebook() +""" + +import os +import sys +import subprocess +from pathlib import Path + + +def setup_notebook(require_openai_key=True, require_memory_server=False): + """ + Set up the notebook environment. + + Args: + require_openai_key: If True, raises error if OPENAI_API_KEY is not set + require_memory_server: If True, checks that Agent Memory Server is accessible + """ + print("🔧 Setting up notebook environment...") + print("=" * 60) + + # Step 1: Install the redis_context_course package if needed + try: + import redis_context_course + print("✅ redis_context_course package already installed") + except ImportError: + print("📦 Installing redis_context_course package...") + + # Find the reference-agent directory + notebook_dir = Path.cwd() + reference_agent_path = None + + # Try common locations + possible_paths = [ + notebook_dir / ".." / ".." / "reference-agent", # From section notebooks + notebook_dir / ".." / "reference-agent", # From notebooks root + notebook_dir / "reference-agent", # From context-engineering root + ] + + for path in possible_paths: + if path.exists() and (path / "setup.py").exists(): + reference_agent_path = path.resolve() + break + + if not reference_agent_path: + print("❌ Could not find reference-agent directory") + print(" Please run from the notebooks directory or ensure reference-agent exists") + raise RuntimeError("reference-agent directory not found") + + # Install the package + result = subprocess.run( + [sys.executable, "-m", "pip", "install", "-q", "-e", str(reference_agent_path)], + capture_output=True, + text=True + ) + + if result.returncode == 0: + print(f"✅ Installed redis_context_course from {reference_agent_path}") + else: + print(f"❌ Failed to install package: {result.stderr}") + raise RuntimeError(f"Package installation failed: {result.stderr}") + + # Step 2: Load environment variables from .env file + try: + from dotenv import load_dotenv + + # Find the .env file (should be in context-engineering root) + notebook_dir = Path.cwd() + env_file = None + + # Try common locations + possible_env_paths = [ + notebook_dir / ".." / ".." / ".env", # From section notebooks + notebook_dir / ".." / ".env", # From notebooks root + notebook_dir / ".env", # From context-engineering root + ] + + for path in possible_env_paths: + if path.exists(): + env_file = path.resolve() + break + + if env_file: + load_dotenv(env_file) + print(f"✅ Loaded environment variables from {env_file}") + else: + print("⚠️ No .env file found - will use system environment variables") + print(" To create one, see SETUP.md") + + except ImportError: + print("⚠️ python-dotenv not installed - skipping .env file loading") + print(" Install with: pip install python-dotenv") + + # Step 3: Verify required environment variables + print("\n📋 Environment Variables:") + print("-" * 60) + + # Check OPENAI_API_KEY + openai_key = os.getenv("OPENAI_API_KEY") + if openai_key: + print(f"✅ OPENAI_API_KEY: Set ({openai_key[:8]}...)") + else: + print("❌ OPENAI_API_KEY: Not set") + if require_openai_key: + raise ValueError( + "OPENAI_API_KEY not found. Please:\n" + "1. Create a .env file in python-recipes/context-engineering/\n" + "2. Add: OPENAI_API_KEY=your-key-here\n" + "3. See SETUP.md for detailed instructions" + ) + + # Check REDIS_URL + redis_url = os.getenv("REDIS_URL", "redis://localhost:6379") + print(f"✅ REDIS_URL: {redis_url}") + + # Check AGENT_MEMORY_URL + memory_url = os.getenv("AGENT_MEMORY_URL", "http://localhost:8000") + print(f"✅ AGENT_MEMORY_URL: {memory_url}") + + # Step 4: Check Agent Memory Server if required + if require_memory_server: + print("\n🔍 Checking Agent Memory Server...") + print("-" * 60) + try: + import requests + response = requests.get(f"{memory_url}/health", timeout=2) + if response.status_code == 200: + print(f"✅ Agent Memory Server is running at {memory_url}") + else: + print(f"⚠️ Agent Memory Server returned status {response.status_code}") + raise RuntimeError( + f"Agent Memory Server is not healthy. Please run:\n" + f" cd python-recipes/context-engineering\n" + f" docker-compose up -d" + ) + except ImportError: + print("⚠️ requests library not installed - skipping health check") + print(" Install with: pip install requests") + except Exception as e: + print(f"❌ Could not connect to Agent Memory Server: {e}") + raise RuntimeError( + f"Agent Memory Server is not accessible at {memory_url}\n" + f"Please run:\n" + f" cd python-recipes/context-engineering\n" + f" docker-compose up -d\n" + f"Then verify with: curl {memory_url}/health" + ) + + print("\n" + "=" * 60) + print("✅ Notebook setup complete!") + print("=" * 60) + + +if __name__ == "__main__": + # Test the setup + setup_notebook(require_openai_key=True, require_memory_server=False) + diff --git a/python-recipes/context-engineering/notebooks/section-1-introduction/01_what_is_context_engineering.ipynb b/python-recipes/context-engineering/notebooks/section-1-introduction/01_what_is_context_engineering.ipynb new file mode 100644 index 00000000..d10fd702 --- /dev/null +++ b/python-recipes/context-engineering/notebooks/section-1-introduction/01_what_is_context_engineering.ipynb @@ -0,0 +1,531 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Redis](https://redis.io/wp-content/uploads/2024/04/Logotype.svg?auto=webp&quality=85,75&width=120)\n", + "\n", + "# What is Context Engineering?\n", + "\n", + "## Introduction\n", + "\n", + "**Context Engineering** is the discipline of designing, implementing, and optimizing context management systems for AI agents and applications. It's the practice of ensuring that AI systems have the right information, at the right time, in the right format to make intelligent decisions and provide relevant responses.\n", + "\n", + "Think of context engineering as the \"memory and awareness system\" for AI agents - it's what allows them to:\n", + "- Remember past conversations and experiences\n", + "- Understand their role and capabilities\n", + "- Access relevant information from large knowledge bases\n", + "- Maintain coherent, personalized interactions over time\n", + "\n", + "## Why Context Engineering Matters\n", + "\n", + "Without proper context engineering, AI agents are like people with severe amnesia - they can't remember what happened five minutes ago, don't know who they're talking to, and can't learn from experience. This leads to:\n", + "\n", + "❌ **Poor User Experience**\n", + "- Repetitive conversations\n", + "- Lack of personalization\n", + "- Inconsistent responses\n", + "\n", + "❌ **Inefficient Operations**\n", + "- Redundant processing\n", + "- Inability to build on previous work\n", + "- Lost context between sessions\n", + "\n", + "❌ **Limited Capabilities**\n", + "- Can't handle complex, multi-step tasks\n", + "- No learning or adaptation\n", + "- Poor integration with existing systems\n", + "\n", + "## Core Components of Context Engineering\n", + "\n", + "Context engineering involves several key components working together:\n", + "\n", + "### 1. **System Context**\n", + "What the AI should know about itself and its environment:\n", + "- Role and responsibilities\n", + "- Available tools and capabilities\n", + "- Operating constraints and guidelines\n", + "- Domain-specific knowledge\n", + "\n", + "### 2. **Memory Management**\n", + "How information is stored, retrieved, and maintained:\n", + "- **Working memory**: Persistent storage focused on the current task, including conversation context and task-related data\n", + "- **Long-term memory**: Knowledge learned across sessions, such as user preferences and important facts\n", + "\n", + "### 3. **Context Retrieval**\n", + "How relevant information is found and surfaced:\n", + "- Semantic search and similarity matching\n", + "- Relevance ranking and filtering\n", + "- Context window management\n", + "\n", + "### 4. **Context Integration**\n", + "How different types of context are combined:\n", + "- Merging multiple information sources\n", + "- Resolving conflicts and inconsistencies\n", + "- Prioritizing information by importance\n", + "\n", + "## Real-World Example: University Class Agent\n", + "\n", + "Let's explore context engineering through a practical example - a university class recommendation agent. This agent helps students find courses, plan their academic journey, and provides personalized recommendations.\n", + "\n", + "### Without Context Engineering\n", + "```\n", + "Student: \"I'm interested in programming courses\"\n", + "Agent: \"Here are all programming courses: CS101, CS201, CS301...\"\n", + "\n", + "Student: \"I prefer online courses\"\n", + "Agent: \"Here are all programming courses: CS101, CS201, CS301...\"\n", + "\n", + "Student: \"What about my major requirements?\"\n", + "Agent: \"I don't know your major. Here are all programming courses...\"\n", + "```\n", + "\n", + "### With Context Engineering\n", + "```\n", + "Student: \"I'm interested in programming courses\"\n", + "Agent: \"Great! I can help you find programming courses. Let me search our catalog...\n", + " Based on your Computer Science major and beginner level, I recommend:\n", + " - CS101: Intro to Programming (online, matches your preference)\n", + " - CS102: Data Structures (hybrid option available)\"\n", + "\n", + "Student: \"Tell me more about CS101\"\n", + "Agent: \"CS101 is perfect for you! It's:\n", + " - Online format (your preference)\n", + " - Beginner-friendly\n", + " - Required for your CS major\n", + " - No prerequisites needed\n", + " - Taught by Prof. Smith (highly rated)\"\n", + "```\n", + "\n", + "## Environment Setup\n", + "\n", + "Before we explore context engineering in action, let's set up our environment with the necessary dependencies and connections." + ] + }, + { + "cell_type": "code", + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-03T22:25:06.287762Z", + "start_time": "2025-10-03T22:25:02.695017Z" + } + }, + "source": [ + "# Install the Redis Context Course package\n", + "%pip install -q -e ../../reference-agent" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r\n", + "\u001B[1m[\u001B[0m\u001B[34;49mnotice\u001B[0m\u001B[1;39;49m]\u001B[0m\u001B[39;49m A new release of pip is available: \u001B[0m\u001B[31;49m24.3.1\u001B[0m\u001B[39;49m -> \u001B[0m\u001B[32;49m25.2\u001B[0m\r\n", + "\u001B[1m[\u001B[0m\u001B[34;49mnotice\u001B[0m\u001B[1;39;49m]\u001B[0m\u001B[39;49m To update, run: \u001B[0m\u001B[32;49mpip install --upgrade pip\u001B[0m\r\n", + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "execution_count": 11 + }, + { + "cell_type": "code", + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-03T20:34:59.039922Z", + "start_time": "2025-10-03T20:34:59.036324Z" + } + }, + "source": [ + "import os\n", + "import sys\n", + "\n", + "# Set up environment - handle both interactive and CI environments\n", + "def _set_env(key: str):\n", + " if key not in os.environ:\n", + " # Check if we're in an interactive environment\n", + " if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty():\n", + " import getpass\n", + " os.environ[key] = getpass.getpass(f\"{key}: \")\n", + " else:\n", + " # Non-interactive environment (like CI) - use a dummy key\n", + " print(f\"⚠️ Non-interactive environment detected. Using dummy {key} for demonstration.\")\n", + " os.environ[key] = \"sk-dummy-key-for-testing-purposes-only\"\n", + "\n", + "_set_env(\"OPENAI_API_KEY\")" + ], + "outputs": [], + "execution_count": 1 + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Setup Redis (uncomment if running in Colab)\n", + "# !curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg\n", + "# !echo \"deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main\" | sudo tee /etc/apt/sources.list.d/redis.list\n", + "# !sudo apt-get update > /dev/null 2>&1\n", + "# !sudo apt-get install redis-server > /dev/null 2>&1\n", + "# !redis-server --daemonize yes\n", + "\n", + "# Set Redis URL\n", + "os.environ[\"REDIS_URL\"] = \"redis://localhost:6379\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Import the Redis Context Course components\n", + "from redis_context_course.models import Course, StudentProfile, DifficultyLevel, CourseFormat\n", + "from redis_context_course import MemoryClient\n", + "from redis_context_course.course_manager import CourseManager\n", + "from redis_context_course.redis_config import redis_config\n", + "\n", + "# Check Redis connection\n", + "redis_available = redis_config.health_check()\n", + "print(f\"Redis connection: {'✅ Connected' if redis_available else '❌ Failed'}\")\n", + "print(\"✅ Redis Context Course package imported successfully\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Context Engineering in Action\n", + "\n", + "Now that our environment is ready, let's explore the different types of context our agent manages:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1. System Context Example\n", + "\n", + "System context defines what the agent knows about itself:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Example of system context - what the agent knows about itself\n", + "system_context = {\n", + " \"role\": \"University Class Recommendation Agent\",\n", + " \"capabilities\": [\n", + " \"Search course catalog\",\n", + " \"Provide personalized recommendations\",\n", + " \"Remember student preferences\",\n", + " \"Track academic progress\",\n", + " \"Answer questions about courses and requirements\"\n", + " ],\n", + " \"knowledge_domains\": [\n", + " \"Computer Science\",\n", + " \"Data Science\", \n", + " \"Mathematics\",\n", + " \"Business Administration\",\n", + " \"Psychology\"\n", + " ],\n", + " \"constraints\": [\n", + " \"Only recommend courses that exist in the catalog\",\n", + " \"Consider prerequisites when making recommendations\",\n", + " \"Respect student preferences and goals\",\n", + " \"Provide accurate course information\"\n", + " ]\n", + "}\n", + "\n", + "print(\"🤖 System Context:\")\n", + "print(f\"Role: {system_context['role']}\")\n", + "print(f\"Capabilities: {len(system_context['capabilities'])} tools available\")\n", + "print(f\"Knowledge Domains: {', '.join(system_context['knowledge_domains'])}\")\n", + "print(f\"Operating Constraints: {len(system_context['constraints'])} rules\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2. Student Context Example\n", + "\n", + "Student context represents what the agent knows about the user:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Example student profile - user context\n", + "student = StudentProfile(\n", + " name=\"Alex Johnson\",\n", + " email=\"alex.johnson@university.edu\",\n", + " major=\"Computer Science\",\n", + " year=2,\n", + " completed_courses=[\"CS101\", \"MATH101\", \"ENG101\"],\n", + " current_courses=[\"CS201\", \"MATH201\"],\n", + " interests=[\"machine learning\", \"web development\", \"data science\"],\n", + " preferred_format=CourseFormat.ONLINE,\n", + " preferred_difficulty=DifficultyLevel.INTERMEDIATE,\n", + " max_credits_per_semester=15\n", + ")\n", + "\n", + "print(\"👤 Student Context:\")\n", + "print(f\"Name: {student.name}\")\n", + "print(f\"Major: {student.major} (Year {student.year})\")\n", + "print(f\"Completed: {len(student.completed_courses)} courses\")\n", + "print(f\"Current: {len(student.current_courses)} courses\")\n", + "print(f\"Interests: {', '.join(student.interests)}\")\n", + "print(f\"Preferences: {student.preferred_format.value}, {student.preferred_difficulty.value} level\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3. Memory Context Example\n", + "\n", + "Memory context includes past conversations and stored knowledge. Our agent uses the Agent Memory Server to store and retrieve memories.\n", + "\n", + "**Note:** This requires the Agent Memory Server to be running. See Section 3 notebooks for detailed memory operations." + ] + }, + { + "cell_type": "code", + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-04T00:40:07.487116Z", + "start_time": "2025-10-04T00:40:06.752895Z" + } + }, + "source": [ + "import os\n", + "\n", + "from agent_memory_client import MemoryAPIClient as MemoryClient, MemoryClientConfig\n", + "from agent_memory_client.models import ClientMemoryRecord\n", + "\n", + "# Initialize memory client\n", + "config = MemoryClientConfig(\n", + " base_url=os.getenv(\"AGENT_MEMORY_URL\", \"http://localhost:8000\"),\n", + " default_namespace=\"redis_university\"\n", + ")\n", + "memory_client = MemoryClient(config=config)\n", + "\n", + "# Example of storing different types of memories\n", + "async def demonstrate_memory_context():\n", + " # Store a preference\n", + " await memory_client.create_long_term_memory([ClientMemoryRecord(\n", + " text=\"I prefer online courses because I work part-time\",\n", + " memory_type=\"semantic\",\n", + " topics=[\"preferences\", \"schedule\"]\n", + " )])\n", + " \n", + " # Store a goal\n", + " await memory_client.create_long_term_memory([ClientMemoryRecord(\n", + " text=\"I want to specialize in machine learning and AI\",\n", + " memory_type=\"semantic\",\n", + " topics=[\"goals\", \"career\"]\n", + " )])\n", + " \n", + " # Store academic performance note\n", + " await memory_client.create_long_term_memory([ClientMemoryRecord(\n", + " text=\"Student struggled with calculus but excelled in programming courses\",\n", + " memory_type=\"semantic\",\n", + " topics=[\"academic_performance\", \"strengths\"]\n", + " )])\n", + " \n", + " print(\"🧠 Memory Context Stored:\")\n", + " print(\"✅ Preference stored\")\n", + " print(\"✅ Goal stored\")\n", + " print(\"✅ Academic performance noted\")\n", + " \n", + " # Retrieve relevant memories using semantic search\n", + " results = await memory_client.search_long_term_memory(\n", + " text=\"course recommendations for machine learning\",\n", + " namespace={\"eq\": \"redis_university\"},\n", + " limit=3\n", + " )\n", + " \n", + " print(f\"\\n🔍 Retrieved {len(results.memories)} relevant memories:\")\n", + " for memory in results.memories:\n", + " print(f\" • [{memory.memory_type}] {memory.text[:60]}...\")\n", + "\n", + "# Run the memory demonstration\n", + "await demonstrate_memory_context()" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "🧠 Memory Context Stored:\n", + "✅ Preference stored\n", + "✅ Goal stored\n", + "✅ Academic performance noted\n", + "\n", + "🔍 Retrieved 3 relevant memories:\n", + " • [MemoryTypeEnum.SEMANTIC] I want to specialize in machine learning and AI...\n", + " • [MemoryTypeEnum.SEMANTIC] The user wants to specialize in machine learning and artific...\n", + " • [MemoryTypeEnum.SEMANTIC] User prefers online courses...\n" + ] + } + ], + "execution_count": 15 + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Context Integration in Practice\n", + "\n", + "Now let's see how all these context types work together in a real interaction:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Example: Context Integration in Practice**\n", + "\n", + "```python\n", + "# Simulate how context is integrated for a recommendation\n", + "async def demonstrate_context_integration():\n", + " print(\"🎯 Context Integration Example\")\n", + " print(\"=\" * 50)\n", + " \n", + " # 1. Student asks for recommendations\n", + " query = \"What courses should I take next semester?\"\n", + " print(f\"Student Query: '{query}'\")\n", + " \n", + " # 2. Retrieve relevant context\n", + " print(\"\\n🔍 Retrieving Context...\")\n", + " \n", + " # Get student context from memory\n", + " results = await memory_client.search_long_term_memory(query, limit=5)\n", + " \n", + " print(\"📋 Available Context:\")\n", + " print(f\" • System Role: University Class Agent\")\n", + " print(f\" • Student: Alex Chen (Computer Science, Year 3)\")\n", + " print(f\" • Completed Courses: 15\")\n", + " print(f\" • Preferences: Online format\")\n", + " print(f\" • Interests: Machine Learning, Web Development...\")\n", + " print(f\" • Stored Memories: 3 preferences, 2 goals\")\n", + " \n", + " # 3. Generate contextual response\n", + " print(\"\\n🤖 Agent Response (Context-Aware):\")\n", + " print(\"-\" * 40)\n", + " print(\"\"\"\n", + "Based on your profile and our previous conversations, here are my recommendations:\n", + "\n", + "🎯 **Personalized for Alex Chen:**\n", + "• Major: Computer Science (Year 3)\n", + "• Format Preference: Online courses\n", + "• Interest in: Machine Learning, Web Development\n", + "• Goal: Specialize in machine learning and AI\n", + "\n", + "📚 **Recommended Courses:**\n", + "1. **CS301: Machine Learning Fundamentals** (Online)\n", + " - Aligns with your AI specialization goal\n", + " - Online format matches your work schedule\n", + "\n", + "2. **CS250: Web Development** (Hybrid)\n", + " - Matches your web development interest\n", + " - Practical skills for part-time work\n", + "\n", + "3. **MATH301: Statistics for Data Science** (Online)\n", + " - Essential for machine learning\n", + " - Builds on your completed MATH201\n", + "\n", + "💡 **Why these recommendations:**\n", + "• All courses align with your machine learning career goal\n", + "• Prioritized online/hybrid formats for your work schedule\n", + "• Total: 10 credits (within your 15-credit preference)\n", + "\"\"\")\n", + "\n", + "await demonstrate_context_integration()\n", + "```\n", + "\n", + "This example shows how the agent combines multiple context sources to provide personalized, relevant recommendations." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Key Takeaways\n", + "\n", + "From this introduction to context engineering, we can see several important principles:\n", + "\n", + "### 1. **Context is Multi-Dimensional**\n", + "- **System context**: What the AI knows about itself\n", + "- **User context**: What the AI knows about the user\n", + "- **Domain context**: What the AI knows about the subject matter\n", + "- **Conversation context**: What has been discussed recently\n", + "- **Historical context**: What has been learned over time\n", + "\n", + "### 2. **Memory is Essential**\n", + "- **Working memory**: Maintains conversation flow and task-related context\n", + "- **Long-term memory**: Enables learning and personalization across sessions\n", + "- **Semantic search**: Allows intelligent retrieval of relevant information\n", + "\n", + "### 3. **Context Must Be Actionable**\n", + "- Information is only valuable if it can be used to improve responses\n", + "- Context should be prioritized by relevance and importance\n", + "- The system must be able to integrate multiple context sources\n", + "\n", + "### 4. **Context Engineering is Iterative**\n", + "- Systems improve as they gather more context\n", + "- Context quality affects response quality\n", + "- Feedback loops help refine context management\n", + "\n", + "## Next Steps\n", + "\n", + "In the next notebook, we'll explore **The Role of a Context Engine** - the technical infrastructure that makes context engineering possible. We'll dive deeper into:\n", + "\n", + "- Vector databases and semantic search\n", + "- Memory architectures and storage patterns\n", + "- Context retrieval and ranking algorithms\n", + "- Integration with LLMs and agent frameworks\n", + "\n", + "## Try It Yourself\n", + "\n", + "Experiment with the concepts we've covered:\n", + "\n", + "1. **Modify the student profile** - Change interests, preferences, or academic history\n", + "2. **Add new memory types** - Store different kinds of information\n", + "3. **Experiment with context retrieval** - Try different queries and see what memories are retrieved\n", + "4. **Think about your own use case** - How would context engineering apply to your domain?\n", + "\n", + "The power of context engineering lies in its ability to make AI systems more intelligent, personalized, and useful. As we'll see in the following notebooks, the technical implementation of these concepts using Redis, LangGraph, and modern AI tools makes it possible to build sophisticated, context-aware applications." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/python-recipes/context-engineering/notebooks/section-1-introduction/02_role_of_context_engine.ipynb b/python-recipes/context-engineering/notebooks/section-1-introduction/02_role_of_context_engine.ipynb new file mode 100644 index 00000000..148405fb --- /dev/null +++ b/python-recipes/context-engineering/notebooks/section-1-introduction/02_role_of_context_engine.ipynb @@ -0,0 +1,849 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Redis](https://redis.io/wp-content/uploads/2024/04/Logotype.svg?auto=webp&quality=85,75&width=120)\n", + "\n", + "# The Role of a Context Engine\n", + "\n", + "## Introduction\n", + "\n", + "A **Context Engine** is the technical infrastructure that powers context engineering. It's the system responsible for storing, retrieving, managing, and serving contextual information to AI agents and applications.\n", + "\n", + "Think of a context engine as the \"brain's memory system\" - it handles both the storage of information and the intelligent retrieval of relevant context when needed. Just as human memory involves complex processes of encoding, storage, and retrieval, a context engine manages these same processes for AI systems.\n", + "\n", + "## What Makes a Context Engine?\n", + "\n", + "A context engine typically consists of several key components:\n", + "\n", + "### 🗄️ **Storage Layer**\n", + "- **Vector databases** for semantic similarity search\n", + "- **Traditional databases** for structured data\n", + "- **Cache systems** for fast access to frequently used context\n", + "- **File systems** for large documents and media\n", + "\n", + "### 🔍 **Retrieval Layer**\n", + "- **Semantic search** using embeddings and vector similarity\n", + "- **Keyword search** for exact matches and structured queries\n", + "- **Hybrid search** combining multiple retrieval methods\n", + "- **Ranking algorithms** to prioritize relevant results\n", + "\n", + "### 🧠 **Memory Management**\n", + "- **Working memory** for active conversations, sessions, and task-related data (persistent)\n", + "- **Long-term memory** for knowledge learned across sessions (user preferences, important facts)\n", + "- **Memory consolidation** for moving important information from working to long-term memory\n", + "\n", + "### 🔄 **Integration Layer**\n", + "- **APIs** for connecting with AI models and applications\n", + "- **Streaming interfaces** for real-time context updates\n", + "- **Batch processing** for large-scale context ingestion\n", + "- **Event systems** for reactive context management\n", + "\n", + "## Redis as a Context Engine\n", + "\n", + "Redis is uniquely positioned to serve as a context engine because it provides:\n", + "\n", + "- **Vector Search**: Native support for semantic similarity search\n", + "- **Multiple Data Types**: JSON documents, strings, hashes, lists, sets, streams, and more\n", + "- **High Performance**: In-memory processing with sub-millisecond latency\n", + "- **Persistence**: Durable storage with various persistence options\n", + "- **Scalability**: Horizontal scaling with Redis Cluster\n", + "- **Rich Ecosystem**: Integrations with AI frameworks and tools\n", + "\n", + "Let's explore how Redis functions as a context engine in our university class agent." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Install the Redis Context Course package\n", + "%pip install -q -e ../../reference-agent" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import json\n", + "import numpy as np\n", + "import sys\n", + "from typing import List, Dict, Any\n", + "\n", + "# Set up environment - handle both interactive and CI environments\n", + "def _set_env(key: str):\n", + " if key not in os.environ:\n", + " # Check if we're in an interactive environment\n", + " if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty():\n", + " import getpass\n", + " os.environ[key] = getpass.getpass(f\"{key}: \")\n", + " else:\n", + " # Non-interactive environment (like CI) - use a dummy key\n", + " print(f\"⚠️ Non-interactive environment detected. Using dummy {key} for demonstration.\")\n", + " os.environ[key] = \"sk-dummy-key-for-testing-purposes-only\"\n", + "\n", + "_set_env(\"OPENAI_API_KEY\")\n", + "os.environ[\"REDIS_URL\"] = \"redis://localhost:6379\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Context Engine Architecture\n", + "\n", + "Let's examine the architecture of our Redis-based context engine:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Import Redis Context Course components with error handling\n", + "try:\n", + " from redis_context_course.redis_config import redis_config\n", + " from redis_context_course import MemoryClient\n", + " from redis_context_course.course_manager import CourseManager\n", + " import redis\n", + " \n", + " PACKAGE_AVAILABLE = True\n", + " print(\"✅ Redis Context Course package imported successfully\")\n", + " \n", + " # Check Redis connection\n", + " redis_healthy = redis_config.health_check()\n", + " print(f\"📡 Redis Connection: {'✅ Healthy' if redis_healthy else '❌ Failed'}\")\n", + " \n", + " if redis_healthy:\n", + " # Show Redis info\n", + " redis_info = redis_config.redis_client.info()\n", + " print(f\"📊 Redis Version: {redis_info.get('redis_version', 'Unknown')}\")\n", + " print(f\"💾 Memory Usage: {redis_info.get('used_memory_human', 'Unknown')}\")\n", + " print(f\"🔗 Connected Clients: {redis_info.get('connected_clients', 'Unknown')}\")\n", + " \n", + " # Show configured indexes\n", + " print(f\"\\n🗂️ Vector Indexes:\")\n", + " print(f\" • Course Catalog: {redis_config.vector_index_name}\")\n", + " print(f\" • Agent Memory: Managed by Agent Memory Server\")\n", + " \n", + " # Show data types in use\n", + " print(f\"\\n📋 Data Types in Use:\")\n", + " print(f\" • Hashes: Course storage\")\n", + " print(f\" • Vectors: Semantic embeddings (1536 dimensions)\")\n", + " print(f\" • Strings: Simple key-value pairs\")\n", + " print(f\" • Sets: Tags and categories\")\n", + " \n", + "except ImportError as e:\n", + " print(f\"⚠️ Package not available: {e}\")\n", + " print(\"📝 This is expected in CI environments. Creating mock objects for demonstration...\")\n", + " \n", + " # Create mock classes\n", + " class MockRedisConfig:\n", + " def __init__(self):\n", + " self.vector_index_name = \"course_catalog_index\"\n", + " \n", + " def health_check(self):\n", + " return False # Simulate Redis not available in CI\n", + " \n", + " class MemoryClient:\n", + " def __init__(self, student_id: str):\n", + " self.student_id = student_id\n", + " print(f\"📝 Mock MemoryClient created for {student_id}\")\n", + " \n", + " async def store_memory(self, content: str, memory_type: str, importance: float = 0.5, metadata: dict = None):\n", + " return \"mock-memory-id-12345\"\n", + " \n", + " async def retrieve_memories(self, query: str, limit: int = 5):\n", + " class MockMemory:\n", + " def __init__(self, content: str, memory_type: str):\n", + " self.content = content\n", + " self.memory_type = memory_type\n", + " \n", + " return [\n", + " MockMemory(\"Student prefers online courses\", \"preference\"),\n", + " MockMemory(\"Goal: AI specialization\", \"goal\"),\n", + " MockMemory(\"Strong programming background\", \"academic_performance\")\n", + " ]\n", + " \n", + " async def get_student_context(self, query: str):\n", + " return {\n", + " \"preferences\": [\"online courses\", \"flexible schedule\"],\n", + " \"goals\": [\"machine learning specialization\"],\n", + " \"general_memories\": [\"programming experience\"],\n", + " \"recent_conversations\": [\"course planning session\"]\n", + " }\n", + " \n", + " class CourseManager:\n", + " def __init__(self):\n", + " print(\"📝 Mock CourseManager created\")\n", + " \n", + " redis_config = MockRedisConfig()\n", + " redis_healthy = False\n", + " PACKAGE_AVAILABLE = False\n", + " print(\"✅ Mock objects created for demonstration\")\n", + "\n", + "# Initialize our context engine components\n", + "print(\"\\n🏗️ Context Engine Architecture\")\n", + "print(\"=\" * 50)\n", + "print(f\"📡 Redis Connection: {'✅ Healthy' if redis_healthy else '❌ Failed (using mock data)'}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Storage Layer Deep Dive\n", + "\n", + "Let's explore how different types of context are stored in Redis:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Demonstrate different storage patterns\n", + "print(\"💾 Storage Layer Patterns\")\n", + "print(\"=\" * 40)\n", + "\n", + "# 1. Structured Data Storage (Hashes)\n", + "print(\"\\n1️⃣ Structured Data (Redis Hashes)\")\n", + "sample_course_data = {\n", + " \"course_code\": \"CS101\",\n", + " \"title\": \"Introduction to Programming\",\n", + " \"credits\": \"3\",\n", + " \"department\": \"Computer Science\",\n", + " \"difficulty_level\": \"beginner\",\n", + " \"format\": \"online\"\n", + "}\n", + "\n", + "print(\"Course data stored as hash:\")\n", + "for key, value in sample_course_data.items():\n", + " print(f\" {key}: {value}\")\n", + "\n", + "# 2. Vector Storage for Semantic Search\n", + "print(\"\\n2️⃣ Vector Embeddings (1536-dimensional)\")\n", + "print(\"Sample embedding vector (first 10 dimensions):\")\n", + "sample_embedding = np.random.rand(10) # Simulated embedding\n", + "print(f\" [{', '.join([f'{x:.4f}' for x in sample_embedding])}...]\")\n", + "print(f\" Full vector: 1536 dimensions, stored as binary data\")\n", + "\n", + "# 3. Memory Storage Patterns\n", + "print(\"\\n3️⃣ Memory Storage (Timestamped Records)\")\n", + "sample_memory = {\n", + " \"id\": \"mem_12345\",\n", + " \"student_id\": \"student_alex\",\n", + " \"content\": \"Student prefers online courses due to work schedule\",\n", + " \"memory_type\": \"preference\",\n", + " \"importance\": \"0.9\",\n", + " \"created_at\": \"1703123456.789\",\n", + " \"metadata\": '{\"context\": \"course_planning\"}'\n", + "}\n", + "\n", + "print(\"Memory record structure:\")\n", + "for key, value in sample_memory.items():\n", + " print(f\" {key}: {value}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Retrieval Layer in Action\n", + "\n", + "The retrieval layer is where the magic happens - turning queries into relevant context:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Demonstrate different retrieval methods\n", + "print(\"🔍 Retrieval Layer Methods\")\n", + "print(\"=\" * 40)\n", + "\n", + "# Initialize managers\n", + "import os\n", + "from agent_memory_client import MemoryClientConfig\n", + "\n", + "config = MemoryClientConfig(\n", + " base_url=os.getenv(\"AGENT_MEMORY_URL\", \"http://localhost:8000\"),\n", + " default_namespace=\"redis_university\"\n", + ")\n", + "memory_client = MemoryClient(config=config)\n", + "course_manager = CourseManager()\n", + "\n", + "async def demonstrate_retrieval_methods():\n", + " # 1. Exact Match Retrieval\n", + " print(\"\\n1️⃣ Exact Match Retrieval\")\n", + " print(\"Query: Find course with code 'CS101'\")\n", + " print(\"Method: Direct key lookup or tag filter\")\n", + " print(\"Use case: Looking up specific courses, IDs, or codes\")\n", + " \n", + " # 2. Semantic Similarity Search\n", + " print(\"\\n2️⃣ Semantic Similarity Search\")\n", + " print(\"Query: 'I want to learn machine learning'\")\n", + " print(\"Process:\")\n", + " print(\" 1. Convert query to embedding vector\")\n", + " print(\" 2. Calculate cosine similarity with stored vectors\")\n", + " print(\" 3. Return top-k most similar results\")\n", + " print(\" 4. Apply similarity threshold filtering\")\n", + " \n", + " # Simulate semantic search process\n", + " query = \"machine learning courses\"\n", + " print(f\"\\n🔍 Simulating semantic search for: '{query}'\")\n", + " \n", + " # This would normally generate an actual embedding\n", + " print(\" Step 1: Generate query embedding... ✅\")\n", + " print(\" Step 2: Search vector index... ✅\")\n", + " print(\" Step 3: Calculate similarities... ✅\")\n", + " print(\" Step 4: Rank and filter results... ✅\")\n", + " \n", + " # 3. Hybrid Search\n", + " print(\"\\n3️⃣ Hybrid Search (Semantic + Filters)\")\n", + " print(\"Query: 'online programming courses for beginners'\")\n", + " print(\"Process:\")\n", + " print(\" 1. Semantic search: 'programming courses'\")\n", + " print(\" 2. Apply filters: format='online', difficulty='beginner'\")\n", + " print(\" 3. Combine and rank results\")\n", + " \n", + " # 4. Memory Retrieval\n", + " print(\"\\n4️⃣ Memory Retrieval\")\n", + " print(\"Query: 'What are my course preferences?'\")\n", + " print(\"Process:\")\n", + " print(\" 1. Semantic search in memory index\")\n", + " print(\" 2. Filter by memory_type='preference'\")\n", + " print(\" 3. Sort by importance and recency\")\n", + " print(\" 4. Return relevant memories\")\n", + "\n", + "await demonstrate_retrieval_methods()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Memory Management System\n", + "\n", + "Let's explore how the context engine manages different types of memory:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Demonstrate memory management\n", + "print(\"🧠 Memory Management System\")\n", + "print(\"=\" * 40)\n", + "\n", + "async def demonstrate_memory_management():\n", + " # Working Memory (Task-Focused Context)\n", + " print(\"\\n📝 Working Memory (Persistent Task Context)\")\n", + " print(\"Purpose: Maintain conversation flow and task-related data\")\n", + " print(\"Storage: Redis Streams and Hashes (LangGraph Checkpointer)\")\n", + " print(\"Lifecycle: Persistent during task, can span multiple sessions\")\n", + " print(\"Example data:\")\n", + " print(\" • Current conversation messages\")\n", + " print(\" • Agent state and workflow position\")\n", + " print(\" • Task-related variables and computations\")\n", + " print(\" • Tool call results and intermediate steps\")\n", + " print(\" • Search results being processed\")\n", + " print(\" • Cached embeddings for current task\")\n", + " \n", + " # Long-term Memory (Cross-Session Knowledge)\n", + " print(\"\\n🗄️ Long-term Memory (Cross-Session Knowledge)\")\n", + " print(\"Purpose: Store knowledge learned across sessions\")\n", + " print(\"Storage: Redis Vector Index with embeddings\")\n", + " print(\"Lifecycle: Persistent across all sessions\")\n", + " print(\"Example data:\")\n", + " \n", + " # Store some example memories\n", + " memory_examples = [\n", + " (\"preference\", \"Student prefers online courses\", 0.9),\n", + " (\"goal\", \"Wants to specialize in AI and machine learning\", 1.0),\n", + " (\"experience\", \"Struggled with calculus but excelled in programming\", 0.8),\n", + " (\"context\", \"Works part-time, needs flexible schedule\", 0.7)\n", + " ]\n", + " \n", + " for memory_type, content, importance in memory_examples:\n", + " print(f\" • [{memory_type.upper()}] {content} (importance: {importance})\")\n", + " \n", + " # Memory Consolidation\n", + " print(\"\\n🔄 Memory Consolidation Process\")\n", + " print(\"Purpose: Move important information from working to long-term memory\")\n", + " print(\"Triggers:\")\n", + " print(\" • Conversation length exceeds threshold (20+ messages)\")\n", + " print(\" • Important preferences or goals mentioned\")\n", + " print(\" • Significant events or decisions made\")\n", + " print(\" • End of session or explicit save commands\")\n", + " \n", + " print(\"\\n📊 Memory Status (Conceptual):\")\n", + " print(f\" • Preferences stored: 1 (online courses)\")\n", + " print(f\" • Goals stored: 1 (AI/ML specialization)\")\n", + " print(f\" • General memories: 2 (calculus struggle, part-time work)\")\n", + " print(f\" • Conversation summaries: 0 (new session)\")\n", + " print(\"\\nNote: See Section 3 notebooks for actual memory implementation.\")\n", + "\n", + "await demonstrate_memory_management()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Integration Layer: Connecting Everything\n", + "\n", + "The integration layer is how the context engine connects with AI models and applications:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Demonstrate integration patterns\n", + "print(\"🔄 Integration Layer Patterns\")\n", + "print(\"=\" * 40)\n", + "\n", + "# 1. LangGraph Integration\n", + "print(\"\\n1️⃣ LangGraph Integration (Checkpointer)\")\n", + "print(\"Purpose: Persistent agent state and conversation history\")\n", + "print(\"Pattern: Redis as state store for workflow nodes\")\n", + "print(\"Benefits:\")\n", + "print(\" • Automatic state persistence\")\n", + "print(\" • Resume conversations across sessions\")\n", + "print(\" • Parallel execution support\")\n", + "print(\" • Built-in error recovery\")\n", + "\n", + "# Show checkpointer configuration\n", + "checkpointer_config = {\n", + " \"redis_client\": \"Connected Redis instance\",\n", + " \"namespace\": \"class_agent\",\n", + " \"serialization\": \"JSON with binary support\",\n", + " \"key_pattern\": \"namespace:thread_id:checkpoint_id\"\n", + "}\n", + "\n", + "print(\"\\nCheckpointer Configuration:\")\n", + "for key, value in checkpointer_config.items():\n", + " print(f\" {key}: {value}\")\n", + "\n", + "# 2. OpenAI Integration\n", + "print(\"\\n2️⃣ OpenAI Integration (Embeddings & Chat)\")\n", + "print(\"Purpose: Generate embeddings and chat completions\")\n", + "print(\"Pattern: Context engine provides relevant information to LLM\")\n", + "print(\"Flow:\")\n", + "print(\" 1. User query → Context engine retrieval\")\n", + "print(\" 2. Retrieved context → System prompt construction\")\n", + "print(\" 3. Enhanced prompt → OpenAI API\")\n", + "print(\" 4. LLM response → Context engine storage\")\n", + "\n", + "# 3. Tool Integration\n", + "print(\"\\n3️⃣ Tool Integration (LangChain Tools)\")\n", + "print(\"Purpose: Expose context engine capabilities as agent tools\")\n", + "print(\"Available tools:\")\n", + "tools_info = [\n", + " (\"search_courses_tool\", \"Semantic search in course catalog\"),\n", + " (\"get_recommendations_tool\", \"Personalized course recommendations\"),\n", + " (\"store_preference_tool\", \"Save user preferences to memory\"),\n", + " (\"store_goal_tool\", \"Save user goals to memory\"),\n", + " (\"get_student_context_tool\", \"Retrieve relevant user context\")\n", + "]\n", + "\n", + "for tool_name, description in tools_info:\n", + " print(f\" • {tool_name}: {description}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Performance Characteristics\n", + "\n", + "Let's examine the performance characteristics of our Redis-based context engine:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Conceptual Example (not executable in this notebook)**\n", + "\n", + "```python\n", + "import time\n", + "import asyncio\n", + "\n", + "# Performance benchmarking\n", + "print(\"⚡ Performance Characteristics\")\n", + "print(\"=\" * 40)\n", + "\n", + "async def benchmark_context_engine():\n", + " # 1. Memory Storage Performance\n", + " print(\"\\n📝 Memory Storage Performance\")\n", + " start_time = time.time()\n", + " \n", + " # Store multiple memories\n", + " memory_tasks = []\n", + " for i in range(10):\n", + "# task = memory_manager.store_memory(\n", + " f\"Test memory {i} for performance benchmarking\",\n", + " \"benchmark\",\n", + " importance=0.5\n", + " )\n", + " memory_tasks.append(task)\n", + " \n", + " await asyncio.gather(*memory_tasks)\n", + " storage_time = time.time() - start_time\n", + " \n", + " print(f\" Stored 10 memories in {storage_time:.3f} seconds\")\n", + " print(f\" Average: {(storage_time/10)*1000:.1f} ms per memory\")\n", + " \n", + " # 2. Memory Retrieval Performance\n", + " print(\"\\n🔍 Memory Retrieval Performance\")\n", + " start_time = time.time()\n", + " \n", + " # Perform multiple retrievals\n", + " retrieval_tasks = []\n", + " for i in range(5):\n", + "# task = memory_manager.retrieve_memories(\n", + " f\"performance test query {i}\",\n", + " limit=5\n", + " )\n", + " retrieval_tasks.append(task)\n", + " \n", + " results = await asyncio.gather(*retrieval_tasks)\n", + " retrieval_time = time.time() - start_time\n", + " \n", + " total_results = sum(len(result) for result in results)\n", + " print(f\" Retrieved {total_results} memories in {retrieval_time:.3f} seconds\")\n", + " print(f\" Average: {(retrieval_time/5)*1000:.1f} ms per query\")\n", + " \n", + " # 3. Context Integration Performance\n", + " print(\"\\n🧠 Context Integration Performance\")\n", + " start_time = time.time()\n", + " \n", + " # Get comprehensive student context\n", + "# context = await memory_manager.get_student_context(\n", + " \"comprehensive context for performance testing\"\n", + " )\n", + " \n", + " integration_time = time.time() - start_time\n", + " context_size = len(str(context))\n", + " \n", + " print(f\" Integrated context in {integration_time:.3f} seconds\")\n", + " print(f\" Context size: {context_size} characters\")\n", + " print(f\" Throughput: {context_size/integration_time:.0f} chars/second\")\n", + "\n", + "# Run performance benchmark\n", + "if redis_config.health_check():\n", + " await benchmark_context_engine()\n", + "else:\n", + " print(\"❌ Redis not available for performance testing\")", + "```\n", + "\n", + "*Note: This demonstrates the concept. See Section 3 notebooks for actual memory implementation using MemoryClient.*\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Context Engine Best Practices\n", + "\n", + "Based on our implementation, here are key best practices for building context engines:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Best practices demonstration\n", + "print(\"💡 Context Engine Best Practices\")\n", + "print(\"=\" * 50)\n", + "\n", + "print(\"\\n1️⃣ **Data Organization**\")\n", + "print(\"✅ Use consistent naming conventions for keys\")\n", + "print(\"✅ Separate different data types into different indexes\")\n", + "print(\"✅ Include metadata for filtering and sorting\")\n", + "print(\"✅ Use appropriate data structures for each use case\")\n", + "\n", + "print(\"\\n2️⃣ **Memory Management**\")\n", + "print(\"✅ Implement memory consolidation strategies\")\n", + "print(\"✅ Use importance scoring for memory prioritization\")\n", + "print(\"✅ Distinguish between working memory (task-focused) and long-term memory (cross-session)\")\n", + "print(\"✅ Monitor memory usage and implement cleanup\")\n", + "\n", + "print(\"\\n3️⃣ **Search Optimization**\")\n", + "print(\"✅ Use appropriate similarity thresholds\")\n", + "print(\"✅ Combine semantic and keyword search when needed\")\n", + "print(\"✅ Implement result ranking and filtering\")\n", + "print(\"✅ Cache frequently accessed embeddings\")\n", + "\n", + "print(\"\\n4️⃣ **Performance Optimization**\")\n", + "print(\"✅ Use connection pooling for Redis clients\")\n", + "print(\"✅ Batch operations when possible\")\n", + "print(\"✅ Implement async operations for I/O\")\n", + "print(\"✅ Monitor and optimize query performance\")\n", + "\n", + "print(\"\\n5️⃣ **Error Handling**\")\n", + "print(\"✅ Implement graceful degradation\")\n", + "print(\"✅ Use circuit breakers for external services\")\n", + "print(\"✅ Log errors with sufficient context\")\n", + "print(\"✅ Provide fallback mechanisms\")\n", + "\n", + "print(\"\\n6️⃣ **Security & Privacy**\")\n", + "print(\"✅ Encrypt sensitive data at rest\")\n", + "print(\"✅ Use secure connections (TLS)\")\n", + "print(\"✅ Implement proper access controls\")\n", + "print(\"✅ Anonymize or pseudonymize personal data\")\n", + "\n", + "# Show example of good key naming\n", + "print(\"\\n📝 Example: Good Key Naming Convention\")\n", + "key_examples = [\n", + " \"course_catalog:CS101\",\n", + " \"agent_memory:student_alex:preference:mem_12345\",\n", + " \"session:thread_abc123:checkpoint:step_5\",\n", + " \"cache:embedding:query_hash_xyz789\"\n", + "]\n", + "\n", + "for key in key_examples:\n", + " print(f\" {key}\")\n", + " \n", + "print(\"\\nPattern: namespace:entity:type:identifier\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Real-World Context Engine Example\n", + "\n", + "Let's see our context engine in action with a realistic scenario:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Conceptual Example (not executable in this notebook)**\n", + "\n", + "```python\n", + "# Real-world scenario demonstration\n", + "print(\"🌍 Real-World Context Engine Scenario\")\n", + "print(\"=\" * 50)\n", + "\n", + "async def realistic_scenario():\n", + " print(\"\\n📚 Scenario: Student Planning Next Semester\")\n", + " print(\"-\" * 40)\n", + " \n", + " # Step 1: Student context retrieval\n", + " print(\"\\n1️⃣ Context Retrieval Phase\")\n", + " query = \"I need help planning my courses for next semester\"\n", + " print(f\"Student Query: '{query}'\")\n", + " \n", + " # Simulate context retrieval\n", + " print(\"\\n🔍 Context Engine Processing:\")\n", + " print(\" • Retrieving student profile...\")\n", + " print(\" • Searching relevant memories...\")\n", + " print(\" • Loading academic history...\")\n", + " print(\" • Checking preferences and goals...\")\n", + " \n", + " # Get actual context\n", + "# context = await memory_manager.get_student_context(query)\n", + " \n", + " print(\"\\n📋 Retrieved Context:\")\n", + " print(f\" • Preferences: {len(context.get('preferences', []))} stored\")\n", + " print(f\" • Goals: {len(context.get('goals', []))} stored\")\n", + " print(f\" • Conversation history: {len(context.get('recent_conversations', []))} summaries\")\n", + " \n", + " # Step 2: Context integration\n", + " print(\"\\n2️⃣ Context Integration Phase\")\n", + " print(\"🧠 Integrating multiple context sources:\")\n", + " \n", + " integrated_context = {\n", + " \"student_profile\": {\n", + " \"major\": \"Computer Science\",\n", + " \"year\": 2,\n", + " \"completed_credits\": 45,\n", + " \"gpa\": 3.7\n", + " },\n", + " \"preferences\": [\n", + " \"Prefers online courses due to work schedule\",\n", + " \"Interested in machine learning and AI\",\n", + " \"Wants hands-on programming experience\"\n", + " ],\n", + " \"constraints\": [\n", + " \"Maximum 15 credits per semester\",\n", + " \"Must complete CS201 prerequisite\",\n", + " \"Available Tuesday/Thursday evenings\"\n", + " ],\n", + " \"goals\": [\n", + " \"Graduate in 4 years\",\n", + " \"Specialize in AI/ML\",\n", + " \"Maintain 3.5+ GPA\"\n", + " ]\n", + " }\n", + " \n", + " for category, items in integrated_context.items():\n", + " print(f\" • {category.title()}: {len(items) if isinstance(items, list) else 'Profile loaded'}\")\n", + " \n", + " # Step 3: Intelligent response generation\n", + " print(\"\\n3️⃣ Response Generation Phase\")\n", + " print(\"🤖 Context-aware response:\")\n", + " print(\"-\" * 30)\n", + " \n", + " response = f\"\"\"\n", + "Based on your profile and our previous conversations, here's my recommendation for next semester:\n", + "\n", + "🎯 **Personalized Plan for CS Year 2 Student:**\n", + "\n", + "**Recommended Courses (12 credits):**\n", + "1. **CS301: Machine Learning Fundamentals** (4 credits, Online)\n", + " → Aligns with your AI specialization goal\n", + " → Available Tuesday evenings (fits your schedule)\n", + " → Prerequisite CS201 will be completed this semester\n", + "\n", + "2. **CS250: Database Systems** (4 credits, Hybrid)\n", + " → Essential for CS major requirements\n", + " → Practical skills valuable for internships\n", + " → Thursday evening lab sessions\n", + "\n", + "3. **MATH301: Statistics** (4 credits, Online)\n", + " → Required for ML specialization\n", + " → Fully online (matches your preference)\n", + " → Self-paced with flexible deadlines\n", + "\n", + "**Why this plan works:**\n", + "✅ Stays within your 15-credit limit\n", + "✅ All courses available in preferred formats\n", + "✅ Fits your Tuesday/Thursday availability\n", + "✅ Advances your AI/ML specialization goal\n", + "✅ Maintains manageable workload for 3.5+ GPA\n", + "\n", + "**Next steps:**\n", + "1. Verify CS201 completion this semester\n", + "2. Check for any schedule conflicts\n", + "3. Register early - these courses fill up quickly!\n", + "\n", + "Would you like me to help you explore any of these courses in more detail?\n", + "\"\"\"\n", + " \n", + " print(response)\n", + " \n", + " # Step 4: Memory consolidation\n", + " print(\"\\n4️⃣ Memory Consolidation Phase\")\n", + " print(\"💾 Storing interaction for future reference:\")\n", + " \n", + " # Store the planning session as a memory\n", + "# planning_memory = await memory_manager.store_memory(\n", + " \"Student requested semester planning help. Recommended CS301, CS250, MATH301 based on AI/ML goals and schedule constraints.\",\n", + " \"planning_session\",\n", + " importance=0.9,\n", + " metadata={\"semester\": \"Spring 2024\", \"credits_planned\": 12}\n", + " )\n", + " \n", + " print(f\" ✅ Planning session stored (ID: {planning_memory[:8]}...)\")\n", + " print(\" ✅ Course preferences updated\")\n", + " print(\" ✅ Academic goals reinforced\")\n", + " print(\" ✅ Context ready for future interactions\")\n", + "\n", + "# Run the realistic scenario\n", + "if redis_config.health_check():\n", + " await realistic_scenario()\n", + "else:\n", + " print(\"❌ Redis not available for scenario demonstration\")", + "```\n", + "\n", + "*Note: This demonstrates the concept. See Section 3 notebooks for actual memory implementation using MemoryClient.*\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Key Takeaways\n", + "\n", + "From our exploration of context engines, several important principles emerge:\n", + "\n", + "### 1. **Multi-Layer Architecture**\n", + "- **Storage Layer**: Handles different data types and access patterns\n", + "- **Retrieval Layer**: Provides intelligent search and ranking\n", + "- **Memory Management**: Orchestrates working memory (task-focused) and long-term memory (cross-session)\n", + "- **Integration Layer**: Connects with AI models and applications\n", + "\n", + "### 2. **Performance is Critical**\n", + "- Context retrieval must be fast (< 100ms for good UX)\n", + "- Memory storage should be efficient and scalable\n", + "- Caching strategies are essential for frequently accessed data\n", + "- Async operations prevent blocking in AI workflows\n", + "\n", + "### 3. **Context Quality Matters**\n", + "- Relevant context improves AI responses dramatically\n", + "- Irrelevant context can confuse or mislead AI models\n", + "- Context ranking and filtering are as important as retrieval\n", + "- Memory consolidation helps maintain context quality by moving important information to long-term storage\n", + "\n", + "### 4. **Integration is Key**\n", + "- Context engines must integrate seamlessly with AI frameworks\n", + "- Tool-based integration provides flexibility and modularity\n", + "- State management integration enables persistent conversations\n", + "- API design affects ease of use and adoption\n", + "\n", + "## Next Steps\n", + "\n", + "In the next section, we'll dive into **Setting up System Context** - how to define what your AI agent should know about itself, its capabilities, and its operating environment. We'll cover:\n", + "\n", + "- System prompt engineering\n", + "- Tool definition and management\n", + "- Capability boundaries and constraints\n", + "- Domain knowledge integration\n", + "\n", + "## Try It Yourself\n", + "\n", + "Experiment with the context engine concepts:\n", + "\n", + "1. **Modify retrieval parameters** - Change similarity thresholds and see how it affects results\n", + "2. **Add new memory types** - Create custom memory categories for your use case\n", + "3. **Experiment with context integration** - Try different ways of combining context sources\n", + "4. **Measure performance** - Benchmark different operations and optimize bottlenecks\n", + "\n", + "The context engine is the foundation that makes sophisticated AI agents possible. Understanding its architecture and capabilities is essential for building effective context engineering solutions." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/python-recipes/context-engineering/notebooks/section-1-introduction/03_project_overview.ipynb b/python-recipes/context-engineering/notebooks/section-1-introduction/03_project_overview.ipynb new file mode 100644 index 00000000..a9de90a9 --- /dev/null +++ b/python-recipes/context-engineering/notebooks/section-1-introduction/03_project_overview.ipynb @@ -0,0 +1,979 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Redis](https://redis.io/wp-content/uploads/2024/04/Logotype.svg?auto=webp&quality=85,75&width=120)\n", + "\n", + "# Project Overview: Redis University Class Agent\n", + "\n", + "## Introduction\n", + "\n", + "Throughout this course, we'll be building and exploring a complete **Redis University Class Agent** - a sophisticated AI agent that helps students find courses, plan their academic journey, and provides personalized recommendations.\n", + "\n", + "This project serves as a comprehensive example of context engineering principles in action, demonstrating how to build intelligent, context-aware AI systems using Redis, LangGraph, and modern AI tools.\n", + "\n", + "## Project Goals\n", + "\n", + "Our Redis University Class Agent is designed to:\n", + "\n", + "### 🎯 **Primary Objectives**\n", + "- **Help students discover relevant courses** based on their interests and goals\n", + "- **Provide personalized recommendations** considering academic history and preferences\n", + "- **Remember student context** across multiple conversations and sessions\n", + "- **Answer questions** about courses, prerequisites, and academic planning\n", + "- **Adapt and learn** from student interactions over time\n", + "\n", + "### 📚 **Educational Objectives**\n", + "- **Demonstrate context engineering concepts** in a real-world scenario\n", + "- **Show Redis capabilities** for AI applications and memory management\n", + "- **Illustrate LangGraph workflows** for complex agent behaviors\n", + "- **Provide a reference implementation** for similar projects\n", + "- **Teach best practices** for building context-aware AI systems\n", + "\n", + "## System Architecture\n", + "\n", + "Our agent follows a modern, scalable architecture:\n", + "\n", + "```\n", + "┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐\n", + "│ User Input │───▶│ LangGraph │───▶│ OpenAI GPT │\n", + "│ (CLI/API) │ │ Agent │ │ (LLM) │\n", + "└─────────────────┘ └─────────────────┘ └─────────────────┘\n", + " │\n", + " ▼\n", + "┌─────────────────────────────────────────────────────────────────┐\n", + "│ Redis Context Engine │\n", + "├─────────────────┬─────────────────┬─────────────────────────────┤\n", + "│ Short-term │ Long-term │ Course Catalog │\n", + "│ Memory │ Memory │ (Vector Search) │\n", + "│ (Checkpointer) │ (Vector Store) │ │\n", + "└─────────────────┴─────────────────┴─────────────────────────────┘\n", + "```\n", + "\n", + "### Key Components\n", + "\n", + "1. **LangGraph Agent**: Orchestrates the conversation flow and decision-making\n", + "2. **Redis Context Engine**: Manages all context and memory operations\n", + "3. **OpenAI Integration**: Provides language understanding and generation\n", + "4. **Tool System**: Enables the agent to search, recommend, and remember\n", + "5. **CLI Interface**: Provides an interactive way to chat with the agent\n", + "\n", + "## Core Features\n", + "\n", + "Let's explore the key features our agent provides:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Install the Redis Context Course package\n", + "%pip install -q -e ../../reference-agent\n", + "\n", + "# Or install from PyPI (when available)\n", + "# %pip install -q redis-context-course" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import sys\n", + "\n", + "# Set up environment - handle both interactive and CI environments\n", + "def _set_env(key: str):\n", + " if key not in os.environ:\n", + " # Check if we're in an interactive environment\n", + " if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty():\n", + " import getpass\n", + " os.environ[key] = getpass.getpass(f\"{key}: \")\n", + " else:\n", + " # Non-interactive environment (like CI) - use a dummy key\n", + " print(f\"⚠️ Non-interactive environment detected. Using dummy {key} for demonstration.\")\n", + " os.environ[key] = \"sk-dummy-key-for-testing-purposes-only\"\n", + "\n", + "_set_env(\"OPENAI_API_KEY\")\n", + "os.environ[\"REDIS_URL\"] = \"redis://localhost:6379\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Feature 1: Intelligent Course Search\n", + "\n", + "The agent can search through course catalogs using both semantic and structured search:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from redis_context_course.course_manager import CourseManager\n", + "from redis_context_course.models import Course, DifficultyLevel, CourseFormat\n", + "from redis_context_course.redis_config import redis_config\n", + "\n", + "print(\"🔍 Feature 1: Intelligent Course Search\")\n", + "print(\"=\" * 50)\n", + "\n", + "# Initialize course manager\n", + "course_manager = CourseManager()\n", + "\n", + "# Example search capabilities\n", + "search_examples = [\n", + " {\n", + " \"query\": \"machine learning courses\",\n", + " \"type\": \"Semantic Search\",\n", + " \"description\": \"Finds courses related to ML, AI, data science, etc.\"\n", + " },\n", + " {\n", + " \"query\": \"online programming courses for beginners\",\n", + " \"type\": \"Hybrid Search\",\n", + " \"description\": \"Combines semantic search with format and difficulty filters\"\n", + " },\n", + " {\n", + " \"query\": \"CS101\",\n", + " \"type\": \"Exact Match\",\n", + " \"description\": \"Direct lookup by course code\"\n", + " },\n", + " {\n", + " \"query\": \"web development with JavaScript\",\n", + " \"type\": \"Semantic + Keywords\",\n", + " \"description\": \"Finds courses matching both concepts and specific technologies\"\n", + " }\n", + "]\n", + "\n", + "print(\"\\n📋 Search Capabilities:\")\n", + "for i, example in enumerate(search_examples, 1):\n", + " print(f\"\\n{i}. **{example['type']}**\")\n", + " print(f\" Query: '{example['query']}'\")\n", + " print(f\" Result: {example['description']}\")\n", + "\n", + "print(\"\\n🎯 Search Features:\")\n", + "features = [\n", + " \"Vector similarity search using OpenAI embeddings\",\n", + " \"Structured filtering by department, difficulty, format\",\n", + " \"Relevance ranking and similarity thresholds\",\n", + " \"Support for complex, multi-criteria queries\",\n", + " \"Fast retrieval with Redis vector indexing\"\n", + "]\n", + "\n", + "for feature in features:\n", + " print(f\" ✅ {feature}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Feature 2: Personalized Recommendations\n", + "\n", + "The agent provides personalized course recommendations based on student profiles and preferences:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from redis_context_course.models import StudentProfile\n", + "\n", + "print(\"🎯 Feature 2: Personalized Recommendations\")\n", + "print(\"=\" * 50)\n", + "\n", + "# Example student profile\n", + "sample_student = StudentProfile(\n", + " name=\"Alex Johnson\",\n", + " email=\"alex@university.edu\",\n", + " major=\"Computer Science\",\n", + " year=2,\n", + " completed_courses=[\"CS101\", \"MATH101\", \"ENG101\"],\n", + " current_courses=[\"CS201\", \"MATH201\"],\n", + " interests=[\"machine learning\", \"web development\", \"data science\"],\n", + " preferred_format=CourseFormat.ONLINE,\n", + " preferred_difficulty=DifficultyLevel.INTERMEDIATE,\n", + " max_credits_per_semester=15\n", + ")\n", + "\n", + "print(\"\\n👤 Sample Student Profile:\")\n", + "print(f\" Name: {sample_student.name}\")\n", + "print(f\" Major: {sample_student.major} (Year {sample_student.year})\")\n", + "print(f\" Interests: {', '.join(sample_student.interests)}\")\n", + "print(f\" Preferences: {sample_student.preferred_format.value}, {sample_student.preferred_difficulty.value}\")\n", + "print(f\" Academic Progress: {len(sample_student.completed_courses)} completed, {len(sample_student.current_courses)} current\")\n", + "\n", + "print(\"\\n🧠 Recommendation Algorithm:\")\n", + "algorithm_steps = [\n", + " \"Analyze student interests and academic history\",\n", + " \"Search for relevant courses using semantic similarity\",\n", + " \"Filter by student preferences (format, difficulty, schedule)\",\n", + " \"Check prerequisites and academic requirements\",\n", + " \"Calculate relevance scores based on multiple factors\",\n", + " \"Rank recommendations by relevance and fit\",\n", + " \"Generate explanations for each recommendation\"\n", + "]\n", + "\n", + "for i, step in enumerate(algorithm_steps, 1):\n", + " print(f\" {i}. {step}\")\n", + "\n", + "print(\"\\n📊 Scoring Factors:\")\n", + "scoring_factors = [\n", + " (\"Major alignment\", \"30%\", \"Courses matching student's major\"),\n", + " (\"Interest matching\", \"25%\", \"Courses related to stated interests\"),\n", + " (\"Preference fit\", \"20%\", \"Format and difficulty preferences\"),\n", + " (\"Academic progression\", \"15%\", \"Appropriate for student's year/level\"),\n", + " (\"Prerequisites met\", \"10%\", \"Student can actually take the course\")\n", + "]\n", + "\n", + "for factor, weight, description in scoring_factors:\n", + " print(f\" • {factor} ({weight}): {description}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Feature 3: Persistent Memory System\n", + "\n", + "The agent remembers student interactions and builds context over time:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from redis_context_course import MemoryClient\n", + "\n", + "print(\"🧠 Feature 3: Persistent Memory System\")\n", + "print(\"=\" * 50)\n", + "\n", + "# Initialize memory manager\n", + "import os\n", + "from agent_memory_client import MemoryClientConfig\n", + "\n", + "config = MemoryClientConfig(\n", + " base_url=os.getenv(\"AGENT_MEMORY_URL\", \"http://localhost:8000\"),\n", + " default_namespace=\"redis_university\"\n", + ")\n", + "memory_client = MemoryClient(config=config)\n", + "\n", + "print(\"\\n📚 Memory Types:\")\n", + "memory_types = [\n", + " {\n", + " \"type\": \"Preferences\",\n", + " \"description\": \"Student preferences for course format, difficulty, schedule\",\n", + " \"example\": \"Prefers online courses due to work schedule\",\n", + " \"importance\": \"High (0.9)\"\n", + " },\n", + " {\n", + " \"type\": \"Goals\",\n", + " \"description\": \"Academic and career objectives\",\n", + " \"example\": \"Wants to specialize in machine learning and AI\",\n", + " \"importance\": \"Very High (1.0)\"\n", + " },\n", + " {\n", + " \"type\": \"Experiences\",\n", + " \"description\": \"Past academic performance and challenges\",\n", + " \"example\": \"Struggled with calculus but excelled in programming\",\n", + " \"importance\": \"Medium (0.8)\"\n", + " },\n", + " {\n", + " \"type\": \"Conversations\",\n", + " \"description\": \"Summaries of important conversations\",\n", + " \"example\": \"Discussed course planning for Spring 2024 semester\",\n", + " \"importance\": \"Medium (0.7)\"\n", + " }\n", + "]\n", + "\n", + "for memory_type in memory_types:\n", + " print(f\"\\n🏷️ **{memory_type['type']}**\")\n", + " print(f\" Description: {memory_type['description']}\")\n", + " print(f\" Example: \\\"{memory_type['example']}\\\"\")\n", + " print(f\" Importance: {memory_type['importance']}\")\n", + "\n", + "print(\"\\n🔄 Memory Operations:\")\n", + "operations = [\n", + " \"**Store**: Save new memories with embeddings for semantic search\",\n", + " \"**Retrieve**: Find relevant memories using similarity search\",\n", + " \"**Consolidate**: Summarize long conversations to manage context\",\n", + " \"**Update**: Modify importance scores based on relevance\",\n", + " \"**Expire**: Remove outdated or irrelevant memories\"\n", + "]\n", + "\n", + "for operation in operations:\n", + " print(f\" • {operation}\")\n", + "\n", + "print(\"\\n⚡ Memory Benefits:\")\n", + "benefits = [\n", + " \"Personalized responses based on student history\",\n", + " \"Consistent experience across multiple sessions\",\n", + " \"Improved recommendations over time\",\n", + " \"Context-aware conversation flow\",\n", + " \"Reduced need to repeat information\"\n", + "]\n", + "\n", + "for benefit in benefits:\n", + " print(f\" ✅ {benefit}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Feature 4: LangGraph Workflow\n", + "\n", + "The agent uses LangGraph for sophisticated workflow orchestration:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"🕸️ Feature 4: LangGraph Workflow\")\n", + "print(\"=\" * 50)\n", + "\n", + "print(\"\\n🔄 Agent Workflow:\")\n", + "print(\"\"\"\n", + "┌─────────────────┐\n", + "│ User Input │\n", + "└─────────┬───────┘\n", + " │\n", + " ▼\n", + "┌─────────────────┐\n", + "│ Retrieve │ ◄─── Get relevant context from memory\n", + "│ Context │ and student profile\n", + "└─────────┬───────┘\n", + " │\n", + " ▼\n", + "┌─────────────────┐\n", + "│ Agent │ ◄─── LLM reasoning with tools\n", + "│ Reasoning │ available for use\n", + "└─────────┬───────┘\n", + " │\n", + " ┌────┴────┐\n", + " │ Tools? │\n", + " └────┬────┘\n", + " │\n", + " ┌─────┴─────┐\n", + " │ Yes │ No\n", + " ▼ ▼\n", + "┌─────────┐ ┌─────────┐\n", + "│ Execute │ │ Generate│\n", + "│ Tools │ │Response │\n", + "└─────┬───┘ └─────┬───┘\n", + " │ │\n", + " └─────┬─────┘\n", + " ▼\n", + "┌─────────────────┐\n", + "│ Store Memory │ ◄─── Save important information\n", + "│ & Update State │ for future conversations\n", + "└─────────────────┘\n", + "\"\"\")\n", + "\n", + "print(\"\\n🛠️ Available Tools:\")\n", + "tools = [\n", + " {\n", + " \"name\": \"search_courses_tool\",\n", + " \"purpose\": \"Search course catalog using semantic and structured queries\",\n", + " \"input\": \"Query string and optional filters\",\n", + " \"output\": \"List of matching courses with details\"\n", + " },\n", + " {\n", + " \"name\": \"get_recommendations_tool\",\n", + " \"purpose\": \"Generate personalized course recommendations\",\n", + " \"input\": \"Student context and preferences\",\n", + " \"output\": \"Ranked list of recommended courses with explanations\"\n", + " },\n", + " {\n", + " \"name\": \"store_preference_tool\",\n", + " \"purpose\": \"Save student preferences to long-term memory\",\n", + " \"input\": \"Preference description and context\",\n", + " \"output\": \"Confirmation of storage\"\n", + " },\n", + " {\n", + " \"name\": \"store_goal_tool\",\n", + " \"purpose\": \"Save student goals and objectives\",\n", + " \"input\": \"Goal description and context\",\n", + " \"output\": \"Confirmation of storage\"\n", + " },\n", + " {\n", + " \"name\": \"get_student_context_tool\",\n", + " \"purpose\": \"Retrieve relevant student context and history\",\n", + " \"input\": \"Query for context retrieval\",\n", + " \"output\": \"Relevant memories and context information\"\n", + " }\n", + "]\n", + "\n", + "for tool in tools:\n", + " print(f\"\\n🔧 **{tool['name']}**\")\n", + " print(f\" Purpose: {tool['purpose']}\")\n", + " print(f\" Input: {tool['input']}\")\n", + " print(f\" Output: {tool['output']}\")\n", + "\n", + "print(\"\\n⚙️ Workflow Benefits:\")\n", + "benefits = [\n", + " \"Structured decision-making process\",\n", + " \"Automatic state persistence across sessions\",\n", + " \"Tool-based extensibility\",\n", + " \"Error handling and recovery\",\n", + " \"Parallel execution support\",\n", + " \"Debugging and observability\"\n", + "]\n", + "\n", + "for benefit in benefits:\n", + " print(f\" ✅ {benefit}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Feature 5: Interactive CLI Interface\n", + "\n", + "The agent provides a rich command-line interface for easy interaction:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"💬 Feature 5: Interactive CLI Interface\")\n", + "print(\"=\" * 50)\n", + "\n", + "print(\"\\n🖥️ CLI Features:\")\n", + "cli_features = [\n", + " \"Rich text formatting with colors and styling\",\n", + " \"Real-time typing indicators and status updates\",\n", + " \"Markdown rendering for formatted responses\",\n", + " \"Command history and session management\",\n", + " \"Help system with examples and guidance\",\n", + " \"Error handling with user-friendly messages\"\n", + "]\n", + "\n", + "for feature in cli_features:\n", + " print(f\" ✅ {feature}\")\n", + "\n", + "print(\"\\n💡 Example Interaction:\")\n", + "print(\"\"\"\n", + "┌─────────────────────────────────────────────────────────────┐\n", + "│ 🎓 Redis University Class Agent │\n", + "│ │\n", + "│ I'm here to help you find courses, plan your academic │\n", + "│ journey, and provide personalized recommendations based │\n", + "│ on your interests and goals. │\n", + "│ │\n", + "│ Type 'help' for commands, 'quit' to exit │\n", + "└─────────────────────────────────────────────────────────────┘\n", + "\n", + "You: I'm interested in machine learning courses\n", + "\n", + "┌─────────────────────────────────────────────────────────────┐\n", + "│ 🤖 Class Agent │\n", + "│ │\n", + "│ Great! I can help you find machine learning courses. │\n", + "│ Let me search our catalog... │\n", + "│ │\n", + "│ **Recommended Courses:** │\n", + "│ │\n", + "│ 1. **CS301: Machine Learning Fundamentals** (4 credits) │\n", + "│ • Beginner-friendly introduction to ML concepts │\n", + "│ • Available online and in-person │\n", + "│ • Prerequisites: CS201, MATH201 │\n", + "│ │\n", + "│ 2. **DS250: Data Science with Python** (3 credits) │\n", + "│ • Practical ML applications │\n", + "│ • Hands-on projects with real datasets │\n", + "│ • Online format available │\n", + "│ │\n", + "│ Would you like more details about any of these courses? │\n", + "└─────────────────────────────────────────────────────────────┘\n", + "\n", + "You: I prefer online courses\n", + "\n", + "┌─────────────────────────────────────────────────────────────┐\n", + "│ 🤖 Class Agent │\n", + "│ │\n", + "│ I'll remember that you prefer online courses! Let me │\n", + "│ update my recommendations to focus on online options... │\n", + "│ │\n", + "│ **Online ML Courses:** │\n", + "│ │\n", + "│ • CS301: Machine Learning Fundamentals (Online) │\n", + "│ • DS250: Data Science with Python (Online) │\n", + "│ • CS401: Advanced Machine Learning (Online) │\n", + "│ │\n", + "│ These courses all offer flexible scheduling perfect for │\n", + "│ online learning. Would you like to know more about the │\n", + "│ schedule and requirements? │\n", + "└─────────────────────────────────────────────────────────────┘\n", + "\"\"\")\n", + "\n", + "print(\"\\n🎯 CLI Benefits:\")\n", + "benefits = [\n", + " \"Natural conversation flow\",\n", + " \"Visual feedback and formatting\",\n", + " \"Easy to use and understand\",\n", + " \"Persistent sessions with memory\",\n", + " \"Rich error messages and help\",\n", + " \"Cross-platform compatibility\"\n", + "]\n", + "\n", + "for benefit in benefits:\n", + " print(f\" ✅ {benefit}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Technical Implementation\n", + "\n", + "Let's examine the technical stack and implementation details:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"🔧 Technical Implementation\")\n", + "print(\"=\" * 50)\n", + "\n", + "print(\"\\n📚 Technology Stack:\")\n", + "tech_stack = [\n", + " {\n", + " \"category\": \"AI & ML\",\n", + " \"technologies\": [\n", + " \"OpenAI GPT-4 (Language Model)\",\n", + " \"OpenAI text-embedding-3-small (Embeddings)\",\n", + " \"LangChain (AI Framework)\",\n", + " \"LangGraph (Agent Workflows)\"\n", + " ]\n", + " },\n", + " {\n", + " \"category\": \"Data & Storage\",\n", + " \"technologies\": [\n", + " \"Redis 8 (Vector Database)\",\n", + " \"RedisVL (Vector Library)\",\n", + " \"Redis OM (Object Mapping)\",\n", + " \"langgraph-checkpoint-redis (State Management)\"\n", + " ]\n", + " },\n", + " {\n", + " \"category\": \"Development\",\n", + " \"technologies\": [\n", + " \"Python 3.8+ (Core Language)\",\n", + " \"Pydantic (Data Validation)\",\n", + " \"Click (CLI Framework)\",\n", + " \"Rich (Terminal UI)\",\n", + " \"AsyncIO (Async Programming)\"\n", + " ]\n", + " },\n", + " {\n", + " \"category\": \"Testing & Quality\",\n", + " \"technologies\": [\n", + " \"Pytest (Testing Framework)\",\n", + " \"Black (Code Formatting)\",\n", + " \"MyPy (Type Checking)\",\n", + " \"isort (Import Sorting)\"\n", + " ]\n", + " }\n", + "]\n", + "\n", + "for stack in tech_stack:\n", + " print(f\"\\n🏷️ **{stack['category']}:**\")\n", + " for tech in stack['technologies']:\n", + " print(f\" • {tech}\")\n", + "\n", + "print(\"\\n🏗️ Architecture Patterns:\")\n", + "patterns = [\n", + " {\n", + " \"pattern\": \"Repository Pattern\",\n", + " \"description\": \"Separate data access logic from business logic\",\n", + " \"implementation\": \"CourseManager and MemoryClient classes\"\n", + " },\n", + " {\n", + " \"pattern\": \"Strategy Pattern\",\n", + " \"description\": \"Different search and retrieval strategies\",\n", + " \"implementation\": \"Semantic, keyword, and hybrid search methods\"\n", + " },\n", + " {\n", + " \"pattern\": \"Observer Pattern\",\n", + " \"description\": \"Memory consolidation and state updates\",\n", + " \"implementation\": \"LangGraph checkpointer and memory triggers\"\n", + " },\n", + " {\n", + " \"pattern\": \"Factory Pattern\",\n", + " \"description\": \"Create different types of memories and courses\",\n", + " \"implementation\": \"Model constructors and data generators\"\n", + " }\n", + "]\n", + "\n", + "for pattern in patterns:\n", + " print(f\"\\n🔧 **{pattern['pattern']}**\")\n", + " print(f\" Purpose: {pattern['description']}\")\n", + " print(f\" Implementation: {pattern['implementation']}\")\n", + "\n", + "print(\"\\n📊 Performance Characteristics:\")\n", + "performance = [\n", + " \"Sub-millisecond Redis operations\",\n", + " \"Vector search in < 50ms for typical queries\",\n", + " \"Memory retrieval in < 100ms\",\n", + " \"Course recommendations in < 200ms\",\n", + " \"Full conversation response in < 2s\",\n", + " \"Supports 1000+ concurrent users (with proper scaling)\"\n", + "]\n", + "\n", + "for metric in performance:\n", + " print(f\" ⚡ {metric}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Getting Started with the Project\n", + "\n", + "Here's how to set up and run the Redis University Class Agent:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"🚀 Getting Started Guide\")\n", + "print(\"=\" * 50)\n", + "\n", + "print(\"\\n📋 Prerequisites:\")\n", + "prerequisites = [\n", + " \"Python 3.8 or higher\",\n", + " \"Redis 8 (local or cloud)\",\n", + " \"OpenAI API key with billing enabled\",\n", + " \"Git for cloning the repository\",\n", + " \"Basic understanding of Python and AI concepts\"\n", + "]\n", + "\n", + "for i, prereq in enumerate(prerequisites, 1):\n", + " print(f\" {i}. {prereq}\")\n", + "\n", + "print(\"\\n🔧 Setup Steps:\")\n", + "setup_steps = [\n", + " {\n", + " \"step\": \"Clone Repository\",\n", + " \"command\": \"git clone https://github.com/redis-developer/redis-ai-resources.git\",\n", + " \"description\": \"Get the source code\"\n", + " },\n", + " {\n", + " \"step\": \"Navigate to Project\",\n", + " \"command\": \"cd redis-ai-resources/python-recipes/context-engineering/reference-agent\",\n", + " \"description\": \"Enter the project directory\"\n", + " },\n", + " {\n", + " \"step\": \"Install Dependencies\",\n", + " \"command\": \"pip install -r requirements.txt\",\n", + " \"description\": \"Install Python packages\"\n", + " },\n", + " {\n", + " \"step\": \"Configure Environment\",\n", + " \"command\": \"cp .env.example .env && nano .env\",\n", + " \"description\": \"Set up API keys and configuration\"\n", + " },\n", + " {\n", + " \"step\": \"Start Redis\",\n", + " \"command\": \"docker run -d --name redis -p 6379:6379 redis:8-alpine\",\n", + " \"description\": \"Launch Redis 8 container\"\n", + " },\n", + " {\n", + " \"step\": \"Generate Data\",\n", + " \"command\": \"python scripts/generate_courses.py --courses-per-major 15\",\n", + " \"description\": \"Create sample course catalog\"\n", + " },\n", + " {\n", + " \"step\": \"Ingest Data\",\n", + " \"command\": \"python scripts/ingest_courses.py --catalog course_catalog.json --clear\",\n", + " \"description\": \"Load data into Redis\"\n", + " },\n", + " {\n", + " \"step\": \"Start Agent\",\n", + " \"command\": \"python src/cli.py --student-id your_name\",\n", + " \"description\": \"Launch the interactive agent\"\n", + " }\n", + "]\n", + "\n", + "for i, step in enumerate(setup_steps, 1):\n", + " print(f\"\\n{i}. **{step['step']}**\")\n", + " print(f\" Command: `{step['command']}`\")\n", + " print(f\" Purpose: {step['description']}\")\n", + "\n", + "print(\"\\n✅ Verification:\")\n", + "verification_steps = [\n", + " \"Redis connection shows ✅ Healthy\",\n", + " \"Course catalog contains 50+ courses\",\n", + " \"Agent responds to 'hello' with a greeting\",\n", + " \"Search for 'programming' returns relevant courses\",\n", + " \"Agent remembers preferences across messages\"\n", + "]\n", + "\n", + "for step in verification_steps:\n", + " print(f\" • {step}\")\n", + "\n", + "print(\"\\n🎯 Next Steps:\")\n", + "next_steps = [\n", + " \"Explore the notebooks in section-2-system-context\",\n", + " \"Try different queries and see how the agent responds\",\n", + " \"Examine the source code to understand implementation\",\n", + " \"Modify the course data or add new majors\",\n", + " \"Extend the agent with new tools and capabilities\"\n", + "]\n", + "\n", + "for step in next_steps:\n", + " print(f\" 📚 {step}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Learning Objectives\n", + "\n", + "By working with this project, you'll learn:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"🎓 Learning Objectives\")\n", + "print(\"=\" * 50)\n", + "\n", + "learning_objectives = [\n", + " {\n", + " \"category\": \"Context Engineering Fundamentals\",\n", + " \"objectives\": [\n", + " \"Understand the principles of context engineering\",\n", + " \"Learn how to design context-aware AI systems\",\n", + " \"Master memory management patterns\",\n", + " \"Implement semantic search and retrieval\"\n", + " ]\n", + " },\n", + " {\n", + " \"category\": \"Redis for AI Applications\",\n", + " \"objectives\": [\n", + " \"Use Redis as a vector database\",\n", + " \"Implement semantic search with RedisVL\",\n", + " \"Manage different data types in Redis\",\n", + " \"Optimize Redis for AI workloads\"\n", + " ]\n", + " },\n", + " {\n", + " \"category\": \"LangGraph Agent Development\",\n", + " \"objectives\": [\n", + " \"Build complex agent workflows\",\n", + " \"Implement tool-based agent architectures\",\n", + " \"Manage agent state and persistence\",\n", + " \"Handle error recovery and resilience\"\n", + " ]\n", + " },\n", + " {\n", + " \"category\": \"AI System Integration\",\n", + " \"objectives\": [\n", + " \"Integrate OpenAI APIs effectively\",\n", + " \"Design scalable AI architectures\",\n", + " \"Implement proper error handling\",\n", + " \"Build user-friendly interfaces\"\n", + " ]\n", + " }\n", + "]\n", + "\n", + "for category in learning_objectives:\n", + " print(f\"\\n📚 **{category['category']}:**\")\n", + " for objective in category['objectives']:\n", + " print(f\" • {objective}\")\n", + "\n", + "print(\"\\n🏆 Skills You'll Develop:\")\n", + "skills = [\n", + " \"Context engineering design and implementation\",\n", + " \"Vector database usage and optimization\",\n", + " \"AI agent architecture and workflows\",\n", + " \"Memory management for AI systems\",\n", + " \"Tool integration and extensibility\",\n", + " \"Performance optimization for AI applications\",\n", + " \"User experience design for AI interfaces\",\n", + " \"Testing and debugging AI systems\"\n", + "]\n", + "\n", + "for skill in skills:\n", + " print(f\" 🎯 {skill}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Course Roadmap\n", + "\n", + "Here's what we'll cover in the upcoming sections:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"🗺️ Course Roadmap\")\n", + "print(\"=\" * 50)\n", + "\n", + "course_sections = [\n", + " {\n", + " \"section\": \"Section 1: Introduction (Current)\",\n", + " \"status\": \"✅ Complete\",\n", + " \"topics\": [\n", + " \"What is Context Engineering?\",\n", + " \"The Role of a Context Engine\",\n", + " \"Project Overview: Redis University Class Agent\"\n", + " ],\n", + " \"key_concepts\": [\"Context fundamentals\", \"Redis architecture\", \"Project structure\"]\n", + " },\n", + " {\n", + " \"section\": \"Section 2: Setting up System Context\",\n", + " \"status\": \"📚 Next\",\n", + " \"topics\": [\n", + " \"Prepping the System Context\",\n", + " \"Defining Available Tools\"\n", + " ],\n", + " \"key_concepts\": [\"System prompts\", \"Tool integration\", \"Agent capabilities\"]\n", + " },\n", + " {\n", + " \"section\": \"Section 3: Memory Management\",\n", + " \"status\": \"🔜 Coming\",\n", + " \"topics\": [\n", + " \"Working Memory with Extraction Strategies\",\n", + " \"Long-term Memory\",\n", + " \"Memory Integration\",\n", + " \"Memory Tools\"\n", + " ],\n", + " \"key_concepts\": [\"Memory types\", \"Consolidation\", \"Retrieval strategies\", \"Tool-based memory\"]\n", + " },\n", + " {\n", + " \"section\": \"Section 4: Optimizations\",\n", + " \"status\": \"🔜 Coming\",\n", + " \"topics\": [\n", + " \"Context Window Management\",\n", + " \"Retrieval Strategies\",\n", + " \"Grounding with Memory\",\n", + " \"Tool Optimization\",\n", + " \"Crafting Data for LLMs\"\n", + " ],\n", + " \"key_concepts\": [\"Token budgets\", \"RAG vs summaries\", \"Grounding\", \"Tool filtering\", \"Structured views\"]\n", + " }\n", + "]\n", + "\n", + "for section in course_sections:\n", + " print(f\"\\n{section['status']} **{section['section']}**\")\n", + " print(\"\\n 📖 Topics:\")\n", + " for topic in section['topics']:\n", + " print(f\" • {topic}\")\n", + " print(\"\\n 🎯 Key Concepts:\")\n", + " for concept in section['key_concepts']:\n", + " print(f\" • {concept}\")\n", + "\n", + "print(\"\\n🎯 Learning Path:\")\n", + "learning_path = [\n", + " \"Start with the fundamentals (Section 1) ✅\",\n", + " \"Set up your development environment\",\n", + " \"Run the reference agent and explore its capabilities\",\n", + " \"Work through system context setup (Section 2)\",\n", + " \"Deep dive into memory management (Section 3)\",\n", + " \"Learn optimization techniques (Section 4)\",\n", + " \"Experiment with extending and customizing the agent\",\n", + " \"Apply concepts to your own use cases\"\n", + "]\n", + "\n", + "for i, step in enumerate(learning_path, 1):\n", + " print(f\" {i}. {step}\")\n", + "\n", + "print(\"\\n💡 Pro Tips:\")\n", + "tips = [\n", + " \"Run the code examples as you read through the notebooks\",\n", + " \"Experiment with different queries and parameters\",\n", + " \"Read the source code to understand implementation details\",\n", + " \"Try modifying the agent for your own domain\",\n", + " \"Join the Redis community for support and discussions\"\n", + "]\n", + "\n", + "for tip in tips:\n", + " print(f\" 💡 {tip}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Conclusion\n", + "\n", + "The Redis University Class Agent represents a comprehensive example of context engineering in practice. It demonstrates how to build intelligent, context-aware AI systems that can:\n", + "\n", + "- **Remember and learn** from user interactions\n", + "- **Provide personalized experiences** based on individual needs\n", + "- **Scale efficiently** using Redis as the context engine\n", + "- **Integrate seamlessly** with modern AI frameworks\n", + "- **Maintain consistency** across multiple sessions and conversations\n", + "\n", + "As we progress through this course, you'll gain hands-on experience with each component of the system, learning not just how to build context-aware AI agents, but understanding the principles and patterns that make them effective.\n", + "\n", + "## Ready to Continue?\n", + "\n", + "Now that you understand the project overview and architecture, you're ready to dive into the technical implementation. In **Section 2: Setting up System Context**, we'll explore:\n", + "\n", + "- How to define what your AI agent should know about itself\n", + "- Techniques for crafting effective system prompts\n", + "- Methods for defining and managing agent tools\n", + "- Best practices for setting capability boundaries\n", + "\n", + "Let's continue building your expertise in context engineering! 🚀" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/python-recipes/context-engineering/notebooks/section-2-system-context/01_system_instructions.ipynb b/python-recipes/context-engineering/notebooks/section-2-system-context/01_system_instructions.ipynb new file mode 100644 index 00000000..e819449a --- /dev/null +++ b/python-recipes/context-engineering/notebooks/section-2-system-context/01_system_instructions.ipynb @@ -0,0 +1,420 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# System Instructions: Crafting Effective System Prompts\n", + "\n", + "## Introduction\n", + "\n", + "In this notebook, you'll learn how to craft effective system prompts that define your agent's behavior, personality, and capabilities. System instructions are the foundation of your agent's context - they tell the LLM what it is, what it can do, and how it should behave.\n", + "\n", + "### What You'll Learn\n", + "\n", + "- What system instructions are and why they matter\n", + "- What belongs in system context vs. retrieved context\n", + "- How to structure effective system prompts\n", + "- How to set agent personality and constraints\n", + "- How different instructions affect agent behavior\n", + "\n", + "### Prerequisites\n", + "\n", + "- Completed Section 1 notebooks\n", + "- Redis 8 running locally\n", + "- OpenAI API key set" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Concepts: System Instructions\n", + "\n", + "### What Are System Instructions?\n", + "\n", + "System instructions (also called system prompts) are the **persistent context** that defines your agent's identity and behavior. They are included in every conversation turn and tell the LLM:\n", + "\n", + "1. **Who it is** - Role and identity\n", + "2. **What it can do** - Capabilities and tools\n", + "3. **How it should behave** - Personality and constraints\n", + "4. **What it knows** - Domain knowledge and context\n", + "\n", + "### System Context vs. Retrieved Context\n", + "\n", + "| System Context | Retrieved Context |\n", + "|----------------|-------------------|\n", + "| **Static** - Same for every turn | **Dynamic** - Changes per query |\n", + "| **Role & behavior** | **Specific facts** |\n", + "| **Always included** | **Conditionally included** |\n", + "| **Examples:** Agent role, capabilities, guidelines | **Examples:** Course details, user preferences, memories |\n", + "\n", + "### Why System Instructions Matter\n", + "\n", + "Good system instructions:\n", + "- ✅ Keep the agent focused on its purpose\n", + "- ✅ Prevent unwanted behaviors\n", + "- ✅ Ensure consistent personality\n", + "- ✅ Guide tool usage\n", + "- ✅ Set user expectations\n", + "\n", + "Poor system instructions:\n", + "- ❌ Lead to off-topic responses\n", + "- ❌ Cause inconsistent behavior\n", + "- ❌ Result in tool misuse\n", + "- ❌ Create confused or unhelpful agents" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from langchain_openai import ChatOpenAI\n", + "from langchain_core.messages import SystemMessage, HumanMessage\n", + "\n", + "# Initialize LLM\n", + "llm = ChatOpenAI(model=\"gpt-4o\", temperature=0.7)\n", + "\n", + "print(\"✅ Setup complete!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Hands-on: Building System Instructions\n", + "\n", + "Let's build system instructions for our Redis University Class Agent step by step." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example 1: Minimal System Instructions\n", + "\n", + "Let's start with the bare minimum and see what happens." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Minimal system prompt\n", + "minimal_prompt = \"You are a helpful assistant.\"\n", + "\n", + "# Test it\n", + "messages = [\n", + " SystemMessage(content=minimal_prompt),\n", + " HumanMessage(content=\"I need help planning my classes for next semester.\")\n", + "]\n", + "\n", + "response = llm.invoke(messages)\n", + "print(\"Response with minimal instructions:\")\n", + "print(response.content)\n", + "print(\"\\n\" + \"=\"*80 + \"\\n\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Problem:** The agent doesn't know it's a class scheduling agent. It might give generic advice instead of using our course catalog and tools." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example 2: Adding Role and Purpose" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Add role and purpose\n", + "role_prompt = \"\"\"You are the Redis University Class Agent.\n", + "\n", + "Your role is to help students:\n", + "- Find courses that match their interests and requirements\n", + "- Plan their academic schedule\n", + "- Check prerequisites and eligibility\n", + "- Get personalized course recommendations\n", + "\"\"\"\n", + "\n", + "# Test it\n", + "messages = [\n", + " SystemMessage(content=role_prompt),\n", + " HumanMessage(content=\"I need help planning my classes for next semester.\")\n", + "]\n", + "\n", + "response = llm.invoke(messages)\n", + "print(\"Response with role and purpose:\")\n", + "print(response.content)\n", + "print(\"\\n\" + \"=\"*80 + \"\\n\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Better!** The agent now understands its role, but it still doesn't know about our tools or how to behave." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example 3: Adding Behavioral Guidelines" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Add behavioral guidelines\n", + "behavior_prompt = \"\"\"You are the Redis University Class Agent.\n", + "\n", + "Your role is to help students:\n", + "- Find courses that match their interests and requirements\n", + "- Plan their academic schedule\n", + "- Check prerequisites and eligibility\n", + "- Get personalized course recommendations\n", + "\n", + "Guidelines:\n", + "- Be helpful, friendly, and encouraging\n", + "- Ask clarifying questions when needed\n", + "- Provide specific course recommendations with details\n", + "- Explain prerequisites and requirements clearly\n", + "- Stay focused on course planning and scheduling\n", + "- If asked about topics outside your domain, politely redirect to course planning\n", + "\"\"\"\n", + "\n", + "# Test with an off-topic question\n", + "messages = [\n", + " SystemMessage(content=behavior_prompt),\n", + " HumanMessage(content=\"What's the weather like today?\")\n", + "]\n", + "\n", + "response = llm.invoke(messages)\n", + "print(\"Response to off-topic question:\")\n", + "print(response.content)\n", + "print(\"\\n\" + \"=\"*80 + \"\\n\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Great!** The agent now stays focused on its purpose and redirects off-topic questions." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example 4: Complete System Instructions\n", + "\n", + "Let's build the complete system instructions for our agent." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Complete system instructions\n", + "complete_prompt = \"\"\"You are the Redis University Class Agent, powered by Redis and the Agent Memory Server.\n", + "\n", + "Your role is to help students:\n", + "- Find courses that match their interests and requirements\n", + "- Plan their academic schedule for upcoming semesters\n", + "- Check prerequisites and course eligibility\n", + "- Get personalized course recommendations based on their goals\n", + "\n", + "You have access to:\n", + "- A complete course catalog with descriptions, prerequisites, and schedules\n", + "- Student preferences and goals (stored in long-term memory)\n", + "- Conversation history (stored in working memory)\n", + "- Tools to search courses and check prerequisites\n", + "\n", + "Guidelines:\n", + "- Be helpful, friendly, and encouraging\n", + "- Ask clarifying questions when you need more information\n", + "- Provide specific course recommendations with course codes and details\n", + "- Explain prerequisites and requirements clearly\n", + "- Remember student preferences and reference them in future conversations\n", + "- Stay focused on course planning and scheduling\n", + "- If asked about topics outside your domain, politely redirect to course planning\n", + "\n", + "Example interactions:\n", + "- Student: \"I'm interested in machine learning\"\n", + " You: \"Great! I can help you find ML courses. What's your current year and have you taken any programming courses?\"\n", + "\n", + "- Student: \"What are the prerequisites for CS401?\"\n", + " You: \"Let me check that for you.\" [Use check_prerequisites tool]\n", + "\"\"\"\n", + "\n", + "print(\"Complete system instructions:\")\n", + "print(complete_prompt)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Testing: Compare Different Instructions\n", + "\n", + "Let's test how different system instructions affect agent behavior." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Test query\n", + "test_query = \"I want to learn about databases but I'm not sure where to start.\"\n", + "\n", + "# Test with different prompts\n", + "prompts = {\n", + " \"Minimal\": minimal_prompt,\n", + " \"With Role\": role_prompt,\n", + " \"With Behavior\": behavior_prompt,\n", + " \"Complete\": complete_prompt\n", + "}\n", + "\n", + "for name, prompt in prompts.items():\n", + " messages = [\n", + " SystemMessage(content=prompt),\n", + " HumanMessage(content=test_query)\n", + " ]\n", + " response = llm.invoke(messages)\n", + " print(f\"\\n{'='*80}\")\n", + " print(f\"{name} Instructions:\")\n", + " print(f\"{'='*80}\")\n", + " print(response.content)\n", + " print()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Key Takeaways\n", + "\n", + "### What to Include in System Instructions\n", + "\n", + "1. **Identity & Role**\n", + " - Who the agent is\n", + " - What domain it operates in\n", + "\n", + "2. **Capabilities**\n", + " - What the agent can do\n", + " - What tools/data it has access to\n", + "\n", + "3. **Behavioral Guidelines**\n", + " - How to interact with users\n", + " - When to ask questions\n", + " - How to handle edge cases\n", + "\n", + "4. **Constraints**\n", + " - What the agent should NOT do\n", + " - How to handle out-of-scope requests\n", + "\n", + "5. **Examples** (optional)\n", + " - Sample interactions\n", + " - Expected behavior patterns\n", + "\n", + "### Best Practices\n", + "\n", + "✅ **Do:**\n", + "- Be specific about the agent's role\n", + "- Include clear behavioral guidelines\n", + "- Set boundaries for out-of-scope requests\n", + "- Use examples to clarify expected behavior\n", + "- Keep instructions concise but complete\n", + "\n", + "❌ **Don't:**\n", + "- Include dynamic data (use retrieved context instead)\n", + "- Make instructions too long (wastes tokens)\n", + "- Be vague about capabilities\n", + "- Forget to set constraints\n", + "- Include contradictory guidelines" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Exercises\n", + "\n", + "1. **Modify the system instructions** to make the agent more formal and academic in tone. Test it with a few queries.\n", + "\n", + "2. **Add a constraint** that the agent should always ask about the student's year (freshman, sophomore, etc.) before recommending courses. Test if it follows this constraint.\n", + "\n", + "3. **Create system instructions** for a different type of agent (e.g., a library assistant, a gym trainer, a recipe recommender). What changes?\n", + "\n", + "4. **Test edge cases**: Try to make the agent break its guidelines. What happens? How can you improve the instructions?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "In this notebook, you learned:\n", + "\n", + "- ✅ System instructions define your agent's identity, capabilities, and behavior\n", + "- ✅ System context is static (same every turn) vs. retrieved context is dynamic\n", + "- ✅ Good instructions include: role, capabilities, guidelines, constraints, and examples\n", + "- ✅ Instructions significantly affect agent behavior and consistency\n", + "- ✅ Start simple and iterate based on testing\n", + "\n", + "**Next:** In the next notebook, we'll define tools that give our agent actual capabilities to search courses and check prerequisites." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} + diff --git a/python-recipes/context-engineering/notebooks/section-2-system-context/02_defining_tools.ipynb b/python-recipes/context-engineering/notebooks/section-2-system-context/02_defining_tools.ipynb new file mode 100644 index 00000000..eb851b17 --- /dev/null +++ b/python-recipes/context-engineering/notebooks/section-2-system-context/02_defining_tools.ipynb @@ -0,0 +1,548 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Defining Tools: Giving Your Agent Capabilities\n", + "\n", + "## Introduction\n", + "\n", + "In this notebook, you'll learn how to define tools that give your agent real capabilities beyond just conversation. Tools allow the LLM to take actions, retrieve data, and interact with external systems.\n", + "\n", + "### What You'll Learn\n", + "\n", + "- What tools are and why they're essential for agents\n", + "- How to define tools with proper schemas\n", + "- How the LLM knows which tool to use\n", + "- How tool descriptions affect LLM behavior\n", + "- Best practices for tool design\n", + "\n", + "### Prerequisites\n", + "\n", + "- Completed `01_system_instructions.ipynb`\n", + "- Redis 8 running locally\n", + "- OpenAI API key set\n", + "- Course data ingested (from Section 1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Concepts: Tools for AI Agents\n", + "\n", + "### What Are Tools?\n", + "\n", + "Tools are **functions that the LLM can call** to perform actions or retrieve information. They extend the agent's capabilities beyond text generation.\n", + "\n", + "**Without tools:**\n", + "- Agent can only generate text based on its training data\n", + "- No access to real-time data\n", + "- Can't take actions\n", + "- Limited to what's in the prompt\n", + "\n", + "**With tools:**\n", + "- Agent can search databases\n", + "- Agent can retrieve current information\n", + "- Agent can perform calculations\n", + "- Agent can take actions (send emails, create records, etc.)\n", + "\n", + "### How Tool Calling Works\n", + "\n", + "1. **LLM receives** user query + system instructions + available tools\n", + "2. **LLM decides** which tool(s) to call (if any)\n", + "3. **LLM generates** tool call with parameters\n", + "4. **System executes** the tool function\n", + "5. **Tool returns** results\n", + "6. **LLM receives** results and generates response\n", + "\n", + "### Tool Schema Components\n", + "\n", + "Every tool needs:\n", + "1. **Name** - Unique identifier\n", + "2. **Description** - What the tool does (critical for selection!)\n", + "3. **Parameters** - Input schema with types and descriptions\n", + "4. **Function** - The actual implementation\n", + "\n", + "### How LLMs Select Tools\n", + "\n", + "The LLM uses:\n", + "- Tool **names** (should be descriptive)\n", + "- Tool **descriptions** (should explain when to use it)\n", + "- Parameter **descriptions** (should explain what each parameter does)\n", + "- **Context** from the conversation\n", + "\n", + "**Key insight:** The LLM only sees the tool schema, not the implementation!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from typing import List, Optional\n", + "from langchain_openai import ChatOpenAI\n", + "from langchain_core.messages import SystemMessage, HumanMessage, AIMessage\n", + "from langchain_core.tools import tool\n", + "from pydantic import BaseModel, Field\n", + "\n", + "# Import our course manager\n", + "from redis_context_course import CourseManager\n", + "\n", + "# Initialize\n", + "llm = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n", + "course_manager = CourseManager()\n", + "\n", + "print(\"✅ Setup complete!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Hands-on: Defining Tools\n", + "\n", + "Let's define tools for our class agent step by step." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Tool 1: Search Courses (Basic)\n", + "\n", + "Let's start with a basic tool to search courses." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Define parameter schema\n", + "class SearchCoursesInput(BaseModel):\n", + " query: str = Field(description=\"Search query for courses\")\n", + " limit: int = Field(default=5, description=\"Maximum number of results\")\n", + "\n", + "# Define the tool\n", + "@tool(args_schema=SearchCoursesInput)\n", + "async def search_courses_basic(query: str, limit: int = 5) -> str:\n", + " \"\"\"Search for courses in the catalog.\"\"\"\n", + " results = await course_manager.search_courses(query, limit=limit)\n", + " \n", + " if not results:\n", + " return \"No courses found matching your query.\"\n", + " \n", + " output = []\n", + " for course in results:\n", + " output.append(\n", + " f\"{course.course_code}: {course.title}\\n\"\n", + " f\" Credits: {course.credits} | {course.format.value}\\n\"\n", + " f\" {course.description[:100]}...\"\n", + " )\n", + " \n", + " return \"\\n\\n\".join(output)\n", + "\n", + "print(\"Tool defined:\", search_courses_basic.name)\n", + "print(\"Description:\", search_courses_basic.description)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Problem:** The description is too vague! The LLM won't know when to use this tool." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Tool 1: Search Courses (Improved)\n", + "\n", + "Let's improve the description to help the LLM understand when to use this tool." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "@tool(args_schema=SearchCoursesInput)\n", + "async def search_courses(query: str, limit: int = 5) -> str:\n", + " \"\"\"\n", + " Search for courses in the Redis University catalog using semantic search.\n", + " \n", + " Use this tool when students ask about:\n", + " - Finding courses on a specific topic (e.g., \"machine learning courses\")\n", + " - Courses in a department (e.g., \"computer science courses\")\n", + " - Courses with specific characteristics (e.g., \"online courses\", \"3-credit courses\")\n", + " \n", + " The search uses semantic matching, so natural language queries work well.\n", + " \"\"\"\n", + " results = await course_manager.search_courses(query, limit=limit)\n", + " \n", + " if not results:\n", + " return \"No courses found matching your query.\"\n", + " \n", + " output = []\n", + " for course in results:\n", + " output.append(\n", + " f\"{course.course_code}: {course.title}\\n\"\n", + " f\" Credits: {course.credits} | {course.format.value} | {course.difficulty_level.value}\\n\"\n", + " f\" {course.description[:150]}...\"\n", + " )\n", + " \n", + " return \"\\n\\n\".join(output)\n", + "\n", + "print(\"✅ Improved tool defined!\")\n", + "print(\"\\nDescription:\")\n", + "print(search_courses.description)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Tool 2: Get Course Details\n", + "\n", + "A tool to get detailed information about a specific course." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class GetCourseDetailsInput(BaseModel):\n", + " course_code: str = Field(description=\"Course code (e.g., 'CS101', 'MATH201')\")\n", + "\n", + "@tool(args_schema=GetCourseDetailsInput)\n", + "async def get_course_details(course_code: str) -> str:\n", + " \"\"\"\n", + " Get detailed information about a specific course by its course code.\n", + " \n", + " Use this tool when:\n", + " - Student asks about a specific course (e.g., \"Tell me about CS101\")\n", + " - You need prerequisites for a course\n", + " - You need full course details (schedule, instructor, etc.)\n", + " \n", + " Returns complete course information including description, prerequisites,\n", + " schedule, credits, and learning objectives.\n", + " \"\"\"\n", + " course = await course_manager.get_course(course_code)\n", + " \n", + " if not course:\n", + " return f\"Course {course_code} not found.\"\n", + " \n", + " prereqs = \"None\" if not course.prerequisites else \", \".join(\n", + " [f\"{p.course_code} (min grade: {p.min_grade})\" for p in course.prerequisites]\n", + " )\n", + " \n", + " return f\"\"\"\n", + "{course.course_code}: {course.title}\n", + "\n", + "Description: {course.description}\n", + "\n", + "Details:\n", + "- Credits: {course.credits}\n", + "- Department: {course.department}\n", + "- Major: {course.major}\n", + "- Difficulty: {course.difficulty_level.value}\n", + "- Format: {course.format.value}\n", + "- Prerequisites: {prereqs}\n", + "\n", + "Learning Objectives:\n", + "\"\"\" + \"\\n\".join([f\"- {obj}\" for obj in course.learning_objectives])\n", + "\n", + "print(\"✅ Tool defined:\", get_course_details.name)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Tool 3: Check Prerequisites\n", + "\n", + "A tool to check if a student meets the prerequisites for a course." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class CheckPrerequisitesInput(BaseModel):\n", + " course_code: str = Field(description=\"Course code to check prerequisites for\")\n", + " completed_courses: List[str] = Field(\n", + " description=\"List of course codes the student has completed\"\n", + " )\n", + "\n", + "@tool(args_schema=CheckPrerequisitesInput)\n", + "async def check_prerequisites(course_code: str, completed_courses: List[str]) -> str:\n", + " \"\"\"\n", + " Check if a student meets the prerequisites for a specific course.\n", + " \n", + " Use this tool when:\n", + " - Student asks \"Can I take [course]?\"\n", + " - Student asks about prerequisites\n", + " - You need to verify eligibility before recommending a course\n", + " \n", + " Returns whether the student is eligible and which prerequisites are missing (if any).\n", + " \"\"\"\n", + " course = await course_manager.get_course(course_code)\n", + " \n", + " if not course:\n", + " return f\"Course {course_code} not found.\"\n", + " \n", + " if not course.prerequisites:\n", + " return f\"✅ {course_code} has no prerequisites. You can take this course!\"\n", + " \n", + " missing = []\n", + " for prereq in course.prerequisites:\n", + " if prereq.course_code not in completed_courses:\n", + " missing.append(f\"{prereq.course_code} (min grade: {prereq.min_grade})\")\n", + " \n", + " if not missing:\n", + " return f\"✅ You meet all prerequisites for {course_code}!\"\n", + " \n", + " return f\"\"\"❌ You're missing prerequisites for {course_code}:\n", + "\n", + "Missing:\n", + "\"\"\" + \"\\n\".join([f\"- {p}\" for p in missing])\n", + "\n", + "print(\"✅ Tool defined:\", check_prerequisites.name)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Testing: Using Tools with an Agent\n", + "\n", + "Let's test our tools with the LLM to see how it selects and uses them." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Bind tools to LLM\n", + "tools = [search_courses, get_course_details, check_prerequisites]\n", + "llm_with_tools = llm.bind_tools(tools)\n", + "\n", + "# System prompt\n", + "system_prompt = \"\"\"You are the Redis University Class Agent.\n", + "Help students find courses and plan their schedule.\n", + "Use the available tools to search courses and check prerequisites.\n", + "\"\"\"\n", + "\n", + "print(\"✅ Agent configured with tools!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Test 1: Search Query" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "messages = [\n", + " SystemMessage(content=system_prompt),\n", + " HumanMessage(content=\"I'm interested in machine learning courses\")\n", + "]\n", + "\n", + "response = llm_with_tools.invoke(messages)\n", + "\n", + "print(\"User: I'm interested in machine learning courses\")\n", + "print(\"\\nAgent decision:\")\n", + "if response.tool_calls:\n", + " for tool_call in response.tool_calls:\n", + " print(f\" Tool: {tool_call['name']}\")\n", + " print(f\" Args: {tool_call['args']}\")\n", + "else:\n", + " print(\" No tool called\")\n", + " print(f\" Response: {response.content}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Test 2: Specific Course Query" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "messages = [\n", + " SystemMessage(content=system_prompt),\n", + " HumanMessage(content=\"Tell me about CS401\")\n", + "]\n", + "\n", + "response = llm_with_tools.invoke(messages)\n", + "\n", + "print(\"User: Tell me about CS401\")\n", + "print(\"\\nAgent decision:\")\n", + "if response.tool_calls:\n", + " for tool_call in response.tool_calls:\n", + " print(f\" Tool: {tool_call['name']}\")\n", + " print(f\" Args: {tool_call['args']}\")\n", + "else:\n", + " print(\" No tool called\")\n", + " print(f\" Response: {response.content}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Test 3: Prerequisites Query" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "messages = [\n", + " SystemMessage(content=system_prompt),\n", + " HumanMessage(content=\"Can I take CS401? I've completed CS101 and CS201.\")\n", + "]\n", + "\n", + "response = llm_with_tools.invoke(messages)\n", + "\n", + "print(\"User: Can I take CS401? I've completed CS101 and CS201.\")\n", + "print(\"\\nAgent decision:\")\n", + "if response.tool_calls:\n", + " for tool_call in response.tool_calls:\n", + " print(f\" Tool: {tool_call['name']}\")\n", + " print(f\" Args: {tool_call['args']}\")\n", + "else:\n", + " print(\" No tool called\")\n", + " print(f\" Response: {response.content}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Key Takeaways\n", + "\n", + "### Tool Design Best Practices\n", + "\n", + "1. **Clear Names**\n", + " - Use descriptive, action-oriented names\n", + " - `search_courses` ✅ vs. `find` ❌\n", + "\n", + "2. **Detailed Descriptions**\n", + " - Explain what the tool does\n", + " - Explain when to use it\n", + " - Include examples\n", + "\n", + "3. **Well-Defined Parameters**\n", + " - Use type hints\n", + " - Add descriptions for each parameter\n", + " - Set sensible defaults\n", + "\n", + "4. **Useful Return Values**\n", + " - Return formatted, readable text\n", + " - Include relevant details\n", + " - Handle errors gracefully\n", + "\n", + "5. **Single Responsibility**\n", + " - Each tool should do one thing well\n", + " - Don't combine unrelated functionality\n", + "\n", + "### How Tool Descriptions Affect Selection\n", + "\n", + "The LLM relies heavily on tool descriptions to decide which tool to use:\n", + "\n", + "- ✅ **Good description**: \"Search for courses using semantic search. Use when students ask about topics, departments, or course characteristics.\"\n", + "- ❌ **Bad description**: \"Search courses\"\n", + "\n", + "**Remember:** The LLM can't see your code, only the schema!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Exercises\n", + "\n", + "1. **Add a new tool** called `get_courses_by_department` that returns all courses in a specific department. Write a good description.\n", + "\n", + "2. **Test tool selection**: Create queries that should trigger each of your three tools. Does the LLM select correctly?\n", + "\n", + "3. **Improve a description**: Take the `search_courses_basic` tool and improve its description. Test if it changes LLM behavior.\n", + "\n", + "4. **Create a tool** for getting a student's current schedule. What parameters does it need? What should it return?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "In this notebook, you learned:\n", + "\n", + "- ✅ Tools extend agent capabilities beyond text generation\n", + "- ✅ Tool schemas include name, description, parameters, and implementation\n", + "- ✅ LLMs select tools based on descriptions and context\n", + "- ✅ Good descriptions are critical for correct tool selection\n", + "- ✅ Each tool should have a single, clear purpose\n", + "\n", + "**Next:** In Section 3, we'll add memory to our agent so it can remember user preferences and past conversations." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} + diff --git a/python-recipes/context-engineering/notebooks/section-2-system-context/03_tool_selection_strategies.ipynb b/python-recipes/context-engineering/notebooks/section-2-system-context/03_tool_selection_strategies.ipynb new file mode 100644 index 00000000..eebebe46 --- /dev/null +++ b/python-recipes/context-engineering/notebooks/section-2-system-context/03_tool_selection_strategies.ipynb @@ -0,0 +1,622 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Tool Selection Strategies: Improving Tool Choice\n", + "\n", + "## Introduction\n", + "\n", + "In this advanced notebook, you'll learn strategies to improve how LLMs select tools. When you have many tools, the LLM can get confused about which one to use. You'll learn techniques to make tool selection more reliable and accurate.\n", + "\n", + "### What You'll Learn\n", + "\n", + "- Common tool selection failures\n", + "- Strategies to improve tool selection\n", + "- Clear naming conventions\n", + "- Detailed descriptions with examples\n", + "- Testing and debugging tool selection\n", + "\n", + "### Prerequisites\n", + "\n", + "- Completed `02_defining_tools.ipynb`\n", + "- Redis 8 running locally\n", + "- OpenAI API key set\n", + "- Course data ingested" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Concepts: Tool Selection Challenges\n", + "\n", + "### The Problem\n", + "\n", + "As you add more tools, the LLM faces challenges:\n", + "\n", + "**With 3 tools:**\n", + "- ✅ Easy to choose\n", + "- ✅ Clear distinctions\n", + "\n", + "**With 10+ tools:**\n", + "- ⚠️ Similar-sounding tools\n", + "- ⚠️ Overlapping functionality\n", + "- ⚠️ Ambiguous queries\n", + "- ⚠️ Wrong tool selection\n", + "\n", + "### Common Tool Selection Failures\n", + "\n", + "**1. Similar Names**\n", + "```python\n", + "# Bad: Confusing names\n", + "get_course() # Get one course?\n", + "get_courses() # Get multiple courses?\n", + "search_course() # Search for courses?\n", + "find_courses() # Find courses?\n", + "```\n", + "\n", + "**2. Vague Descriptions**\n", + "```python\n", + "# Bad: Too vague\n", + "def search_courses():\n", + " \"\"\"Search for courses.\"\"\"\n", + " \n", + "# Good: Specific\n", + "def search_courses():\n", + " \"\"\"Search for courses using semantic search.\n", + " Use when students ask about topics, departments, or characteristics.\n", + " Example: 'machine learning courses' or 'online courses'\n", + " \"\"\"\n", + "```\n", + "\n", + "**3. Overlapping Functionality**\n", + "```python\n", + "# Bad: Unclear when to use which\n", + "search_courses(query) # Semantic search\n", + "filter_courses(department) # Filter by department\n", + "find_courses_by_topic(topic) # Find by topic\n", + "\n", + "# Good: One tool with clear parameters\n", + "search_courses(query, filters) # One tool, clear purpose\n", + "```\n", + "\n", + "### How LLMs Select Tools\n", + "\n", + "The LLM considers:\n", + "1. **Tool name** - First impression\n", + "2. **Tool description** - Main decision factor\n", + "3. **Parameter descriptions** - Confirms choice\n", + "4. **Context** - User's query and conversation\n", + "\n", + "**Key insight:** The LLM can't see your code, only the schema!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from typing import List, Optional, Dict, Any\n", + "from langchain_openai import ChatOpenAI\n", + "from langchain_core.messages import SystemMessage, HumanMessage\n", + "from langchain_core.tools import tool\n", + "from pydantic import BaseModel, Field\n", + "from redis_context_course import CourseManager\n", + "\n", + "# Initialize\n", + "llm = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n", + "course_manager = CourseManager()\n", + "\n", + "print(\"✅ Setup complete\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Strategy 1: Clear Naming Conventions\n", + "\n", + "Use consistent, descriptive names that clearly indicate what the tool does." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Bad Example: Confusing Names" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Bad: Confusing, similar names\n", + "class GetCourseInput(BaseModel):\n", + " code: str = Field(description=\"Course code\")\n", + "\n", + "@tool(args_schema=GetCourseInput)\n", + "async def get(code: str) -> str:\n", + " \"\"\"Get a course.\"\"\"\n", + " course = await course_manager.get_course(code)\n", + " return str(course) if course else \"Not found\"\n", + "\n", + "@tool(args_schema=GetCourseInput)\n", + "async def fetch(code: str) -> str:\n", + " \"\"\"Fetch a course.\"\"\"\n", + " course = await course_manager.get_course(code)\n", + " return str(course) if course else \"Not found\"\n", + "\n", + "@tool(args_schema=GetCourseInput)\n", + "async def retrieve(code: str) -> str:\n", + " \"\"\"Retrieve a course.\"\"\"\n", + " course = await course_manager.get_course(code)\n", + " return str(course) if course else \"Not found\"\n", + "\n", + "print(\"❌ BAD: Three tools that do the same thing with vague names!\")\n", + "print(\" - get, fetch, retrieve - which one to use?\")\n", + "print(\" - LLM will be confused\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Good Example: Clear, Descriptive Names" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Good: Clear, specific names\n", + "class SearchCoursesInput(BaseModel):\n", + " query: str = Field(description=\"Natural language search query\")\n", + " limit: int = Field(default=5, description=\"Max results\")\n", + "\n", + "@tool(args_schema=SearchCoursesInput)\n", + "async def search_courses_by_topic(query: str, limit: int = 5) -> str:\n", + " \"\"\"Search courses using semantic search based on topics or descriptions.\"\"\"\n", + " results = await course_manager.search_courses(query, limit=limit)\n", + " return \"\\n\".join([f\"{c.course_code}: {c.title}\" for c in results])\n", + "\n", + "class GetCourseDetailsInput(BaseModel):\n", + " course_code: str = Field(description=\"Specific course code like 'CS101'\")\n", + "\n", + "@tool(args_schema=GetCourseDetailsInput)\n", + "async def get_course_details_by_code(course_code: str) -> str:\n", + " \"\"\"Get detailed information about a specific course by its course code.\"\"\"\n", + " course = await course_manager.get_course(course_code)\n", + " return str(course) if course else \"Course not found\"\n", + "\n", + "class ListCoursesInput(BaseModel):\n", + " department: str = Field(description=\"Department code like 'CS' or 'MATH'\")\n", + "\n", + "@tool(args_schema=ListCoursesInput)\n", + "async def list_courses_by_department(department: str) -> str:\n", + " \"\"\"List all courses in a specific department.\"\"\"\n", + " # Implementation would filter by department\n", + " return f\"Courses in {department} department\"\n", + "\n", + "print(\"✅ GOOD: Clear, specific names that indicate purpose\")\n", + "print(\" - search_courses_by_topic: For semantic search\")\n", + "print(\" - get_course_details_by_code: For specific course\")\n", + "print(\" - list_courses_by_department: For department listing\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Strategy 2: Detailed Descriptions with Examples\n", + "\n", + "Write descriptions that explain WHEN to use the tool, not just WHAT it does." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Bad Example: Vague Description" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Bad: Vague description\n", + "@tool(args_schema=SearchCoursesInput)\n", + "async def search_courses_bad(query: str, limit: int = 5) -> str:\n", + " \"\"\"Search for courses.\"\"\"\n", + " results = await course_manager.search_courses(query, limit=limit)\n", + " return \"\\n\".join([f\"{c.course_code}: {c.title}\" for c in results])\n", + "\n", + "print(\"❌ BAD: 'Search for courses' - too vague!\")\n", + "print(\" - When should I use this?\")\n", + "print(\" - What kind of search?\")\n", + "print(\" - What queries work?\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Good Example: Detailed Description with Examples" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Good: Detailed description with examples\n", + "@tool(args_schema=SearchCoursesInput)\n", + "async def search_courses_good(query: str, limit: int = 5) -> str:\n", + " \"\"\"\n", + " Search for courses using semantic search based on topics, descriptions, or characteristics.\n", + " \n", + " Use this tool when students ask about:\n", + " - Topics or subjects: \"machine learning courses\", \"database courses\"\n", + " - Course characteristics: \"online courses\", \"beginner courses\", \"3-credit courses\"\n", + " - General exploration: \"what courses are available in AI?\"\n", + " \n", + " Do NOT use this tool when:\n", + " - Student asks about a specific course code (use get_course_details_by_code instead)\n", + " - Student wants all courses in a department (use list_courses_by_department instead)\n", + " \n", + " The search uses semantic matching, so natural language queries work well.\n", + " \n", + " Examples:\n", + " - \"machine learning courses\" → finds CS401, CS402, etc.\n", + " - \"beginner programming\" → finds CS101, CS102, etc.\n", + " - \"online data science courses\" → finds online courses about data science\n", + " \"\"\"\n", + " results = await course_manager.search_courses(query, limit=limit)\n", + " return \"\\n\".join([f\"{c.course_code}: {c.title}\" for c in results])\n", + "\n", + "print(\"✅ GOOD: Detailed description with:\")\n", + "print(\" - What it does\")\n", + "print(\" - When to use it\")\n", + "print(\" - When NOT to use it\")\n", + "print(\" - Examples of good queries\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Strategy 3: Parameter Descriptions\n", + "\n", + "Add detailed descriptions to parameters to guide the LLM." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Bad: Minimal parameter descriptions\n", + "class BadInput(BaseModel):\n", + " query: str\n", + " limit: int\n", + "\n", + "print(\"❌ BAD: No parameter descriptions\")\n", + "print()\n", + "\n", + "# Good: Detailed parameter descriptions\n", + "class GoodInput(BaseModel):\n", + " query: str = Field(\n", + " description=\"Natural language search query. Can be topics (e.g., 'machine learning'), \"\n", + " \"characteristics (e.g., 'online courses'), or general questions \"\n", + " \"(e.g., 'beginner programming courses')\"\n", + " )\n", + " limit: int = Field(\n", + " default=5,\n", + " description=\"Maximum number of results to return. Default is 5. \"\n", + " \"Use 3 for quick answers, 10 for comprehensive results.\"\n", + " )\n", + "\n", + "print(\"✅ GOOD: Detailed parameter descriptions\")\n", + "print(\" - Explains what the parameter is\")\n", + "print(\" - Gives examples\")\n", + "print(\" - Suggests values\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Testing Tool Selection\n", + "\n", + "Let's test how well the LLM selects tools with different queries." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create tools with good descriptions\n", + "tools = [\n", + " search_courses_good,\n", + " get_course_details_by_code,\n", + " list_courses_by_department\n", + "]\n", + "\n", + "llm_with_tools = llm.bind_tools(tools)\n", + "\n", + "# Test queries\n", + "test_queries = [\n", + " \"I'm interested in machine learning courses\",\n", + " \"Tell me about CS401\",\n", + " \"What courses does the Computer Science department offer?\",\n", + " \"Show me beginner programming courses\",\n", + " \"What are the prerequisites for CS301?\",\n", + "]\n", + "\n", + "print(\"=\" * 80)\n", + "print(\"TESTING TOOL SELECTION\")\n", + "print(\"=\" * 80)\n", + "\n", + "for query in test_queries:\n", + " messages = [\n", + " SystemMessage(content=\"You are a class scheduling agent. Use the appropriate tool.\"),\n", + " HumanMessage(content=query)\n", + " ]\n", + " \n", + " response = llm_with_tools.invoke(messages)\n", + " \n", + " print(f\"\\nQuery: {query}\")\n", + " if response.tool_calls:\n", + " tool_call = response.tool_calls[0]\n", + " print(f\"✅ Selected: {tool_call['name']}\")\n", + " print(f\" Args: {tool_call['args']}\")\n", + " else:\n", + " print(\"❌ No tool selected\")\n", + "\n", + "print(\"\\n\" + \"=\" * 80)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Strategy 4: Testing Edge Cases\n", + "\n", + "Test ambiguous queries to find tool selection issues." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Ambiguous queries that could match multiple tools\n", + "ambiguous_queries = [\n", + " \"What courses are available?\", # Could be search or list\n", + " \"Tell me about CS courses\", # Could be search or list\n", + " \"I want to learn programming\", # Could be search\n", + " \"CS401\", # Just a course code\n", + "]\n", + "\n", + "print(\"=\" * 80)\n", + "print(\"TESTING AMBIGUOUS QUERIES\")\n", + "print(\"=\" * 80)\n", + "\n", + "for query in ambiguous_queries:\n", + " messages = [\n", + " SystemMessage(content=\"You are a class scheduling agent. Use the appropriate tool.\"),\n", + " HumanMessage(content=query)\n", + " ]\n", + " \n", + " response = llm_with_tools.invoke(messages)\n", + " \n", + " print(f\"\\nQuery: '{query}'\")\n", + " if response.tool_calls:\n", + " tool_call = response.tool_calls[0]\n", + " print(f\"Selected: {tool_call['name']}\")\n", + " print(f\"Args: {tool_call['args']}\")\n", + " print(\"Is this the right choice? 🤔\")\n", + " else:\n", + " print(\"No tool selected - might ask for clarification\")\n", + "\n", + "print(\"\\n\" + \"=\" * 80)\n", + "print(\"💡 TIP: If selection is wrong, improve tool descriptions!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Strategy 5: Reducing Tool Confusion\n", + "\n", + "When you have many similar tools, consider consolidating them." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"=\" * 80)\n", + "print(\"CONSOLIDATING SIMILAR TOOLS\")\n", + "print(\"=\" * 80)\n", + "\n", + "print(\"\\n❌ BAD: Many similar tools\")\n", + "print(\" - search_courses_by_topic()\")\n", + "print(\" - search_courses_by_department()\")\n", + "print(\" - search_courses_by_difficulty()\")\n", + "print(\" - search_courses_by_format()\")\n", + "print(\" → LLM confused about which to use!\")\n", + "\n", + "print(\"\\n✅ GOOD: One flexible tool\")\n", + "print(\" - search_courses(query, filters={})\")\n", + "print(\" → One tool, clear purpose, flexible parameters\")\n", + "\n", + "# Example of consolidated tool\n", + "class ConsolidatedSearchInput(BaseModel):\n", + " query: str = Field(description=\"Natural language search query\")\n", + " department: Optional[str] = Field(default=None, description=\"Filter by department (e.g., 'CS')\")\n", + " difficulty: Optional[str] = Field(default=None, description=\"Filter by difficulty (beginner/intermediate/advanced)\")\n", + " format: Optional[str] = Field(default=None, description=\"Filter by format (online/in-person/hybrid)\")\n", + " limit: int = Field(default=5, description=\"Max results\")\n", + "\n", + "@tool(args_schema=ConsolidatedSearchInput)\n", + "async def search_courses_consolidated(\n", + " query: str,\n", + " department: Optional[str] = None,\n", + " difficulty: Optional[str] = None,\n", + " format: Optional[str] = None,\n", + " limit: int = 5\n", + ") -> str:\n", + " \"\"\"\n", + " Search for courses with optional filters.\n", + " \n", + " Use this tool for any course search. You can:\n", + " - Search by topic: query=\"machine learning\"\n", + " - Filter by department: department=\"CS\"\n", + " - Filter by difficulty: difficulty=\"beginner\"\n", + " - Filter by format: format=\"online\"\n", + " - Combine filters: query=\"databases\", department=\"CS\", difficulty=\"intermediate\"\n", + " \"\"\"\n", + " # Implementation would use filters\n", + " return f\"Searching for: {query} with filters\"\n", + "\n", + "print(\"\\n✅ Benefits of consolidation:\")\n", + "print(\" - Fewer tools = less confusion\")\n", + "print(\" - One clear purpose\")\n", + "print(\" - Flexible with optional parameters\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Key Takeaways\n", + "\n", + "### Naming Conventions\n", + "\n", + "✅ **Do:**\n", + "- Use descriptive, action-oriented names\n", + "- Include the object/entity in the name\n", + "- Be specific: `search_courses_by_topic` not `search`\n", + "\n", + "❌ **Don't:**\n", + "- Use vague names: `get`, `fetch`, `find`\n", + "- Create similar-sounding tools\n", + "- Use abbreviations or jargon\n", + "\n", + "### Description Best Practices\n", + "\n", + "Include:\n", + "1. **What it does** - Clear explanation\n", + "2. **When to use it** - Specific scenarios\n", + "3. **When NOT to use it** - Avoid confusion\n", + "4. **Examples** - Show expected inputs\n", + "5. **Edge cases** - Handle ambiguity\n", + "\n", + "### Parameter Descriptions\n", + "\n", + "For each parameter:\n", + "- Explain what it is\n", + "- Give examples\n", + "- Suggest typical values\n", + "- Explain constraints\n", + "\n", + "### Testing Strategy\n", + "\n", + "1. **Test typical queries** - Does it select correctly?\n", + "2. **Test edge cases** - What about ambiguous queries?\n", + "3. **Test similar queries** - Does it distinguish between tools?\n", + "4. **Iterate descriptions** - Improve based on failures\n", + "\n", + "### When to Consolidate Tools\n", + "\n", + "Consolidate when:\n", + "- ✅ Tools have similar purposes\n", + "- ✅ Differences can be parameters\n", + "- ✅ LLM gets confused\n", + "\n", + "Keep separate when:\n", + "- ✅ Fundamentally different operations\n", + "- ✅ Different return types\n", + "- ✅ Clear, distinct use cases" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Exercises\n", + "\n", + "1. **Improve a tool**: Take a tool with a vague description and rewrite it with examples and clear guidance.\n", + "\n", + "2. **Test tool selection**: Create 10 test queries and verify the LLM selects the right tool each time.\n", + "\n", + "3. **Find confusion**: Create two similar tools and test queries that could match either. How can you improve the descriptions?\n", + "\n", + "4. **Consolidate tools**: If you have 5+ similar tools, try consolidating them into 1-2 flexible tools." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "In this notebook, you learned:\n", + "\n", + "- ✅ Clear naming conventions prevent confusion\n", + "- ✅ Detailed descriptions with examples guide tool selection\n", + "- ✅ Parameter descriptions help the LLM use tools correctly\n", + "- ✅ Testing edge cases reveals selection issues\n", + "- ✅ Consolidating similar tools reduces confusion\n", + "\n", + "**Key insight:** Tool selection quality depends entirely on your descriptions. The LLM can't see your code - invest time in writing clear, detailed tool schemas with examples and guidance." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} + diff --git a/python-recipes/context-engineering/notebooks/section-3-memory/01_working_memory.ipynb b/python-recipes/context-engineering/notebooks/section-3-memory/01_working_memory.ipynb new file mode 100644 index 00000000..700665d1 --- /dev/null +++ b/python-recipes/context-engineering/notebooks/section-3-memory/01_working_memory.ipynb @@ -0,0 +1,408 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Redis](https://redis.io/wp-content/uploads/2024/04/Logotype.svg?auto=webp&quality=85,75&width=120)\n", + "\n", + "# Working Memory\n", + "\n", + "## Introduction\n", + "\n", + "This notebook demonstrates how to implement working memory, which is session-scoped data that persists across multiple turns of a conversation. Working memory stores conversation messages and task-related context, giving LLMs the knowledge they need to maintain coherent, context-aware conversations.\n", + "\n", + "### Key Concepts\n", + "\n", + "- **Working Memory**: Persistent storage for current conversation messages and task-specific context\n", + "- **Long-term Memory**: Cross-session knowledge (user preferences, important facts learned over time)\n", + "- **Session Scope**: Working memory is tied to a specific conversation session\n", + "- **Message History**: The sequence of user and assistant messages that form the conversation\n", + "\n", + "### The Problem We're Solving\n", + "\n", + "LLMs are stateless - they don't inherently remember previous messages in a conversation. Working memory solves this by:\n", + "- Storing conversation messages so the LLM can reference earlier parts of the conversation\n", + "- Maintaining task-specific context (like current goals, preferences mentioned in this session)\n", + "- Persisting this information across multiple turns of the conversation\n", + "- Providing a foundation for extracting important information to long-term storage\n", + "\n", + "Because working memory stores messages, we can extract long-term data from it. When using the Agent Memory Server, extraction happens automatically in the background based on a configured strategy that controls what kind of information gets extracted." + ] + }, + { + "cell_type": "code", + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-03T20:32:31.983697Z", + "start_time": "2025-10-03T20:32:28.032067Z" + } + }, + "source": [ + "# Install the Redis Context Course package\n", + "%pip install -q -e ../../reference-agent" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r\n", + "\u001B[1m[\u001B[0m\u001B[34;49mnotice\u001B[0m\u001B[1;39;49m]\u001B[0m\u001B[39;49m A new release of pip is available: \u001B[0m\u001B[31;49m24.3.1\u001B[0m\u001B[39;49m -> \u001B[0m\u001B[32;49m25.2\u001B[0m\r\n", + "\u001B[1m[\u001B[0m\u001B[34;49mnotice\u001B[0m\u001B[1;39;49m]\u001B[0m\u001B[39;49m To update, run: \u001B[0m\u001B[32;49mpip install --upgrade pip\u001B[0m\r\n", + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "execution_count": 10 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-03T20:32:48.128143Z", + "start_time": "2025-10-03T20:32:48.092640Z" + } + }, + "cell_type": "code", + "source": [ + "import os\n", + "from dotenv import load_dotenv\n", + "\n", + "# Load environment variables from .env file\n", + "load_dotenv()\n", + "\n", + "# Verify required environment variables are set\n", + "if not os.getenv(\"OPENAI_API_KEY\"):\n", + " raise ValueError(\n", + " \"OPENAI_API_KEY not found. Please create a .env file with your OpenAI API key. \"\n", + " \"See SETUP.md for instructions.\"\n", + " )\n", + "\n", + "print(\"✅ Environment variables loaded\")\n", + "print(f\" REDIS_URL: {os.getenv('REDIS_URL', 'redis://localhost:6379')}\")\n", + "print(f\" AGENT_MEMORY_URL: {os.getenv('AGENT_MEMORY_URL', 'http://localhost:8000')}\")\n", + "print(f\" OPENAI_API_KEY: {'✓ Set' if os.getenv('OPENAI_API_KEY') else '✗ Not set'}\")" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✅ Environment variables loaded\n", + " REDIS_URL: redis://localhost:6379\n", + " AGENT_MEMORY_URL: http://localhost:8000\n", + " OPENAI_API_KEY: ✓ Set\n" + ] + } + ], + "execution_count": 11 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## 1. Working Memory Structure\n", + "\n", + "Working memory contains the essential context for the current conversation:\n", + "\n", + "- **Messages**: The conversation history (user and assistant messages)\n", + "- **Session ID**: Identifies this specific conversation\n", + "- **User ID**: Identifies the user across sessions\n", + "- **Task Data**: Optional task-specific context (current goals, temporary state)\n", + "\n", + "This structure gives the LLM everything it needs to understand the current conversation context.\n", + "\n", + "Let's import the memory client to work with working memory:" + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-02T22:01:32.779633Z", + "start_time": "2025-10-02T22:01:32.776671Z" + } + }, + "cell_type": "code", + "source": [ + "from redis_context_course import MemoryClient\n", + "\n", + "print(\"✅ Memory server client imported successfully\")" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✅ Memory server client imported successfully\n" + ] + } + ], + "execution_count": 7 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## 2. Storing and Retrieving Conversation Context\n", + "\n", + "Let's see how working memory stores and retrieves conversation context:" + ] + }, + { + "cell_type": "code", + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-02T22:01:39.218627Z", + "start_time": "2025-10-02T22:01:39.167246Z" + } + }, + "source": [ + "import os\n", + "from agent_memory_client import MemoryClientConfig\n", + "\n", + "# Initialize memory client for working memory\n", + "student_id = \"demo_student_working_memory\"\n", + "session_id = \"session_001\"\n", + "config = MemoryClientConfig(\n", + " base_url=os.getenv(\"AGENT_MEMORY_URL\", \"http://localhost:8000\"),\n", + " default_namespace=\"redis_university\"\n", + ")\n", + "memory_client = MemoryClient(config=config)\n", + "\n", + "print(\"✅ Memory client initialized successfully\")\n", + "print(f\"📊 User ID: {student_id}\")\n", + "print(f\"📊 Session ID: {session_id}\")\n", + "print(\"\\nWorking memory will store conversation messages for this session.\")" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✅ Memory client initialized successfully\n", + "📊 User ID: demo_student_working_memory\n", + "📊 Session ID: session_001\n", + "\n", + "Working memory will store conversation messages for this session.\n" + ] + } + ], + "execution_count": 8 + }, + { + "cell_type": "code", + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-02T22:01:47.863402Z", + "start_time": "2025-10-02T22:01:47.590762Z" + } + }, + "source": [ + "# Simulate a conversation using working memory\n", + "\n", + "print(\"💬 Simulating Conversation with Working Memory\")\n", + "print(\"=\" * 50)\n", + "\n", + "# Create messages for the conversation\n", + "messages = [\n", + " {\"role\": \"user\", \"content\": \"I prefer online courses because I work part-time\"},\n", + " {\"role\": \"assistant\", \"content\": \"I understand you prefer online courses due to your work schedule.\"},\n", + " {\"role\": \"user\", \"content\": \"My goal is to specialize in machine learning\"},\n", + " {\"role\": \"assistant\", \"content\": \"Machine learning is an excellent specialization!\"},\n", + " {\"role\": \"user\", \"content\": \"What courses do you recommend?\"},\n", + "]\n", + "\n", + "# Save to working memory\n", + "from agent_memory_client.models import WorkingMemory, MemoryMessage\n", + "\n", + "# Convert messages to MemoryMessage format\n", + "memory_messages = [MemoryMessage(**msg) for msg in messages]\n", + "\n", + "# Create WorkingMemory object\n", + "working_memory = WorkingMemory(\n", + " session_id=session_id,\n", + " user_id=student_id,\n", + " messages=memory_messages,\n", + " memories=[],\n", + " data={}\n", + ")\n", + "\n", + "await memory_client.put_working_memory(\n", + " session_id=session_id,\n", + " memory=working_memory,\n", + " user_id=student_id,\n", + " model_name=\"gpt-4o\"\n", + ")\n", + "\n", + "print(\"✅ Conversation saved to working memory\")\n", + "print(f\"📊 Messages: {len(messages)}\")\n", + "print(\"\\nThese messages are now available as context for the LLM.\")\n", + "print(\"The LLM can reference earlier parts of the conversation.\")\n", + "\n", + "# Retrieve working memory\n", + "_, working_memory = await memory_client.get_or_create_working_memory(\n", + " session_id=session_id,\n", + " model_name=\"gpt-4o\",\n", + " user_id=student_id,\n", + ")\n", + "\n", + "if working_memory:\n", + " print(f\"\\n📋 Retrieved {len(working_memory.messages)} messages from working memory\")\n", + " print(\"This is the conversation context that would be provided to the LLM.\")" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "💬 Simulating Conversation with Working Memory\n", + "==================================================\n", + "15:01:47 httpx INFO HTTP Request: PUT http://localhost:8000/v1/working-memory/session_001?user_id=demo_student_working_memory&model_name=gpt-4o \"HTTP/1.1 500 Internal Server Error\"\n" + ] + }, + { + "ename": "MemoryServerError", + "evalue": "HTTP 500: dial tcp [::1]:8000: connect: connection refused\n", + "output_type": "error", + "traceback": [ + "\u001B[0;31m---------------------------------------------------------------------------\u001B[0m", + "\u001B[0;31mHTTPStatusError\u001B[0m Traceback (most recent call last)", + "File \u001B[0;32m~/src/redis-ai-resources/env/lib/python3.11/site-packages/agent_memory_client/client.py:457\u001B[0m, in \u001B[0;36mMemoryAPIClient.put_working_memory\u001B[0;34m(self, session_id, memory, user_id, model_name, context_window_max)\u001B[0m\n\u001B[1;32m 452\u001B[0m response \u001B[38;5;241m=\u001B[39m \u001B[38;5;28;01mawait\u001B[39;00m \u001B[38;5;28mself\u001B[39m\u001B[38;5;241m.\u001B[39m_client\u001B[38;5;241m.\u001B[39mput(\n\u001B[1;32m 453\u001B[0m \u001B[38;5;124mf\u001B[39m\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124m/v1/working-memory/\u001B[39m\u001B[38;5;132;01m{\u001B[39;00msession_id\u001B[38;5;132;01m}\u001B[39;00m\u001B[38;5;124m\"\u001B[39m,\n\u001B[1;32m 454\u001B[0m json\u001B[38;5;241m=\u001B[39mmemory\u001B[38;5;241m.\u001B[39mmodel_dump(exclude_none\u001B[38;5;241m=\u001B[39m\u001B[38;5;28;01mTrue\u001B[39;00m, mode\u001B[38;5;241m=\u001B[39m\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mjson\u001B[39m\u001B[38;5;124m\"\u001B[39m),\n\u001B[1;32m 455\u001B[0m params\u001B[38;5;241m=\u001B[39mparams,\n\u001B[1;32m 456\u001B[0m )\n\u001B[0;32m--> 457\u001B[0m \u001B[43mresponse\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mraise_for_status\u001B[49m\u001B[43m(\u001B[49m\u001B[43m)\u001B[49m\n\u001B[1;32m 458\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m WorkingMemoryResponse(\u001B[38;5;241m*\u001B[39m\u001B[38;5;241m*\u001B[39mresponse\u001B[38;5;241m.\u001B[39mjson())\n", + "File \u001B[0;32m~/src/redis-ai-resources/env/lib/python3.11/site-packages/httpx/_models.py:829\u001B[0m, in \u001B[0;36mResponse.raise_for_status\u001B[0;34m(self)\u001B[0m\n\u001B[1;32m 828\u001B[0m message \u001B[38;5;241m=\u001B[39m message\u001B[38;5;241m.\u001B[39mformat(\u001B[38;5;28mself\u001B[39m, error_type\u001B[38;5;241m=\u001B[39merror_type)\n\u001B[0;32m--> 829\u001B[0m \u001B[38;5;28;01mraise\u001B[39;00m HTTPStatusError(message, request\u001B[38;5;241m=\u001B[39mrequest, response\u001B[38;5;241m=\u001B[39m\u001B[38;5;28mself\u001B[39m)\n", + "\u001B[0;31mHTTPStatusError\u001B[0m: Server error '500 Internal Server Error' for url 'http://localhost:8000/v1/working-memory/session_001?user_id=demo_student_working_memory&model_name=gpt-4o'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500", + "\nDuring handling of the above exception, another exception occurred:\n", + "\u001B[0;31mMemoryServerError\u001B[0m Traceback (most recent call last)", + "Cell \u001B[0;32mIn[9], line 30\u001B[0m\n\u001B[1;32m 21\u001B[0m \u001B[38;5;66;03m# Create WorkingMemory object\u001B[39;00m\n\u001B[1;32m 22\u001B[0m working_memory \u001B[38;5;241m=\u001B[39m WorkingMemory(\n\u001B[1;32m 23\u001B[0m session_id\u001B[38;5;241m=\u001B[39msession_id,\n\u001B[1;32m 24\u001B[0m user_id\u001B[38;5;241m=\u001B[39mstudent_id,\n\u001B[0;32m (...)\u001B[0m\n\u001B[1;32m 27\u001B[0m data\u001B[38;5;241m=\u001B[39m{}\n\u001B[1;32m 28\u001B[0m )\n\u001B[0;32m---> 30\u001B[0m \u001B[38;5;28;01mawait\u001B[39;00m memory_client\u001B[38;5;241m.\u001B[39mput_working_memory(\n\u001B[1;32m 31\u001B[0m session_id\u001B[38;5;241m=\u001B[39msession_id,\n\u001B[1;32m 32\u001B[0m memory\u001B[38;5;241m=\u001B[39mworking_memory,\n\u001B[1;32m 33\u001B[0m user_id\u001B[38;5;241m=\u001B[39mstudent_id,\n\u001B[1;32m 34\u001B[0m model_name\u001B[38;5;241m=\u001B[39m\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mgpt-4o\u001B[39m\u001B[38;5;124m\"\u001B[39m\n\u001B[1;32m 35\u001B[0m )\n\u001B[1;32m 37\u001B[0m \u001B[38;5;28mprint\u001B[39m(\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124m✅ Conversation saved to working memory\u001B[39m\u001B[38;5;124m\"\u001B[39m)\n\u001B[1;32m 38\u001B[0m \u001B[38;5;28mprint\u001B[39m(\u001B[38;5;124mf\u001B[39m\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124m📊 Messages: \u001B[39m\u001B[38;5;132;01m{\u001B[39;00m\u001B[38;5;28mlen\u001B[39m(messages)\u001B[38;5;132;01m}\u001B[39;00m\u001B[38;5;124m\"\u001B[39m)\n", + "File \u001B[0;32m~/src/redis-ai-resources/env/lib/python3.11/site-packages/agent_memory_client/client.py:460\u001B[0m, in \u001B[0;36mMemoryAPIClient.put_working_memory\u001B[0;34m(self, session_id, memory, user_id, model_name, context_window_max)\u001B[0m\n\u001B[1;32m 458\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m WorkingMemoryResponse(\u001B[38;5;241m*\u001B[39m\u001B[38;5;241m*\u001B[39mresponse\u001B[38;5;241m.\u001B[39mjson())\n\u001B[1;32m 459\u001B[0m \u001B[38;5;28;01mexcept\u001B[39;00m httpx\u001B[38;5;241m.\u001B[39mHTTPStatusError \u001B[38;5;28;01mas\u001B[39;00m e:\n\u001B[0;32m--> 460\u001B[0m \u001B[38;5;28;43mself\u001B[39;49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43m_handle_http_error\u001B[49m\u001B[43m(\u001B[49m\u001B[43me\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mresponse\u001B[49m\u001B[43m)\u001B[49m\n", + "File \u001B[0;32m~/src/redis-ai-resources/env/lib/python3.11/site-packages/agent_memory_client/client.py:167\u001B[0m, in \u001B[0;36mMemoryAPIClient._handle_http_error\u001B[0;34m(self, response)\u001B[0m\n\u001B[1;32m 165\u001B[0m \u001B[38;5;28;01mexcept\u001B[39;00m \u001B[38;5;167;01mException\u001B[39;00m:\n\u001B[1;32m 166\u001B[0m message \u001B[38;5;241m=\u001B[39m \u001B[38;5;124mf\u001B[39m\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mHTTP \u001B[39m\u001B[38;5;132;01m{\u001B[39;00mresponse\u001B[38;5;241m.\u001B[39mstatus_code\u001B[38;5;132;01m}\u001B[39;00m\u001B[38;5;124m: \u001B[39m\u001B[38;5;132;01m{\u001B[39;00mresponse\u001B[38;5;241m.\u001B[39mtext\u001B[38;5;132;01m}\u001B[39;00m\u001B[38;5;124m\"\u001B[39m\n\u001B[0;32m--> 167\u001B[0m \u001B[38;5;28;01mraise\u001B[39;00m MemoryServerError(message, response\u001B[38;5;241m.\u001B[39mstatus_code)\n\u001B[1;32m 168\u001B[0m \u001B[38;5;66;03m# This should never be reached, but mypy needs to know this never returns\u001B[39;00m\n\u001B[1;32m 169\u001B[0m \u001B[38;5;28;01mraise\u001B[39;00m MemoryServerError(\n\u001B[1;32m 170\u001B[0m \u001B[38;5;124mf\u001B[39m\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mUnexpected status code: \u001B[39m\u001B[38;5;132;01m{\u001B[39;00mresponse\u001B[38;5;241m.\u001B[39mstatus_code\u001B[38;5;132;01m}\u001B[39;00m\u001B[38;5;124m\"\u001B[39m, response\u001B[38;5;241m.\u001B[39mstatus_code\n\u001B[1;32m 171\u001B[0m )\n", + "\u001B[0;31mMemoryServerError\u001B[0m: HTTP 500: dial tcp [::1]:8000: connect: connection refused\n" + ] + } + ], + "execution_count": 9 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## 3. Automatic Extraction to Long-Term Memory\n", + "\n", + "Because working memory stores messages, we can extract important long-term information from it. When using the Agent Memory Server, this extraction happens automatically in the background.\n", + "\n", + "The extraction strategy controls what kind of information gets extracted:\n", + "- User preferences (e.g., \"I prefer online courses\")\n", + "- Goals (e.g., \"I want to specialize in machine learning\")\n", + "- Important facts (e.g., \"I work part-time\")\n", + "- Key decisions or outcomes from the conversation\n", + "\n", + "This extracted information becomes long-term memory that persists across sessions.\n", + "\n", + "Let's check what information was automatically extracted from our working memory:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Check what was extracted to long-term memory\n", + "import asyncio\n", + "from agent_memory_client import MemoryAPIClient as MemoryClient, MemoryClientConfig\n", + "\n", + "# Ensure memory_client is defined (in case cells are run out of order)\n", + "if 'memory_client' not in globals():\n", + " # Initialize memory client with proper config\n", + " import os\n", + " config = MemoryClientConfig(\n", + " base_url=os.getenv(\"AGENT_MEMORY_URL\", \"http://localhost:8000\"),\n", + " default_namespace=\"redis_university\"\n", + " )\n", + " memory_client = MemoryClient(config=config)\n", + "\n", + "await asyncio.sleep(2) # Give the extraction process time to complete\n", + "\n", + "# Search for extracted memories\n", + "extracted_memories = await memory_client.search_long_term_memory(\n", + " text=\"preferences goals\",\n", + " limit=10\n", + ")\n", + "\n", + "print(\"🧠 Extracted to Long-term Memory\")\n", + "print(\"=\" * 50)\n", + "\n", + "if extracted_memories.memories:\n", + " for i, memory in enumerate(extracted_memories.memories, 1):\n", + " print(f\"{i}. {memory.text}\")\n", + " print(f\" Type: {memory.memory_type} | Topics: {', '.join(memory.topics)}\")\n", + " print()\n", + "else:\n", + " print(\"No memories extracted yet (extraction may take a moment)\")\n", + " print(\"\\nThe Agent Memory Server automatically extracts:\")\n", + " print(\"- User preferences (e.g., 'prefers online courses')\")\n", + " print(\"- Goals (e.g., 'wants to specialize in machine learning')\")\n", + " print(\"- Important facts (e.g., 'works part-time')\")\n", + " print(\"\\nThis happens in the background based on the configured extraction strategy.\")" + ] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## 4. Summary\n", + "\n", + "In this notebook, you learned:\n", + "\n", + "- ✅ **The Core Problem**: LLMs are stateless and need working memory to maintain conversation context\n", + "- ✅ **Working Memory Solution**: Stores messages and task-specific context for the current session\n", + "- ✅ **Message Storage**: Conversation history gives the LLM knowledge of what was said earlier\n", + "- ✅ **Automatic Extraction**: Important information is extracted to long-term memory in the background\n", + "- ✅ **Extraction Strategy**: Controls what kind of information gets extracted from working memory\n", + "\n", + "**Key API Methods:**\n", + "```python\n", + "# Save working memory (stores messages for this session)\n", + "await memory_client.put_working_memory(session_id, memory, user_id, model_name)\n", + "\n", + "# Retrieve working memory (gets conversation context)\n", + "_, working_memory = await memory_client.get_or_create_working_memory(\n", + " session_id, model_name, user_id\n", + ")\n", + "\n", + "# Search long-term memories (extracted from working memory)\n", + "memories = await memory_client.search_long_term_memory(text, limit)\n", + "```\n", + "\n", + "**The Key Insight:**\n", + "Working memory solves the fundamental problem of giving LLMs knowledge of the current conversation. Because it stores messages, we can also extract long-term data from it. The extraction strategy controls what gets extracted, and this happens automatically in the background when using the Agent Memory Server.\n", + "\n", + "## Next Steps\n", + "\n", + "See the next notebooks to learn about:\n", + "- Long-term memory and how it persists across sessions\n", + "- Memory tools that give LLMs explicit control over what gets remembered\n", + "- Integrating working and long-term memory in your applications" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/python-recipes/context-engineering/notebooks/section-3-memory/02_long_term_memory.ipynb b/python-recipes/context-engineering/notebooks/section-3-memory/02_long_term_memory.ipynb new file mode 100644 index 00000000..f805048b --- /dev/null +++ b/python-recipes/context-engineering/notebooks/section-3-memory/02_long_term_memory.ipynb @@ -0,0 +1,520 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Long-term Memory: Cross-Session Knowledge\n", + "\n", + "## Introduction\n", + "\n", + "In this notebook, you'll learn about long-term memory - persistent knowledge that survives across sessions. While working memory handles the current conversation, long-term memory stores important facts, preferences, and experiences that should be remembered indefinitely.\n", + "\n", + "### What You'll Learn\n", + "\n", + "- What long-term memory is and why it's essential\n", + "- The three types of long-term memories: semantic, episodic, and message\n", + "- How to store and retrieve long-term memories\n", + "- How semantic search works with memories\n", + "- How automatic deduplication prevents redundancy\n", + "\n", + "### Prerequisites\n", + "\n", + "- Completed Section 2 notebooks\n", + "- Completed `01_working_memory_with_extraction_strategies.ipynb`\n", + "- Redis 8 running locally\n", + "- Agent Memory Server running\n", + "- OpenAI API key set" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Concepts: Long-term Memory\n", + "\n", + "### What is Long-term Memory?\n", + "\n", + "Long-term memory is **persistent, cross-session knowledge** about users, preferences, and important facts. Unlike working memory (which is session-scoped), long-term memory:\n", + "\n", + "- ✅ Survives across sessions\n", + "- ✅ Accessible from any conversation\n", + "- ✅ Searchable via semantic vector search\n", + "- ✅ Automatically deduplicated\n", + "- ✅ Organized by user/namespace\n", + "\n", + "### Working Memory vs. Long-term Memory\n", + "\n", + "| Working Memory | Long-term Memory |\n", + "|----------------|------------------|\n", + "| **Session-scoped** | **User-scoped** |\n", + "| Current conversation | Important facts |\n", + "| TTL-based (expires) | Persistent |\n", + "| Full message history | Extracted knowledge |\n", + "| Loaded/saved each turn | Searched when needed |\n", + "\n", + "### Three Types of Long-term Memories\n", + "\n", + "The Agent Memory Server supports three types of long-term memories:\n", + "\n", + "1. **Semantic Memory** - Facts and knowledge\n", + " - Example: \"Student prefers online courses\"\n", + " - Example: \"Student's major is Computer Science\"\n", + " - Example: \"Student wants to graduate in 2026\"\n", + "\n", + "2. **Episodic Memory** - Events and experiences\n", + " - Example: \"Student enrolled in CS101 on 2024-09-15\"\n", + " - Example: \"Student asked about machine learning on 2024-09-20\"\n", + " - Example: \"Student completed Data Structures course\"\n", + "\n", + "3. **Message Memory** - Important conversation snippets\n", + " - Example: Full conversation about career goals\n", + " - Example: Detailed discussion about course preferences\n", + "\n", + "### How Semantic Search Works\n", + "\n", + "Long-term memories are stored with vector embeddings, enabling semantic search:\n", + "\n", + "- Query: \"What does the student like?\"\n", + "- Finds: \"Student prefers online courses\", \"Student enjoys programming\"\n", + "- Even though exact words don't match!\n", + "\n", + "### Automatic Deduplication\n", + "\n", + "The Agent Memory Server automatically prevents duplicate memories:\n", + "\n", + "- **Hash-based**: Exact duplicates are rejected\n", + "- **Semantic**: Similar memories are merged\n", + "- Keeps memory storage efficient" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "import os\n", + "from dotenv import load_dotenv\n", + "\n", + "# Load environment variables from .env file\n", + "load_dotenv()\n", + "\n", + "# Verify required environment variables are set\n", + "if not os.getenv(\"OPENAI_API_KEY\"):\n", + " raise ValueError(\n", + " \"OPENAI_API_KEY not found. Please create a .env file with your OpenAI API key. \"\n", + " \"See SETUP.md for instructions.\"\n", + " )\n", + "\n", + "print(\"✅ Environment variables loaded\")" + ] + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "import asyncio\n", + "from datetime import datetime\n", + "from agent_memory_client import MemoryAPIClient as MemoryClient, MemoryClientConfig\n", + "from agent_memory_client.models import ClientMemoryRecord\n", + "from agent_memory_client.filters import MemoryType\n", + "\n", + "# Initialize memory client\n", + "student_id = \"student_123\"\n", + "config = MemoryClientConfig(\n", + " base_url=os.getenv(\"AGENT_MEMORY_URL\", \"http://localhost:8000\"),\n", + " default_namespace=\"redis_university\"\n", + ")\n", + "memory_client = MemoryClient(config=config)\n", + "\n", + "print(f\"✅ Memory client initialized for {student_id}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Hands-on: Working with Long-term Memory" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example 1: Storing Semantic Memories (Facts)\n", + "\n", + "Let's store some facts about the student." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Store student preferences\n", + "await memory_client.create_long_term_memory([ClientMemoryRecord(\n", + " text=\"Student prefers online courses over in-person classes\",\n", + " memory_type=\"semantic\",\n", + " topics=[\"preferences\", \"course_format\"]\n", + ")])\n", + "\n", + "await memory_client.create_long_term_memory([ClientMemoryRecord(\n", + " text=\"Student's major is Computer Science with a focus on AI/ML\",\n", + " memory_type=\"semantic\",\n", + " topics=[\"academic_info\", \"major\"]\n", + ")])\n", + "\n", + "await memory_client.create_long_term_memory([ClientMemoryRecord(\n", + " text=\"Student wants to graduate in Spring 2026\",\n", + " memory_type=\"semantic\",\n", + " topics=[\"goals\", \"graduation\"]\n", + ")])\n", + "\n", + "await memory_client.create_long_term_memory([ClientMemoryRecord(\n", + " text=\"Student prefers morning classes, no classes on Fridays\",\n", + " memory_type=\"semantic\",\n", + " topics=[\"preferences\", \"schedule\"]\n", + ")])\n", + "\n", + "print(\"✅ Stored 4 semantic memories (facts about the student)\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example 2: Storing Episodic Memories (Events)\n", + "\n", + "Let's store some events and experiences." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Store course enrollment events\n", + "await memory_client.create_long_term_memory([ClientMemoryRecord(\n", + " text=\"Student enrolled in CS101: Introduction to Programming on 2024-09-01\",\n", + " memory_type=\"episodic\",\n", + " topics=[\"enrollment\", \"courses\", \"CS101\"]\n", + ")])\n", + "\n", + "await memory_client.create_long_term_memory([ClientMemoryRecord(\n", + " text=\"Student completed CS101 with grade A on 2024-12-15\",\n", + " memory_type=\"episodic\",\n", + " topics=[\"completion\", \"grades\", \"CS101\"]\n", + ")])\n", + "\n", + "await memory_client.create_long_term_memory([ClientMemoryRecord(\n", + " text=\"Student asked about machine learning courses on 2024-09-20\",\n", + " memory_type=\"episodic\",\n", + " topics=[\"inquiry\", \"machine_learning\"]\n", + ")])\n", + "\n", + "print(\"✅ Stored 3 episodic memories (events and experiences)\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example 3: Searching Memories with Semantic Search\n", + "\n", + "Now let's search for memories using natural language queries." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Search for preferences\n", + "print(\"Query: 'What does the student prefer?'\\n\")\n", + "results = await memory_client.search_long_term_memory(\n", + " text=\"What does the student prefer?\",\n", + " limit=3\n", + ")\n", + "\n", + "for i, memory in enumerate(results.memories, 1):\n", + " print(f\"{i}. {memory.text}\")\n", + " print(f\" Type: {memory.memory_type} | Topics: {', '.join(memory.topics)}\")\n", + " print()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Search for academic information\n", + "print(\"Query: 'What is the student studying?'\\n\")\n", + "results = await memory_client.search_long_term_memory(\n", + " text=\"What is the student studying?\",\n", + " limit=3\n", + ")\n", + "\n", + "for i, memory in enumerate(results.memories, 1):\n", + " print(f\"{i}. {memory.text}\")\n", + " print(f\" Type: {memory.memory_type}\")\n", + " print()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Search for course history\n", + "print(\"Query: 'What courses has the student taken?'\\n\")\n", + "results = await memory_client.search_long_term_memory(\n", + " text=\"What courses has the student taken?\",\n", + " limit=3\n", + ")\n", + "\n", + "for i, memory in enumerate(results.memories, 1):\n", + " print(f\"{i}. {memory.text}\")\n", + " print(f\" Type: {memory.memory_type} | Topics: {', '.join(memory.topics or [])}\")\n", + " print()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example 4: Demonstrating Deduplication\n", + "\n", + "Let's try to store duplicate memories and see how deduplication works." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Try to store an exact duplicate\n", + "print(\"Attempting to store exact duplicate...\")\n", + "try:\n", + " await memory_client.create_long_term_memory([ClientMemoryRecord(\n", + " text=\"Student prefers online courses over in-person classes\",\n", + " memory_type=\"semantic\",\n", + " topics=[\"preferences\", \"course_format\"]\n", + ")])\n", + " print(\"❌ Duplicate was stored (unexpected)\")\n", + "except Exception as e:\n", + " print(f\"✅ Duplicate rejected: {e}\")\n", + "\n", + "# Try to store a semantically similar memory\n", + "print(\"\\nAttempting to store semantically similar memory...\")\n", + "try:\n", + " await memory_client.create_long_term_memory([ClientMemoryRecord(\n", + " text=\"Student likes taking classes online instead of on campus\",\n", + " memory_type=\"semantic\",\n", + " topics=[\"preferences\", \"course_format\"]\n", + ")])\n", + " print(\"Memory stored (may be merged with existing similar memory)\")\n", + "except Exception as e:\n", + " print(f\"✅ Similar memory rejected: {e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example 5: Cross-Session Memory Access\n", + "\n", + "Let's simulate a new session and show that memories persist." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create a new memory client (simulating a new session)\n", + "config = MemoryClientConfig(\n", + " base_url=os.getenv(\"AGENT_MEMORY_URL\", \"http://localhost:8000\"),\n", + " default_namespace=\"redis_university\"\n", + ")\n", + "new_session_client = MemoryClient(config=config)\n", + "\n", + "print(\"New session started for the same student\\n\")\n", + "\n", + "# Search for memories from the new session\n", + "print(\"Query: 'What do I prefer?'\\n\")\n", + "results = await new_session_client.search_long_term_memory(\n", + " text=\"What do I prefer?\",\n", + " limit=3\n", + ")\n", + "\n", + "print(\"✅ Memories accessible from new session:\\n\")\n", + "for i, memory in enumerate(results.memories, 1):\n", + " print(f\"{i}. {memory.text}\")\n", + " print()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example 6: Filtering by Memory Type and Topics" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get all semantic memories\n", + "print(\"All semantic memories (facts):\\n\")\n", + "results = await memory_client.search_long_term_memory(\n", + " text=\"\", # Empty query returns all\n", + " memory_type=MemoryType(eq=\"semantic\"),\n", + " limit=10\n", + ")\n", + "\n", + "for i, memory in enumerate(results.memories, 1):\n", + " print(f\"{i}. {memory.text}\")\n", + " print(f\" Topics: {', '.join(memory.topics)}\")\n", + " print()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get all episodic memories\n", + "print(\"All episodic memories (events):\\n\")\n", + "results = await memory_client.search_long_term_memory(\n", + " text=\"\",\n", + " memory_type=MemoryType(eq=\"episodic\"),\n", + " limit=10\n", + ")\n", + "\n", + "for i, memory in enumerate(results.memories, 1):\n", + " print(f\"{i}. {memory.text}\")\n", + " print(f\" Topics: {', '.join(memory.topics or [])}\")\n", + " print()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Key Takeaways\n", + "\n", + "### When to Use Long-term Memory\n", + "\n", + "Store in long-term memory:\n", + "- ✅ User preferences and settings\n", + "- ✅ Important facts about the user\n", + "- ✅ Goals and objectives\n", + "- ✅ Significant events and milestones\n", + "- ✅ Completed courses and achievements\n", + "\n", + "Don't store in long-term memory:\n", + "- ❌ Temporary conversation context\n", + "- ❌ Trivial details\n", + "- ❌ Information that changes frequently\n", + "- ❌ Sensitive data without proper handling\n", + "\n", + "### Memory Types Guide\n", + "\n", + "**Semantic (Facts):**\n", + "- \"Student prefers X\"\n", + "- \"Student's major is Y\"\n", + "- \"Student wants to Z\"\n", + "\n", + "**Episodic (Events):**\n", + "- \"Student enrolled in X on DATE\"\n", + "- \"Student completed Y with grade Z\"\n", + "- \"Student asked about X on DATE\"\n", + "\n", + "**Message (Conversations):**\n", + "- Important conversation snippets\n", + "- Detailed discussions worth preserving\n", + "\n", + "### Best Practices\n", + "\n", + "1. **Use descriptive topics** - Makes filtering and categorization easier\n", + "2. **Write clear memory text** - Will be searched semantically\n", + "3. **Include relevant details in text** - Dates, names, and context help with retrieval\n", + "4. **Let deduplication work** - Don't worry about duplicates\n", + "5. **Search before storing** - Check if similar memory exists" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Exercises\n", + "\n", + "1. **Store your own memories**: Create 5 semantic and 3 episodic memories about a fictional student. Search for them.\n", + "\n", + "2. **Test semantic search**: Create memories with different wordings but similar meanings. Search with various queries to see what matches.\n", + "\n", + "3. **Explore topics**: Add rich topics to episodic memories. How can you use topic filtering in your agent?\n", + "\n", + "4. **Cross-session test**: Create a memory, close the notebook, restart, and verify the memory persists." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "In this notebook, you learned:\n", + "\n", + "- ✅ Long-term memory stores persistent, cross-session knowledge\n", + "- ✅ Three types: semantic (facts), episodic (events), message (conversations)\n", + "- ✅ Semantic search enables natural language queries\n", + "- ✅ Automatic deduplication prevents redundancy\n", + "- ✅ Memories are user-scoped and accessible from any session\n", + "\n", + "**Next:** In the next notebook, we'll integrate working memory and long-term memory to build a complete memory system for our agent." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/python-recipes/context-engineering/notebooks/section-3-memory/03_memory_integration.ipynb b/python-recipes/context-engineering/notebooks/section-3-memory/03_memory_integration.ipynb new file mode 100644 index 00000000..2e35b7e4 --- /dev/null +++ b/python-recipes/context-engineering/notebooks/section-3-memory/03_memory_integration.ipynb @@ -0,0 +1,571 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Memory Integration: Combining Working and Long-term Memory\n", + "\n", + "## Introduction\n", + "\n", + "In this notebook, you'll learn how to integrate working memory and long-term memory to create a complete memory system for your agent. You'll see how these two types of memory work together to provide both conversation context and persistent knowledge.\n", + "\n", + "### What You'll Learn\n", + "\n", + "- How working and long-term memory complement each other\n", + "- When to use each type of memory\n", + "- How to build a complete memory flow\n", + "- How automatic extraction works\n", + "- How to test multi-session conversations\n", + "\n", + "### Prerequisites\n", + "\n", + "- Completed `01_working_memory_with_extraction_strategies.ipynb`\n", + "- Completed `02_long_term_memory.ipynb`\n", + "- Redis 8 running locally\n", + "- Agent Memory Server running\n", + "- OpenAI API key set" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Concepts: Memory Integration\n", + "\n", + "### The Complete Memory Architecture\n", + "\n", + "A production agent needs both types of memory:\n", + "\n", + "```\n", + "┌─────────────────────────────────────────────────┐\n", + "│ User Query │\n", + "└─────────────────────────────────────────────────┘\n", + " ↓\n", + "┌─────────────────────────────────────────────────┐\n", + "│ 1. Load Working Memory (current conversation) │\n", + "└─────────────────────────────────────────────────┘\n", + " ↓\n", + "┌─────────────────────────────────────────────────┐\n", + "│ 2. Search Long-term Memory (relevant facts) │\n", + "└─────────────────────────────────────────────────┘\n", + " ↓\n", + "┌─────────────────────────────────────────────────┐\n", + "│ 3. Agent Processes with Full Context │\n", + "└─────────────────────────────────────────────────┘\n", + " ↓\n", + "┌─────────────────────────────────────────────────┐\n", + "│ 4. Save Working Memory (with new messages) │\n", + "│ → Automatic extraction to long-term │\n", + "└─────────────────────────────────────────────────┘\n", + "```\n", + "\n", + "### Memory Flow in Detail\n", + "\n", + "**Turn 1:**\n", + "1. Load working memory (empty)\n", + "2. Search long-term memory (empty)\n", + "3. Process query\n", + "4. Save working memory\n", + "5. Extract important facts → long-term memory\n", + "\n", + "**Turn 2 (same session):**\n", + "1. Load working memory (has Turn 1 messages)\n", + "2. Search long-term memory (has extracted facts)\n", + "3. Process query with full context\n", + "4. Save working memory (Turn 1 + Turn 2)\n", + "5. Extract new facts → long-term memory\n", + "\n", + "**Turn 3 (new session, same user):**\n", + "1. Load working memory (empty - new session)\n", + "2. Search long-term memory (has all extracted facts)\n", + "3. Process query with long-term context\n", + "4. Save working memory (Turn 3 only)\n", + "5. Extract facts → long-term memory\n", + "\n", + "### When to Use Each Memory Type\n", + "\n", + "| Scenario | Working Memory | Long-term Memory |\n", + "|----------|----------------|------------------|\n", + "| Current conversation | ✅ Always | ❌ No |\n", + "| User preferences | ❌ No | ✅ Yes |\n", + "| Recent context | ✅ Yes | ❌ No |\n", + "| Important facts | ❌ No | ✅ Yes |\n", + "| Cross-session data | ❌ No | ✅ Yes |\n", + "| Temporary info | ✅ Yes | ❌ No |" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import asyncio\n", + "from datetime import datetime\n", + "from langchain_openai import ChatOpenAI\n", + "from langchain_core.messages import SystemMessage, HumanMessage, AIMessage\n", + "from agent_memory_client import MemoryAPIClient as MemoryClient, MemoryClientConfig\n", + "\n", + "# Initialize\n", + "student_id = \"student_456\"\n", + "session_id_1 = \"session_001\"\n", + "session_id_2 = \"session_002\"\n", + "\n", + "# Initialize memory client with proper config\n", + "import os\n", + "config = MemoryClientConfig(\n", + " base_url=os.getenv(\"AGENT_MEMORY_URL\", \"http://localhost:8000\"),\n", + " default_namespace=\"redis_university\"\n", + ")\n", + "memory_client = MemoryClient(config=config)\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-4o\", temperature=0.7)\n", + "\n", + "print(f\"✅ Setup complete for {student_id}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Hands-on: Building Complete Memory Flow" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Session 1, Turn 1: First Interaction\n", + "\n", + "Let's simulate the first turn of a conversation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"=\" * 80)\n", + "print(\"SESSION 1, TURN 1\")\n", + "print(\"=\" * 80)\n", + "\n", + "# Step 1: Load working memory (empty for first turn)\n", + "print(\"\\n1. Loading working memory...\")\n", + "# For first turn, working memory is empty\n", + "working_memory = None\n", + "print(f\" Messages in working memory: 0 (new session)\")\n", + "\n", + "# Step 2: Search long-term memory (empty for first interaction)\n", + "print(\"\\n2. Searching long-term memory...\")\n", + "user_query = \"Hi! I'm interested in learning about databases.\"\n", + "long_term_memories = await memory_client.search_long_term_memory(\n", + " text=user_query,\n", + " limit=3\n", + ")\n", + "print(f\" Relevant memories found: {len(long_term_memories.memories)}\")\n", + "\n", + "# Step 3: Process with LLM\n", + "print(\"\\n3. Processing with LLM...\")\n", + "messages = [\n", + " SystemMessage(content=\"You are a helpful class scheduling agent for Redis University.\"),\n", + " HumanMessage(content=user_query)\n", + "]\n", + "response = llm.invoke(messages)\n", + "print(f\"\\n User: {user_query}\")\n", + "print(f\" Agent: {response.content}\")\n", + "\n", + "# Step 4: Save working memory\n", + "print(\"\\n4. Saving working memory...\")\n", + "from agent_memory_client.models import WorkingMemory, MemoryMessage\n", + "\n", + "# Convert messages to MemoryMessage format\n", + "memory_messages = [\n", + " MemoryMessage(role=\"user\", content=user_query),\n", + " MemoryMessage(role=\"assistant\", content=response.content)\n", + "]\n", + "\n", + "# Create WorkingMemory object\n", + "working_memory = WorkingMemory(\n", + " session_id=session_id_1,\n", + " user_id=\"demo_user\",\n", + " messages=memory_messages,\n", + " memories=[],\n", + " data={}\n", + ")\n", + "\n", + "await memory_client.put_working_memory(\n", + " session_id=session_id_1,\n", + " memory=working_memory,\n", + " user_id=\"demo_user\",\n", + " model_name=\"gpt-4o\"\n", + ")\n", + "print(\" ✅ Working memory saved\")\n", + "print(\" ✅ Agent Memory Server will automatically extract important facts to long-term memory\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Session 1, Turn 2: Continuing the Conversation\n", + "\n", + "Let's continue the conversation in the same session." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"\\n\" + \"=\" * 80)\n", + "print(\"SESSION 1, TURN 2\")\n", + "print(\"=\" * 80)\n", + "\n", + "# Step 1: Load working memory (now has Turn 1)\n", + "print(\"\\n1. Loading working memory...\")\n", + "_, working_memory = await memory_client.get_or_create_working_memory(\n", + " session_id=session_id_1,\n", + " user_id=\"demo_user\",\n", + " model_name=\"gpt-4o\"\n", + ")\n", + "print(f\" Messages in working memory: {len(working_memory.messages)}\")\n", + "print(\" Previous context available: ✅\")\n", + "\n", + "# Step 2: Search long-term memory\n", + "print(\"\\n2. Searching long-term memory...\")\n", + "user_query_2 = \"I prefer online courses and morning classes.\"\n", + "long_term_memories = await memory_client.search_long_term_memory(\n", + " text=user_query_2,\n", + " limit=3\n", + ")\n", + "print(f\" Relevant memories found: {len(long_term_memories.memories)}\")\n", + "\n", + "# Step 3: Process with LLM (with conversation history)\n", + "print(\"\\n3. Processing with LLM...\")\n", + "messages = [\n", + " SystemMessage(content=\"You are a helpful class scheduling agent for Redis University.\"),\n", + "]\n", + "\n", + "# Add working memory messages\n", + "for msg in working_memory.messages:\n", + " if msg.role == \"user\":\n", + " messages.append(HumanMessage(content=msg.content))\n", + " elif msg.role == \"assistant\":\n", + " messages.append(AIMessage(content=msg.content))\n", + "\n", + "# Add new query\n", + "messages.append(HumanMessage(content=user_query_2))\n", + "\n", + "response = llm.invoke(messages)\n", + "print(f\"\\n User: {user_query_2}\")\n", + "print(f\" Agent: {response.content}\")\n", + "\n", + "# Step 4: Save working memory (with both turns)\n", + "print(\"\\n4. Saving working memory...\")\n", + "all_messages = [\n", + " {\"role\": msg.role, \"content\": msg.content}\n", + " for msg in working_memory.messages\n", + "]\n", + "all_messages.extend([\n", + " {\"role\": \"user\", \"content\": user_query_2},\n", + " {\"role\": \"assistant\", \"content\": response.content}\n", + "])\n", + "\n", + "from agent_memory_client.models import WorkingMemory, MemoryMessage\n", + "\n", + "# Convert messages to MemoryMessage format\n", + "memory_messages = [MemoryMessage(**msg) for msg in all_messages]\n", + "\n", + "# Create WorkingMemory object\n", + "working_memory = WorkingMemory(\n", + " session_id=session_id_1,\n", + " user_id=\"demo_user\",\n", + " messages=memory_messages,\n", + " memories=[],\n", + " data={}\n", + ")\n", + "\n", + "await memory_client.put_working_memory(\n", + " session_id=session_id_1,\n", + " memory=working_memory,\n", + " user_id=\"demo_user\",\n", + " model_name=\"gpt-4o\"\n", + ")\n", + "print(\" ✅ Working memory saved with both turns\")\n", + "print(\" ✅ Preferences will be extracted to long-term memory\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Verify Automatic Extraction\n", + "\n", + "Let's check if the Agent Memory Server extracted facts to long-term memory." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Wait a moment for extraction to complete\n", + "print(\"Waiting for automatic extraction...\")\n", + "await asyncio.sleep(2)\n", + "\n", + "# Search for extracted memories\n", + "print(\"\\nSearching for extracted memories...\\n\")\n", + "memories = await memory_client.search_long_term_memory(\n", + " text=\"student preferences\",\n", + " limit=5\n", + ")\n", + "\n", + "if memories:\n", + " print(\"✅ Extracted memories found:\\n\")\n", + " for i, memory in enumerate(memories.memories, 1):\n", + " print(f\"{i}. {memory.text}\")\n", + " print(f\" Type: {memory.memory_type} | Topics: {', '.join(memory.topics)}\")\n", + " print()\n", + "else:\n", + " print(\"⏳ No memories extracted yet (extraction may take a moment)\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Session 2: New Session, Same User\n", + "\n", + "Now let's start a completely new session with the same user. Working memory will be empty, but long-term memory persists." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"\\n\" + \"=\" * 80)\n", + "print(\"SESSION 2, TURN 1 (New Session, Same User)\")\n", + "print(\"=\" * 80)\n", + "\n", + "# Step 1: Load working memory (empty - new session)\n", + "print(\"\\n1. Loading working memory...\")\n", + "# For new session, working memory is empty\n", + "working_memory = None\n", + "print(f\" Messages in working memory: 0\")\n", + "print(\" (Empty - this is a new session)\")\n", + "\n", + "# Step 2: Search long-term memory (has data from Session 1)\n", + "print(\"\\n2. Searching long-term memory...\")\n", + "user_query_3 = \"What database courses do you recommend for me?\"\n", + "long_term_memories = await memory_client.search_long_term_memory(\n", + " text=user_query_3,\n", + " limit=5\n", + ")\n", + "print(f\" Relevant memories found: {len(long_term_memories.memories)}\")\n", + "if long_term_memories.memories:\n", + " print(\"\\n Retrieved memories:\")\n", + " for memory in long_term_memories.memories:\n", + " print(f\" - {memory.text}\")\n", + "\n", + "# Step 3: Process with LLM (with long-term context)\n", + "print(\"\\n3. Processing with LLM...\")\n", + "context = \"\\n\".join([f\"- {m.text}\" for m in long_term_memories.memories])\n", + "system_prompt = f\"\"\"You are a helpful class scheduling agent for Redis University.\n", + "\n", + "What you know about this student:\n", + "{context}\n", + "\"\"\"\n", + "\n", + "messages = [\n", + " SystemMessage(content=system_prompt),\n", + " HumanMessage(content=user_query_3)\n", + "]\n", + "\n", + "response = llm.invoke(messages)\n", + "print(f\"\\n User: {user_query_3}\")\n", + "print(f\" Agent: {response.content}\")\n", + "print(\"\\n ✅ Agent used long-term memory to personalize response!\")\n", + "\n", + "# Step 4: Save working memory\n", + "print(\"\\n4. Saving working memory...\")\n", + "from agent_memory_client.models import WorkingMemory, MemoryMessage\n", + "\n", + "# Convert messages to MemoryMessage format\n", + "memory_messages = [\n", + " MemoryMessage(role=\"user\", content=user_query_3),\n", + " MemoryMessage(role=\"assistant\", content=response.content)\n", + "]\n", + "\n", + "# Create WorkingMemory object\n", + "working_memory = WorkingMemory(\n", + " session_id=session_id_2,\n", + " user_id=\"demo_user\",\n", + " messages=memory_messages,\n", + " memories=[],\n", + " data={}\n", + ")\n", + "\n", + "await memory_client.put_working_memory(\n", + " session_id=session_id_2,\n", + " memory=working_memory,\n", + " user_id=\"demo_user\",\n", + " model_name=\"gpt-4o\"\n", + ")\n", + "print(\" ✅ Working memory saved for new session\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Testing: Memory Consolidation\n", + "\n", + "Let's verify that both sessions' data is consolidated in long-term memory." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"\\n\" + \"=\" * 80)\n", + "print(\"MEMORY CONSOLIDATION CHECK\")\n", + "print(\"=\" * 80)\n", + "\n", + "# Check all memories about the student\n", + "print(\"\\nAll memories about this student:\\n\")\n", + "all_memories = await memory_client.search_long_term_memory(\n", + " text=\"\", # Empty query returns all\n", + " limit=20\n", + ")\n", + "\n", + "semantic_memories = [m for m in all_memories.memories if m.memory_type == \"semantic\"]\n", + "episodic_memories = [m for m in all_memories.memories if m.memory_type == \"episodic\"]\n", + "\n", + "print(f\"Semantic memories (facts): {len(semantic_memories)}\")\n", + "for memory in semantic_memories:\n", + " print(f\" - {memory.text}\")\n", + "\n", + "print(f\"\\nEpisodic memories (events): {len(episodic_memories)}\")\n", + "for memory in episodic_memories:\n", + " print(f\" - {memory.text}\")\n", + "\n", + "print(\"\\n✅ All memories from both sessions are consolidated in long-term memory!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Key Takeaways\n", + "\n", + "### Memory Integration Pattern\n", + "\n", + "**Every conversation turn:**\n", + "1. Load working memory (conversation history)\n", + "2. Search long-term memory (relevant facts)\n", + "3. Process with full context\n", + "4. Save working memory (triggers extraction)\n", + "\n", + "### Automatic Extraction\n", + "\n", + "The Agent Memory Server automatically:\n", + "- ✅ Analyzes conversations\n", + "- ✅ Extracts important facts\n", + "- ✅ Stores in long-term memory\n", + "- ✅ Deduplicates similar memories\n", + "- ✅ Organizes by type and topics\n", + "\n", + "### Memory Lifecycle\n", + "\n", + "```\n", + "User says something\n", + " ↓\n", + "Stored in working memory (session-scoped)\n", + " ↓\n", + "Automatic extraction analyzes importance\n", + " ↓\n", + "Important facts → long-term memory (user-scoped)\n", + " ↓\n", + "Available in future sessions\n", + "```\n", + "\n", + "### Best Practices\n", + "\n", + "1. **Always load working memory first** - Get conversation context\n", + "2. **Search long-term memory for relevant facts** - Use semantic search\n", + "3. **Combine both in system prompt** - Give LLM full context\n", + "4. **Save working memory after each turn** - Enable extraction\n", + "5. **Trust automatic extraction** - Don't manually extract everything" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Exercises\n", + "\n", + "1. **Multi-turn conversation**: Have a 5-turn conversation about course planning. Verify memories are extracted.\n", + "\n", + "2. **Cross-session test**: Start a new session and ask \"What do you know about me?\" Does the agent remember?\n", + "\n", + "3. **Memory search**: Try different search queries to find specific memories. How does semantic search perform?\n", + "\n", + "4. **Extraction timing**: How long does automatic extraction take? Test with different conversation lengths." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "In this notebook, you learned:\n", + "\n", + "- ✅ Working and long-term memory work together for complete context\n", + "- ✅ Load working memory → search long-term → process → save working memory\n", + "- ✅ Automatic extraction moves important facts to long-term memory\n", + "- ✅ Long-term memory persists across sessions\n", + "- ✅ This pattern enables truly personalized, context-aware agents\n", + "\n", + "**Next:** In Section 4, we'll explore optimizations like context window management, retrieval strategies, and grounding techniques." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/python-recipes/context-engineering/notebooks/section-3-memory/04_memory_tools.ipynb b/python-recipes/context-engineering/notebooks/section-3-memory/04_memory_tools.ipynb new file mode 100644 index 00000000..bec6a120 --- /dev/null +++ b/python-recipes/context-engineering/notebooks/section-3-memory/04_memory_tools.ipynb @@ -0,0 +1,565 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Memory Tools: Giving the LLM Control Over Memory\n", + "\n", + "## Introduction\n", + "\n", + "In this advanced notebook, you'll learn how to give your agent control over its own memory using tools. Instead of automatically extracting memories, you can let the LLM decide what to remember and when to search for memories. The Agent Memory Server SDK provides built-in memory tools for this.\n", + "\n", + "### What You'll Learn\n", + "\n", + "- Why give the LLM control over memory\n", + "- Agent Memory Server's built-in memory tools\n", + "- How to configure memory tools for your agent\n", + "- When the LLM decides to store vs. search memories\n", + "- Best practices for memory-aware agents\n", + "\n", + "### Prerequisites\n", + "\n", + "- Completed all Section 3 notebooks\n", + "- Redis 8 running locally\n", + "- Agent Memory Server running\n", + "- OpenAI API key set" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Concepts: Tool-Based Memory Management\n", + "\n", + "### Two Approaches to Memory\n", + "\n", + "#### 1. Automatic Memory (What We've Been Doing)\n", + "\n", + "```python\n", + "# Agent has conversation\n", + "# → Save working memory\n", + "# → Agent Memory Server automatically extracts important facts\n", + "# → Facts stored in long-term memory\n", + "```\n", + "\n", + "**Pros:**\n", + "- ✅ Fully automatic\n", + "- ✅ No LLM overhead in your application\n", + "- ✅ Consistent extraction\n", + "- ✅ Faster - extraction happens in the background after response is sent\n", + "\n", + "**Cons:**\n", + "- ⚠️ Your application's LLM can't directly control what gets extracted\n", + "- ⚠️ May extract too much or too little\n", + "- ⚠️ Can't dynamically decide what's important based on conversation context\n", + "\n", + "**Note:** You can configure custom extraction prompts on the memory server to guide what gets extracted, but your client application's LLM doesn't have direct control over the extraction process.\n", + "\n", + "#### 2. Tool-Based Memory (This Notebook)\n", + "\n", + "```python\n", + "# Agent has conversation\n", + "# → LLM decides: \"This is important, I should remember it\"\n", + "# → LLM calls store_memory tool\n", + "# → Fact stored in long-term memory\n", + "\n", + "# Later...\n", + "# → LLM decides: \"I need to know about the user's preferences\"\n", + "# → LLM calls search_memories tool\n", + "# → Retrieves relevant memories\n", + "```\n", + "\n", + "**Pros:**\n", + "- ✅ Your application's LLM has full control\n", + "- ✅ Can decide what's important in real-time\n", + "- ✅ Can search when needed\n", + "- ✅ More intelligent, context-aware behavior\n", + "\n", + "**Cons:**\n", + "- ⚠️ Requires tool calls (more tokens)\n", + "- ⚠️ Slower - tool calls add latency to every response\n", + "- ⚠️ LLM might forget to store/search\n", + "- ⚠️ Less consistent\n", + "\n", + "### When to Use Tool-Based Memory\n", + "\n", + "**Use tool-based memory when:**\n", + "- ✅ Agent needs fine-grained control\n", + "- ✅ Importance is context-dependent\n", + "- ✅ Agent should decide when to search\n", + "- ✅ Building advanced, autonomous agents\n", + "\n", + "**Use automatic memory when:**\n", + "- ✅ Simple, consistent extraction is fine\n", + "- ✅ Want to minimize token usage\n", + "- ✅ Building straightforward agents\n", + "\n", + "**Best: Use both!**\n", + "- Automatic extraction for baseline\n", + "- Tools for explicit control\n", + "\n", + "### Agent Memory Server's Built-in Tools\n", + "\n", + "The Agent Memory Server SDK provides:\n", + "\n", + "1. **`store_memory`** - Store important information\n", + "2. **`search_memories`** - Search for relevant memories\n", + "3. **`update_memory`** - Update existing memories\n", + "4. **`delete_memory`** - Remove memories\n", + "\n", + "These are pre-built, tested, and optimized!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import asyncio\n", + "from langchain_openai import ChatOpenAI\n", + "from langchain_openai import ChatOpenAI\n", + "from langchain_core.messages import SystemMessage, HumanMessage, AIMessage, ToolMessage\n", + "from agent_memory_client import create_memory_client\n", + "from agent_memory_client.integrations.langchain import get_memory_tools\n", + "import asyncio\n", + "import os\n", + "\n", + "# Initialize\n", + "student_id = \"student_memory_tools\"\n", + "session_id = \"tool_demo\"\n", + "\n", + "# Initialize memory client using the new async factory\n", + "base_url = os.getenv(\"AGENT_MEMORY_URL\", \"http://localhost:8000\")\n", + "memory_client = await create_memory_client(base_url)\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-4o\", temperature=0.7)\n", + "\n", + "print(f\"✅ Setup complete for {student_id}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Exploring Agent Memory Server's Memory Tools\n", + "\n", + "Let's create tools that wrap the Agent Memory Server's memory operations." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Getting Memory Tools with LangChain Integration\n", + "\n", + "The memory client now has built-in LangChain/LangGraph integration! Just call `get_memory_tools()` and you get ready-to-use LangChain tools." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get LangChain-compatible memory tools from the client\n", + "# This returns a list of StructuredTool objects ready to use with LangChain/LangGraph\n", + "memory_tools = get_memory_tools(\n", + " memory_client=memory_client,\n", + " session_id=session_id,\n", + " user_id=student_id\n", + ")\n", + "\n", + "print(\"Available memory tools:\")\n", + "for tool in memory_tools:\n", + " print(f\"\\n - {tool.name}: {tool.description[:80]}...\")\n", + " if hasattr(tool, 'args_schema') and tool.args_schema:\n", + " print(f\" Schema: {tool.args_schema.model_json_schema()}\")\n", + "\n", + "print(f\"\\n✅ Got {len(memory_tools)} LangChain tools from memory client\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Key Insight: Built-in LangChain Integration\n", + "\n", + "The `get_memory_tools()` function returns LangChain `StructuredTool` objects that:\n", + "- Work seamlessly with LangChain's `llm.bind_tools()` and LangGraph agents\n", + "- Handle all the memory client API calls internally\n", + "- Are pre-configured with your session_id and user_id\n", + "\n", + "No manual wrapping needed - just use them like any other LangChain tool!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Testing Memory Tools with an Agent\n", + "\n", + "Let's create an agent that uses these memory tools." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Configure agent with memory tools\n", + "llm_with_tools = llm.bind_tools(memory_tools)\n", + "\n", + "system_prompt = \"\"\"You are a class scheduling agent for Redis University.\n", + "\n", + "You have access to memory tools:\n", + "- create_long_term_memory: Store important information about the student\n", + "- search_long_term_memory: Search for information you've stored before\n", + "\n", + "Use these tools intelligently:\n", + "- When students share preferences, goals, or important facts → store them\n", + "- When you need to recall information → search for it\n", + "- When making recommendations → search for preferences first\n", + "\n", + "Be proactive about using memory to provide personalized service.\n", + "\"\"\"\n", + "\n", + "print(\"✅ Agent configured with LangChain memory tools\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example 1: Agent Stores a Preference" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"=\" * 80)\n", + "print(\"EXAMPLE 1: Agent Stores a Preference\")\n", + "print(\"=\" * 80)\n", + "\n", + "user_message = \"I prefer online courses because I work part-time.\"\n", + "\n", + "messages = [\n", + " SystemMessage(content=system_prompt),\n", + " HumanMessage(content=user_message)\n", + "]\n", + "\n", + "print(f\"\\n👤 User: {user_message}\")\n", + "\n", + "# First response - should call create_long_term_memory\n", + "response = llm_with_tools.invoke(messages)\n", + "\n", + "if response.tool_calls:\n", + " print(\"\\n🤖 Agent decision: Store this preference\")\n", + " for tool_call in response.tool_calls:\n", + " print(f\" Tool: {tool_call['name']}\")\n", + " print(f\" Args: {tool_call['args']}\")\n", + " \n", + " # Find and execute the tool\n", + " tool = next((t for t in memory_tools if t.name == tool_call['name']), None)\n", + " if tool:\n", + " try:\n", + " result = await tool.ainvoke(tool_call['args'])\n", + " print(f\" Result: {result}\")\n", + " result_content = str(result)\n", + " except Exception as e:\n", + " print(f\" Error: {e}\")\n", + " result_content = f\"Error: {str(e)}\"\n", + " \n", + " # Add tool result to messages\n", + " messages.append(response)\n", + " messages.append(ToolMessage(\n", + " content=result_content,\n", + " tool_call_id=tool_call['id']\n", + " ))\n", + " \n", + " # Get final response\n", + " final_response = llm_with_tools.invoke(messages)\n", + " print(f\"\\n🤖 Agent: {final_response.content}\")\n", + "else:\n", + " print(f\"\\n🤖 Agent: {response.content}\")\n", + " print(\"\\n⚠️ Agent didn't use memory tool\")\n", + "\n", + "print(\"\\n\" + \"=\" * 80)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example 2: Agent Searches for Memories" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"\\n\" + \"=\" * 80)\n", + "print(\"EXAMPLE 2: Agent Searches for Memories\")\n", + "print(\"=\" * 80)\n", + "\n", + "# Wait a moment for memory to be stored\n", + "await asyncio.sleep(1)\n", + "\n", + "user_message = \"What courses would you recommend for me?\"\n", + "\n", + "messages = [\n", + " SystemMessage(content=system_prompt),\n", + " HumanMessage(content=user_message)\n", + "]\n", + "\n", + "print(f\"\\n👤 User: {user_message}\")\n", + "\n", + "# First response - should call search_long_term_memory\n", + "response = llm_with_tools.invoke(messages)\n", + "\n", + "if response.tool_calls:\n", + " print(\"\\n🤖 Agent decision: Search for preferences first\")\n", + " for tool_call in response.tool_calls:\n", + " print(f\" Tool: {tool_call['name']}\")\n", + " print(f\" Args: {tool_call['args']}\")\n", + " \n", + " # Find and execute the tool\n", + " tool = next((t for t in memory_tools if t.name == tool_call['name']), None)\n", + " if tool:\n", + " try:\n", + " result = await tool.ainvoke(tool_call['args'])\n", + " print(f\"\\n Retrieved memories:\")\n", + " print(f\" {result}\")\n", + " result_content = str(result)\n", + " except Exception as e:\n", + " print(f\"\\n Error: {e}\")\n", + " result_content = f\"Error: {str(e)}\"\n", + " \n", + " # Add tool result to messages\n", + " messages.append(response)\n", + " messages.append(ToolMessage(\n", + " content=result_content,\n", + " tool_call_id=tool_call['id']\n", + " ))\n", + " \n", + " # Get final response\n", + " final_response = llm_with_tools.invoke(messages)\n", + " print(f\"\\n🤖 Agent: {final_response.content}\")\n", + " print(\"\\n✅ Agent used memories to personalize recommendation!\")\n", + "else:\n", + " print(f\"\\n🤖 Agent: {response.content}\")\n", + " print(\"\\n⚠️ Agent didn't search memories\")\n", + "\n", + "print(\"\\n\" + \"=\" * 80)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example 3: Multi-Turn Conversation with Memory" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"\\n\" + \"=\" * 80)\n", + "print(\"EXAMPLE 3: Multi-Turn Conversation\")\n", + "print(\"=\" * 80)\n", + "\n", + "async def chat_with_memory(user_message, conversation_history):\n", + " \"\"\"Helper function for conversation with memory tools.\"\"\"\n", + " messages = [SystemMessage(content=system_prompt)]\n", + " messages.extend(conversation_history)\n", + " messages.append(HumanMessage(content=user_message))\n", + " \n", + " # Get response\n", + " response = llm_with_tools.invoke(messages)\n", + " \n", + " # Handle tool calls\n", + " if response.tool_calls:\n", + " messages.append(response)\n", + " \n", + " for tool_call in response.tool_calls:\n", + " # Execute tool\n", + " if tool_call['name'] == 'store_memory':\n", + " result = await store_memory.ainvoke(tool_call['args'])\n", + " elif tool_call['name'] == 'search_memories':\n", + " result = await search_memories.ainvoke(tool_call['args'])\n", + " else:\n", + " result = \"Unknown tool\"\n", + " \n", + " messages.append(ToolMessage(\n", + " content=result,\n", + " tool_call_id=tool_call['id']\n", + " ))\n", + " \n", + " # Get final response after tool execution\n", + " response = llm_with_tools.invoke(messages)\n", + " \n", + " # Update conversation history\n", + " conversation_history.append(HumanMessage(content=user_message))\n", + " conversation_history.append(AIMessage(content=response.content))\n", + " \n", + " return response.content, conversation_history\n", + "\n", + "# Have a conversation\n", + "conversation = []\n", + "\n", + "queries = [\n", + " \"I'm a junior majoring in Computer Science.\",\n", + " \"I want to focus on machine learning and AI.\",\n", + " \"What do you know about me so far?\",\n", + "]\n", + "\n", + "for query in queries:\n", + " print(f\"\\n👤 User: {query}\")\n", + " response, conversation = await chat_with_memory(query, conversation)\n", + " print(f\"🤖 Agent: {response}\")\n", + " await asyncio.sleep(1)\n", + "\n", + "print(\"\\n\" + \"=\" * 80)\n", + "print(\"✅ Agent proactively stored and retrieved memories!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Key Takeaways\n", + "\n", + "### Benefits of Memory Tools\n", + "\n", + "✅ **LLM Control:**\n", + "- Agent decides what's important\n", + "- Agent decides when to search\n", + "- More intelligent behavior\n", + "\n", + "✅ **Flexibility:**\n", + "- Can store context-dependent information\n", + "- Can search on-demand\n", + "- Can update/delete memories\n", + "\n", + "✅ **Transparency:**\n", + "- You can see when agent stores/searches\n", + "- Easier to debug\n", + "- More explainable\n", + "\n", + "### When to Use Memory Tools\n", + "\n", + "**Use memory tools when:**\n", + "- ✅ Building advanced, autonomous agents\n", + "- ✅ Agent needs fine-grained control\n", + "- ✅ Importance is context-dependent\n", + "- ✅ Want explicit memory operations\n", + "\n", + "**Use automatic extraction when:**\n", + "- ✅ Simple, consistent extraction is fine\n", + "- ✅ Want to minimize token usage\n", + "- ✅ Building straightforward agents\n", + "\n", + "**Best practice: Combine both!**\n", + "- Automatic extraction as baseline\n", + "- Tools for explicit control\n", + "\n", + "### Tool Design Best Practices\n", + "\n", + "1. **Clear descriptions** - Explain when to use each tool\n", + "2. **Good examples** - Show typical usage\n", + "3. **Error handling** - Handle failures gracefully\n", + "4. **Feedback** - Return clear success/failure messages\n", + "\n", + "### Common Patterns\n", + "\n", + "**Store after learning:**\n", + "```\n", + "User: \"I prefer online courses\"\n", + "Agent: [stores memory] \"Got it, I'll remember that!\"\n", + "```\n", + "\n", + "**Search before recommending:**\n", + "```\n", + "User: \"What courses should I take?\"\n", + "Agent: [searches memories] \"Based on your preferences...\"\n", + "```\n", + "\n", + "**Proactive recall:**\n", + "```\n", + "User: \"Tell me about CS401\"\n", + "Agent: [searches memories] \"I remember you're interested in ML...\"\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Exercises\n", + "\n", + "1. **Test memory decisions**: Have a 10-turn conversation. Does the agent store and search appropriately?\n", + "\n", + "2. **Add update tool**: Create an `update_memory` tool that lets the agent modify existing memories.\n", + "\n", + "3. **Compare approaches**: Build two agents - one with automatic extraction, one with tools. Which performs better?\n", + "\n", + "4. **Memory strategy**: Design a system prompt that guides the agent on when to use memory tools." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "In this notebook, you learned:\n", + "\n", + "- ✅ Memory tools give the LLM control over memory operations\n", + "- ✅ Agent Memory Server provides built-in memory tools\n", + "- ✅ Tools enable intelligent, context-aware memory management\n", + "- ✅ Combine automatic extraction with tools for best results\n", + "- ✅ Clear tool descriptions guide proper usage\n", + "\n", + "**Key insight:** Tool-based memory management enables more sophisticated agents that can decide what to remember and when to recall information. This is especially powerful for autonomous agents that need fine-grained control over their memory." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/python-recipes/context-engineering/notebooks/section-4-optimizations/01_context_window_management.ipynb b/python-recipes/context-engineering/notebooks/section-4-optimizations/01_context_window_management.ipynb new file mode 100644 index 00000000..85fb4afa --- /dev/null +++ b/python-recipes/context-engineering/notebooks/section-4-optimizations/01_context_window_management.ipynb @@ -0,0 +1,561 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Context Window Management: Handling Token Limits\n", + "\n", + "## Introduction\n", + "\n", + "In this notebook, you'll learn about context window limits and how to manage them effectively. Every LLM has a maximum number of tokens it can process, and long conversations can exceed this limit. The Agent Memory Server provides automatic summarization to handle this.\n", + "\n", + "### What You'll Learn\n", + "\n", + "- What context windows are and why they matter\n", + "- How to count tokens in conversations\n", + "- Why summarization is necessary\n", + "- How to configure Agent Memory Server summarization\n", + "- How summarization works in practice\n", + "\n", + "### Prerequisites\n", + "\n", + "- Completed Section 3 notebooks\n", + "- Redis 8 running locally\n", + "- Agent Memory Server running\n", + "- OpenAI API key set" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Concepts: Context Windows and Token Limits\n", + "\n", + "### What is a Context Window?\n", + "\n", + "A **context window** is the maximum amount of text (measured in tokens) that an LLM can process in a single request. This includes:\n", + "\n", + "- System instructions\n", + "- Conversation history\n", + "- Retrieved context (memories, documents)\n", + "- User's current message\n", + "- Space for the response\n", + "\n", + "### Common Context Window Sizes\n", + "\n", + "| Model | Context Window | Notes |\n", + "|-------|----------------|-------|\n", + "| GPT-4o | 128K tokens | ~96,000 words |\n", + "| GPT-4 Turbo | 128K tokens | ~96,000 words |\n", + "| GPT-3.5 Turbo | 16K tokens | ~12,000 words |\n", + "| Claude 3 Opus | 200K tokens | ~150,000 words |\n", + "\n", + "### The Problem: Long Conversations\n", + "\n", + "As conversations grow, they consume more tokens:\n", + "\n", + "```\n", + "Turn 1: System (500) + Messages (200) = 700 tokens ✅\n", + "Turn 5: System (500) + Messages (1,000) = 1,500 tokens ✅\n", + "Turn 20: System (500) + Messages (4,000) = 4,500 tokens ✅\n", + "Turn 50: System (500) + Messages (10,000) = 10,500 tokens ✅\n", + "Turn 100: System (500) + Messages (20,000) = 20,500 tokens ⚠️\n", + "Turn 200: System (500) + Messages (40,000) = 40,500 tokens ⚠️\n", + "```\n", + "\n", + "Eventually, you'll hit the limit!\n", + "\n", + "### Why Summarization is Necessary\n", + "\n", + "Without summarization:\n", + "- ❌ Conversations eventually fail\n", + "- ❌ Costs increase linearly with conversation length\n", + "- ❌ Latency increases with more tokens\n", + "- ❌ Important early context gets lost\n", + "\n", + "With summarization:\n", + "- ✅ Conversations can continue indefinitely\n", + "- ✅ Costs stay manageable\n", + "- ✅ Latency stays consistent\n", + "- ✅ Important context is preserved in summaries\n", + "\n", + "### How Agent Memory Server Handles This\n", + "\n", + "The Agent Memory Server automatically:\n", + "1. **Monitors message count** in working memory\n", + "2. **Triggers summarization** when threshold is reached\n", + "3. **Creates summary** of older messages\n", + "4. **Replaces old messages** with summary\n", + "5. **Keeps recent messages** for context\n", + "\n", + "### Token Budgets\n", + "\n", + "A **token budget** is how you allocate your context window:\n", + "\n", + "```\n", + "Total: 128K tokens\n", + "├─ System instructions: 1K tokens\n", + "├─ Working memory: 8K tokens\n", + "├─ Long-term memories: 2K tokens\n", + "├─ Retrieved context: 4K tokens\n", + "├─ User message: 500 tokens\n", + "└─ Response space: 2K tokens\n", + " ────────────────────────────\n", + " Used: 17.5K / 128K (13.7%)\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import asyncio\n", + "import tiktoken\n", + "from langchain_openai import ChatOpenAI\n", + "from langchain_core.messages import SystemMessage, HumanMessage, AIMessage\n", + "from agent_memory_client import MemoryAPIClient as MemoryClient, MemoryClientConfig\n", + "\n", + "# Initialize\n", + "student_id = \"student_context_demo\"\n", + "session_id = \"long_conversation\"\n", + "\n", + "# Initialize memory client with proper config\n", + "import os\n", + "config = MemoryClientConfig(\n", + " base_url=os.getenv(\"AGENT_MEMORY_URL\", \"http://localhost:8000\"),\n", + " default_namespace=\"redis_university\"\n", + ")\n", + "memory_client = MemoryClient(config=config)\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-4o\", temperature=0.7)\n", + "\n", + "# Initialize tokenizer for counting\n", + "tokenizer = tiktoken.encoding_for_model(\"gpt-4o\")\n", + "\n", + "def count_tokens(text: str) -> int:\n", + " \"\"\"Count tokens in text.\"\"\"\n", + " return len(tokenizer.encode(text))\n", + "\n", + "print(f\"✅ Setup complete for {student_id}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Hands-on: Understanding Token Counts" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example 1: Counting Tokens in Messages" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Ensure count_tokens is defined (in case cells are run out of order)\n", + "if \"count_tokens\" not in globals():\n", + " import tiktoken\n", + " tokenizer = tiktoken.encoding_for_model(\"gpt-4o\")\n", + " def count_tokens(text: str) -> int:\n", + " return len(tokenizer.encode(text))\n", + "\n", + "# Example messages\n", + "messages = [\n", + " \"Hi, I'm interested in machine learning courses.\",\n", + " \"Can you recommend some courses for beginners?\",\n", + " \"What are the prerequisites for CS401?\",\n", + " \"I've completed CS101 and CS201. Can I take CS401?\",\n", + " \"Great! When is CS401 offered?\"\n", + "]\n", + "\n", + "print(\"Token counts for individual messages:\\n\")\n", + "total_tokens = 0\n", + "for i, msg in enumerate(messages, 1):\n", + " tokens = count_tokens(msg)\n", + " total_tokens += tokens\n", + " print(f\"{i}. \\\"{msg}\\\"\")\n", + " print(f\" Tokens: {tokens}\\n\")\n", + "\n", + "print(f\"Total tokens for 5 messages: {total_tokens}\")\n", + "print(f\"Average tokens per message: {total_tokens / len(messages):.1f}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example 2: Token Growth Over Conversation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Ensure count_tokens is defined (in case cells are run out of order)\n", + "if \"count_tokens\" not in globals():\n", + " import tiktoken\n", + " tokenizer = tiktoken.encoding_for_model(\"gpt-4o\")\n", + " def count_tokens(text: str) -> int:\n", + " return len(tokenizer.encode(text))\n", + "\n", + "# Simulate conversation growth\n", + "system_prompt = \"\"\"You are a helpful class scheduling agent for Redis University.\n", + "Help students find courses and plan their schedule.\"\"\"\n", + "\n", + "system_tokens = count_tokens(system_prompt)\n", + "print(f\"System prompt tokens: {system_tokens}\\n\")\n", + "\n", + "# Simulate growing conversation\n", + "conversation_tokens = 0\n", + "avg_message_tokens = 50 # Typical message size\n", + "\n", + "print(\"Token growth over conversation turns:\\n\")\n", + "print(f\"{'Turn':<6} {'Messages':<10} {'Conv Tokens':<12} {'Total Tokens':<12} {'% of 128K'}\")\n", + "print(\"-\" * 60)\n", + "\n", + "for turn in [1, 5, 10, 20, 50, 100, 200, 500, 1000]:\n", + " # Each turn = user message + assistant message\n", + " conversation_tokens = turn * 2 * avg_message_tokens\n", + " total_tokens = system_tokens + conversation_tokens\n", + " percentage = (total_tokens / 128000) * 100\n", + " \n", + " print(f\"{turn:<6} {turn*2:<10} {conversation_tokens:<12,} {total_tokens:<12,} {percentage:>6.1f}%\")\n", + "\n", + "print(\"\\n⚠️ Without summarization, long conversations will eventually exceed limits!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Configuring Summarization\n", + "\n", + "The Agent Memory Server provides automatic summarization. Let's see how to configure it." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Understanding Summarization Settings\n", + "\n", + "The Agent Memory Server uses these settings:\n", + "\n", + "**Message Count Threshold:**\n", + "- When working memory exceeds this many messages, summarization triggers\n", + "- Default: 20 messages (10 turns)\n", + "- Configurable per session\n", + "\n", + "**Summarization Strategy:**\n", + "- **Recent + Summary**: Keep recent N messages, summarize older ones\n", + "- **Sliding Window**: Keep only recent N messages\n", + "- **Full Summary**: Summarize everything\n", + "\n", + "**What Gets Summarized:**\n", + "- Older conversation messages\n", + "- Key facts and decisions\n", + "- Important context\n", + "\n", + "**What Stays:**\n", + "- Recent messages (for immediate context)\n", + "- System instructions\n", + "- Long-term memories (separate from working memory)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example 3: Demonstrating Summarization\n", + "\n", + "Let's create a conversation that triggers summarization." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Helper function for conversation\n", + "async def have_conversation_turn(user_message, session_id):\n", + " \"\"\"Simulate a conversation turn.\"\"\"\n", + " # Get working memory\n", + " _, working_memory = await memory_client.get_or_create_working_memory(\n", + " session_id=session_id,\n", + " model_name=\"gpt-4o\"\n", + " )\n", + " \n", + " # Build messages\n", + " messages = [SystemMessage(content=\"You are a helpful class scheduling agent.\")]\n", + " \n", + " if working_memory and working_memory.messages:\n", + " for msg in working_memory.messages:\n", + " if msg.role == \"user\":\n", + " messages.append(HumanMessage(content=msg.content))\n", + " elif msg.role == \"assistant\":\n", + " messages.append(AIMessage(content=msg.content))\n", + " \n", + " messages.append(HumanMessage(content=user_message))\n", + " \n", + " # Get response\n", + " response = llm.invoke(messages)\n", + " \n", + " # Save to working memory\n", + " all_messages = []\n", + " if working_memory and working_memory.messages:\n", + " all_messages = [{\"role\": m.role, \"content\": m.content} for m in working_memory.messages]\n", + " \n", + " all_messages.extend([\n", + " {\"role\": \"user\", \"content\": user_message},\n", + " {\"role\": \"assistant\", \"content\": response.content}\n", + " ])\n", + " \n", + " from agent_memory_client.models import WorkingMemory, MemoryMessage\n", + " \n", + " # Convert messages to MemoryMessage format\n", + " memory_messages = [MemoryMessage(**msg) for msg in all_messages]\n", + " \n", + " # Create WorkingMemory object\n", + " working_memory = WorkingMemory(\n", + " session_id=session_id,\n", + " user_id=\"demo_user\",\n", + " messages=memory_messages,\n", + " memories=[],\n", + " data={}\n", + " )\n", + " \n", + " await memory_client.put_working_memory(\n", + " session_id=session_id,\n", + " memory=working_memory,\n", + " user_id=\"demo_user\",\n", + " model_name=\"gpt-4o\"\n", + " )\n", + " \n", + " return response.content, len(all_messages)\n", + "\n", + "print(\"✅ Helper function defined\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Have a multi-turn conversation\n", + "print(\"=\" * 80)\n", + "print(\"DEMONSTRATING SUMMARIZATION\")\n", + "print(\"=\" * 80)\n", + "\n", + "conversation_queries = [\n", + " \"Hi, I'm a computer science major interested in AI.\",\n", + " \"What machine learning courses do you offer?\",\n", + " \"Tell me about CS401.\",\n", + " \"What are the prerequisites?\",\n", + " \"I've completed CS101 and CS201.\",\n", + " \"Can I take CS401 next semester?\",\n", + " \"When is it offered?\",\n", + " \"Is it available online?\",\n", + " \"What about CS402?\",\n", + " \"Can I take both CS401 and CS402?\",\n", + " \"What's the workload like?\",\n", + " \"Are there any projects?\",\n", + "]\n", + "\n", + "for i, query in enumerate(conversation_queries, 1):\n", + " print(f\"\\nTurn {i}:\")\n", + " print(f\"User: {query}\")\n", + " \n", + " response, message_count = await have_conversation_turn(query, session_id)\n", + " \n", + " print(f\"Agent: {response[:100]}...\")\n", + " print(f\"Total messages in working memory: {message_count}\")\n", + " \n", + " if message_count > 20:\n", + " print(\"⚠️ Message count exceeds threshold - summarization may trigger\")\n", + " \n", + " await asyncio.sleep(0.5) # Rate limiting\n", + "\n", + "print(\"\\n\" + \"=\" * 80)\n", + "print(\"✅ Conversation complete\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example 4: Checking Working Memory After Summarization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Check working memory state\n", + "print(\"\\nChecking working memory state...\\n\")\n", + "\n", + "_, working_memory = await memory_client.get_or_create_working_memory(\n", + " session_id=session_id,\n", + " model_name=\"gpt-4o\"\n", + ")\n", + "\n", + "if working_memory:\n", + " print(f\"Total messages: {len(working_memory.messages)}\")\n", + " print(f\"\\nMessage breakdown:\")\n", + " \n", + " user_msgs = [m for m in working_memory.messages if m.role == \"user\"]\n", + " assistant_msgs = [m for m in working_memory.messages if m.role == \"assistant\"]\n", + " system_msgs = [m for m in working_memory.messages if m.role == \"system\"]\n", + " \n", + " print(f\" User messages: {len(user_msgs)}\")\n", + " print(f\" Assistant messages: {len(assistant_msgs)}\")\n", + " print(f\" System messages (summaries): {len(system_msgs)}\")\n", + " \n", + " # Check for summary messages\n", + " if system_msgs:\n", + " print(\"\\n✅ Summarization occurred! Summary messages found:\")\n", + " for msg in system_msgs:\n", + " print(f\"\\n Summary: {msg.content[:200]}...\")\n", + " else:\n", + " print(\"\\n⏳ No summarization yet (may need more messages or time)\")\n", + "else:\n", + " print(\"No working memory found\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Key Takeaways\n", + "\n", + "### Context Window Management Strategy\n", + "\n", + "1. **Monitor token usage** - Know your limits\n", + "2. **Set message thresholds** - Trigger summarization before hitting limits\n", + "3. **Keep recent context** - Don't summarize everything\n", + "4. **Use long-term memory** - Important facts go there, not working memory\n", + "5. **Trust automatic summarization** - Agent Memory Server handles it\n", + "\n", + "### Token Budget Best Practices\n", + "\n", + "**Allocate wisely:**\n", + "- System instructions: 1-2K tokens\n", + "- Working memory: 4-8K tokens\n", + "- Long-term memories: 2-4K tokens\n", + "- Retrieved context: 2-4K tokens\n", + "- Response space: 2-4K tokens\n", + "\n", + "**Total: ~15-20K tokens (leaves plenty of headroom)**\n", + "\n", + "### When Summarization Happens\n", + "\n", + "The Agent Memory Server triggers summarization when:\n", + "- ✅ Message count exceeds threshold (default: 20)\n", + "- ✅ Token count approaches limits\n", + "- ✅ Configured summarization strategy activates\n", + "\n", + "### What Summarization Preserves\n", + "\n", + "✅ **Preserved:**\n", + "- Key facts and decisions\n", + "- Important context\n", + "- Recent messages (full text)\n", + "- Long-term memories (separate storage)\n", + "\n", + "❌ **Compressed:**\n", + "- Older conversation details\n", + "- Redundant information\n", + "- Small talk\n", + "\n", + "### Why This Matters\n", + "\n", + "Without proper context window management:\n", + "- ❌ Conversations fail when limits are hit\n", + "- ❌ Costs grow linearly with conversation length\n", + "- ❌ Performance degrades with more tokens\n", + "\n", + "With proper management:\n", + "- ✅ Conversations can continue indefinitely\n", + "- ✅ Costs stay predictable\n", + "- ✅ Performance stays consistent\n", + "- ✅ Important context is preserved" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Exercises\n", + "\n", + "1. **Calculate your token budget**: For your agent, allocate tokens across system prompt, working memory, long-term memories, and response space.\n", + "\n", + "2. **Test long conversations**: Have a 50-turn conversation and monitor token usage. When does summarization trigger?\n", + "\n", + "3. **Compare strategies**: Test different message thresholds (10, 20, 50). How does it affect conversation quality?\n", + "\n", + "4. **Measure costs**: Calculate the cost difference between keeping full history vs. using summarization for a 100-turn conversation." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "In this notebook, you learned:\n", + "\n", + "- ✅ Context windows have token limits that conversations can exceed\n", + "- ✅ Token budgets help allocate context window space\n", + "- ✅ Summarization is necessary for long conversations\n", + "- ✅ Agent Memory Server provides automatic summarization\n", + "- ✅ Proper management enables indefinite conversations\n", + "\n", + "**Key insight:** Context window management isn't about proving you need summarization - it's about understanding the constraints and using the right tools (like Agent Memory Server) to handle them automatically." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/python-recipes/context-engineering/notebooks/section-4-optimizations/02_retrieval_strategies.ipynb b/python-recipes/context-engineering/notebooks/section-4-optimizations/02_retrieval_strategies.ipynb new file mode 100644 index 00000000..b7c2afc1 --- /dev/null +++ b/python-recipes/context-engineering/notebooks/section-4-optimizations/02_retrieval_strategies.ipynb @@ -0,0 +1,624 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Retrieval Strategies: RAG, Summaries, and Hybrid Approaches\n", + "\n", + "## Introduction\n", + "\n", + "In this notebook, you'll learn different strategies for retrieving and providing context to your agent. Not all context should be included all the time - you need smart retrieval strategies to provide relevant information efficiently.\n", + "\n", + "### What You'll Learn\n", + "\n", + "- Different retrieval strategies (full context, RAG, summaries, hybrid)\n", + "- When to use each strategy\n", + "- How to optimize vector search parameters\n", + "- How to measure retrieval quality and performance\n", + "\n", + "### Prerequisites\n", + "\n", + "- Completed Section 3 notebooks\n", + "- Redis 8 running locally\n", + "- Agent Memory Server running\n", + "- OpenAI API key set\n", + "- Course data ingested" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Concepts: Retrieval Strategies\n", + "\n", + "### The Context Retrieval Problem\n", + "\n", + "You have a large knowledge base (courses, memories, documents), but you can't include everything in every request. You need to:\n", + "\n", + "1. **Find relevant information** - What's related to the user's query?\n", + "2. **Limit context size** - Stay within token budgets\n", + "3. **Maintain quality** - Don't miss important information\n", + "4. **Optimize performance** - Fast retrieval, low latency\n", + "\n", + "### Strategy 1: Full Context (Naive)\n", + "\n", + "**Approach:** Include everything in every request\n", + "\n", + "```python\n", + "# Include entire course catalog\n", + "all_courses = get_all_courses() # 500 courses\n", + "context = \"\\n\".join([str(course) for course in all_courses])\n", + "```\n", + "\n", + "**Pros:**\n", + "- ✅ Never miss relevant information\n", + "- ✅ Simple to implement\n", + "\n", + "**Cons:**\n", + "- ❌ Exceeds token limits quickly\n", + "- ❌ Expensive (more tokens = higher cost)\n", + "- ❌ Slow (more tokens = higher latency)\n", + "- ❌ Dilutes relevant information with noise\n", + "\n", + "**Verdict:** ❌ Don't use for production\n", + "\n", + "### Strategy 2: RAG (Retrieval-Augmented Generation)\n", + "\n", + "**Approach:** Retrieve only relevant information using semantic search\n", + "\n", + "```python\n", + "# Search for relevant courses\n", + "query = \"machine learning courses\"\n", + "relevant_courses = search_courses(query, limit=5)\n", + "context = \"\\n\".join([str(course) for course in relevant_courses])\n", + "```\n", + "\n", + "**Pros:**\n", + "- ✅ Only includes relevant information\n", + "- ✅ Stays within token budgets\n", + "- ✅ Fast and cost-effective\n", + "- ✅ Semantic search finds related content\n", + "\n", + "**Cons:**\n", + "- ⚠️ May miss relevant information if search isn't perfect\n", + "- ⚠️ Requires good embeddings and search tuning\n", + "\n", + "**Verdict:** ✅ Good for most use cases\n", + "\n", + "### Strategy 3: Summaries\n", + "\n", + "**Approach:** Pre-compute summaries of large datasets\n", + "\n", + "```python\n", + "# Use pre-computed course catalog summary\n", + "summary = get_course_catalog_summary() # \"CS: 50 courses, MATH: 30 courses...\"\n", + "context = summary\n", + "```\n", + "\n", + "**Pros:**\n", + "- ✅ Very compact (low token usage)\n", + "- ✅ Fast (no search needed)\n", + "- ✅ Provides high-level overview\n", + "\n", + "**Cons:**\n", + "- ❌ Loses details\n", + "- ❌ May not have specific information needed\n", + "- ⚠️ Requires pre-computation\n", + "\n", + "**Verdict:** ✅ Good for overviews, combine with RAG for details\n", + "\n", + "### Strategy 4: Hybrid (Best)\n", + "\n", + "**Approach:** Combine summaries + targeted retrieval\n", + "\n", + "```python\n", + "# Start with summary for overview\n", + "summary = get_course_catalog_summary()\n", + "\n", + "# Add specific relevant courses\n", + "relevant_courses = search_courses(query, limit=3)\n", + "\n", + "context = f\"{summary}\\n\\nRelevant courses:\\n{courses}\"\n", + "```\n", + "\n", + "**Pros:**\n", + "- ✅ Best of both worlds\n", + "- ✅ Overview + specific details\n", + "- ✅ Efficient token usage\n", + "- ✅ High quality results\n", + "\n", + "**Cons:**\n", + "- ⚠️ More complex to implement\n", + "- ⚠️ Requires pre-computed summaries\n", + "\n", + "**Verdict:** ✅ Best for production systems" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import time\n", + "import asyncio\n", + "from typing import List\n", + "import tiktoken\n", + "from langchain_openai import ChatOpenAI\n", + "from langchain_core.messages import SystemMessage, HumanMessage\n", + "from redis_context_course import CourseManager, MemoryClient, MemoryClientConfig\n", + "\n", + "# Initialize\n", + "course_manager = CourseManager()\n", + "# Initialize memory client with proper config\n", + "import os\n", + "config = MemoryClientConfig(\n", + " base_url=os.getenv(\"AGENT_MEMORY_URL\", \"http://localhost:8000\"),\n", + " default_namespace=\"redis_university\"\n", + ")\n", + "memory_client = MemoryClient(config=config)\n", + "llm = ChatOpenAI(model=\"gpt-4o\", temperature=0.7)\n", + "tokenizer = tiktoken.encoding_for_model(\"gpt-4o\")\n", + "\n", + "def count_tokens(text: str) -> int:\n", + " return len(tokenizer.encode(text))\n", + "\n", + "print(\"✅ Setup complete\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Hands-on: Comparing Retrieval Strategies" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Strategy 1: Full Context (Bad)\n", + "\n", + "Let's try including all courses and see what happens." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"=\" * 80)\n", + "print(\"STRATEGY 1: FULL CONTEXT (Naive)\")\n", + "print(\"=\" * 80)\n", + "\n", + "# Get all courses\n", + "all_courses = await course_manager.get_all_courses()\n", + "print(f\"\\nTotal courses in catalog: {len(all_courses)}\")\n", + "\n", + "# Build full context\n", + "full_context = \"\\n\\n\".join([\n", + " f\"{c.course_code}: {c.title}\\n{c.description}\\nCredits: {c.credits} | {c.format.value}\"\n", + " for c in all_courses[:50] # Limit to 50 for demo\n", + "])\n", + "\n", + "tokens = count_tokens(full_context)\n", + "print(f\"\\nTokens for 50 courses: {tokens:,}\")\n", + "print(f\"Estimated tokens for all {len(all_courses)} courses: {(tokens * len(all_courses) / 50):,.0f}\")\n", + "\n", + "# Try to use it\n", + "user_query = \"I'm interested in machine learning courses\"\n", + "system_prompt = f\"\"\"You are a class scheduling agent.\n", + "\n", + "Available courses:\n", + "{full_context[:2000]}...\n", + "\"\"\"\n", + "\n", + "start_time = time.time()\n", + "messages = [\n", + " SystemMessage(content=system_prompt),\n", + " HumanMessage(content=user_query)\n", + "]\n", + "response = llm.invoke(messages)\n", + "latency = time.time() - start_time\n", + "\n", + "print(f\"\\nQuery: {user_query}\")\n", + "print(f\"Response: {response.content[:200]}...\")\n", + "print(f\"\\nLatency: {latency:.2f}s\")\n", + "print(f\"Total tokens used: ~{count_tokens(system_prompt) + count_tokens(user_query):,}\")\n", + "\n", + "print(\"\\n❌ PROBLEMS:\")\n", + "print(\" - Too many tokens (expensive)\")\n", + "print(\" - High latency\")\n", + "print(\" - Relevant info buried in noise\")\n", + "print(\" - Doesn't scale to full catalog\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Strategy 2: RAG with Semantic Search (Good)\n", + "\n", + "Now let's use semantic search to retrieve only relevant courses." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"\\n\" + \"=\" * 80)\n", + "print(\"STRATEGY 2: RAG (Semantic Search)\")\n", + "print(\"=\" * 80)\n", + "\n", + "user_query = \"I'm interested in machine learning courses\"\n", + "\n", + "# Search for relevant courses\n", + "start_time = time.time()\n", + "relevant_courses = await course_manager.search_courses(\n", + " query=user_query,\n", + " limit=5\n", + ")\n", + "search_time = time.time() - start_time\n", + "\n", + "print(f\"\\nSearch time: {search_time:.3f}s\")\n", + "print(f\"Courses found: {len(relevant_courses)}\")\n", + "\n", + "# Build context from relevant courses only\n", + "rag_context = \"\\n\\n\".join([\n", + " f\"{c.course_code}: {c.title}\\n{c.description}\\nCredits: {c.credits} | {c.format.value}\"\n", + " for c in relevant_courses\n", + "])\n", + "\n", + "tokens = count_tokens(rag_context)\n", + "print(f\"Context tokens: {tokens:,}\")\n", + "\n", + "# Use it\n", + "system_prompt = f\"\"\"You are a class scheduling agent.\n", + "\n", + "Relevant courses:\n", + "{rag_context}\n", + "\"\"\"\n", + "\n", + "start_time = time.time()\n", + "messages = [\n", + " SystemMessage(content=system_prompt),\n", + " HumanMessage(content=user_query)\n", + "]\n", + "response = llm.invoke(messages)\n", + "latency = time.time() - start_time\n", + "\n", + "print(f\"\\nQuery: {user_query}\")\n", + "print(f\"Response: {response.content[:200]}...\")\n", + "print(f\"\\nTotal latency: {latency:.2f}s\")\n", + "print(f\"Total tokens used: ~{count_tokens(system_prompt) + count_tokens(user_query):,}\")\n", + "\n", + "print(\"\\n✅ BENEFITS:\")\n", + "print(\" - Much fewer tokens (cheaper)\")\n", + "print(\" - Lower latency\")\n", + "print(\" - Only relevant information\")\n", + "print(\" - Scales to any catalog size\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Strategy 3: Pre-computed Summary\n", + "\n", + "Let's create a summary of the course catalog." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"\\n\" + \"=\" * 80)\n", + "print(\"STRATEGY 3: PRE-COMPUTED SUMMARY\")\n", + "print(\"=\" * 80)\n", + "\n", + "# Create a summary (in production, this would be pre-computed)\n", + "all_courses = await course_manager.get_all_courses()\n", + "\n", + "# Group by department\n", + "by_department = {}\n", + "for course in all_courses:\n", + " dept = course.department\n", + " if dept not in by_department:\n", + " by_department[dept] = []\n", + " by_department[dept].append(course)\n", + "\n", + "# Create summary\n", + "summary_lines = [\"Course Catalog Summary:\\n\"]\n", + "for dept, courses in sorted(by_department.items()):\n", + " summary_lines.append(f\"{dept}: {len(courses)} courses\")\n", + " # Add a few example courses\n", + " examples = [f\"{c.course_code} ({c.title})\" for c in courses[:2]]\n", + " summary_lines.append(f\" Examples: {', '.join(examples)}\")\n", + "\n", + "summary = \"\\n\".join(summary_lines)\n", + "\n", + "print(f\"\\nSummary:\\n{summary}\")\n", + "print(f\"\\nSummary tokens: {count_tokens(summary):,}\")\n", + "\n", + "# Use it\n", + "user_query = \"What departments offer courses?\"\n", + "system_prompt = f\"\"\"You are a class scheduling agent.\n", + "\n", + "{summary}\n", + "\"\"\"\n", + "\n", + "start_time = time.time()\n", + "messages = [\n", + " SystemMessage(content=system_prompt),\n", + " HumanMessage(content=user_query)\n", + "]\n", + "response = llm.invoke(messages)\n", + "latency = time.time() - start_time\n", + "\n", + "print(f\"\\nQuery: {user_query}\")\n", + "print(f\"Response: {response.content}\")\n", + "print(f\"\\nLatency: {latency:.2f}s\")\n", + "\n", + "print(\"\\n✅ BENEFITS:\")\n", + "print(\" - Very compact (minimal tokens)\")\n", + "print(\" - Fast (no search needed)\")\n", + "print(\" - Good for overview questions\")\n", + "\n", + "print(\"\\n⚠️ LIMITATIONS:\")\n", + "print(\" - Lacks specific details\")\n", + "print(\" - Can't answer detailed questions\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Strategy 4: Hybrid (Best)\n", + "\n", + "Combine summary + targeted retrieval for the best results." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"\\n\" + \"=\" * 80)\n", + "print(\"STRATEGY 4: HYBRID (Summary + RAG)\")\n", + "print(\"=\" * 80)\n", + "\n", + "user_query = \"I'm interested in machine learning. What's available?\"\n", + "\n", + "# Start with summary\n", + "summary_context = summary\n", + "\n", + "# Add targeted retrieval\n", + "relevant_courses = await course_manager.search_courses(\n", + " query=user_query,\n", + " limit=3\n", + ")\n", + "\n", + "detailed_context = \"\\n\\n\".join([\n", + " f\"{c.course_code}: {c.title}\\n{c.description}\\nCredits: {c.credits} | {c.format.value}\"\n", + " for c in relevant_courses\n", + "])\n", + "\n", + "# Combine\n", + "hybrid_context = f\"\"\"{summary_context}\n", + "\n", + "Relevant courses for your query:\n", + "{detailed_context}\n", + "\"\"\"\n", + "\n", + "tokens = count_tokens(hybrid_context)\n", + "print(f\"\\nHybrid context tokens: {tokens:,}\")\n", + "\n", + "# Use it\n", + "system_prompt = f\"\"\"You are a class scheduling agent.\n", + "\n", + "{hybrid_context}\n", + "\"\"\"\n", + "\n", + "start_time = time.time()\n", + "messages = [\n", + " SystemMessage(content=system_prompt),\n", + " HumanMessage(content=user_query)\n", + "]\n", + "response = llm.invoke(messages)\n", + "latency = time.time() - start_time\n", + "\n", + "print(f\"\\nQuery: {user_query}\")\n", + "print(f\"Response: {response.content}\")\n", + "print(f\"\\nLatency: {latency:.2f}s\")\n", + "print(f\"Total tokens: ~{count_tokens(system_prompt) + count_tokens(user_query):,}\")\n", + "\n", + "print(\"\\n✅ BENEFITS:\")\n", + "print(\" - Overview + specific details\")\n", + "print(\" - Efficient token usage\")\n", + "print(\" - High quality responses\")\n", + "print(\" - Best of all strategies\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Optimizing Vector Search Parameters\n", + "\n", + "Let's explore how to tune semantic search for better results." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"\\n\" + \"=\" * 80)\n", + "print(\"OPTIMIZING SEARCH PARAMETERS\")\n", + "print(\"=\" * 80)\n", + "\n", + "user_query = \"beginner programming courses\"\n", + "\n", + "# Test different limits\n", + "print(f\"\\nQuery: '{user_query}'\\n\")\n", + "\n", + "for limit in [3, 5, 10]:\n", + " results = await course_manager.search_courses(\n", + " query=user_query,\n", + " limit=limit\n", + " )\n", + " \n", + " print(f\"Limit={limit}: Found {len(results)} courses\")\n", + " for i, course in enumerate(results, 1):\n", + " print(f\" {i}. {course.course_code}: {course.title}\")\n", + " print()\n", + "\n", + "print(\"💡 TIP: Start with limit=5, adjust based on your needs\")\n", + "print(\" - Too few: May miss relevant results\")\n", + "print(\" - Too many: Wastes tokens, adds noise\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Performance Comparison\n", + "\n", + "Let's compare all strategies side-by-side." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"\\n\" + \"=\" * 80)\n", + "print(\"STRATEGY COMPARISON\")\n", + "print(\"=\" * 80)\n", + "\n", + "print(f\"\\n{'Strategy':<20} {'Tokens':<10} {'Latency':<10} {'Quality':<10} {'Scalability'}\")\n", + "print(\"-\" * 70)\n", + "print(f\"{'Full Context':<20} {'50,000+':<10} {'High':<10} {'Good':<10} {'Poor'}\")\n", + "print(f\"{'RAG (Semantic)':<20} {'500-2K':<10} {'Low':<10} {'Good':<10} {'Excellent'}\")\n", + "print(f\"{'Summary Only':<20} {'100-500':<10} {'Very Low':<10} {'Limited':<10} {'Excellent'}\")\n", + "print(f\"{'Hybrid':<20} {'1K-3K':<10} {'Low':<10} {'Excellent':<10} {'Excellent'}\")\n", + "\n", + "print(\"\\n✅ RECOMMENDATION: Use Hybrid strategy for production\")\n", + "print(\" - Provides overview + specific details\")\n", + "print(\" - Efficient token usage\")\n", + "print(\" - Scales to any dataset size\")\n", + "print(\" - High quality results\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Key Takeaways\n", + "\n", + "### Choosing a Retrieval Strategy\n", + "\n", + "**Use RAG when:**\n", + "- ✅ You need specific, detailed information\n", + "- ✅ Dataset is large\n", + "- ✅ Queries are specific\n", + "\n", + "**Use Summaries when:**\n", + "- ✅ You need high-level overviews\n", + "- ✅ Queries are general\n", + "- ✅ Token budget is tight\n", + "\n", + "**Use Hybrid when:**\n", + "- ✅ You want the best quality\n", + "- ✅ You can pre-compute summaries\n", + "- ✅ Building production systems\n", + "\n", + "### Optimization Tips\n", + "\n", + "1. **Start with RAG** - Simple and effective\n", + "2. **Add summaries** - For overview context\n", + "3. **Tune search limits** - Balance relevance vs. tokens\n", + "4. **Pre-compute summaries** - Don't generate on every request\n", + "5. **Monitor performance** - Track tokens, latency, quality\n", + "\n", + "### Vector Search Best Practices\n", + "\n", + "- ✅ Use semantic search for finding relevant content\n", + "- ✅ Start with limit=5, adjust as needed\n", + "- ✅ Use filters when you have structured criteria\n", + "- ✅ Test with real user queries\n", + "- ✅ Monitor search quality over time" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Exercises\n", + "\n", + "1. **Implement hybrid retrieval**: Create a function that combines summary + RAG for any query.\n", + "\n", + "2. **Measure quality**: Test each strategy with 10 different queries. Which gives the best responses?\n", + "\n", + "3. **Optimize search**: Experiment with different search limits. What's the sweet spot for your use case?\n", + "\n", + "4. **Create summaries**: Build pre-computed summaries for different views (by department, by difficulty, by format)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "In this notebook, you learned:\n", + "\n", + "- ✅ Different retrieval strategies have different trade-offs\n", + "- ✅ RAG (semantic search) is efficient and scalable\n", + "- ✅ Summaries provide compact overviews\n", + "- ✅ Hybrid approach combines the best of both\n", + "- ✅ Proper retrieval is key to production-quality agents\n", + "\n", + "**Key insight:** Don't include everything - retrieve smartly. The hybrid strategy (summaries + targeted RAG) provides the best balance of quality, efficiency, and scalability." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/python-recipes/context-engineering/notebooks/section-4-optimizations/03_grounding_with_memory.ipynb b/python-recipes/context-engineering/notebooks/section-4-optimizations/03_grounding_with_memory.ipynb new file mode 100644 index 00000000..a599238b --- /dev/null +++ b/python-recipes/context-engineering/notebooks/section-4-optimizations/03_grounding_with_memory.ipynb @@ -0,0 +1,547 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Grounding with Memory: Using Context to Resolve References\n", + "\n", + "## Introduction\n", + "\n", + "In this notebook, you'll learn about grounding - how agents use memory to understand references and maintain context across a conversation. When users say \"that course\" or \"my advisor\", the agent needs to know what they're referring to. The Agent Memory Server's extracted memories provide this grounding automatically.\n", + "\n", + "### What You'll Learn\n", + "\n", + "- What grounding is and why it matters\n", + "- How extracted memories provide grounding\n", + "- How to handle references to people, places, and things\n", + "- How memory enables natural conversation flow\n", + "\n", + "### Prerequisites\n", + "\n", + "- Completed Section 3 notebooks\n", + "- Redis 8 running locally\n", + "- Agent Memory Server running\n", + "- OpenAI API key set" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Concepts: Grounding\n", + "\n", + "### What is Grounding?\n", + "\n", + "**Grounding** is the process of connecting references in conversation to their actual meanings. When someone says:\n", + "\n", + "- \"Tell me more about **that course**\" - Which course?\n", + "- \"When does **she** teach?\" - Who is \"she\"?\n", + "- \"Is **it** available online?\" - What is \"it\"?\n", + "- \"What about **the other one**?\" - Which one?\n", + "\n", + "The agent needs to **ground** these references to specific entities mentioned earlier in the conversation.\n", + "\n", + "### Grounding Without Memory (Bad)\n", + "\n", + "```\n", + "User: I'm interested in machine learning.\n", + "Agent: Great! We have CS401: Machine Learning.\n", + "\n", + "User: Tell me more about that course.\n", + "Agent: Which course are you asking about? ❌\n", + "```\n", + "\n", + "### Grounding With Memory (Good)\n", + "\n", + "```\n", + "User: I'm interested in machine learning.\n", + "Agent: Great! We have CS401: Machine Learning.\n", + "[Memory extracted: \"Student interested in CS401\"]\n", + "\n", + "User: Tell me more about that course.\n", + "Agent: CS401 covers supervised learning, neural networks... ✅\n", + "[Memory grounds \"that course\" to CS401]\n", + "```\n", + "\n", + "### How Agent Memory Server Provides Grounding\n", + "\n", + "The Agent Memory Server automatically:\n", + "1. **Extracts entities** from conversations (courses, people, places)\n", + "2. **Stores them** in long-term memory with context\n", + "3. **Retrieves them** when similar references appear\n", + "4. **Provides context** to ground ambiguous references\n", + "\n", + "### Types of References\n", + "\n", + "**Pronouns:**\n", + "- \"it\", \"that\", \"this\", \"those\"\n", + "- \"he\", \"she\", \"they\"\n", + "\n", + "**Descriptions:**\n", + "- \"the ML class\"\n", + "- \"my advisor\"\n", + "- \"the main campus\"\n", + "\n", + "**Implicit references:**\n", + "- \"What are the prerequisites?\" (for what?)\n", + "- \"When does it meet?\" (what meets?)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import asyncio\n", + "from langchain_openai import ChatOpenAI\n", + "from langchain_core.messages import SystemMessage, HumanMessage, AIMessage\n", + "from agent_memory_client import MemoryAPIClient as MemoryClient, MemoryClientConfig\n", + "\n", + "# Initialize\n", + "student_id = \"student_789\"\n", + "session_id = \"grounding_demo\"\n", + "\n", + "# Initialize memory client with proper config\n", + "import os\n", + "config = MemoryClientConfig(\n", + " base_url=os.getenv(\"AGENT_MEMORY_URL\", \"http://localhost:8000\"),\n", + " default_namespace=\"redis_university\"\n", + ")\n", + "memory_client = MemoryClient(config=config)\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-4o\", temperature=0.7)\n", + "\n", + "print(f\"✅ Setup complete for {student_id}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Hands-on: Grounding Through Conversation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example 1: Grounding Course References\n", + "\n", + "Let's have a conversation where we refer to courses in different ways." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "async def chat_turn(user_message, conversation_history):\n", + " \"\"\"Helper function to process a conversation turn.\"\"\"\n", + " \n", + " # Search long-term memory for context\n", + " memories = await memory_client.search_long_term_memory(\n", + " text=user_message,\n", + " limit=5\n", + " )\n", + " \n", + " # Build context from memories\n", + " memory_context = \"\\n\".join([f\"- {m.text}\" for m in memories.memories]) if memories.memories else \"None\"\n", + " \n", + " system_prompt = f\"\"\"You are a helpful class scheduling agent for Redis University.\n", + "\n", + "What you remember about this student:\n", + "{memory_context}\n", + "\n", + "Use this context to understand references like \"that course\", \"it\", \"the one I mentioned\", etc.\n", + "\"\"\"\n", + " \n", + " # Build messages\n", + " messages = [SystemMessage(content=system_prompt)]\n", + " messages.extend(conversation_history)\n", + " messages.append(HumanMessage(content=user_message))\n", + " \n", + " # Get response\n", + " response = llm.invoke(messages)\n", + " \n", + " # Update conversation history\n", + " conversation_history.append(HumanMessage(content=user_message))\n", + " conversation_history.append(AIMessage(content=response.content))\n", + " \n", + " # Save to working memory (triggers extraction)\n", + " messages_to_save = [\n", + " {\"role\": \"user\" if isinstance(m, HumanMessage) else \"assistant\", \"content\": m.content}\n", + " for m in conversation_history\n", + " ]\n", + " from agent_memory_client.models import WorkingMemory, MemoryMessage\n", + " \n", + " # Convert messages to MemoryMessage format\n", + " memory_messages = [MemoryMessage(**msg) for msg in messages_to_save]\n", + " \n", + " # Create WorkingMemory object\n", + " working_memory = WorkingMemory(\n", + " session_id=session_id,\n", + " user_id=\"demo_user\",\n", + " messages=memory_messages,\n", + " memories=[],\n", + " data={}\n", + " )\n", + " \n", + " await memory_client.put_working_memory(\n", + " session_id=session_id,\n", + " memory=working_memory,\n", + " user_id=\"demo_user\",\n", + " model_name=\"gpt-4o\"\n", + " )\n", + " \n", + " return response.content, conversation_history\n", + "\n", + "print(\"✅ Helper function defined\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Start conversation\n", + "conversation = []\n", + "\n", + "print(\"=\" * 80)\n", + "print(\"CONVERSATION: Grounding Course References\")\n", + "print(\"=\" * 80)\n", + "\n", + "# Turn 1: Mention a specific course\n", + "print(\"\\n👤 User: I'm interested in CS401, the machine learning course.\")\n", + "response, conversation = await chat_turn(\n", + " \"I'm interested in CS401, the machine learning course.\",\n", + " conversation\n", + ")\n", + "print(f\"🤖 Agent: {response}\")\n", + "\n", + "# Wait for extraction\n", + "await asyncio.sleep(2)\n", + "\n", + "# Turn 2: Use pronoun \"it\"\n", + "print(\"\\n👤 User: What are the prerequisites for it?\")\n", + "response, conversation = await chat_turn(\n", + " \"What are the prerequisites for it?\",\n", + " conversation\n", + ")\n", + "print(f\"🤖 Agent: {response}\")\n", + "print(\"\\n✅ Agent grounded 'it' to CS401\")\n", + "\n", + "# Turn 3: Use description \"that ML class\"\n", + "print(\"\\n👤 User: Is that ML class available online?\")\n", + "response, conversation = await chat_turn(\n", + " \"Is that ML class available online?\",\n", + " conversation\n", + ")\n", + "print(f\"🤖 Agent: {response}\")\n", + "print(\"\\n✅ Agent grounded 'that ML class' to CS401\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example 2: Grounding People References\n", + "\n", + "Let's have a conversation about people (advisors, professors)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# New conversation\n", + "conversation = []\n", + "\n", + "print(\"\\n\" + \"=\" * 80)\n", + "print(\"CONVERSATION: Grounding People References\")\n", + "print(\"=\" * 80)\n", + "\n", + "# Turn 1: Mention a person\n", + "print(\"\\n👤 User: My advisor is Professor Smith from the CS department.\")\n", + "response, conversation = await chat_turn(\n", + " \"My advisor is Professor Smith from the CS department.\",\n", + " conversation\n", + ")\n", + "print(f\"🤖 Agent: {response}\")\n", + "\n", + "await asyncio.sleep(2)\n", + "\n", + "# Turn 2: Use pronoun \"she\"\n", + "print(\"\\n👤 User: What courses does she teach?\")\n", + "response, conversation = await chat_turn(\n", + " \"What courses does she teach?\",\n", + " conversation\n", + ")\n", + "print(f\"🤖 Agent: {response}\")\n", + "print(\"\\n✅ Agent grounded 'she' to Professor Smith\")\n", + "\n", + "# Turn 3: Use description \"my advisor\"\n", + "print(\"\\n👤 User: Can my advisor help me with course selection?\")\n", + "response, conversation = await chat_turn(\n", + " \"Can my advisor help me with course selection?\",\n", + " conversation\n", + ")\n", + "print(f\"🤖 Agent: {response}\")\n", + "print(\"\\n✅ Agent grounded 'my advisor' to Professor Smith\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example 3: Grounding Place References\n", + "\n", + "Let's talk about campus locations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# New conversation\n", + "conversation = []\n", + "\n", + "print(\"\\n\" + \"=\" * 80)\n", + "print(\"CONVERSATION: Grounding Place References\")\n", + "print(\"=\" * 80)\n", + "\n", + "# Turn 1: Mention a place\n", + "print(\"\\n👤 User: I prefer taking classes at the downtown campus.\")\n", + "response, conversation = await chat_turn(\n", + " \"I prefer taking classes at the downtown campus.\",\n", + " conversation\n", + ")\n", + "print(f\"🤖 Agent: {response}\")\n", + "\n", + "await asyncio.sleep(2)\n", + "\n", + "# Turn 2: Use pronoun \"there\"\n", + "print(\"\\n👤 User: What CS courses are offered there?\")\n", + "response, conversation = await chat_turn(\n", + " \"What CS courses are offered there?\",\n", + " conversation\n", + ")\n", + "print(f\"🤖 Agent: {response}\")\n", + "print(\"\\n✅ Agent grounded 'there' to downtown campus\")\n", + "\n", + "# Turn 3: Use description \"that campus\"\n", + "print(\"\\n👤 User: How do I get to that campus?\")\n", + "response, conversation = await chat_turn(\n", + " \"How do I get to that campus?\",\n", + " conversation\n", + ")\n", + "print(f\"🤖 Agent: {response}\")\n", + "print(\"\\n✅ Agent grounded 'that campus' to downtown campus\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example 4: Complex Multi-Reference Conversation\n", + "\n", + "Let's have a longer conversation with multiple entities to ground." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# New conversation\n", + "conversation = []\n", + "\n", + "print(\"\\n\" + \"=\" * 80)\n", + "print(\"CONVERSATION: Complex Multi-Reference\")\n", + "print(\"=\" * 80)\n", + "\n", + "# Turn 1\n", + "print(\"\\n👤 User: I'm looking at CS401 and CS402. Which one should I take first?\")\n", + "response, conversation = await chat_turn(\n", + " \"I'm looking at CS401 and CS402. Which one should I take first?\",\n", + " conversation\n", + ")\n", + "print(f\"🤖 Agent: {response}\")\n", + "\n", + "await asyncio.sleep(2)\n", + "\n", + "# Turn 2\n", + "print(\"\\n👤 User: What about the other one? When is it offered?\")\n", + "response, conversation = await chat_turn(\n", + " \"What about the other one? When is it offered?\",\n", + " conversation\n", + ")\n", + "print(f\"🤖 Agent: {response}\")\n", + "print(\"\\n✅ Agent grounded 'the other one' to the second course mentioned\")\n", + "\n", + "# Turn 3\n", + "print(\"\\n👤 User: Can I take both in the same semester?\")\n", + "response, conversation = await chat_turn(\n", + " \"Can I take both in the same semester?\",\n", + " conversation\n", + ")\n", + "print(f\"🤖 Agent: {response}\")\n", + "print(\"\\n✅ Agent grounded 'both' to CS401 and CS402\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Verify Extracted Memories\n", + "\n", + "Let's check what memories were extracted to enable grounding." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"\\n\" + \"=\" * 80)\n", + "print(\"EXTRACTED MEMORIES (Enable Grounding)\")\n", + "print(\"=\" * 80)\n", + "\n", + "# Get all memories\n", + "all_memories = await memory_client.search_long_term_memory(\n", + " text=\"\",\n", + " limit=20\n", + ")\n", + "\n", + "print(\"\\nMemories that enable grounding:\\n\")\n", + "for i, memory in enumerate(all_memories.memories, 1):\n", + " print(f\"{i}. {memory.text}\")\n", + " print(f\" Type: {memory.memory_type} | Topics: {', '.join(memory.topics)}\")\n", + " print()\n", + "\n", + "print(\"✅ These memories provide the context needed to ground references!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Key Takeaways\n", + "\n", + "### How Grounding Works\n", + "\n", + "1. **User mentions entity** (course, person, place)\n", + "2. **Agent Memory Server extracts** entity to long-term memory\n", + "3. **User makes reference** (\"it\", \"that\", \"she\", etc.)\n", + "4. **Semantic search retrieves** relevant memories\n", + "5. **Agent grounds reference** using memory context\n", + "\n", + "### Types of Grounding\n", + "\n", + "**Direct references:**\n", + "- \"CS401\" → Specific course\n", + "- \"Professor Smith\" → Specific person\n", + "\n", + "**Pronoun references:**\n", + "- \"it\" → Last mentioned thing\n", + "- \"she\" → Last mentioned person\n", + "- \"there\" → Last mentioned place\n", + "\n", + "**Description references:**\n", + "- \"that ML class\" → Course about ML\n", + "- \"my advisor\" → Student's advisor\n", + "- \"the downtown campus\" → Specific campus\n", + "\n", + "**Implicit references:**\n", + "- \"What are the prerequisites?\" → For the course we're discussing\n", + "- \"When does it meet?\" → The course mentioned\n", + "\n", + "### Why Memory-Based Grounding Works\n", + "\n", + "✅ **Automatic** - No manual entity tracking needed\n", + "✅ **Semantic** - Understands similar references\n", + "✅ **Persistent** - Works across sessions\n", + "✅ **Contextual** - Uses conversation history\n", + "✅ **Natural** - Enables human-like conversation\n", + "\n", + "### Best Practices\n", + "\n", + "1. **Include memory context in system prompt** - Give LLM grounding information\n", + "2. **Search with user's query** - Find relevant entities\n", + "3. **Trust semantic search** - It finds related memories\n", + "4. **Let extraction happen** - Don't manually track entities\n", + "5. **Test with pronouns** - Verify grounding works" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Exercises\n", + "\n", + "1. **Test ambiguous references**: Have a conversation mentioning multiple courses, then use \"it\". Does the agent ground correctly?\n", + "\n", + "2. **Cross-session grounding**: Start a new session and refer to entities from a previous session. Does it work?\n", + "\n", + "3. **Complex conversation**: Have a 10-turn conversation with multiple entities. Track how grounding evolves.\n", + "\n", + "4. **Grounding failure**: Try to break grounding by using very ambiguous references. What happens?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "In this notebook, you learned:\n", + "\n", + "- ✅ Grounding connects references to their actual meanings\n", + "- ✅ Agent Memory Server's extracted memories provide grounding automatically\n", + "- ✅ Semantic search retrieves relevant context for grounding\n", + "- ✅ Grounding enables natural, human-like conversations\n", + "- ✅ No manual entity tracking needed - memory handles it\n", + "\n", + "**Key insight:** Memory-based grounding is what makes agents feel intelligent and context-aware. Without it, every reference needs to be explicit, making conversations robotic and frustrating." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/python-recipes/context-engineering/notebooks/section-4-optimizations/04_tool_optimization.ipynb b/python-recipes/context-engineering/notebooks/section-4-optimizations/04_tool_optimization.ipynb new file mode 100644 index 00000000..943cd6be --- /dev/null +++ b/python-recipes/context-engineering/notebooks/section-4-optimizations/04_tool_optimization.ipynb @@ -0,0 +1,654 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Tool Optimization: Selective Tool Exposure\n", + "\n", + "## Introduction\n", + "\n", + "In this advanced notebook, you'll learn how to optimize tool usage by selectively exposing tools based on context. When you have many tools, showing all of them to the LLM on every request wastes tokens and can cause confusion. You'll learn the \"tool shed\" pattern and dynamic tool selection.\n", + "\n", + "### What You'll Learn\n", + "\n", + "- The tool shed pattern (selective tool exposure)\n", + "- Dynamic tool selection based on context\n", + "- Reducing tool confusion\n", + "- Measuring improvement in tool selection\n", + "- When to use tool optimization\n", + "\n", + "### Prerequisites\n", + "\n", + "- Completed Section 2 notebooks\n", + "- Completed `section-2-system-context/03_tool_selection_strategies.ipynb`\n", + "- Redis 8 running locally\n", + "- OpenAI API key set" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Concepts: The Tool Overload Problem\n", + "\n", + "### The Problem with Many Tools\n", + "\n", + "As your agent grows, you add more tools:\n", + "\n", + "```python\n", + "tools = [\n", + " search_courses, # 1\n", + " get_course_details, # 2\n", + " check_prerequisites, # 3\n", + " enroll_in_course, # 4\n", + " drop_course, # 5\n", + " get_student_schedule, # 6\n", + " check_schedule_conflicts, # 7\n", + " get_course_reviews, # 8\n", + " submit_course_review, # 9\n", + " get_instructor_info, # 10\n", + " # ... 20 more tools\n", + "]\n", + "```\n", + "\n", + "**Problems:**\n", + "- ❌ **Token waste**: Tool schemas consume tokens\n", + "- ❌ **Confusion**: Too many choices\n", + "- ❌ **Slower**: More tools = more processing\n", + "- ❌ **Wrong selection**: Similar tools confuse LLM\n", + "\n", + "### The Tool Shed Pattern\n", + "\n", + "**Idea:** Don't show all tools at once. Show only relevant tools based on context.\n", + "\n", + "```python\n", + "# Instead of showing all 30 tools...\n", + "all_tools = [tool1, tool2, ..., tool30]\n", + "\n", + "# Show only relevant tools\n", + "if query_type == \"search\":\n", + " relevant_tools = [search_courses, get_course_details]\n", + "elif query_type == \"enrollment\":\n", + " relevant_tools = [enroll_in_course, drop_course, check_conflicts]\n", + "elif query_type == \"review\":\n", + " relevant_tools = [get_course_reviews, submit_review]\n", + "```\n", + "\n", + "**Benefits:**\n", + "- ✅ Fewer tokens\n", + "- ✅ Less confusion\n", + "- ✅ Faster processing\n", + "- ✅ Better tool selection\n", + "\n", + "### Dynamic Tool Selection Strategies\n", + "\n", + "**1. Query-based filtering:**\n", + "```python\n", + "if \"search\" in query or \"find\" in query:\n", + " tools = search_tools\n", + "elif \"enroll\" in query or \"register\" in query:\n", + " tools = enrollment_tools\n", + "```\n", + "\n", + "**2. Intent classification:**\n", + "```python\n", + "intent = classify_intent(query) # \"search\", \"enroll\", \"review\"\n", + "tools = tool_groups[intent]\n", + "```\n", + "\n", + "**3. Conversation state:**\n", + "```python\n", + "if conversation_state == \"browsing\":\n", + " tools = [search, get_details]\n", + "elif conversation_state == \"enrolling\":\n", + " tools = [enroll, check_conflicts]\n", + "```\n", + "\n", + "**4. Hierarchical tools:**\n", + "```python\n", + "# First: Show high-level tools\n", + "tools = [search_courses, manage_enrollment, view_reviews]\n", + "\n", + "# Then: Show specific tools based on choice\n", + "if user_chose == \"manage_enrollment\":\n", + " tools = [enroll, drop, swap, check_conflicts]\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import re\n", + "from typing import List, Dict, Any\n", + "from langchain_openai import ChatOpenAI\n", + "from langchain_core.messages import SystemMessage, HumanMessage\n", + "from langchain_core.tools import tool\n", + "from pydantic import BaseModel, Field\n", + "from redis_context_course import CourseManager\n", + "\n", + "# Initialize\n", + "llm = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n", + "course_manager = CourseManager()\n", + "\n", + "print(\"✅ Setup complete\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Creating Tool Groups\n", + "\n", + "Let's organize tools into logical groups." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Define tools (simplified for demo)\n", + "class SearchInput(BaseModel):\n", + " query: str = Field(description=\"Search query\")\n", + "\n", + "@tool(args_schema=SearchInput)\n", + "async def search_courses(query: str) -> str:\n", + " \"\"\"Search for courses by topic or description.\"\"\"\n", + " return f\"Searching for: {query}\"\n", + "\n", + "@tool(args_schema=SearchInput)\n", + "async def get_course_details(query: str) -> str:\n", + " \"\"\"Get detailed information about a specific course.\"\"\"\n", + " return f\"Details for: {query}\"\n", + "\n", + "@tool(args_schema=SearchInput)\n", + "async def check_prerequisites(query: str) -> str:\n", + " \"\"\"Check prerequisites for a course.\"\"\"\n", + " return f\"Prerequisites for: {query}\"\n", + "\n", + "@tool(args_schema=SearchInput)\n", + "async def enroll_in_course(query: str) -> str:\n", + " \"\"\"Enroll student in a course.\"\"\"\n", + " return f\"Enrolling in: {query}\"\n", + "\n", + "@tool(args_schema=SearchInput)\n", + "async def drop_course(query: str) -> str:\n", + " \"\"\"Drop a course from student's schedule.\"\"\"\n", + " return f\"Dropping: {query}\"\n", + "\n", + "@tool(args_schema=SearchInput)\n", + "async def check_schedule_conflicts(query: str) -> str:\n", + " \"\"\"Check for schedule conflicts.\"\"\"\n", + " return f\"Checking conflicts for: {query}\"\n", + "\n", + "@tool(args_schema=SearchInput)\n", + "async def get_course_reviews(query: str) -> str:\n", + " \"\"\"Get reviews for a course.\"\"\"\n", + " return f\"Reviews for: {query}\"\n", + "\n", + "@tool(args_schema=SearchInput)\n", + "async def submit_course_review(query: str) -> str:\n", + " \"\"\"Submit a review for a course.\"\"\"\n", + " return f\"Submitting review for: {query}\"\n", + "\n", + "# Organize into groups\n", + "TOOL_GROUPS = {\n", + " \"search\": [\n", + " search_courses,\n", + " get_course_details,\n", + " check_prerequisites\n", + " ],\n", + " \"enrollment\": [\n", + " enroll_in_course,\n", + " drop_course,\n", + " check_schedule_conflicts\n", + " ],\n", + " \"reviews\": [\n", + " get_course_reviews,\n", + " submit_course_review\n", + " ]\n", + "}\n", + "\n", + "ALL_TOOLS = [\n", + " search_courses,\n", + " get_course_details,\n", + " check_prerequisites,\n", + " enroll_in_course,\n", + " drop_course,\n", + " check_schedule_conflicts,\n", + " get_course_reviews,\n", + " submit_course_review\n", + "]\n", + "\n", + "print(f\"✅ Created {len(ALL_TOOLS)} tools in {len(TOOL_GROUPS)} groups\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Strategy 1: Query-Based Tool Filtering\n", + "\n", + "Select tools based on keywords in the query." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def select_tools_by_keywords(query: str) -> List:\n", + " \"\"\"Select relevant tools based on query keywords.\"\"\"\n", + " query_lower = query.lower()\n", + " \n", + " # Search-related keywords\n", + " if any(word in query_lower for word in ['search', 'find', 'show', 'what', 'which', 'tell me about']):\n", + " return TOOL_GROUPS[\"search\"]\n", + " \n", + " # Enrollment-related keywords\n", + " elif any(word in query_lower for word in ['enroll', 'register', 'drop', 'add', 'remove', 'conflict']):\n", + " return TOOL_GROUPS[\"enrollment\"]\n", + " \n", + " # Review-related keywords\n", + " elif any(word in query_lower for word in ['review', 'rating', 'feedback', 'opinion']):\n", + " return TOOL_GROUPS[\"reviews\"]\n", + " \n", + " # Default: return search tools\n", + " else:\n", + " return TOOL_GROUPS[\"search\"]\n", + "\n", + "# Test it\n", + "test_queries = [\n", + " \"I want to search for machine learning courses\",\n", + " \"Can I enroll in CS401?\",\n", + " \"What are the reviews for CS301?\",\n", + " \"Tell me about database courses\"\n", + "]\n", + "\n", + "print(\"=\" * 80)\n", + "print(\"QUERY-BASED TOOL FILTERING\")\n", + "print(\"=\" * 80)\n", + "\n", + "for query in test_queries:\n", + " selected_tools = select_tools_by_keywords(query)\n", + " tool_names = [t.name for t in selected_tools]\n", + " print(f\"\\nQuery: {query}\")\n", + " print(f\"Selected tools: {', '.join(tool_names)}\")\n", + " print(f\"Count: {len(selected_tools)} / {len(ALL_TOOLS)} tools\")\n", + "\n", + "print(\"\\n\" + \"=\" * 80)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Strategy 2: Intent Classification\n", + "\n", + "Use the LLM to classify intent, then select tools." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "async def classify_intent(query: str) -> str:\n", + " \"\"\"Classify user intent using LLM.\"\"\"\n", + " prompt = f\"\"\"Classify the user's intent into one of these categories:\n", + "- search: Looking for courses or information\n", + "- enrollment: Enrolling, dropping, or managing courses\n", + "- reviews: Reading or writing course reviews\n", + "\n", + "User query: \"{query}\"\n", + "\n", + "Respond with only the category name (search, enrollment, or reviews).\n", + "\"\"\"\n", + " \n", + " messages = [\n", + " SystemMessage(content=\"You are a helpful assistant that classifies user intents.\"),\n", + " HumanMessage(content=prompt)\n", + " ]\n", + " \n", + " response = llm.invoke(messages)\n", + " intent = response.content.strip().lower()\n", + " \n", + " # Validate intent\n", + " if intent not in TOOL_GROUPS:\n", + " intent = \"search\" # Default\n", + " \n", + " return intent\n", + "\n", + "async def select_tools_by_intent(query: str) -> List:\n", + " \"\"\"Select tools based on classified intent.\"\"\"\n", + " intent = await classify_intent(query)\n", + " return TOOL_GROUPS[intent], intent\n", + "\n", + "# Test it\n", + "print(\"\\n\" + \"=\" * 80)\n", + "print(\"INTENT-BASED TOOL FILTERING\")\n", + "print(\"=\" * 80)\n", + "\n", + "for query in test_queries:\n", + " selected_tools, intent = await select_tools_by_intent(query)\n", + " tool_names = [t.name for t in selected_tools]\n", + " print(f\"\\nQuery: {query}\")\n", + " print(f\"Intent: {intent}\")\n", + " print(f\"Selected tools: {', '.join(tool_names)}\")\n", + " print(f\"Count: {len(selected_tools)} / {len(ALL_TOOLS)} tools\")\n", + "\n", + "print(\"\\n\" + \"=\" * 80)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Comparing: All Tools vs. Filtered Tools\n", + "\n", + "Let's compare tool selection with and without filtering." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"\\n\" + \"=\" * 80)\n", + "print(\"COMPARISON: ALL TOOLS vs. FILTERED TOOLS\")\n", + "print(\"=\" * 80)\n", + "\n", + "test_query = \"I want to enroll in CS401\"\n", + "\n", + "# Approach 1: All tools\n", + "print(f\"\\nQuery: {test_query}\")\n", + "print(\"\\n--- APPROACH 1: Show all tools ---\")\n", + "llm_all_tools = llm.bind_tools(ALL_TOOLS)\n", + "messages = [\n", + " SystemMessage(content=\"You are a class scheduling agent.\"),\n", + " HumanMessage(content=test_query)\n", + "]\n", + "response_all = llm_all_tools.invoke(messages)\n", + "\n", + "if response_all.tool_calls:\n", + " print(f\"Selected tool: {response_all.tool_calls[0]['name']}\")\n", + "print(f\"Tools shown: {len(ALL_TOOLS)}\")\n", + "\n", + "# Approach 2: Filtered tools\n", + "print(\"\\n--- APPROACH 2: Show filtered tools ---\")\n", + "filtered_tools = select_tools_by_keywords(test_query)\n", + "llm_filtered_tools = llm.bind_tools(filtered_tools)\n", + "response_filtered = llm_filtered_tools.invoke(messages)\n", + "\n", + "if response_filtered.tool_calls:\n", + " print(f\"Selected tool: {response_filtered.tool_calls[0]['name']}\")\n", + "print(f\"Tools shown: {len(filtered_tools)}\")\n", + "\n", + "print(\"\\n✅ Benefits of filtering:\")\n", + "print(f\" - Reduced tools: {len(ALL_TOOLS)} → {len(filtered_tools)}\")\n", + "print(f\" - Token savings: ~{(len(ALL_TOOLS) - len(filtered_tools)) * 100} tokens\")\n", + "print(f\" - Less confusion: Fewer irrelevant tools\")\n", + "\n", + "print(\"\\n\" + \"=\" * 80)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Strategy 3: Hierarchical Tools\n", + "\n", + "Start with high-level tools, then drill down." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"\\n\" + \"=\" * 80)\n", + "print(\"HIERARCHICAL TOOL APPROACH\")\n", + "print(\"=\" * 80)\n", + "\n", + "# High-level tools\n", + "@tool\n", + "async def browse_courses(query: str) -> str:\n", + " \"\"\"Browse and search for courses. Use this for finding courses.\"\"\"\n", + " return \"Browsing courses...\"\n", + "\n", + "@tool\n", + "async def manage_enrollment(query: str) -> str:\n", + " \"\"\"Manage course enrollment (enroll, drop, check conflicts). Use this for enrollment actions.\"\"\"\n", + " return \"Managing enrollment...\"\n", + "\n", + "@tool\n", + "async def view_reviews(query: str) -> str:\n", + " \"\"\"View or submit course reviews. Use this for review-related queries.\"\"\"\n", + " return \"Viewing reviews...\"\n", + "\n", + "high_level_tools = [browse_courses, manage_enrollment, view_reviews]\n", + "\n", + "print(\"\\nStep 1: Show high-level tools\")\n", + "print(f\"Tools: {[t.name for t in high_level_tools]}\")\n", + "print(f\"Count: {len(high_level_tools)} tools\")\n", + "\n", + "print(\"\\nStep 2: User selects 'manage_enrollment'\")\n", + "print(\"Now show specific enrollment tools:\")\n", + "enrollment_tools = TOOL_GROUPS[\"enrollment\"]\n", + "print(f\"Tools: {[t.name for t in enrollment_tools]}\")\n", + "print(f\"Count: {len(enrollment_tools)} tools\")\n", + "\n", + "print(\"\\n✅ Benefits:\")\n", + "print(\" - Start simple (3 tools)\")\n", + "print(\" - Drill down as needed\")\n", + "print(\" - User-guided filtering\")\n", + "\n", + "print(\"\\n\" + \"=\" * 80)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Measuring Improvement\n", + "\n", + "Let's measure the impact of tool filtering." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"\\n\" + \"=\" * 80)\n", + "print(\"MEASURING IMPROVEMENT\")\n", + "print(\"=\" * 80)\n", + "\n", + "# Test queries with expected tools\n", + "test_cases = [\n", + " (\"Find machine learning courses\", \"search_courses\"),\n", + " (\"Enroll me in CS401\", \"enroll_in_course\"),\n", + " (\"Show reviews for CS301\", \"get_course_reviews\"),\n", + " (\"Drop CS201 from my schedule\", \"drop_course\"),\n", + " (\"What are the prerequisites for CS401?\", \"check_prerequisites\"),\n", + "]\n", + "\n", + "print(\"\\nTesting tool selection accuracy...\\n\")\n", + "\n", + "correct_all = 0\n", + "correct_filtered = 0\n", + "\n", + "for query, expected_tool in test_cases:\n", + " # Test with all tools\n", + " llm_all = llm.bind_tools(ALL_TOOLS)\n", + " response_all = llm_all.invoke([\n", + " SystemMessage(content=\"You are a class scheduling agent.\"),\n", + " HumanMessage(content=query)\n", + " ])\n", + " selected_all = response_all.tool_calls[0]['name'] if response_all.tool_calls else None\n", + " \n", + " # Test with filtered tools\n", + " filtered = select_tools_by_keywords(query)\n", + " llm_filtered = llm.bind_tools(filtered)\n", + " response_filtered = llm_filtered.invoke([\n", + " SystemMessage(content=\"You are a class scheduling agent.\"),\n", + " HumanMessage(content=query)\n", + " ])\n", + " selected_filtered = response_filtered.tool_calls[0]['name'] if response_filtered.tool_calls else None\n", + " \n", + " # Check correctness\n", + " if selected_all == expected_tool:\n", + " correct_all += 1\n", + " if selected_filtered == expected_tool:\n", + " correct_filtered += 1\n", + " \n", + " print(f\"Query: {query}\")\n", + " print(f\" Expected: {expected_tool}\")\n", + " print(f\" All tools: {selected_all} {'✅' if selected_all == expected_tool else '❌'}\")\n", + " print(f\" Filtered: {selected_filtered} {'✅' if selected_filtered == expected_tool else '❌'}\")\n", + " print()\n", + "\n", + "print(\"=\" * 80)\n", + "print(f\"\\nAccuracy with all tools: {correct_all}/{len(test_cases)} ({correct_all/len(test_cases)*100:.0f}%)\")\n", + "print(f\"Accuracy with filtered tools: {correct_filtered}/{len(test_cases)} ({correct_filtered/len(test_cases)*100:.0f}%)\")\n", + "\n", + "print(\"\\n✅ Tool filtering improves:\")\n", + "print(\" - Selection accuracy\")\n", + "print(\" - Token efficiency\")\n", + "print(\" - Processing speed\")\n", + "\n", + "print(\"\\n\" + \"=\" * 80)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Key Takeaways\n", + "\n", + "### When to Use Tool Filtering\n", + "\n", + "**Use tool filtering when:**\n", + "- ✅ You have 10+ tools\n", + "- ✅ Tools have distinct use cases\n", + "- ✅ Token budget is tight\n", + "- ✅ Tool confusion is an issue\n", + "\n", + "**Don't filter when:**\n", + "- ❌ You have < 5 tools\n", + "- ❌ All tools are frequently used\n", + "- ❌ Tools are highly related\n", + "\n", + "### Filtering Strategies\n", + "\n", + "**1. Keyword-based (Simple)**\n", + "- ✅ Fast, no LLM call\n", + "- ✅ Easy to implement\n", + "- ⚠️ Can be brittle\n", + "\n", + "**2. Intent classification (Better)**\n", + "- ✅ More accurate\n", + "- ✅ Handles variations\n", + "- ⚠️ Requires LLM call\n", + "\n", + "**3. Hierarchical (Best for many tools)**\n", + "- ✅ Scales well\n", + "- ✅ User-guided\n", + "- ⚠️ More complex\n", + "\n", + "### Implementation Tips\n", + "\n", + "1. **Group logically** - Organize tools by use case\n", + "2. **Start simple** - Use keyword filtering first\n", + "3. **Measure impact** - Track accuracy and token usage\n", + "4. **Iterate** - Refine based on real usage\n", + "5. **Have fallback** - Default to search tools if unsure\n", + "\n", + "### Token Savings\n", + "\n", + "Typical tool schema: ~100 tokens\n", + "\n", + "**Example:**\n", + "- 30 tools × 100 tokens = 3,000 tokens\n", + "- Filtered to 5 tools × 100 tokens = 500 tokens\n", + "- **Savings: 2,500 tokens per request!**\n", + "\n", + "Over 1,000 requests:\n", + "- Savings: 2.5M tokens\n", + "- Cost savings: ~$5-10 (depending on model)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Exercises\n", + "\n", + "1. **Create tool groups**: Organize your agent's tools into logical groups. How many groups make sense?\n", + "\n", + "2. **Implement filtering**: Add keyword-based filtering to your agent. Measure token savings.\n", + "\n", + "3. **Test accuracy**: Create 20 test queries. Does filtering improve or hurt tool selection accuracy?\n", + "\n", + "4. **Hierarchical design**: Design a hierarchical tool structure for a complex agent with 30+ tools." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "In this notebook, you learned:\n", + "\n", + "- ✅ Tool filtering reduces token usage and confusion\n", + "- ✅ The tool shed pattern: show only relevant tools\n", + "- ✅ Multiple filtering strategies: keywords, intent, hierarchical\n", + "- ✅ Filtering improves accuracy and efficiency\n", + "- ✅ Essential for agents with many tools\n", + "\n", + "**Key insight:** Don't show all tools all the time. Selective tool exposure based on context improves tool selection, reduces token usage, and makes your agent more efficient. This is especially important as your agent grows and accumulates more tools." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} + diff --git a/python-recipes/context-engineering/notebooks/section-4-optimizations/05_crafting_data_for_llms.ipynb b/python-recipes/context-engineering/notebooks/section-4-optimizations/05_crafting_data_for_llms.ipynb new file mode 100644 index 00000000..43e2f2c9 --- /dev/null +++ b/python-recipes/context-engineering/notebooks/section-4-optimizations/05_crafting_data_for_llms.ipynb @@ -0,0 +1,840 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Crafting Data for LLMs: Creating Structured Views\n", + "\n", + "## Introduction\n", + "\n", + "In this advanced notebook, you'll learn how to create structured \"views\" or \"dashboards\" of data specifically optimized for LLM consumption. This goes beyond simple chunking and retrieval - you'll pre-compute summaries and organize data in ways that give your agent a high-level understanding while keeping token usage low.\n", + "\n", + "### What You'll Learn\n", + "\n", + "- Why pre-computed views matter\n", + "- How to create course catalog summary views\n", + "- How to build user profile views\n", + "- Techniques for retrieve → summarize → stitch → save\n", + "- When to use structured views vs. RAG\n", + "\n", + "### Prerequisites\n", + "\n", + "- Completed all Section 3 notebooks\n", + "- Completed Section 4 notebooks 01-03\n", + "- Redis 8 running locally\n", + "- Agent Memory Server running\n", + "- OpenAI API key set" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Concepts: Structured Data Views\n", + "\n", + "### Beyond Chunking and RAG\n", + "\n", + "Traditional approaches:\n", + "- **Chunking**: Split documents into pieces, retrieve relevant chunks\n", + "- **RAG**: Search for relevant documents/records on each query\n", + "\n", + "These work well, but have limitations:\n", + "- ❌ No high-level overview\n", + "- ❌ May miss important context\n", + "- ❌ Requires search on every request\n", + "- ❌ Can't see relationships across data\n", + "\n", + "### Structured Views Approach\n", + "\n", + "**Pre-compute summaries** that give the LLM:\n", + "- ✅ High-level overview of entire dataset\n", + "- ✅ Organized, structured information\n", + "- ✅ Key metadata for finding details\n", + "- ✅ Relationships between entities\n", + "\n", + "### Two Key Patterns\n", + "\n", + "#### 1. Course Catalog Summary View\n", + "\n", + "Instead of searching courses every time, give the agent:\n", + "```\n", + "Course Catalog Overview:\n", + "\n", + "Computer Science (50 courses):\n", + "- CS101: Intro to Programming (3 credits, beginner)\n", + "- CS201: Data Structures (3 credits, intermediate)\n", + "- CS401: Machine Learning (4 credits, advanced)\n", + "...\n", + "\n", + "Mathematics (30 courses):\n", + "- MATH101: Calculus I (4 credits, beginner)\n", + "...\n", + "```\n", + "\n", + "**Benefits:**\n", + "- Agent knows what's available\n", + "- Can reference specific courses\n", + "- Can suggest alternatives\n", + "- Compact (1-2K tokens for 100s of courses)\n", + "\n", + "#### 2. User Profile View\n", + "\n", + "Instead of searching memories every time, give the agent:\n", + "```\n", + "Student Profile: student_123\n", + "\n", + "Academic Info:\n", + "- Major: Computer Science\n", + "- Year: Junior\n", + "- GPA: 3.7\n", + "- Expected Graduation: Spring 2026\n", + "\n", + "Completed Courses (12):\n", + "- CS101 (A), CS201 (A-), CS301 (B+)\n", + "- MATH101 (A), MATH201 (B)\n", + "...\n", + "\n", + "Preferences:\n", + "- Prefers online courses\n", + "- Morning classes only\n", + "- No classes on Fridays\n", + "- Interested in AI/ML\n", + "\n", + "Goals:\n", + "- Graduate in 2026\n", + "- Focus on machine learning\n", + "- Maintain 3.5+ GPA\n", + "```\n", + "\n", + "**Benefits:**\n", + "- Agent has complete user context\n", + "- No need to search memories\n", + "- Personalized from turn 1\n", + "- Compact (500-1K tokens)\n", + "\n", + "### The Pattern: Retrieve → Summarize → Stitch → Save\n", + "\n", + "1. **Retrieve**: Get all relevant data from storage\n", + "2. **Summarize**: Use LLM to create concise summaries\n", + "3. **Stitch**: Combine summaries into structured view\n", + "4. **Save**: Store as string or JSON blob\n", + "\n", + "### When to Use Structured Views\n", + "\n", + "**Use structured views when:**\n", + "- ✅ Data changes infrequently\n", + "- ✅ Agent needs overview + details\n", + "- ✅ Same data used across many requests\n", + "- ✅ Relationships matter\n", + "\n", + "**Use RAG when:**\n", + "- ✅ Data changes frequently\n", + "- ✅ Dataset is huge (can't summarize all)\n", + "- ✅ Only need specific details\n", + "- ✅ Query-specific retrieval needed\n", + "\n", + "**Best: Combine both!**\n", + "- Structured view for overview\n", + "- RAG for specific details" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import json\n", + "import asyncio\n", + "from typing import List, Dict, Any\n", + "import tiktoken\n", + "from langchain_openai import ChatOpenAI\n", + "from langchain_core.messages import SystemMessage, HumanMessage\n", + "from redis_context_course import CourseManager, MemoryClient, MemoryClientConfig, redis_config\n", + "\n", + "# Initialize\n", + "course_manager = CourseManager()\n", + "# Initialize memory client with proper config\n", + "import os\n", + "config = MemoryClientConfig(\n", + " base_url=os.getenv(\"AGENT_MEMORY_URL\", \"http://localhost:8000\"),\n", + " default_namespace=\"redis_university\"\n", + ")\n", + "memory_client = MemoryClient(config=config)\n", + "llm = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n", + "tokenizer = tiktoken.encoding_for_model(\"gpt-4o\")\n", + "redis_client = redis_config.redis_client\n", + "\n", + "def count_tokens(text: str) -> int:\n", + " return len(tokenizer.encode(text))\n", + "\n", + "print(\"✅ Setup complete\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example 1: Course Catalog Summary View\n", + "\n", + "Let's create a high-level summary of the entire course catalog." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 1: Retrieve All Courses" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"=\" * 80)\n", + "print(\"CREATING COURSE CATALOG SUMMARY VIEW\")\n", + "print(\"=\" * 80)\n", + "\n", + "# Step 1: Retrieve all courses\n", + "print(\"\\n1. Retrieving all courses...\")\n", + "all_courses = await course_manager.get_all_courses()\n", + "print(f\" Retrieved {len(all_courses)} courses\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 2: Organize by Department" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Step 2: Organize by department\n", + "print(\"\\n2. Organizing by department...\")\n", + "by_department = {}\n", + "for course in all_courses:\n", + " dept = course.department\n", + " if dept not in by_department:\n", + " by_department[dept] = []\n", + " by_department[dept].append(course)\n", + "\n", + "print(f\" Found {len(by_department)} departments\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 3: Summarize Each Department" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Step 3: Summarize each department\n", + "print(\"\\n3. Creating summaries for each department...\")\n", + "\n", + "async def summarize_department(dept_name: str, courses: List) -> str:\n", + " \"\"\"Create a concise summary of courses in a department.\"\"\"\n", + " \n", + " # Build course list\n", + " course_list = \"\\n\".join([\n", + " f\"- {c.course_code}: {c.title} ({c.credits} credits, {c.difficulty_level.value})\"\n", + " for c in courses[:10] # Limit for demo\n", + " ])\n", + " \n", + " # Ask LLM to create one-sentence descriptions\n", + " prompt = f\"\"\"Create a one-sentence description for each course. Be concise.\n", + "\n", + "Courses:\n", + "{course_list}\n", + "\n", + "Format: COURSE_CODE: One sentence description\n", + "\"\"\"\n", + " \n", + " messages = [\n", + " SystemMessage(content=\"You are a helpful assistant that creates concise course descriptions.\"),\n", + " HumanMessage(content=prompt)\n", + " ]\n", + " \n", + " response = llm.invoke(messages)\n", + " return response.content\n", + "\n", + "# Summarize first 3 departments (for demo)\n", + "dept_summaries = {}\n", + "for dept_name in list(by_department.keys())[:3]:\n", + " print(f\" Summarizing {dept_name}...\")\n", + " summary = await summarize_department(dept_name, by_department[dept_name])\n", + " dept_summaries[dept_name] = summary\n", + " await asyncio.sleep(0.5) # Rate limiting\n", + "\n", + "print(f\" Created {len(dept_summaries)} department summaries\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 4: Stitch Into Complete View" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Step 4: Stitch into complete view\n", + "print(\"\\n4. Stitching into complete catalog view...\")\n", + "\n", + "catalog_view_parts = [\"Redis University Course Catalog\\n\" + \"=\" * 40 + \"\\n\"]\n", + "\n", + "for dept_name, summary in dept_summaries.items():\n", + " course_count = len(by_department[dept_name])\n", + " catalog_view_parts.append(f\"\\n{dept_name} ({course_count} courses):\")\n", + " catalog_view_parts.append(summary)\n", + "\n", + "catalog_view = \"\\n\".join(catalog_view_parts)\n", + "\n", + "print(f\" View created!\")\n", + "print(f\" Total tokens: {count_tokens(catalog_view):,}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 5: Save to Redis" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Step 5: Save to Redis\n", + "print(\"\\n5. Saving to Redis...\")\n", + "\n", + "redis_client.set(\"course_catalog_view\", catalog_view)\n", + "\n", + "print(\" ✅ Saved to Redis as 'course_catalog_view'\")\n", + "\n", + "# Display the view\n", + "print(\"\\n\" + \"=\" * 80)\n", + "print(\"COURSE CATALOG VIEW\")\n", + "print(\"=\" * 80)\n", + "print(catalog_view)\n", + "print(\"\\n\" + \"=\" * 80)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Using the Catalog View" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Load and use the view\n", + "print(\"\\nUsing the catalog view in an agent...\\n\")\n", + "\n", + "catalog_view = redis_client.get(\"course_catalog_view\") or \"\"\n", + "\n", + "# Define a tool for retrieving course details by course code\n", + "from langchain_core.tools import tool\n", + "from typing import List\n", + "\n", + "@tool\n", + "async def get_course_details(course_codes: List[str]) -> str:\n", + " \"\"\"Get detailed information about one or more courses by their course codes.\n", + " \n", + " Args:\n", + " course_codes: List of course codes (e.g., ['CS101', 'MATH201'])\n", + " \n", + " Returns:\n", + " Formatted string with detailed course information\n", + " \"\"\"\n", + " if not course_codes:\n", + " return \"No course codes provided.\"\n", + " \n", + " result = []\n", + " for code in course_codes:\n", + " course = await course_manager.get_course_by_code(code)\n", + " if course:\n", + " result.append(f\"\"\"Course: {course.course_code} - {course.title}\n", + "Department: {course.department}\n", + "Description: {course.description}\n", + "Credits: {course.credits} | Difficulty: {course.difficulty_level}\n", + "Format: {course.format}\n", + "Instructor: {course.instructor}\n", + "Prerequisites: {', '.join([p.course_code for p in course.prerequisites]) if course.prerequisites else 'None'}\"\"\")\n", + " else:\n", + " result.append(f\"Course {code}: Not found\")\n", + " \n", + " return \"\\n\\n\".join(result)\n", + "\n", + "# Bind the tool to the LLM\n", + "llm_with_tools = llm.bind_tools([get_course_details])\n", + "\n", + "system_prompt = f\"\"\"You are a class scheduling agent for Redis University.\n", + "\n", + "{catalog_view}\n", + "\n", + "Use this overview to help students understand what's available.\n", + "When students ask about specific courses, use the get_course_details tool with the course codes from the overview above.\n", + "\"\"\"\n", + "\n", + "user_query = \"What departments offer courses? I'm interested in computer science.\"\n", + "\n", + "messages = [\n", + " SystemMessage(content=system_prompt),\n", + " HumanMessage(content=user_query)\n", + "]\n", + "\n", + "response = llm_with_tools.invoke(messages)\n", + "\n", + "print(f\"User: {user_query}\")\n", + "print(f\"\\nAgent: {response.content}\")\n", + "if response.tool_calls:\n", + " print(f\"\\n🔧 Agent wants to use tools: {[tc['name'] for tc in response.tool_calls]}\")\n", + "print(\"\\n✅ Agent has high-level overview and can search for details!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example 2: User Profile View\n", + "\n", + "Let's create a comprehensive user profile from various data sources." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 1: Retrieve User Data\n", + "\n", + "**The Hard Part: Data Integration**\n", + "\n", + "In production, creating user profile views requires:\n", + "\n", + "1. **Data Pipeline Architecture**\n", + " - Pull from multiple systems: Student Information System (SIS), Learning Management System (LMS), registration database, etc.\n", + " - Handle different data formats, APIs, and update frequencies\n", + " - Deal with data quality issues, missing fields, and inconsistencies\n", + "\n", + "2. **Scheduled Jobs**\n", + " - Nightly batch jobs to rebuild all profiles\n", + " - Incremental updates when specific events occur (course registration, grade posted)\n", + " - Balance freshness vs. computational cost\n", + "\n", + "3. **Data Selection Strategy**\n", + " - **What to include?** Not everything in your database belongs in the profile\n", + " - **What to exclude?** PII, irrelevant historical data, system metadata\n", + " - **What to aggregate?** Raw grades vs. GPA, individual courses vs. course count\n", + " - **What to denormalize?** Join course codes with titles, departments, etc.\n", + "\n", + "4. **Real-World Complexity**\n", + " - Students may have data in multiple systems that need reconciliation\n", + " - Historical data may use different course codes or structures\n", + " - Some data may be sensitive and require access controls\n", + " - Profile size must be managed (can't include every interaction)\n", + "\n", + "**For this demo**, we simulate the *output* of such a pipeline - a clean, structured dataset ready for profile creation. In production, getting to this point is often the hardest part!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"\\n\" + \"=\" * 80)\n", + "print(\"CREATING USER PROFILE VIEW\")\n", + "print(\"=\" * 80)\n", + "\n", + "# Step 1: Retrieve user data from various sources\n", + "print(\"\\n1. Retrieving user data...\")\n", + "\n", + "# In production, this data comes from a data pipeline that:\n", + "# - Queries multiple systems (SIS, LMS, registration DB)\n", + "# - Joins and denormalizes data\n", + "# - Filters to relevant fields only\n", + "# - Runs on a schedule (nightly batch or event-triggered)\n", + "# For this demo, we simulate the pipeline's output:\n", + "user_data = {\n", + " \"student_id\": \"student_123\",\n", + " \"name\": \"Alex Johnson\",\n", + " \"major\": \"Computer Science\",\n", + " \"year\": \"Junior\",\n", + " \"gpa\": 3.7,\n", + " \"expected_graduation\": \"Spring 2026\",\n", + " \"completed_courses\": [\n", + " {\"code\": \"CS101\", \"title\": \"Intro to Programming\", \"grade\": \"A\"},\n", + " {\"code\": \"CS201\", \"title\": \"Data Structures\", \"grade\": \"A-\"},\n", + " {\"code\": \"CS301\", \"title\": \"Algorithms\", \"grade\": \"B+\"},\n", + " {\"code\": \"MATH101\", \"title\": \"Calculus I\", \"grade\": \"A\"},\n", + " {\"code\": \"MATH201\", \"title\": \"Calculus II\", \"grade\": \"B\"},\n", + " ],\n", + " \"current_courses\": [\n", + " \"CS401\", \"CS402\", \"MATH301\"\n", + " ]\n", + "}\n", + "\n", + "# Get memories\n", + "memories = await memory_client.search_long_term_memory(\n", + " text=\"\", # Get all\n", + " limit=20\n", + ")\n", + "\n", + "print(f\" Retrieved user data and {len(memories.memories)} memories\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 2: Summarize Each Section" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Step 2: Create summaries for each section\n", + "print(\"\\n2. Creating section summaries...\")\n", + "\n", + "# Academic info (structured, no LLM needed)\n", + "academic_info = f\"\"\"Academic Info:\n", + "- Major: {user_data['major']}\n", + "- Year: {user_data['year']}\n", + "- GPA: {user_data['gpa']}\n", + "- Expected Graduation: {user_data['expected_graduation']}\n", + "\"\"\"\n", + "\n", + "# Completed courses (structured)\n", + "completed_courses = \"Completed Courses (\" + str(len(user_data['completed_courses'])) + \"):\\n\"\n", + "completed_courses += \"\\n\".join([\n", + " f\"- {c['code']}: {c['title']} (Grade: {c['grade']})\"\n", + " for c in user_data['completed_courses']\n", + "])\n", + "\n", + "# Current courses\n", + "current_courses = \"Current Courses:\\n- \" + \", \".join(user_data['current_courses'])\n", + "\n", + "# Summarize memories with LLM\n", + "if memories.memories:\n", + " memory_text = \"\\n\".join([f\"- {m.text}\" for m in memories.memories[:10]])\n", + " \n", + " prompt = f\"\"\"Summarize these student memories into two sections:\n", + "1. Preferences (course format, schedule, etc.)\n", + "2. Goals (academic, career, etc.)\n", + "\n", + "Be concise. Use bullet points.\n", + "\n", + "Memories:\n", + "{memory_text}\n", + "\"\"\"\n", + " \n", + " messages = [\n", + " SystemMessage(content=\"You are a helpful assistant that summarizes student information.\"),\n", + " HumanMessage(content=prompt)\n", + " ]\n", + " \n", + " response = llm.invoke(messages)\n", + " preferences_and_goals = response.content\n", + "else:\n", + " preferences_and_goals = \"Preferences:\\n- None recorded\\n\\nGoals:\\n- None recorded\"\n", + "\n", + "print(\" Created all section summaries\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 3: Stitch Into Profile View" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Step 3: Stitch into complete profile\n", + "print(\"\\n3. Stitching into complete profile view...\")\n", + "\n", + "profile_view = f\"\"\"Student Profile: {user_data['student_id']}\n", + "{'=' * 50}\n", + "\n", + "{academic_info}\n", + "\n", + "{completed_courses}\n", + "\n", + "{current_courses}\n", + "\n", + "{preferences_and_goals}\n", + "\"\"\"\n", + "\n", + "print(f\" Profile created!\")\n", + "print(f\" Total tokens: {count_tokens(profile_view):,}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 4: Save as JSON" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Step 4: Save to Redis (as JSON for structured access)\n", + "print(\"\\n4. Saving to Redis...\")\n", + "\n", + "profile_data = {\n", + " \"student_id\": user_data['student_id'],\n", + " \"profile_text\": profile_view,\n", + " \"last_updated\": \"2024-09-30\",\n", + " \"token_count\": count_tokens(profile_view)\n", + "}\n", + "\n", + "redis_client.set(\n", + " f\"user_profile:{user_data['student_id']}\",\n", + " json.dumps(profile_data)\n", + ")\n", + "\n", + "print(f\" ✅ Saved to Redis as 'user_profile:{user_data['student_id']}'\")\n", + "\n", + "# Display the profile\n", + "print(\"\\n\" + \"=\" * 80)\n", + "print(\"USER PROFILE VIEW\")\n", + "print(\"=\" * 80)\n", + "print(profile_view)\n", + "print(\"=\" * 80)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Using the Profile View" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Load and use the profile\n", + "print(\"\\nUsing the profile view in an agent...\\n\")\n", + "\n", + "profile_data = redis_client.get(f\"user_profile:{user_data['student_id']}\")\n", + "profile_json = json.loads(profile_data) if profile_data else {}\n", + "profile_text = profile_json.get('profile_text', 'No profile available')\n", + "\n", + "system_prompt = f\"\"\"You are a class scheduling agent for Redis University.\n", + "\n", + "{profile_text}\n", + "\n", + "Use this profile to provide personalized recommendations.\n", + "\"\"\"\n", + "\n", + "user_query = \"What courses should I take next semester?\"\n", + "\n", + "messages = [\n", + " SystemMessage(content=system_prompt),\n", + " HumanMessage(content=user_query)\n", + "]\n", + "\n", + "response = llm.invoke(messages)\n", + "\n", + "print(f\"User: {user_query}\")\n", + "print(f\"\\nAgent: {response.content}\")\n", + "print(\"\\n✅ Agent has complete user context from turn 1!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Key Takeaways\n", + "\n", + "### The Pattern: Retrieve → Summarize → Stitch → Save\n", + "\n", + "1. **Retrieve**: Get all relevant data\n", + " - From databases, APIs, memories\n", + " - Organize by category/section\n", + "\n", + "2. **Summarize**: Create concise summaries\n", + " - Use LLM for complex data\n", + " - Use templates for structured data\n", + " - Keep it compact (one-sentence descriptions)\n", + "\n", + "3. **Stitch**: Combine into complete view\n", + " - Organize logically\n", + " - Add headers and structure\n", + " - Format for LLM consumption\n", + "\n", + "4. **Save**: Store for reuse\n", + " - Redis for fast access\n", + " - String or JSON format\n", + " - Include metadata (timestamp, token count)\n", + "\n", + "### When to Refresh Views\n", + "\n", + "**Course Catalog View:**\n", + "- When courses are added/removed\n", + "- When descriptions change\n", + "- Typically: Daily or weekly\n", + "\n", + "**User Profile View:**\n", + "- When user completes a course\n", + "- When preferences change\n", + "- When new memories are added\n", + "- Typically: After each session or daily\n", + "\n", + "### Scheduling Considerations\n", + "\n", + "In production, you'd use:\n", + "- **Cron jobs** for periodic updates\n", + "- **Event triggers** for immediate updates\n", + "- **Background workers** for async processing\n", + "\n", + "For this course, we focus on the **function-level logic**, not the scheduling infrastructure.\n", + "\n", + "### Benefits of Structured Views\n", + "\n", + "✅ **Performance:**\n", + "- No search needed on every request\n", + "- Pre-computed, ready to use\n", + "- Fast retrieval from Redis\n", + "\n", + "✅ **Quality:**\n", + "- Agent has complete overview\n", + "- Better context understanding\n", + "- More personalized responses\n", + "\n", + "✅ **Efficiency:**\n", + "- Compact token usage\n", + "- Organized information\n", + "- Easy to maintain\n", + "\n", + "### Combining with RAG\n", + "\n", + "**Best practice: Use both!**\n", + "\n", + "```python\n", + "# Load structured views\n", + "catalog_view = load_catalog_view()\n", + "profile_view = load_profile_view(user_id)\n", + "\n", + "# Add targeted RAG\n", + "relevant_courses = search_courses(query, limit=3)\n", + "\n", + "# Combine\n", + "context = f\"\"\"\n", + "{catalog_view}\n", + "\n", + "{profile_view}\n", + "\n", + "Relevant courses for this query:\n", + "{relevant_courses}\n", + "\"\"\"\n", + "```\n", + "\n", + "This gives you:\n", + "- Overview (from views)\n", + "- Personalization (from profile)\n", + "- Specific details (from RAG)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Exercises\n", + "\n", + "1. **Create a department view**: Build a detailed view for a single department with all its courses.\n", + "\n", + "2. **Build a schedule view**: Create a view of a student's current schedule with times, locations, and conflicts.\n", + "\n", + "3. **Optimize token usage**: Experiment with different summary lengths. What's the sweet spot?\n", + "\n", + "4. **Implement refresh logic**: Write a function that determines when a view needs to be refreshed." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "In this notebook, you learned:\n", + "\n", + "- ✅ Structured views provide high-level overviews for LLMs\n", + "- ✅ The pattern: Retrieve → Summarize → Stitch → Save\n", + "- ✅ Course catalog views give agents complete course knowledge\n", + "- ✅ User profile views enable personalization from turn 1\n", + "- ✅ Combine views with RAG for best results\n", + "\n", + "**Key insight:** Pre-computing structured views is an advanced technique that goes beyond simple RAG. It gives your agent a \"mental model\" of the domain, enabling better understanding and more intelligent responses." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/python-recipes/context-engineering/reference-agent/.env.example b/python-recipes/context-engineering/reference-agent/.env.example new file mode 100644 index 00000000..b51eae74 --- /dev/null +++ b/python-recipes/context-engineering/reference-agent/.env.example @@ -0,0 +1,23 @@ +# Redis University Class Agent - Environment Configuration + +# OpenAI API Configuration +OPENAI_API_KEY=your_openai_api_key_here + +# Redis Configuration +REDIS_URL=redis://localhost:6379 +# For Redis Cloud, use: redis://username:password@host:port + +# Vector Index Names +VECTOR_INDEX_NAME=course_catalog +MEMORY_INDEX_NAME=agent_memory + +# LangGraph Configuration +CHECKPOINT_NAMESPACE=class_agent + +# Optional: Logging Configuration +LOG_LEVEL=INFO + +# Optional: Agent Configuration +DEFAULT_STUDENT_ID=demo_student +MAX_CONVERSATION_LENGTH=20 +MEMORY_SIMILARITY_THRESHOLD=0.7 diff --git a/python-recipes/context-engineering/reference-agent/LICENSE b/python-recipes/context-engineering/reference-agent/LICENSE new file mode 100644 index 00000000..626b8bc9 --- /dev/null +++ b/python-recipes/context-engineering/reference-agent/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Redis Ltd. + +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. diff --git a/python-recipes/context-engineering/reference-agent/MANIFEST.in b/python-recipes/context-engineering/reference-agent/MANIFEST.in new file mode 100644 index 00000000..afa4f343 --- /dev/null +++ b/python-recipes/context-engineering/reference-agent/MANIFEST.in @@ -0,0 +1,23 @@ +# Include the README and license files +include README.md +include LICENSE +include requirements.txt +include .env.example + +# Include configuration files +include pyproject.toml +include setup.py + +# Include data files +recursive-include redis_context_course/data *.json +recursive-include redis_context_course/templates *.txt + +# Include test files +recursive-include tests *.py + +# Exclude development and build files +exclude .gitignore +exclude .env +recursive-exclude * __pycache__ +recursive-exclude * *.py[co] +recursive-exclude * .DS_Store diff --git a/python-recipes/context-engineering/reference-agent/README.md b/python-recipes/context-engineering/reference-agent/README.md new file mode 100644 index 00000000..d042b9a3 --- /dev/null +++ b/python-recipes/context-engineering/reference-agent/README.md @@ -0,0 +1,293 @@ +# Redis Context Course + +A complete reference implementation of a context-aware AI agent for university course recommendations and academic planning. This package demonstrates key context engineering concepts using Redis, LangGraph, and OpenAI. + +## Features + +- 🧠 **Dual Memory System**: Working memory (task-focused) and long-term memory (cross-session knowledge) +- 🔍 **Semantic Search**: Vector-based course discovery and recommendations +- 🛠️ **Tool Integration**: Extensible tool system for course search and memory management +- 💬 **Context Awareness**: Maintains student preferences, goals, and conversation history +- 🎯 **Personalized Recommendations**: AI-powered course suggestions based on student profile +- 📚 **Course Catalog Management**: Complete system for storing and retrieving course information + +## Installation + +### From PyPI (Recommended) + +```bash +pip install redis-context-course +``` + +### From Source + +```bash +git clone https://github.com/redis-developer/redis-ai-resources.git +cd redis-ai-resources/python-recipes/context-engineering/reference-agent +pip install -e . +``` + +## Quick Start + +### 1. Set Up Environment + +```bash +# Copy the example environment file +cp .env.example .env + +# Edit .env with your OpenAI API key and Redis URL +export OPENAI_API_KEY="your-openai-api-key" +export REDIS_URL="redis://localhost:6379" +``` + +### 2. Start Redis 8 + +For local development: +```bash +# Using Docker +docker run -d --name redis -p 6379:6379 redis:8-alpine + +# Or install Redis 8 locally +# See: https://redis.io/docs/latest/operate/oss_and_stack/install/ +``` + +### 3. Start Redis Agent Memory Server + +The agent uses [Redis Agent Memory Server](https://github.com/redis/agent-memory-server) for memory management: + +```bash +# Install Agent Memory Server +pip install agent-memory-server + +# Start the server (in a separate terminal) +uv run agent-memory api --no-worker + +# Or with Docker +docker run -d --name agent-memory \ + -p 8000:8000 \ + -e REDIS_URL=redis://host.docker.internal:6379 \ + -e OPENAI_API_KEY=your-key \ + redis/agent-memory-server +``` + +Set the Agent Memory Server URL (optional, defaults to localhost:8000): +```bash +export AGENT_MEMORY_URL="http://localhost:8000" +``` + +### 4. Generate Sample Data + +```bash +generate-courses --courses-per-major 15 --output course_catalog.json +``` + +### 5. Ingest Data into Redis + +```bash +ingest-courses --catalog course_catalog.json --clear +``` + +### 6. Start the Agent + +```bash +redis-class-agent --student-id your_student_id +``` + +## Python API Usage + +```python +import asyncio +from redis_context_course import ClassAgent, MemoryClient, CourseManager + +async def main(): + # Initialize the agent (uses Agent Memory Server) + agent = ClassAgent("student_123") + + # Chat with the agent + response = await agent.chat("I'm interested in machine learning courses") + print(response) + + # Use individual components + memory_manager = MemoryManager("student_123") + await memory_manager.store_preference("I prefer online courses") + + course_manager = CourseManager() + courses = await course_manager.search_courses("programming") + +if __name__ == "__main__": + asyncio.run(main()) +``` + +## Architecture + +### Core Components + +- **Agent**: LangGraph-based workflow orchestration +- **Memory Client**: Interface to Redis Agent Memory Server + - Working memory: Session-scoped, task-focused context + - Long-term memory: Cross-session, persistent knowledge +- **Course Manager**: Course storage and recommendation engine +- **Models**: Data structures for courses and students +- **Redis Config**: Redis connections and index management + +### Command Line Tools + +After installation, you have access to these command-line tools: + +- `redis-class-agent`: Interactive chat interface with the agent +- `generate-courses`: Generate sample course catalog data +- `ingest-courses`: Load course data into Redis + +### Memory System + +The agent uses [Redis Agent Memory Server](https://github.com/redis/agent-memory-server) for a production-ready dual-memory architecture: + +1. **Working Memory**: Session-scoped, task-focused context + - Conversation messages + - Current task state + - Task-related data + - TTL-based (default: 1 hour) + - Automatic extraction to long-term storage + +2. **Long-term Memory**: Cross-session, persistent knowledge + - Student preferences and goals + - Important facts learned over time + - Vector-indexed for semantic search + - Automatic deduplication + - Three memory types: semantic, episodic, message + +**Key Features:** +- Automatic memory extraction from conversations +- Semantic vector search with OpenAI embeddings +- Hash-based and semantic deduplication +- Rich metadata (topics, entities, timestamps) +- MCP server support for Claude Desktop + +### Tool System + +The agent has access to several tools: + +- `search_courses_tool`: Find courses based on queries and filters +- `get_recommendations_tool`: Get personalized course recommendations +- `store_preference_tool`: Save student preferences +- `store_goal_tool`: Save student goals +- `get_student_context_tool`: Retrieve relevant student context + +## Usage Examples + +### Basic Conversation + +``` +You: I'm interested in learning programming +Agent: I'd be happy to help you find programming courses! Let me search for some options... + +[Agent searches courses and provides recommendations] + +You: I prefer online courses +Agent: I'll remember that you prefer online courses. Let me find online programming options for you... +``` + +### Course Search + +``` +You: What data science courses are available? +Agent: [Searches and displays relevant data science courses with details] + +You: Show me beginner-friendly options +Agent: [Filters results for beginner difficulty level] +``` + +### Memory and Context + +``` +You: I want to focus on machine learning +Agent: I'll remember that you're interested in machine learning. This will help me provide better recommendations in the future. + +[Later in conversation or new session] +You: What courses should I take? +Agent: Based on your interest in machine learning and preference for online courses, here are my recommendations... +``` + +## Configuration + +### Environment Variables + +- `OPENAI_API_KEY`: Your OpenAI API key (required) +- `REDIS_URL`: Redis connection URL (default: redis://localhost:6379) +- `VECTOR_INDEX_NAME`: Name for course vector index (default: course_catalog) +- `MEMORY_INDEX_NAME`: Name for memory vector index (default: agent_memory) + +### Customization + +The agent is designed to be easily extensible: + +1. **Add New Tools**: Extend the tool system in `agent.py` +2. **Modify Memory Logic**: Customize memory storage and retrieval in `memory.py` +3. **Extend Course Data**: Add new fields to course models in `models.py` +4. **Custom Recommendations**: Modify recommendation logic in `course_manager.py` + +## Development + +### Running Tests + +```bash +pytest tests/ +``` + +### Code Formatting + +```bash +black src/ scripts/ +isort src/ scripts/ +``` + +### Type Checking + +```bash +mypy src/ +``` + +## Project Structure + +``` +reference-agent/ +├── redis_context_course/ # Main package +│ ├── agent.py # LangGraph agent implementation +│ ├── memory.py # Long-term memory manager +│ ├── working_memory.py # Working memory implementation +│ ├── working_memory_tools.py # Memory management tools +│ ├── course_manager.py # Course search and recommendations +│ ├── models.py # Data models +│ ├── redis_config.py # Redis configuration +│ ├── cli.py # Command-line interface +│ └── scripts/ # Data generation and ingestion +├── tests/ # Test suite +├── examples/ # Usage examples +│ └── basic_usage.py # Basic package usage demo +├── data/ # Generated course data +├── README.md # This file +├── requirements.txt # Dependencies +└── setup.py # Package setup + +``` + +## Educational Use + +This reference implementation is designed for educational purposes to demonstrate: + +- Context engineering principles +- Memory management in AI agents (working memory vs. long-term memory) +- Tool integration patterns +- Vector search and semantic retrieval +- LangGraph workflow design +- Redis as an AI infrastructure component + +See the accompanying notebooks in the `../notebooks/` directory for detailed explanations and tutorials. + +### Learning Path + +1. **Start with the notebooks**: `../notebooks/` contains step-by-step tutorials +2. **Explore the examples**: `examples/basic_usage.py` shows basic package usage +3. **Read the source code**: Well-documented code in `redis_context_course/` +4. **Run the agent**: Try the interactive CLI to see it in action +5. **Extend and experiment**: Modify the code to learn by doing diff --git a/python-recipes/context-engineering/reference-agent/examples/advanced_agent_example.py b/python-recipes/context-engineering/reference-agent/examples/advanced_agent_example.py new file mode 100644 index 00000000..92f1869b --- /dev/null +++ b/python-recipes/context-engineering/reference-agent/examples/advanced_agent_example.py @@ -0,0 +1,292 @@ +""" +Advanced Agent Example + +This example demonstrates patterns from all sections of the Context Engineering course: +- Section 2: System context and tools +- Section 3: Memory management +- Section 4: Optimizations (token management, retrieval strategies, tool filtering) + +This is a production-ready pattern that combines all the techniques. +""" + +import asyncio +from langchain_openai import ChatOpenAI +from langchain_core.messages import SystemMessage, HumanMessage, AIMessage + +from redis_context_course import ( + CourseManager, + MemoryClient, + create_course_tools, + create_memory_tools, + count_tokens, + estimate_token_budget, + filter_tools_by_intent, + format_context_for_llm, + create_summary_view, +) + + +class AdvancedClassAgent: + """ + Advanced class scheduling agent with all optimizations. + + Features: + - Tool filtering based on intent + - Token budget management + - Hybrid retrieval (summary + specific items) + - Memory integration + - Grounding support + """ + + def __init__( + self, + student_id: str, + session_id: str = "default_session", + model: str = "gpt-4o", + enable_tool_filtering: bool = True, + enable_memory_tools: bool = False + ): + self.student_id = student_id + self.session_id = session_id + self.llm = ChatOpenAI(model=model, temperature=0.7) + self.course_manager = CourseManager() + self.memory_client = MemoryClient( + user_id=student_id, + namespace="redis_university" + ) + + # Configuration + self.enable_tool_filtering = enable_tool_filtering + self.enable_memory_tools = enable_memory_tools + + # Create tools + self.course_tools = create_course_tools(self.course_manager) + self.memory_tools = create_memory_tools( + self.memory_client, + session_id=self.session_id, + user_id=self.student_id + ) if enable_memory_tools else [] + + # Organize tools by category (for filtering) + self.tool_groups = { + "search": self.course_tools, + "memory": self.memory_tools, + } + + # Pre-compute course catalog summary (Section 4 pattern) + self.catalog_summary = None + + async def initialize(self): + """Initialize the agent (pre-compute summaries).""" + # Create course catalog summary + all_courses = await self.course_manager.get_all_courses() + self.catalog_summary = await create_summary_view( + items=all_courses, + group_by_field="department", + max_items_per_group=5 + ) + print(f"✅ Agent initialized with {len(all_courses)} courses") + + async def chat( + self, + user_message: str, + session_id: str, + conversation_history: list = None + ) -> tuple[str, list]: + """ + Process a user message with all optimizations. + + Args: + user_message: User's message + session_id: Session ID for working memory + conversation_history: Previous messages in this session + + Returns: + Tuple of (response, updated_conversation_history) + """ + if conversation_history is None: + conversation_history = [] + + # Step 1: Load working memory + working_memory = await self.memory_client.get_working_memory( + session_id=session_id, + model_name="gpt-4o" + ) + + # Step 2: Search long-term memory for relevant context + long_term_memories = await self.memory_client.search_memories( + query=user_message, + limit=5 + ) + + # Step 3: Build context (Section 4 pattern) + system_prompt = self._build_system_prompt(long_term_memories) + + # Step 4: Estimate token budget (Section 4 pattern) + token_budget = estimate_token_budget( + system_prompt=system_prompt, + working_memory_messages=len(working_memory.messages) if working_memory else 0, + long_term_memories=len(long_term_memories), + retrieved_context_items=0, # Will add if we do RAG + ) + + print(f"\n📊 Token Budget:") + print(f" System: {token_budget['system_prompt']}") + print(f" Working Memory: {token_budget['working_memory']}") + print(f" Long-term Memory: {token_budget['long_term_memory']}") + print(f" Total: {token_budget['total_input']} tokens") + + # Step 5: Select tools based on intent (Section 4 pattern) + if self.enable_tool_filtering: + relevant_tools = filter_tools_by_intent( + query=user_message, + tool_groups=self.tool_groups, + default_group="search" + ) + print(f"\n🔧 Selected {len(relevant_tools)} relevant tools") + else: + relevant_tools = self.course_tools + self.memory_tools + print(f"\n🔧 Using all {len(relevant_tools)} tools") + + # Step 6: Bind tools and invoke LLM + llm_with_tools = self.llm.bind_tools(relevant_tools) + + # Build messages + messages = [SystemMessage(content=system_prompt)] + + # Add working memory + if working_memory and working_memory.messages: + for msg in working_memory.messages: + if msg.role == "user": + messages.append(HumanMessage(content=msg.content)) + elif msg.role == "assistant": + messages.append(AIMessage(content=msg.content)) + + # Add current message + messages.append(HumanMessage(content=user_message)) + + # Get response + response = llm_with_tools.invoke(messages) + + # Handle tool calls if any + if response.tool_calls: + print(f"\n🛠️ Agent called {len(response.tool_calls)} tool(s)") + # In a full implementation, you'd execute tools here + # For this example, we'll just note them + for tool_call in response.tool_calls: + print(f" - {tool_call['name']}") + + # Step 7: Save to working memory (triggers automatic extraction) + conversation_history.append(HumanMessage(content=user_message)) + conversation_history.append(AIMessage(content=response.content)) + + messages_to_save = [ + {"role": "user" if isinstance(m, HumanMessage) else "assistant", "content": m.content} + for m in conversation_history + ] + + await self.memory_client.save_working_memory( + session_id=session_id, + messages=messages_to_save + ) + + return response.content, conversation_history + + def _build_system_prompt(self, long_term_memories: list) -> str: + """ + Build system prompt with all context. + + This uses the format_context_for_llm pattern from Section 4. + """ + base_instructions = """You are a helpful class scheduling agent for Redis University. +Help students find courses, check prerequisites, and plan their schedule. + +Use the available tools to search courses and check prerequisites. +Be friendly, helpful, and personalized based on what you know about the student. +""" + + # Format memories + memory_context = None + if long_term_memories: + memory_lines = [f"- {m.text}" for m in long_term_memories] + memory_context = "What you know about this student:\n" + "\n".join(memory_lines) + + # Use the formatting helper + return format_context_for_llm( + system_instructions=base_instructions, + summary_view=self.catalog_summary, + memories=memory_context + ) + + +async def main(): + """Run the advanced agent example.""" + print("=" * 80) + print("ADVANCED CLASS AGENT EXAMPLE") + print("=" * 80) + + # Initialize agent + agent = AdvancedClassAgent( + student_id="demo_student", + enable_tool_filtering=True, + enable_memory_tools=False # Set to True to give LLM control over memory + ) + + await agent.initialize() + + # Simulate a conversation + session_id = "demo_session" + conversation = [] + + queries = [ + "Hi! I'm interested in machine learning courses.", + "What are the prerequisites for CS401?", + "I've completed CS101 and CS201. Can I take CS401?", + ] + + for i, query in enumerate(queries, 1): + print(f"\n{'=' * 80}") + print(f"TURN {i}") + print(f"{'=' * 80}") + print(f"\n👤 User: {query}") + + response, conversation = await agent.chat( + user_message=query, + session_id=session_id, + conversation_history=conversation + ) + + print(f"\n🤖 Agent: {response}") + + # Small delay between turns + await asyncio.sleep(1) + + print(f"\n{'=' * 80}") + print("✅ Conversation complete!") + print(f"{'=' * 80}") + + # Show final statistics + print("\n📈 Final Statistics:") + print(f" Turns: {len(queries)}") + print(f" Messages in conversation: {len(conversation)}") + + # Check what was extracted to long-term memory + print("\n🧠 Checking long-term memory...") + await asyncio.sleep(2) # Wait for extraction + + memories = await agent.memory_client.search_memories( + query="", + limit=10 + ) + + if memories: + print(f" Extracted {len(memories)} memories:") + for memory in memories: + print(f" - {memory.text}") + else: + print(" No memories extracted yet (may take a moment)") + + +if __name__ == "__main__": + asyncio.run(main()) + diff --git a/python-recipes/context-engineering/reference-agent/examples/basic_usage.py b/python-recipes/context-engineering/reference-agent/examples/basic_usage.py new file mode 100644 index 00000000..5a3172e4 --- /dev/null +++ b/python-recipes/context-engineering/reference-agent/examples/basic_usage.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +""" +Demo script showing how to use the redis-context-course package. + +This script demonstrates the basic usage of the package components +without requiring external dependencies like Redis or OpenAI. +""" + +import asyncio +from datetime import time +from redis_context_course.models import ( + Course, StudentProfile, DifficultyLevel, CourseFormat, + Semester, DayOfWeek, CourseSchedule, Prerequisite +) + + +def demo_models(): + """Demonstrate the data models.""" + print("🎓 Redis Context Course - Demo") + print("=" * 50) + + print("\n📚 Creating a sample course:") + + # Create a course schedule + schedule = CourseSchedule( + days=[DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY], + start_time=time(10, 0), + end_time=time(11, 30), + location="Science Hall 101" + ) + + # Create prerequisites + prereq = Prerequisite( + course_code="CS101", + course_title="Introduction to Programming", + minimum_grade="C", + can_be_concurrent=False + ) + + # Create a course + course = Course( + course_code="CS201", + title="Data Structures and Algorithms", + description="Study of fundamental data structures and algorithms including arrays, linked lists, trees, graphs, sorting, and searching.", + credits=4, + difficulty_level=DifficultyLevel.INTERMEDIATE, + format=CourseFormat.HYBRID, + department="Computer Science", + major="Computer Science", + prerequisites=[prereq], + schedule=schedule, + semester=Semester.FALL, + year=2024, + instructor="Dr. Jane Smith", + max_enrollment=50, + current_enrollment=35, + tags=["algorithms", "data structures", "programming"], + learning_objectives=[ + "Implement common data structures", + "Analyze algorithm complexity", + "Solve problems using appropriate data structures", + "Understand time and space complexity" + ] + ) + + print(f" Course: {course.course_code} - {course.title}") + print(f" Credits: {course.credits}") + print(f" Difficulty: {course.difficulty_level.value}") + print(f" Format: {course.format.value}") + print(f" Schedule: {', '.join([day.value for day in course.schedule.days])}") + print(f" Time: {course.schedule.start_time} - {course.schedule.end_time}") + print(f" Prerequisites: {len(course.prerequisites)} required") + print(f" Enrollment: {course.current_enrollment}/{course.max_enrollment}") + + print("\n👤 Creating a student profile:") + + student = StudentProfile( + name="Alex Johnson", + email="alex.johnson@university.edu", + major="Computer Science", + year=2, + completed_courses=["CS101", "MATH101", "ENG101"], + current_courses=["CS201", "MATH201"], + interests=["machine learning", "web development", "data science"], + preferred_format=CourseFormat.ONLINE, + preferred_difficulty=DifficultyLevel.INTERMEDIATE, + max_credits_per_semester=15 + ) + + print(f" Name: {student.name}") + print(f" Major: {student.major} (Year {student.year})") + print(f" Completed: {len(student.completed_courses)} courses") + print(f" Current: {len(student.current_courses)} courses") + print(f" Interests: {', '.join(student.interests)}") + print(f" Preferences: {student.preferred_format.value}, {student.preferred_difficulty.value}") + + return course, student + + +def demo_package_info(): + """Show package information.""" + print("\n📦 Package Information:") + + import redis_context_course + + print(f" Version: {redis_context_course.__version__}") + print(f" Author: {redis_context_course.__author__}") + print(f" Description: {redis_context_course.__description__}") + + print("\n🔧 Available Components:") + components = [ + ("Models", "Data structures for courses, students, and memory"), + ("MemoryManager", "Handles long-term memory (cross-session knowledge)"), + ("WorkingMemory", "Handles working memory (task-focused context)"), + ("CourseManager", "Course storage and recommendation engine"), + ("ClassAgent", "LangGraph-based conversational agent"), + ("RedisConfig", "Redis connection and index management") + ] + + for name, description in components: + available = "✅" if getattr(redis_context_course, name, None) is not None else "❌" + print(f" {available} {name}: {description}") + + print("\n💡 Note: Some components require external dependencies (Redis, OpenAI)") + print(" Install with: pip install redis-context-course") + print(" Then set up Redis and OpenAI API key to use all features") + + +def demo_usage_examples(): + """Show usage examples.""" + print("\n💻 Usage Examples:") + + print("\n1. Basic Model Usage:") + print("```python") + print("from redis_context_course.models import Course, DifficultyLevel") + print("") + print("# Create a course") + print("course = Course(") + print(" course_code='CS101',") + print(" title='Introduction to Programming',") + print(" difficulty_level=DifficultyLevel.BEGINNER,") + print(" # ... other fields") + print(")") + print("```") + + print("\n2. Agent Usage (requires dependencies):") + print("```python") + print("import asyncio") + print("from redis_context_course import ClassAgent") + print("") + print("async def main():") + print(" agent = ClassAgent('student_123')") + print(" response = await agent.chat('I want to learn programming')") + print(" print(response)") + print("") + print("asyncio.run(main())") + print("```") + + print("\n3. Command Line Usage:") + print("```bash") + print("# Generate sample course data") + print("generate-courses --courses-per-major 10") + print("") + print("# Ingest data into Redis") + print("ingest-courses --catalog course_catalog.json") + print("") + print("# Start interactive agent") + print("redis-class-agent --student-id your_name") + print("```") + + +def main(): + """Run the demo.""" + try: + # Demo the models + course, student = demo_models() + + # Show package info + demo_package_info() + + # Show usage examples + demo_usage_examples() + + print("\n🎉 Demo completed successfully!") + print("\nNext steps:") + print("1. Install Redis 8: docker run -d --name redis -p 6379:6379 redis:8-alpine") + print("2. Set OPENAI_API_KEY environment variable") + print("3. Try the interactive agent: redis-class-agent --student-id demo") + + except Exception as e: + print(f"❌ Demo failed: {e}") + return 1 + + return 0 + + +if __name__ == "__main__": + exit(main()) diff --git a/python-recipes/context-engineering/reference-agent/pyproject.toml b/python-recipes/context-engineering/reference-agent/pyproject.toml new file mode 100644 index 00000000..73be1811 --- /dev/null +++ b/python-recipes/context-engineering/reference-agent/pyproject.toml @@ -0,0 +1,143 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "redis-context-course" +version = "1.0.0" +authors = [ + {name = "Redis AI Resources Team", email = "redis-ai@redis.com"}, +] +description = "Context Engineering with Redis - University Class Agent Reference Implementation" +readme = "README.md" +license = {text = "MIT"} +requires-python = ">=3.8" +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: Education", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Database", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +keywords = [ + "redis", + "ai", + "context-engineering", + "langraph", + "openai", + "vector-database", + "semantic-search", + "memory-management", + "chatbot", + "recommendation-system", +] +dependencies = [ + "langgraph>=0.2.0,<0.3.0", + "langgraph-checkpoint>=1.0.0", + "langgraph-checkpoint-redis>=0.1.0", + "redis>=6.0.0", + "redisvl>=0.8.0", + "openai>=1.0.0", + "langchain>=0.2.0", + "langchain-openai>=0.1.0", + "langchain-core>=0.2.0", + "langchain-community>=0.2.0", + "pydantic>=1.8.0,<3.0.0", + "python-dotenv>=1.0.0", + "click>=8.0.0", + "rich>=13.0.0", + "faker>=20.0.0", + "pandas>=2.0.0", + "numpy>=1.24.0", + "tiktoken>=0.5.0", + "python-ulid>=3.0.0", + "agent-memory-client>=0.12.3", +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.0.0", + "pytest-asyncio>=0.21.0", + "black>=23.0.0", + "isort>=5.12.0", + "mypy>=1.5.0", + "flake8>=6.0.0", +] +docs = [ + "sphinx>=5.0.0", + "sphinx-rtd-theme>=1.0.0", + "myst-parser>=0.18.0", +] + +[project.urls] +Homepage = "https://github.com/redis-developer/redis-ai-resources" +Documentation = "https://github.com/redis-developer/redis-ai-resources/blob/main/python-recipes/context-engineering/README.md" +Repository = "https://github.com/redis-developer/redis-ai-resources.git" +"Bug Reports" = "https://github.com/redis-developer/redis-ai-resources/issues" + +[project.scripts] +redis-class-agent = "redis_context_course.cli:main" +generate-courses = "redis_context_course.scripts.generate_courses:main" +ingest-courses = "redis_context_course.scripts.ingest_courses:main" + +[tool.setuptools.packages.find] +where = ["."] +include = ["redis_context_course*"] + +[tool.setuptools.package-data] +redis_context_course = ["data/*.json", "templates/*.txt"] + +[tool.black] +line-length = 88 +target-version = ['py38'] +include = '\.pyi?$' +extend-exclude = ''' +/( + # directories + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | build + | dist +)/ +''' + +[tool.isort] +profile = "black" +multi_line_output = 3 +line_length = 88 +known_first_party = ["redis_context_course"] + +[tool.mypy] +python_version = "3.8" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true +disallow_incomplete_defs = true +check_untyped_defs = true +disallow_untyped_decorators = true +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_no_return = true +warn_unreachable = true +strict_equality = true + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = "-v --tb=short" +asyncio_mode = "auto" diff --git a/python-recipes/context-engineering/reference-agent/redis_context_course/__init__.py b/python-recipes/context-engineering/reference-agent/redis_context_course/__init__.py new file mode 100644 index 00000000..4845ba36 --- /dev/null +++ b/python-recipes/context-engineering/reference-agent/redis_context_course/__init__.py @@ -0,0 +1,123 @@ +""" +Redis Context Course - Context Engineering Reference Implementation + +This package provides a complete reference implementation of a context-aware +AI agent for university course recommendations and academic planning. + +The agent demonstrates key context engineering concepts: +- System context management +- Working memory and long-term memory (via Redis Agent Memory Server) +- Tool integration and usage +- Semantic search and retrieval +- Personalized recommendations + +Main Components: +- agent: LangGraph-based agent implementation +- models: Data models for courses and students +- memory_client: Interface to Redis Agent Memory Server +- course_manager: Course storage and recommendation engine +- redis_config: Redis configuration and connections +- cli: Command-line interface + +Installation: + pip install redis-context-course agent-memory-server + +Usage: + from redis_context_course import ClassAgent, MemoryClient + + # Initialize agent (uses Agent Memory Server) + agent = ClassAgent("student_id") + + # Chat with agent + response = await agent.chat("I'm interested in machine learning courses") + +Command Line Tools: + redis-class-agent --student-id your_name + generate-courses --courses-per-major 15 + ingest-courses --catalog course_catalog.json +""" + +# Import core models (these have minimal dependencies) +from .models import ( + Course, Major, StudentProfile, + CourseRecommendation, AgentResponse, Prerequisite, + CourseSchedule, DifficultyLevel, CourseFormat, + Semester, DayOfWeek +) + +# Import agent components +from .agent import ClassAgent, AgentState + +# Import memory client directly from agent_memory_client +from agent_memory_client import MemoryAPIClient as MemoryClient +from agent_memory_client import MemoryClientConfig +from .course_manager import CourseManager +from .redis_config import RedisConfig, redis_config + +# Import tools (used in notebooks) +from .tools import ( + create_course_tools, + create_memory_tools, + select_tools_by_keywords +) + +# Import optimization helpers (from Section 4) +from .optimization_helpers import ( + count_tokens, + estimate_token_budget, + hybrid_retrieval, + create_summary_view, + create_user_profile_view, + filter_tools_by_intent, + classify_intent_with_llm, + extract_references, + format_context_for_llm +) + +__version__ = "1.0.0" +__author__ = "Redis AI Resources Team" +__email__ = "redis-ai@redis.com" +__license__ = "MIT" +__description__ = "Context Engineering with Redis - University Class Agent Reference Implementation" + +__all__ = [ + # Core classes + "ClassAgent", + "AgentState", + "MemoryClient", + "MemoryClientConfig", + "CourseManager", + "RedisConfig", + "redis_config", + + # Data models + "Course", + "Major", + "StudentProfile", + "CourseRecommendation", + "AgentResponse", + "Prerequisite", + "CourseSchedule", + + # Enums + "DifficultyLevel", + "CourseFormat", + "Semester", + "DayOfWeek", + + # Tools (for notebooks) + "create_course_tools", + "create_memory_tools", + "select_tools_by_keywords", + + # Optimization helpers (Section 4) + "count_tokens", + "estimate_token_budget", + "hybrid_retrieval", + "create_summary_view", + "create_user_profile_view", + "filter_tools_by_intent", + "classify_intent_with_llm", + "extract_references", + "format_context_for_llm", +] diff --git a/python-recipes/context-engineering/reference-agent/redis_context_course/agent.py b/python-recipes/context-engineering/reference-agent/redis_context_course/agent.py new file mode 100644 index 00000000..3fc440e2 --- /dev/null +++ b/python-recipes/context-engineering/reference-agent/redis_context_course/agent.py @@ -0,0 +1,437 @@ +""" +LangGraph agent implementation for the Redis University Class Agent. + +This module implements the main agent logic using LangGraph for workflow orchestration, +with Redis Agent Memory Server for memory management. + +Memory Architecture: +- LangGraph Checkpointer (Redis): Low-level graph state persistence for resuming execution +- Working Memory (Agent Memory Server): Session-scoped conversation and task context + * Automatically extracts important facts to long-term storage + * Loaded at start of conversation turn, saved at end +- Long-term Memory (Agent Memory Server): Cross-session knowledge (preferences, facts) + * Searchable via semantic vector search + * Accessible via tools +""" + +import json +from typing import List, Dict, Any, Optional, Annotated +from datetime import datetime + +from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, SystemMessage +from langchain_core.tools import tool +from langchain_openai import ChatOpenAI +from langgraph.graph import StateGraph, END +from langgraph.graph.message import add_messages +from langgraph.prebuilt import ToolNode +from pydantic import BaseModel + +from .models import StudentProfile, CourseRecommendation, AgentResponse +from agent_memory_client import MemoryAPIClient, MemoryClientConfig +from .course_manager import CourseManager +from .redis_config import redis_config + + +class AgentState(BaseModel): + """State for the LangGraph agent.""" + messages: Annotated[List[BaseMessage], add_messages] + student_id: str + student_profile: Optional[StudentProfile] = None + current_query: str = "" + recommendations: List[CourseRecommendation] = [] + context: Dict[str, Any] = {} + next_action: str = "respond" + + +class ClassAgent: + """Redis University Class Agent using LangGraph and Agent Memory Server.""" + + def __init__(self, student_id: str, session_id: Optional[str] = None): + self.student_id = student_id + self.session_id = session_id or f"session_{student_id}" + + # Initialize memory client with proper config + config = MemoryClientConfig( + base_url=os.getenv("AGENT_MEMORY_URL", "http://localhost:8000"), + default_namespace="redis_university" + ) + self.memory_client = MemoryAPIClient(config=config) + self.course_manager = CourseManager() + self.llm = ChatOpenAI(model="gpt-4o", temperature=0.7) + + # Build the agent graph + self.graph = self._build_graph() + + def _build_graph(self) -> StateGraph: + """ + Build the LangGraph workflow. + + The graph uses: + 1. Redis checkpointer for low-level graph state persistence (resuming nodes) + 2. Agent Memory Server for high-level memory management (working + long-term) + """ + # Define tools + tools = [ + self._search_courses_tool, + self._get_recommendations_tool, + self._store_memory_tool, + self._search_memories_tool + ] + + # Create tool node + tool_node = ToolNode(tools) + + # Define the graph + workflow = StateGraph(AgentState) + + # Add nodes + workflow.add_node("load_working_memory", self._load_working_memory) + workflow.add_node("retrieve_context", self._retrieve_context) + workflow.add_node("agent", self._agent_node) + workflow.add_node("tools", tool_node) + workflow.add_node("respond", self._respond_node) + workflow.add_node("save_working_memory", self._save_working_memory) + + # Define edges + workflow.set_entry_point("load_working_memory") + workflow.add_edge("load_working_memory", "retrieve_context") + workflow.add_edge("retrieve_context", "agent") + workflow.add_conditional_edges( + "agent", + self._should_use_tools, + { + "tools": "tools", + "respond": "respond" + } + ) + workflow.add_edge("tools", "agent") + workflow.add_edge("respond", "save_working_memory") + workflow.add_edge("save_working_memory", END) + + # Compile with Redis checkpointer for graph state persistence + # Note: This is separate from Agent Memory Server's working memory + return workflow.compile(checkpointer=redis_config.checkpointer) + + async def _load_working_memory(self, state: AgentState) -> AgentState: + """ + Load working memory from Agent Memory Server. + + Working memory contains: + - Conversation messages from this session + - Structured memories awaiting promotion to long-term storage + - Session-specific data + + This is the first node in the graph, loading context for the current turn. + """ + # Get or create working memory for this session + _, working_memory = await self.memory_client.get_or_create_working_memory( + session_id=self.session_id, + user_id=self.student_id, + model_name="gpt-4o" + ) + + # If we have working memory, add previous messages to state + if working_memory and working_memory.messages: + # Convert MemoryMessage objects to LangChain messages + for msg in working_memory.messages: + if msg.role == "user": + state.messages.append(HumanMessage(content=msg.content)) + elif msg.role == "assistant": + state.messages.append(AIMessage(content=msg.content)) + + return state + + async def _retrieve_context(self, state: AgentState) -> AgentState: + """Retrieve relevant context for the current conversation.""" + # Get the latest human message + human_messages = [msg for msg in state.messages if isinstance(msg, HumanMessage)] + if human_messages: + state.current_query = human_messages[-1].content + + # Search long-term memories for relevant context + if state.current_query: + memories = await self.memory_client.search_memories( + query=state.current_query, + limit=5 + ) + + # Build context from memories + context = { + "preferences": [], + "goals": [], + "recent_facts": [] + } + + for memory in memories: + if memory.memory_type == "semantic": + if "preference" in memory.topics: + context["preferences"].append(memory.text) + elif "goal" in memory.topics: + context["goals"].append(memory.text) + else: + context["recent_facts"].append(memory.text) + + state.context = context + + return state + + async def _agent_node(self, state: AgentState) -> AgentState: + """Main agent reasoning node.""" + # Build system message with context + system_prompt = self._build_system_prompt(state.context) + + # Prepare messages for the LLM + messages = [SystemMessage(content=system_prompt)] + state.messages + + # Get LLM response with tools + response = await self.llm.bind_tools(self._get_tools()).ainvoke(messages) + state.messages.append(response) + + return state + + def _should_use_tools(self, state: AgentState) -> str: + """Determine if tools should be used or if we should respond.""" + last_message = state.messages[-1] + if hasattr(last_message, 'tool_calls') and last_message.tool_calls: + return "tools" + return "respond" + + async def _respond_node(self, state: AgentState) -> AgentState: + """Generate final response.""" + # The response is already in the last message + return state + + async def _save_working_memory(self, state: AgentState) -> AgentState: + """ + Save working memory to Agent Memory Server. + + This is the final node in the graph. It saves the conversation to working memory, + and the Agent Memory Server automatically: + 1. Stores the conversation messages + 2. Extracts important facts to long-term storage + 3. Manages memory deduplication and compaction + + This demonstrates the key concept of working memory: it's persistent storage + for task-focused context that automatically promotes important information + to long-term memory. + """ + # Convert LangChain messages to simple dict format + messages = [] + for msg in state.messages: + if isinstance(msg, HumanMessage): + messages.append({"role": "user", "content": msg.content}) + elif isinstance(msg, AIMessage): + messages.append({"role": "assistant", "content": msg.content}) + + # Save to working memory + # The Agent Memory Server will automatically extract important memories + # to long-term storage based on its configured extraction strategy + from agent_memory_client.models import WorkingMemory, MemoryMessage + + # Convert messages to MemoryMessage format + memory_messages = [MemoryMessage(**msg) for msg in messages] + + # Create WorkingMemory object + working_memory = WorkingMemory( + session_id=self.session_id, + user_id=self.student_id, + messages=memory_messages, + memories=[], + data={} + ) + + await self.memory_client.put_working_memory( + session_id=self.session_id, + memory=working_memory, + user_id=self.student_id, + model_name="gpt-4o" + ) + + return state + + def _build_system_prompt(self, context: Dict[str, Any]) -> str: + """Build system prompt with current context.""" + prompt = """You are a helpful Redis University Class Agent powered by Redis Agent Memory Server. + Your role is to help students find courses, plan their academic journey, and provide personalized + recommendations based on their interests and goals. + + Memory Architecture: + + 1. LangGraph Checkpointer (Redis): + - Low-level graph state persistence for resuming execution + - You don't interact with this directly + + 2. Working Memory (Agent Memory Server): + - Session-scoped, task-focused context + - Contains conversation messages and task-related data + - Automatically loaded at the start of each turn + - Automatically saved at the end of each turn + - Agent Memory Server automatically extracts important facts to long-term storage + + 3. Long-term Memory (Agent Memory Server): + - Cross-session, persistent knowledge (preferences, goals, facts) + - Searchable via semantic vector search + - You can store memories directly using the store_memory tool + - You can search memories using the search_memories tool + + You have access to tools to: + - search_courses: Search for courses in the catalog + - get_recommendations: Get personalized course recommendations + - store_memory: Store important facts in long-term memory (preferences, goals, etc.) + - search_memories: Search existing long-term memories + + Current student context (from long-term memory):""" + + if context.get("preferences"): + prompt += f"\n\nPreferences:\n" + "\n".join(f"- {p}" for p in context['preferences']) + + if context.get("goals"): + prompt += f"\n\nGoals:\n" + "\n".join(f"- {g}" for g in context['goals']) + + if context.get("recent_facts"): + prompt += f"\n\nRecent Facts:\n" + "\n".join(f"- {f}" for f in context['recent_facts']) + + prompt += """ + + Guidelines: + - Be helpful, friendly, and encouraging + - Ask clarifying questions when needed + - Provide specific course recommendations when appropriate + - When you learn important preferences or goals, use store_memory to save them + - Reference previous context from long-term memory when relevant + - Explain course prerequisites and requirements clearly + - The conversation is automatically saved to working memory + """ + + return prompt + + @tool + async def _search_courses_tool(self, query: str, filters: Optional[Dict[str, Any]] = None) -> str: + """Search for courses based on a query and optional filters.""" + courses = await self.course_manager.search_courses(query, filters or {}) + + if not courses: + return "No courses found matching your criteria." + + result = f"Found {len(courses)} courses:\n\n" + for course in courses[:5]: # Limit to top 5 results + result += f"**{course.course_code}: {course.title}**\n" + result += f"Department: {course.department} | Credits: {course.credits} | Difficulty: {course.difficulty_level.value}\n" + result += f"Description: {course.description[:200]}...\n\n" + + return result + + @tool + async def _get_recommendations_tool(self, query: str = "", limit: int = 3) -> str: + """Get personalized course recommendations for the student.""" + # For now, create a basic student profile + # In a real implementation, this would be retrieved from storage + student_profile = StudentProfile( + name="Student", + email="student@example.com", + interests=["programming", "data science", "web development"] + ) + + recommendations = await self.course_manager.recommend_courses( + student_profile, query, limit + ) + + if not recommendations: + return "No recommendations available at this time." + + result = f"Here are {len(recommendations)} personalized course recommendations:\n\n" + for i, rec in enumerate(recommendations, 1): + result += f"{i}. **{rec.course.course_code}: {rec.course.title}**\n" + result += f" Relevance: {rec.relevance_score:.2f} | Credits: {rec.course.credits}\n" + result += f" Reasoning: {rec.reasoning}\n" + result += f" Prerequisites met: {'Yes' if rec.prerequisites_met else 'No'}\n\n" + + return result + + @tool + async def _store_memory_tool( + self, + text: str, + memory_type: str = "semantic", + topics: Optional[List[str]] = None + ) -> str: + """ + Store important information in long-term memory. + + Args: + text: The information to store (e.g., "Student prefers online courses") + memory_type: Type of memory - "semantic" for facts/preferences, "episodic" for events + topics: Related topics for filtering (e.g., ["preferences", "courses"]) + """ + from agent_memory_client.models import ClientMemoryRecord + + memory = ClientMemoryRecord( + text=text, + user_id=self.student_id, + memory_type=memory_type, + topics=topics or [] + ) + + await self.memory_client.create_long_term_memory([memory]) + return f"Stored in long-term memory: {text}" + + @tool + async def _search_memories_tool( + self, + query: str, + limit: int = 5 + ) -> str: + """ + Search long-term memories using semantic search. + + Args: + query: Search query (e.g., "student preferences") + limit: Maximum number of results to return + """ + from agent_memory_client.models import UserId + + results = await self.memory_client.search_long_term_memory( + text=query, + user_id=UserId(eq=self.student_id), + limit=limit + ) + + if not results.memories: + return "No relevant memories found." + + result = f"Found {len(results.memories)} relevant memories:\n\n" + for i, memory in enumerate(results.memories, 1): + result += f"{i}. {memory.text}\n" + if memory.topics: + result += f" Topics: {', '.join(memory.topics)}\n" + result += "\n" + + return result + + def _get_tools(self): + """Get list of tools for the agent.""" + return [ + self._search_courses_tool, + self._get_recommendations_tool, + self._store_memory_tool, + self._search_memories_tool + ] + + async def chat(self, message: str, thread_id: str = "default") -> str: + """Main chat interface for the agent.""" + # Create initial state + initial_state = AgentState( + messages=[HumanMessage(content=message)], + student_id=self.student_id + ) + + # Run the graph + config = {"configurable": {"thread_id": thread_id}} + result = await self.graph.ainvoke(initial_state, config) + + # Return the last AI message + ai_messages = [msg for msg in result.messages if isinstance(msg, AIMessage)] + if ai_messages: + return ai_messages[-1].content + + return "I'm sorry, I couldn't process your request." diff --git a/python-recipes/context-engineering/reference-agent/redis_context_course/cli.py b/python-recipes/context-engineering/reference-agent/redis_context_course/cli.py new file mode 100644 index 00000000..ae38fc33 --- /dev/null +++ b/python-recipes/context-engineering/reference-agent/redis_context_course/cli.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +""" +Command-line interface for the Redis University Class Agent. + +This CLI provides an interactive way to chat with the agent and demonstrates +the context engineering concepts in practice. +""" + +import asyncio +import os +import sys +from typing import Optional +import click +from rich.console import Console +from rich.panel import Panel +from rich.prompt import Prompt +from rich.markdown import Markdown +from dotenv import load_dotenv + +from .agent import ClassAgent +from .redis_config import redis_config + +# Load environment variables +load_dotenv() + +console = Console() + + +class ChatCLI: + """Interactive chat CLI for the Class Agent.""" + + def __init__(self, student_id: str): + self.student_id = student_id + self.agent = None + self.thread_id = "cli_session" + + async def initialize(self): + """Initialize the agent and check connections.""" + console.print("[yellow]Initializing Redis University Class Agent...[/yellow]") + + # Check Redis connection + if not redis_config.health_check(): + console.print("[red]❌ Redis connection failed. Please check your Redis server.[/red]") + return False + + console.print("[green]✅ Redis connection successful[/green]") + + # Initialize agent + try: + self.agent = ClassAgent(self.student_id) + console.print("[green]✅ Agent initialized successfully[/green]") + return True + except Exception as e: + console.print(f"[red]❌ Agent initialization failed: {e}[/red]") + return False + + async def run_chat(self): + """Run the interactive chat loop.""" + if not await self.initialize(): + return + + # Welcome message + welcome_panel = Panel( + "[bold blue]Welcome to Redis University Class Agent![/bold blue]\n\n" + "I'm here to help you find courses, plan your academic journey, and provide " + "personalized recommendations based on your interests and goals.\n\n" + "[dim]Type 'help' for commands, 'quit' to exit[/dim]", + title="🎓 Class Agent", + border_style="blue" + ) + console.print(welcome_panel) + + while True: + try: + # Get user input + user_input = Prompt.ask("\n[bold cyan]You[/bold cyan]") + + if user_input.lower() in ['quit', 'exit', 'bye']: + console.print("[yellow]Goodbye! Have a great day! 👋[/yellow]") + break + + if user_input.lower() == 'help': + self.show_help() + continue + + if user_input.lower() == 'clear': + console.clear() + continue + + # Show thinking indicator + with console.status("[bold green]Agent is thinking...", spinner="dots"): + response = await self.agent.chat(user_input, self.thread_id) + + # Display agent response + agent_panel = Panel( + Markdown(response), + title="🤖 Class Agent", + border_style="green" + ) + console.print(agent_panel) + + except KeyboardInterrupt: + console.print("\n[yellow]Chat interrupted. Type 'quit' to exit.[/yellow]") + except Exception as e: + console.print(f"[red]Error: {e}[/red]") + + def show_help(self): + """Show help information.""" + help_text = """ + **Available Commands:** + + • `help` - Show this help message + • `clear` - Clear the screen + • `quit` / `exit` / `bye` - Exit the chat + + **Example Queries:** + + • "I'm interested in computer science courses" + • "What programming courses are available?" + • "I want to learn about data science" + • "Show me beginner-friendly courses" + • "I prefer online courses" + • "What are the prerequisites for CS101?" + + **Features:** + + • 🧠 **Memory**: I remember your preferences and goals + • 🔍 **Search**: I can find courses based on your interests + • 💡 **Recommendations**: I provide personalized course suggestions + • 📚 **Context**: I understand your academic journey + """ + + help_panel = Panel( + Markdown(help_text), + title="📖 Help", + border_style="yellow" + ) + console.print(help_panel) + + +@click.command() +@click.option('--student-id', default='demo_student', help='Student ID for the session') +@click.option('--redis-url', help='Redis connection URL') +def main(student_id: str, redis_url: Optional[str]): + """Start the Redis University Class Agent CLI.""" + + # Set Redis URL if provided + if redis_url: + os.environ['REDIS_URL'] = redis_url + + # Check for required environment variables + if not os.getenv('OPENAI_API_KEY'): + console.print("[red]❌ OPENAI_API_KEY environment variable is required[/red]") + console.print("[yellow]Please set your OpenAI API key:[/yellow]") + console.print("export OPENAI_API_KEY='your-api-key-here'") + sys.exit(1) + + # Start the chat + chat_cli = ChatCLI(student_id) + + try: + asyncio.run(chat_cli.run_chat()) + except KeyboardInterrupt: + console.print("\n[yellow]Goodbye! 👋[/yellow]") + + +if __name__ == "__main__": + main() diff --git a/python-recipes/context-engineering/reference-agent/redis_context_course/course_manager.py b/python-recipes/context-engineering/reference-agent/redis_context_course/course_manager.py new file mode 100644 index 00000000..717e020c --- /dev/null +++ b/python-recipes/context-engineering/reference-agent/redis_context_course/course_manager.py @@ -0,0 +1,356 @@ +""" +Course management system for the Class Agent. + +This module handles course storage, retrieval, and recommendation logic +using Redis vector search for semantic course discovery. +""" + +import json +from typing import List, Optional, Dict, Any +import numpy as np + +from redisvl.query import VectorQuery, FilterQuery +from redisvl.query.filter import Tag, Num + +from .models import Course, CourseRecommendation, StudentProfile, DifficultyLevel, CourseFormat +from .redis_config import redis_config + + +class CourseManager: + """Manages course data and provides recommendation functionality.""" + + def __init__(self): + self.redis_client = redis_config.redis_client + self.vector_index = redis_config.vector_index + self.embeddings = redis_config.embeddings + + def _build_filters(self, filters: Dict[str, Any]) -> str: + """Build filter expressions for Redis queries using RedisVL filter classes.""" + if not filters: + return "" + + filter_conditions = [] + + if "department" in filters: + filter_conditions.append(Tag("department") == filters["department"]) + if "major" in filters: + filter_conditions.append(Tag("major") == filters["major"]) + if "difficulty_level" in filters: + filter_conditions.append(Tag("difficulty_level") == filters["difficulty_level"]) + if "format" in filters: + filter_conditions.append(Tag("format") == filters["format"]) + if "semester" in filters: + filter_conditions.append(Tag("semester") == filters["semester"]) + if "year" in filters: + filter_conditions.append(Num("year") == filters["year"]) + if "credits_min" in filters: + min_credits = filters["credits_min"] + max_credits = filters.get("credits_max", 10) + filter_conditions.append(Num("credits") >= min_credits) + if max_credits != min_credits: + filter_conditions.append(Num("credits") <= max_credits) + + # Combine filters with AND logic + if filter_conditions: + combined_filter = filter_conditions[0] + for condition in filter_conditions[1:]: + combined_filter = combined_filter & condition + return combined_filter + + return "" + + async def store_course(self, course: Course) -> str: + """Store a course in Redis with vector embedding.""" + # Create searchable content for embedding + content = f"{course.title} {course.description} {course.department} {course.major} {' '.join(course.tags)} {' '.join(course.learning_objectives)}" + + # Generate embedding + embedding = await self.embeddings.aembed_query(content) + + # Prepare course data for storage + course_data = { + "id": course.id, + "course_code": course.course_code, + "title": course.title, + "description": course.description, + "department": course.department, + "major": course.major, + "difficulty_level": course.difficulty_level.value, + "format": course.format.value, + "semester": course.semester.value, + "year": course.year, + "credits": course.credits, + "tags": "|".join(course.tags), + "instructor": course.instructor, + "max_enrollment": course.max_enrollment, + "current_enrollment": course.current_enrollment, + "learning_objectives": json.dumps(course.learning_objectives), + "prerequisites": json.dumps([p.dict() for p in course.prerequisites]), + "schedule": json.dumps(course.schedule.dict()) if course.schedule else "", + "created_at": course.created_at.timestamp(), + "updated_at": course.updated_at.timestamp(), + "content_vector": np.array(embedding, dtype=np.float32).tobytes() + } + + # Store in Redis + key = f"{redis_config.vector_index_name}:{course.id}" + self.redis_client.hset(key, mapping=course_data) + + return course.id + + async def get_course(self, course_id: str) -> Optional[Course]: + """Retrieve a course by ID.""" + key = f"{redis_config.vector_index_name}:{course_id}" + course_data = self.redis_client.hgetall(key) + + if not course_data: + return None + + return self._dict_to_course(course_data) + + async def get_course_by_code(self, course_code: str) -> Optional[Course]: + """Retrieve a course by course code.""" + query = FilterQuery( + filter_expression=Tag("course_code") == course_code, + return_fields=["id", "course_code", "title", "description", "department", "major", + "difficulty_level", "format", "semester", "year", "credits", "tags", + "instructor", "max_enrollment", "current_enrollment", "learning_objectives", + "prerequisites", "schedule", "created_at", "updated_at"] + ) + results = self.vector_index.query(query) + + if results.docs: + return self._dict_to_course(results.docs[0].__dict__) + return None + + async def get_all_courses(self) -> List[Course]: + """Retrieve all courses from the catalog.""" + # Use search with empty query to get all courses + return await self.search_courses(query="", limit=1000, similarity_threshold=0.0) + + async def search_courses( + self, + query: str, + filters: Optional[Dict[str, Any]] = None, + limit: int = 10, + similarity_threshold: float = 0.6 + ) -> List[Course]: + """Search courses using semantic similarity.""" + # Generate query embedding + query_embedding = await self.embeddings.aembed_query(query) + + # Build vector query + vector_query = VectorQuery( + vector=query_embedding, + vector_field_name="content_vector", + return_fields=["id", "course_code", "title", "description", "department", "major", + "difficulty_level", "format", "semester", "year", "credits", "tags", + "instructor", "max_enrollment", "current_enrollment", "learning_objectives", + "prerequisites", "schedule", "created_at", "updated_at"], + num_results=limit + ) + + # Apply filters using the helper method + filter_expression = self._build_filters(filters or {}) + if filter_expression: + vector_query.set_filter(filter_expression) + + # Execute search + results = self.vector_index.query(vector_query) + + # Convert results to Course objects + courses = [] + # Handle both list and object with .docs attribute + result_list = results if isinstance(results, list) else results.docs + for result in result_list: + if result.vector_score >= similarity_threshold: + course = self._dict_to_course(result.__dict__) + if course: + courses.append(course) + + return courses + + async def recommend_courses( + self, + student_profile: StudentProfile, + query: str = "", + limit: int = 5 + ) -> List[CourseRecommendation]: + """Generate personalized course recommendations.""" + # Build search query based on student profile and interests + search_terms = [] + + if query: + search_terms.append(query) + + if student_profile.interests: + search_terms.extend(student_profile.interests) + + if student_profile.major: + search_terms.append(student_profile.major) + + search_query = " ".join(search_terms) if search_terms else "courses" + + # Build filters based on student preferences + filters = {} + if student_profile.preferred_format: + filters["format"] = student_profile.preferred_format.value + if student_profile.preferred_difficulty: + filters["difficulty_level"] = student_profile.preferred_difficulty.value + + # Search for relevant courses + courses = await self.search_courses( + query=search_query, + filters=filters, + limit=limit * 2 # Get more to filter out completed courses + ) + + # Generate recommendations with scoring + recommendations = [] + for course in courses: + # Skip if already completed or currently enrolled + if (course.course_code in student_profile.completed_courses or + course.course_code in student_profile.current_courses): + continue + + # Check prerequisites + prerequisites_met = self._check_prerequisites(course, student_profile) + + # Calculate relevance score + relevance_score = self._calculate_relevance_score(course, student_profile, query) + + # Generate reasoning + reasoning = self._generate_reasoning(course, student_profile, relevance_score) + + recommendation = CourseRecommendation( + course=course, + relevance_score=relevance_score, + reasoning=reasoning, + prerequisites_met=prerequisites_met, + fits_schedule=True, # Simplified for now + fits_preferences=self._fits_preferences(course, student_profile) + ) + + recommendations.append(recommendation) + + if len(recommendations) >= limit: + break + + # Sort by relevance score + recommendations.sort(key=lambda x: x.relevance_score, reverse=True) + + return recommendations[:limit] + + def _dict_to_course(self, data: Dict[str, Any]) -> Optional[Course]: + """Convert Redis hash data to Course object.""" + try: + from .models import Prerequisite, CourseSchedule + + # Parse prerequisites + prerequisites = [] + if data.get("prerequisites"): + prereq_data = json.loads(data["prerequisites"]) + prerequisites = [Prerequisite(**p) for p in prereq_data] + + # Parse schedule + schedule = None + if data.get("schedule"): + schedule_data = json.loads(data["schedule"]) + if schedule_data: + schedule = CourseSchedule(**schedule_data) + + # Parse learning objectives + learning_objectives = [] + if data.get("learning_objectives"): + learning_objectives = json.loads(data["learning_objectives"]) + + course = Course( + id=data["id"], + course_code=data["course_code"], + title=data["title"], + description=data["description"], + department=data["department"], + major=data["major"], + difficulty_level=DifficultyLevel(data["difficulty_level"]), + format=CourseFormat(data["format"]), + semester=data["semester"], + year=int(data["year"]), + credits=int(data["credits"]), + tags=data["tags"].split("|") if data.get("tags") else [], + instructor=data["instructor"], + max_enrollment=int(data["max_enrollment"]), + current_enrollment=int(data["current_enrollment"]), + learning_objectives=learning_objectives, + prerequisites=prerequisites, + schedule=schedule + ) + + return course + except Exception as e: + print(f"Error converting data to Course: {e}") + return None + + def _check_prerequisites(self, course: Course, student: StudentProfile) -> bool: + """Check if student meets course prerequisites.""" + for prereq in course.prerequisites: + if prereq.course_code not in student.completed_courses: + if not prereq.can_be_concurrent or prereq.course_code not in student.current_courses: + return False + return True + + def _calculate_relevance_score(self, course: Course, student: StudentProfile, query: str) -> float: + """Calculate relevance score for a course recommendation.""" + score = 0.5 # Base score + + # Major match + if student.major and course.major.lower() == student.major.lower(): + score += 0.3 + + # Interest match + for interest in student.interests: + if (interest.lower() in course.title.lower() or + interest.lower() in course.description.lower() or + interest.lower() in " ".join(course.tags).lower()): + score += 0.1 + + # Difficulty preference + if student.preferred_difficulty and course.difficulty_level == student.preferred_difficulty: + score += 0.1 + + # Format preference + if student.preferred_format and course.format == student.preferred_format: + score += 0.1 + + # Ensure score is between 0 and 1 + return min(1.0, max(0.0, score)) + + def _fits_preferences(self, course: Course, student: StudentProfile) -> bool: + """Check if course fits student preferences.""" + if student.preferred_format and course.format != student.preferred_format: + return False + if student.preferred_difficulty and course.difficulty_level != student.preferred_difficulty: + return False + return True + + def _generate_reasoning(self, course: Course, student: StudentProfile, score: float) -> str: + """Generate human-readable reasoning for the recommendation.""" + reasons = [] + + if student.major and course.major.lower() == student.major.lower(): + reasons.append(f"matches your {student.major} major") + + matching_interests = [ + interest for interest in student.interests + if (interest.lower() in course.title.lower() or + interest.lower() in course.description.lower()) + ] + if matching_interests: + reasons.append(f"aligns with your interests in {', '.join(matching_interests)}") + + if student.preferred_difficulty and course.difficulty_level == student.preferred_difficulty: + reasons.append(f"matches your preferred {course.difficulty_level.value} difficulty level") + + if not reasons: + reasons.append("is relevant to your academic goals") + + return f"This course {', '.join(reasons)}." diff --git a/python-recipes/context-engineering/reference-agent/redis_context_course/models.py b/python-recipes/context-engineering/reference-agent/redis_context_course/models.py new file mode 100644 index 00000000..45aeb4ec --- /dev/null +++ b/python-recipes/context-engineering/reference-agent/redis_context_course/models.py @@ -0,0 +1,141 @@ +""" +Data models for the Redis University Class Agent. + +This module defines the core data structures used throughout the application, +including courses, majors, prerequisites, and student information. +""" + +from datetime import datetime, time +from enum import Enum +from typing import List, Optional, Dict, Any +from pydantic import BaseModel, Field, ConfigDict +from ulid import ULID + + +class DifficultyLevel(str, Enum): + """Course difficulty levels.""" + BEGINNER = "beginner" + INTERMEDIATE = "intermediate" + ADVANCED = "advanced" + GRADUATE = "graduate" + + +class CourseFormat(str, Enum): + """Course delivery formats.""" + IN_PERSON = "in_person" + ONLINE = "online" + HYBRID = "hybrid" + + +class Semester(str, Enum): + """Academic semesters.""" + FALL = "fall" + SPRING = "spring" + SUMMER = "summer" + WINTER = "winter" + + +class DayOfWeek(str, Enum): + """Days of the week for scheduling.""" + MONDAY = "monday" + TUESDAY = "tuesday" + WEDNESDAY = "wednesday" + THURSDAY = "thursday" + FRIDAY = "friday" + SATURDAY = "saturday" + SUNDAY = "sunday" + + +class CourseSchedule(BaseModel): + """Course schedule information.""" + days: List[DayOfWeek] + start_time: time + end_time: time + location: Optional[str] = None + + model_config = ConfigDict( + json_encoders={ + time: lambda v: v.strftime("%H:%M") + } + ) + + +class Prerequisite(BaseModel): + """Course prerequisite information.""" + course_code: str + course_title: str + minimum_grade: Optional[str] = "C" + can_be_concurrent: bool = False + + +class Course(BaseModel): + """Complete course information.""" + id: str = Field(default_factory=lambda: str(ULID())) + course_code: str # e.g., "CS101" + title: str + description: str + credits: int + difficulty_level: DifficultyLevel + format: CourseFormat + department: str + major: str + prerequisites: List[Prerequisite] = Field(default_factory=list) + schedule: Optional[CourseSchedule] = None + semester: Semester + year: int + instructor: str + max_enrollment: int + current_enrollment: int = 0 + tags: List[str] = Field(default_factory=list) + learning_objectives: List[str] = Field(default_factory=list) + created_at: datetime = Field(default_factory=datetime.now) + updated_at: datetime = Field(default_factory=datetime.now) + + +class Major(BaseModel): + """Academic major information.""" + id: str = Field(default_factory=lambda: str(ULID())) + name: str + code: str # e.g., "CS", "MATH", "ENG" + department: str + description: str + required_credits: int + core_courses: List[str] = Field(default_factory=list) # Course codes + elective_courses: List[str] = Field(default_factory=list) # Course codes + career_paths: List[str] = Field(default_factory=list) + created_at: datetime = Field(default_factory=datetime.now) + + +class StudentProfile(BaseModel): + """Student profile and preferences.""" + id: str = Field(default_factory=lambda: str(ULID())) + name: str + email: str + major: Optional[str] = None + year: int = 1 # 1-4 for undergraduate, 5+ for graduate + completed_courses: List[str] = Field(default_factory=list) # Course codes + current_courses: List[str] = Field(default_factory=list) # Course codes + interests: List[str] = Field(default_factory=list) + preferred_format: Optional[CourseFormat] = None + preferred_difficulty: Optional[DifficultyLevel] = None + max_credits_per_semester: int = 15 + created_at: datetime = Field(default_factory=datetime.now) + updated_at: datetime = Field(default_factory=datetime.now) + + +class CourseRecommendation(BaseModel): + """Course recommendation with reasoning.""" + course: Course + relevance_score: float = Field(ge=0.0, le=1.0) + reasoning: str + prerequisites_met: bool + fits_schedule: bool = True + fits_preferences: bool = True + + +class AgentResponse(BaseModel): + """Structured response from the agent.""" + message: str + recommendations: List[CourseRecommendation] = Field(default_factory=list) + suggested_actions: List[str] = Field(default_factory=list) + metadata: Dict[str, Any] = Field(default_factory=dict) diff --git a/python-recipes/context-engineering/reference-agent/redis_context_course/optimization_helpers.py b/python-recipes/context-engineering/reference-agent/redis_context_course/optimization_helpers.py new file mode 100644 index 00000000..61121848 --- /dev/null +++ b/python-recipes/context-engineering/reference-agent/redis_context_course/optimization_helpers.py @@ -0,0 +1,388 @@ +""" +Optimization helpers for context engineering. + +This module contains helper functions and patterns demonstrated in Section 4 +of the Context Engineering course. These are production-ready patterns for: +- Context window management +- Retrieval strategies +- Tool optimization +- Data crafting for LLMs +""" + +import json +from typing import List, Dict, Any, Optional +import tiktoken +from langchain_openai import ChatOpenAI +from langchain_core.messages import SystemMessage, HumanMessage + + +# Token Counting (from Section 4, notebook 01_context_window_management.ipynb) +def count_tokens(text: str, model: str = "gpt-4o") -> int: + """ + Count tokens in text for a specific model. + + Args: + text: Text to count tokens for + model: Model name (default: gpt-4o) + + Returns: + Number of tokens + """ + try: + encoding = tiktoken.encoding_for_model(model) + except KeyError: + encoding = tiktoken.get_encoding("cl100k_base") + + return len(encoding.encode(text)) + + +def estimate_token_budget( + system_prompt: str, + working_memory_messages: int, + long_term_memories: int, + retrieved_context_items: int, + avg_message_tokens: int = 50, + avg_memory_tokens: int = 100, + avg_context_tokens: int = 200, + response_tokens: int = 2000 +) -> Dict[str, int]: + """ + Estimate token budget for a conversation turn. + + Args: + system_prompt: System prompt text + working_memory_messages: Number of messages in working memory + long_term_memories: Number of long-term memories to include + retrieved_context_items: Number of retrieved context items + avg_message_tokens: Average tokens per message + avg_memory_tokens: Average tokens per memory + avg_context_tokens: Average tokens per context item + response_tokens: Tokens reserved for response + + Returns: + Dictionary with token breakdown + """ + system_tokens = count_tokens(system_prompt) + working_memory_tokens = working_memory_messages * avg_message_tokens + long_term_tokens = long_term_memories * avg_memory_tokens + context_tokens = retrieved_context_items * avg_context_tokens + + total_input = system_tokens + working_memory_tokens + long_term_tokens + context_tokens + total_with_response = total_input + response_tokens + + return { + "system_prompt": system_tokens, + "working_memory": working_memory_tokens, + "long_term_memory": long_term_tokens, + "retrieved_context": context_tokens, + "response_space": response_tokens, + "total_input": total_input, + "total_with_response": total_with_response, + "percentage_of_128k": (total_with_response / 128000) * 100 + } + + +# Retrieval Strategies (from Section 4, notebook 02_retrieval_strategies.ipynb) +async def hybrid_retrieval( + query: str, + summary_view: str, + search_function, + limit: int = 3 +) -> str: + """ + Hybrid retrieval: Combine pre-computed summary with targeted search. + + This is the recommended strategy for production systems. + + Args: + query: User's query + summary_view: Pre-computed summary/overview + search_function: Async function that searches for specific items + limit: Number of specific items to retrieve + + Returns: + Combined context string + """ + # Get specific relevant items + specific_items = await search_function(query, limit=limit) + + # Combine summary + specific items + context = f"""{summary_view} + +Relevant items for this query: +{specific_items} +""" + + return context + + +# Structured Views (from Section 4, notebook 05_crafting_data_for_llms.ipynb) +async def create_summary_view( + items: List[Any], + group_by_field: str, + llm: Optional[ChatOpenAI] = None, + max_items_per_group: int = 10 +) -> str: + """ + Create a structured summary view of items. + + This implements the "Retrieve → Summarize → Stitch → Save" pattern. + + Args: + items: List of items to summarize + group_by_field: Field to group items by + llm: LLM for generating summaries (optional) + max_items_per_group: Max items to include per group + + Returns: + Formatted summary view + """ + # Step 1: Group items + groups = {} + for item in items: + group_key = getattr(item, group_by_field, "Other") + if group_key not in groups: + groups[group_key] = [] + groups[group_key].append(item) + + # Step 2 & 3: Summarize and stitch + summary_parts = ["Summary View\n" + "=" * 50 + "\n"] + + for group_name, group_items in sorted(groups.items()): + summary_parts.append(f"\n{group_name} ({len(group_items)} items):") + + # Include first N items + for item in group_items[:max_items_per_group]: + # Customize this based on your item type + summary_parts.append(f"- {str(item)[:100]}...") + + if len(group_items) > max_items_per_group: + summary_parts.append(f" ... and {len(group_items) - max_items_per_group} more") + + return "\n".join(summary_parts) + + +async def create_user_profile_view( + user_data: Dict[str, Any], + memories: List[Any], + llm: ChatOpenAI +) -> str: + """ + Create a comprehensive user profile view. + + This combines structured data with LLM-summarized memories. + + Args: + user_data: Structured user data (dict) + memories: List of user memories + llm: LLM for summarizing memories + + Returns: + Formatted profile view + """ + # Structured sections (no LLM needed) + profile_parts = [ + f"User Profile: {user_data.get('user_id', 'Unknown')}", + "=" * 50, + "" + ] + + # Add structured data + if "academic_info" in user_data: + profile_parts.append("Academic Info:") + for key, value in user_data["academic_info"].items(): + profile_parts.append(f"- {key}: {value}") + profile_parts.append("") + + # Summarize memories with LLM + if memories: + memory_text = "\n".join([f"- {m.text}" for m in memories[:20]]) + + prompt = f"""Summarize these user memories into organized sections. +Be concise. Use bullet points. + +Memories: +{memory_text} + +Create sections for: +1. Preferences +2. Goals +3. Important Facts +""" + + messages = [ + SystemMessage(content="You are a helpful assistant that summarizes user information."), + HumanMessage(content=prompt) + ] + + response = llm.invoke(messages) + profile_parts.append(response.content) + + return "\n".join(profile_parts) + + +# Tool Optimization (from Section 4, notebook 04_tool_optimization.ipynb) +def filter_tools_by_intent( + query: str, + tool_groups: Dict[str, List], + default_group: str = "search" +) -> List: + """ + Filter tools based on query intent using keyword matching. + + For production, consider using LLM-based intent classification. + + Args: + query: User's query + tool_groups: Dictionary mapping intent to tool lists + default_group: Default group if no match + + Returns: + List of relevant tools + """ + query_lower = query.lower() + + # Define keyword patterns for each intent + intent_patterns = { + "search": ['search', 'find', 'show', 'what', 'which', 'tell me about', 'list'], + "memory": ['remember', 'recall', 'know about', 'preferences', 'store', 'save'], + "enrollment": ['enroll', 'register', 'drop', 'add', 'remove', 'conflict'], + "review": ['review', 'rating', 'feedback', 'opinion', 'rate'], + } + + # Check each intent + for intent, keywords in intent_patterns.items(): + if any(keyword in query_lower for keyword in keywords): + return tool_groups.get(intent, tool_groups.get(default_group, [])) + + # Default + return tool_groups.get(default_group, []) + + +async def classify_intent_with_llm( + query: str, + intents: List[str], + llm: ChatOpenAI +) -> str: + """ + Classify user intent using LLM. + + More accurate than keyword matching but requires an LLM call. + + Args: + query: User's query + intents: List of possible intents + llm: LLM for classification + + Returns: + Classified intent + """ + intent_list = "\n".join([f"- {intent}" for intent in intents]) + + prompt = f"""Classify the user's intent into one of these categories: +{intent_list} + +User query: "{query}" + +Respond with only the category name. +""" + + messages = [ + SystemMessage(content="You are a helpful assistant that classifies user intents."), + HumanMessage(content=prompt) + ] + + response = llm.invoke(messages) + intent = response.content.strip().lower() + + # Validate + if intent not in intents: + intent = intents[0] # Default to first intent + + return intent + + +# Grounding Helpers (from Section 4, notebook 03_grounding_with_memory.ipynb) +def extract_references(query: str) -> Dict[str, List[str]]: + """ + Extract references from a query that need grounding. + + This is a simple pattern matcher. For production, consider using NER. + + Args: + query: User's query + + Returns: + Dictionary of reference types and their values + """ + references = { + "pronouns": [], + "demonstratives": [], + "implicit": [] + } + + query_lower = query.lower() + + # Pronouns + pronouns = ['it', 'that', 'this', 'those', 'these', 'he', 'she', 'they', 'them'] + for pronoun in pronouns: + if f" {pronoun} " in f" {query_lower} ": + references["pronouns"].append(pronoun) + + # Demonstratives + if "the one" in query_lower or "the other" in query_lower: + references["demonstratives"].append("the one/other") + + # Implicit references (questions without explicit subject) + implicit_patterns = [ + "what are the prerequisites", + "when is it offered", + "how many credits", + "is it available" + ] + for pattern in implicit_patterns: + if pattern in query_lower: + references["implicit"].append(pattern) + + return references + + +# Utility Functions +def format_context_for_llm( + system_instructions: str, + summary_view: Optional[str] = None, + user_profile: Optional[str] = None, + retrieved_items: Optional[str] = None, + memories: Optional[str] = None +) -> str: + """ + Format various context sources into a single system prompt. + + This is the recommended way to combine different context sources. + + Args: + system_instructions: Base system instructions + summary_view: Pre-computed summary view + user_profile: User profile view + retrieved_items: Retrieved specific items + memories: Relevant memories + + Returns: + Formatted system prompt + """ + parts = [system_instructions] + + if summary_view: + parts.append(f"\n## Overview\n{summary_view}") + + if user_profile: + parts.append(f"\n## User Profile\n{user_profile}") + + if memories: + parts.append(f"\n## Relevant Memories\n{memories}") + + if retrieved_items: + parts.append(f"\n## Specific Information\n{retrieved_items}") + + return "\n".join(parts) + diff --git a/python-recipes/context-engineering/reference-agent/redis_context_course/redis_config.py b/python-recipes/context-engineering/reference-agent/redis_context_course/redis_config.py new file mode 100644 index 00000000..11ba17ef --- /dev/null +++ b/python-recipes/context-engineering/reference-agent/redis_context_course/redis_config.py @@ -0,0 +1,161 @@ +""" +Redis configuration and connection management for the Class Agent. + +This module handles all Redis connections, including vector storage +and checkpointing. +""" + +import os +from typing import Optional +import redis +from redisvl.index import SearchIndex +from redisvl.schema import IndexSchema +from langchain_openai import OpenAIEmbeddings +from langgraph.checkpoint.redis import RedisSaver + + +class RedisConfig: + """Redis configuration management.""" + + def __init__( + self, + redis_url: Optional[str] = None, + vector_index_name: str = "course_catalog", + checkpoint_namespace: str = "class_agent" + ): + self.redis_url = redis_url or os.getenv("REDIS_URL", "redis://localhost:6379") + self.vector_index_name = vector_index_name + self.checkpoint_namespace = checkpoint_namespace + + # Initialize connections + self._redis_client = None + self._vector_index = None + self._checkpointer = None + self._embeddings = None + + @property + def redis_client(self) -> redis.Redis: + """Get Redis client instance.""" + if self._redis_client is None: + self._redis_client = redis.from_url(self.redis_url, decode_responses=True) + return self._redis_client + + @property + def embeddings(self) -> OpenAIEmbeddings: + """Get OpenAI embeddings instance.""" + if self._embeddings is None: + self._embeddings = OpenAIEmbeddings(model="text-embedding-3-small") + return self._embeddings + + @property + def vector_index(self) -> SearchIndex: + """Get or create vector search index for courses.""" + if self._vector_index is None: + schema = IndexSchema.from_dict({ + "index": { + "name": self.vector_index_name, + "prefix": f"{self.vector_index_name}:", + "storage_type": "hash" + }, + "fields": [ + { + "name": "id", + "type": "tag" + }, + { + "name": "course_code", + "type": "tag" + }, + { + "name": "title", + "type": "text" + }, + { + "name": "description", + "type": "text" + }, + { + "name": "department", + "type": "tag" + }, + { + "name": "major", + "type": "tag" + }, + { + "name": "difficulty_level", + "type": "tag" + }, + { + "name": "format", + "type": "tag" + }, + { + "name": "semester", + "type": "tag" + }, + { + "name": "year", + "type": "numeric" + }, + { + "name": "credits", + "type": "numeric" + }, + { + "name": "tags", + "type": "tag" + }, + { + "name": "content_vector", + "type": "vector", + "attrs": { + "dims": 1536, + "distance_metric": "cosine", + "algorithm": "hnsw", + "datatype": "float32" + } + } + ] + }) + + self._vector_index = SearchIndex(schema) + self._vector_index.connect(redis_url=self.redis_url) + + # Create index if it doesn't exist + try: + self._vector_index.create(overwrite=False) + except Exception: + # Index likely already exists + pass + + return self._vector_index + + @property + def checkpointer(self) -> RedisSaver: + """Get Redis checkpointer for LangGraph state management.""" + if self._checkpointer is None: + self._checkpointer = RedisSaver( + redis_client=self.redis_client, + namespace=self.checkpoint_namespace + ) + self._checkpointer.setup() + return self._checkpointer + + def health_check(self) -> bool: + """Check if Redis connection is healthy.""" + try: + return self.redis_client.ping() + except Exception: + return False + + def cleanup(self): + """Clean up connections.""" + if self._redis_client: + self._redis_client.close() + if self._vector_index: + self._vector_index.disconnect() + + +# Global configuration instance +redis_config = RedisConfig() diff --git a/python-recipes/context-engineering/reference-agent/redis_context_course/scripts/__init__.py b/python-recipes/context-engineering/reference-agent/redis_context_course/scripts/__init__.py new file mode 100644 index 00000000..2f2a0b5c --- /dev/null +++ b/python-recipes/context-engineering/reference-agent/redis_context_course/scripts/__init__.py @@ -0,0 +1,12 @@ +""" +Scripts package for Redis Context Course. + +This package contains command-line scripts for data generation, +ingestion, and other utilities for the context engineering course. + +Available scripts: +- generate_courses: Generate sample course catalog data +- ingest_courses: Ingest course data into Redis +""" + +__all__ = ["generate_courses", "ingest_courses"] diff --git a/python-recipes/context-engineering/reference-agent/redis_context_course/scripts/generate_courses.py b/python-recipes/context-engineering/reference-agent/redis_context_course/scripts/generate_courses.py new file mode 100644 index 00000000..3c61a155 --- /dev/null +++ b/python-recipes/context-engineering/reference-agent/redis_context_course/scripts/generate_courses.py @@ -0,0 +1,427 @@ +#!/usr/bin/env python3 +""" +Course catalog generation script for the Redis University Class Agent. + +This script generates realistic course data including courses, majors, prerequisites, +and other academic metadata for demonstration and testing purposes. +""" + +import json +import random +import sys +import os +from datetime import time +from typing import List, Dict, Any +from faker import Faker +import click + +from redis_context_course.models import ( + Course, Major, Prerequisite, CourseSchedule, + DifficultyLevel, CourseFormat, Semester, DayOfWeek +) + +fake = Faker() + + +class CourseGenerator: + """Generates realistic course catalog data.""" + + def __init__(self): + self.majors_data = self._define_majors() + self.course_templates = self._define_course_templates() + self.generated_courses = [] + self.generated_majors = [] + + def _define_majors(self) -> Dict[str, Dict[str, Any]]: + """Define major programs with their characteristics.""" + return { + "Computer Science": { + "code": "CS", + "department": "Computer Science", + "description": "Study of computational systems, algorithms, and software design", + "required_credits": 120, + "career_paths": ["Software Engineer", "Data Scientist", "Systems Architect", "AI Researcher"] + }, + "Data Science": { + "code": "DS", + "department": "Data Science", + "description": "Interdisciplinary field using statistics, programming, and domain expertise", + "required_credits": 120, + "career_paths": ["Data Analyst", "Machine Learning Engineer", "Business Intelligence Analyst"] + }, + "Mathematics": { + "code": "MATH", + "department": "Mathematics", + "description": "Study of numbers, structures, patterns, and logical reasoning", + "required_credits": 120, + "career_paths": ["Mathematician", "Statistician", "Actuary", "Research Scientist"] + }, + "Business Administration": { + "code": "BUS", + "department": "Business", + "description": "Management, finance, marketing, and organizational behavior", + "required_credits": 120, + "career_paths": ["Business Analyst", "Project Manager", "Consultant", "Entrepreneur"] + }, + "Psychology": { + "code": "PSY", + "department": "Psychology", + "description": "Scientific study of mind, behavior, and mental processes", + "required_credits": 120, + "career_paths": ["Clinical Psychologist", "Counselor", "Research Psychologist", "HR Specialist"] + } + } + + def _define_course_templates(self) -> Dict[str, List[Dict[str, Any]]]: + """Define course templates for each major.""" + return { + "Computer Science": [ + { + "title_template": "Introduction to Programming", + "description": "Fundamental programming concepts using Python. Variables, control structures, functions, and basic data structures.", + "difficulty": DifficultyLevel.BEGINNER, + "credits": 3, + "tags": ["programming", "python", "fundamentals"], + "learning_objectives": [ + "Write basic Python programs", + "Understand variables and data types", + "Use control structures effectively", + "Create and use functions" + ] + }, + { + "title_template": "Data Structures and Algorithms", + "description": "Study of fundamental data structures and algorithms. Arrays, linked lists, trees, graphs, sorting, and searching.", + "difficulty": DifficultyLevel.INTERMEDIATE, + "credits": 4, + "tags": ["algorithms", "data structures", "problem solving"], + "learning_objectives": [ + "Implement common data structures", + "Analyze algorithm complexity", + "Solve problems using appropriate data structures", + "Understand time and space complexity" + ] + }, + { + "title_template": "Database Systems", + "description": "Design and implementation of database systems. SQL, normalization, transactions, and database administration.", + "difficulty": DifficultyLevel.INTERMEDIATE, + "credits": 3, + "tags": ["databases", "sql", "data management"], + "learning_objectives": [ + "Design relational databases", + "Write complex SQL queries", + "Understand database normalization", + "Implement database transactions" + ] + }, + { + "title_template": "Machine Learning", + "description": "Introduction to machine learning algorithms and applications. Supervised and unsupervised learning, neural networks.", + "difficulty": DifficultyLevel.ADVANCED, + "credits": 4, + "tags": ["machine learning", "ai", "statistics"], + "learning_objectives": [ + "Understand ML algorithms", + "Implement classification and regression models", + "Evaluate model performance", + "Apply ML to real-world problems" + ] + }, + { + "title_template": "Web Development", + "description": "Full-stack web development using modern frameworks. HTML, CSS, JavaScript, React, and backend APIs.", + "difficulty": DifficultyLevel.INTERMEDIATE, + "credits": 3, + "tags": ["web development", "javascript", "react", "apis"], + "learning_objectives": [ + "Build responsive web interfaces", + "Develop REST APIs", + "Use modern JavaScript frameworks", + "Deploy web applications" + ] + } + ], + "Data Science": [ + { + "title_template": "Statistics for Data Science", + "description": "Statistical methods and probability theory for data analysis. Hypothesis testing, regression, and statistical inference.", + "difficulty": DifficultyLevel.INTERMEDIATE, + "credits": 4, + "tags": ["statistics", "probability", "data analysis"], + "learning_objectives": [ + "Apply statistical methods to data", + "Perform hypothesis testing", + "Understand probability distributions", + "Conduct statistical inference" + ] + }, + { + "title_template": "Data Visualization", + "description": "Creating effective visualizations for data communication. Tools include Python matplotlib, seaborn, and Tableau.", + "difficulty": DifficultyLevel.BEGINNER, + "credits": 3, + "tags": ["visualization", "python", "tableau", "communication"], + "learning_objectives": [ + "Create effective data visualizations", + "Choose appropriate chart types", + "Use visualization tools", + "Communicate insights through visuals" + ] + } + ], + "Mathematics": [ + { + "title_template": "Calculus I", + "description": "Differential calculus including limits, derivatives, and applications. Foundation for advanced mathematics.", + "difficulty": DifficultyLevel.INTERMEDIATE, + "credits": 4, + "tags": ["calculus", "derivatives", "limits"], + "learning_objectives": [ + "Understand limits and continuity", + "Calculate derivatives", + "Apply calculus to real problems", + "Understand fundamental theorem" + ] + }, + { + "title_template": "Linear Algebra", + "description": "Vector spaces, matrices, eigenvalues, and linear transformations. Essential for data science and engineering.", + "difficulty": DifficultyLevel.INTERMEDIATE, + "credits": 3, + "tags": ["linear algebra", "matrices", "vectors"], + "learning_objectives": [ + "Perform matrix operations", + "Understand vector spaces", + "Calculate eigenvalues and eigenvectors", + "Apply linear algebra to problems" + ] + } + ], + "Business Administration": [ + { + "title_template": "Principles of Management", + "description": "Fundamental management concepts including planning, organizing, leading, and controlling organizational resources.", + "difficulty": DifficultyLevel.BEGINNER, + "credits": 3, + "tags": ["management", "leadership", "organization"], + "learning_objectives": [ + "Understand management principles", + "Apply leadership concepts", + "Organize teams effectively", + "Control organizational resources" + ] + }, + { + "title_template": "Marketing Strategy", + "description": "Strategic marketing planning, market analysis, consumer behavior, and digital marketing techniques.", + "difficulty": DifficultyLevel.INTERMEDIATE, + "credits": 3, + "tags": ["marketing", "strategy", "consumer behavior"], + "learning_objectives": [ + "Develop marketing strategies", + "Analyze market opportunities", + "Understand consumer behavior", + "Implement digital marketing" + ] + } + ], + "Psychology": [ + { + "title_template": "Introduction to Psychology", + "description": "Overview of psychological principles, research methods, and major areas of study in psychology.", + "difficulty": DifficultyLevel.BEGINNER, + "credits": 3, + "tags": ["psychology", "research methods", "behavior"], + "learning_objectives": [ + "Understand psychological principles", + "Learn research methods", + "Explore areas of psychology", + "Apply psychological concepts" + ] + }, + { + "title_template": "Cognitive Psychology", + "description": "Study of mental processes including perception, memory, thinking, and problem-solving.", + "difficulty": DifficultyLevel.INTERMEDIATE, + "credits": 3, + "tags": ["cognitive psychology", "memory", "perception"], + "learning_objectives": [ + "Understand cognitive processes", + "Study memory systems", + "Analyze problem-solving", + "Explore perception mechanisms" + ] + } + ] + } + + def generate_majors(self) -> List[Major]: + """Generate major objects.""" + majors = [] + for name, data in self.majors_data.items(): + major = Major( + name=name, + code=data["code"], + department=data["department"], + description=data["description"], + required_credits=data["required_credits"], + career_paths=data["career_paths"] + ) + majors.append(major) + + self.generated_majors = majors + return majors + + def generate_courses(self, courses_per_major: int = 10) -> List[Course]: + """Generate course objects for all majors.""" + courses = [] + course_counter = 1 + + for major_name, major_data in self.majors_data.items(): + templates = self.course_templates.get(major_name, []) + + # Generate courses based on templates and variations + for i in range(courses_per_major): + if templates: + template = random.choice(templates) + else: + # Fallback template for majors without specific templates + template = { + "title_template": f"{major_name} Course {i+1}", + "description": f"Advanced topics in {major_name.lower()}", + "difficulty": random.choice(list(DifficultyLevel)), + "credits": random.choice([3, 4]), + "tags": [major_name.lower().replace(" ", "_")], + "learning_objectives": [f"Understand {major_name} concepts"] + } + + # Create course code + course_code = f"{major_data['code']}{course_counter:03d}" + course_counter += 1 + + # Generate schedule + schedule = self._generate_schedule() + + # Generate prerequisites (some courses have them) + prerequisites = [] + if i > 2 and random.random() < 0.3: # 30% chance for advanced courses + # Add 1-2 prerequisites from earlier courses + prereq_count = random.randint(1, 2) + for _ in range(prereq_count): + prereq_num = random.randint(1, max(1, course_counter - 10)) + prereq_code = f"{major_data['code']}{prereq_num:03d}" + prereq = Prerequisite( + course_code=prereq_code, + course_title=f"Prerequisite Course {prereq_num}", + minimum_grade=random.choice(["C", "C+", "B-"]), + can_be_concurrent=random.random() < 0.2 + ) + prerequisites.append(prereq) + + course = Course( + course_code=course_code, + title=template["title_template"], + description=template["description"], + credits=template["credits"], + difficulty_level=template["difficulty"], + format=random.choice(list(CourseFormat)), + department=major_data["department"], + major=major_name, + prerequisites=prerequisites, + schedule=schedule, + semester=random.choice(list(Semester)), + year=2024, + instructor=fake.name(), + max_enrollment=random.randint(20, 100), + current_enrollment=random.randint(0, 80), + tags=template["tags"], + learning_objectives=template["learning_objectives"] + ) + + courses.append(course) + + self.generated_courses = courses + return courses + + def _generate_schedule(self) -> CourseSchedule: + """Generate a random course schedule.""" + # Common schedule patterns + patterns = [ + ([DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY], 50), # MWF + ([DayOfWeek.TUESDAY, DayOfWeek.THURSDAY], 75), # TR + ([DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY], 75), # MW + ([DayOfWeek.TUESDAY], 150), # T (long class) + ([DayOfWeek.THURSDAY], 150), # R (long class) + ] + + days, duration = random.choice(patterns) + + # Generate start time (8 AM to 6 PM) + start_hour = random.randint(8, 18) + start_time = time(start_hour, random.choice([0, 30])) + + # Calculate end time + end_hour = start_hour + (duration // 60) + end_minute = start_time.minute + (duration % 60) + if end_minute >= 60: + end_hour += 1 + end_minute -= 60 + + end_time = time(end_hour, end_minute) + + # Generate location + buildings = ["Science Hall", "Engineering Building", "Liberal Arts Center", "Business Complex", "Technology Center"] + room_number = random.randint(100, 999) + location = f"{random.choice(buildings)} {room_number}" + + return CourseSchedule( + days=days, + start_time=start_time, + end_time=end_time, + location=location + ) + + def save_to_json(self, filename: str): + """Save generated data to JSON file.""" + data = { + "majors": [major.dict() for major in self.generated_majors], + "courses": [course.dict() for course in self.generated_courses] + } + + with open(filename, 'w') as f: + json.dump(data, f, indent=2, default=str) + + print(f"Generated {len(self.generated_majors)} majors and {len(self.generated_courses)} courses") + print(f"Data saved to {filename}") + + +@click.command() +@click.option('--output', '-o', default='course_catalog.json', help='Output JSON file') +@click.option('--courses-per-major', '-c', default=10, help='Number of courses per major') +@click.option('--seed', '-s', type=int, help='Random seed for reproducible generation') +def main(output: str, courses_per_major: int, seed: int): + """Generate course catalog data for the Redis University Class Agent.""" + + if seed: + random.seed(seed) + fake.seed_instance(seed) + + generator = CourseGenerator() + + print("Generating majors...") + majors = generator.generate_majors() + + print(f"Generating {courses_per_major} courses per major...") + courses = generator.generate_courses(courses_per_major) + + print(f"Saving to {output}...") + generator.save_to_json(output) + + print("\nGeneration complete!") + print(f"Total majors: {len(majors)}") + print(f"Total courses: {len(courses)}") + + +if __name__ == "__main__": + main() diff --git a/python-recipes/context-engineering/reference-agent/redis_context_course/scripts/ingest_courses.py b/python-recipes/context-engineering/reference-agent/redis_context_course/scripts/ingest_courses.py new file mode 100644 index 00000000..f6cb3a37 --- /dev/null +++ b/python-recipes/context-engineering/reference-agent/redis_context_course/scripts/ingest_courses.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +""" +Course catalog ingestion script for the Redis University Class Agent. + +This script loads course catalog data from JSON files and ingests it into Redis +with proper vector indexing for semantic search capabilities. +""" + +import json +import asyncio +import sys +import os +from typing import List, Dict, Any +import click +from rich.console import Console +from rich.progress import Progress, TaskID +from dotenv import load_dotenv + +from redis_context_course.models import Course, Major, DifficultyLevel, CourseFormat, Semester, DayOfWeek, Prerequisite, CourseSchedule +from redis_context_course.course_manager import CourseManager +from redis_context_course.redis_config import redis_config + +# Load environment variables +load_dotenv() + +console = Console() + + +class CourseIngestionPipeline: + """Pipeline for ingesting course catalog data into Redis.""" + + def __init__(self): + self.course_manager = CourseManager() + self.redis_client = redis_config.redis_client + + def load_catalog_from_json(self, filename: str) -> Dict[str, List[Dict[str, Any]]]: + """Load course catalog data from JSON file.""" + try: + with open(filename, 'r') as f: + data = json.load(f) + + console.print(f"[green]✅ Loaded catalog from {filename}[/green]") + console.print(f" Majors: {len(data.get('majors', []))}") + console.print(f" Courses: {len(data.get('courses', []))}") + + return data + except FileNotFoundError: + console.print(f"[red]❌ File not found: {filename}[/red]") + raise + except json.JSONDecodeError as e: + console.print(f"[red]❌ Invalid JSON in {filename}: {e}[/red]") + raise + + def _dict_to_course(self, course_data: Dict[str, Any]) -> Course: + """Convert dictionary data to Course object.""" + # Parse prerequisites + prerequisites = [] + for prereq_data in course_data.get('prerequisites', []): + prereq = Prerequisite(**prereq_data) + prerequisites.append(prereq) + + # Parse schedule + schedule = None + if course_data.get('schedule'): + schedule_data = course_data['schedule'] + # Convert day strings to DayOfWeek enums + days = [DayOfWeek(day) for day in schedule_data['days']] + schedule_data['days'] = days + schedule = CourseSchedule(**schedule_data) + + # Create course object + course = Course( + id=course_data.get('id'), + course_code=course_data['course_code'], + title=course_data['title'], + description=course_data['description'], + credits=course_data['credits'], + difficulty_level=DifficultyLevel(course_data['difficulty_level']), + format=CourseFormat(course_data['format']), + department=course_data['department'], + major=course_data['major'], + prerequisites=prerequisites, + schedule=schedule, + semester=Semester(course_data['semester']), + year=course_data['year'], + instructor=course_data['instructor'], + max_enrollment=course_data['max_enrollment'], + current_enrollment=course_data['current_enrollment'], + tags=course_data.get('tags', []), + learning_objectives=course_data.get('learning_objectives', []) + ) + + return course + + def _dict_to_major(self, major_data: Dict[str, Any]) -> Major: + """Convert dictionary data to Major object.""" + return Major( + id=major_data.get('id'), + name=major_data['name'], + code=major_data['code'], + department=major_data['department'], + description=major_data['description'], + required_credits=major_data['required_credits'], + core_courses=major_data.get('core_courses', []), + elective_courses=major_data.get('elective_courses', []), + career_paths=major_data.get('career_paths', []) + ) + + async def ingest_courses(self, courses_data: List[Dict[str, Any]]) -> int: + """Ingest courses into Redis with progress tracking.""" + ingested_count = 0 + + with Progress() as progress: + task = progress.add_task("[green]Ingesting courses...", total=len(courses_data)) + + for course_data in courses_data: + try: + course = self._dict_to_course(course_data) + await self.course_manager.store_course(course) + ingested_count += 1 + progress.update(task, advance=1) + except Exception as e: + console.print(f"[red]❌ Failed to ingest course {course_data.get('course_code', 'unknown')}: {e}[/red]") + + return ingested_count + + def ingest_majors(self, majors_data: List[Dict[str, Any]]) -> int: + """Ingest majors into Redis.""" + ingested_count = 0 + + with Progress() as progress: + task = progress.add_task("[blue]Ingesting majors...", total=len(majors_data)) + + for major_data in majors_data: + try: + major = self._dict_to_major(major_data) + # Store major data in Redis (simple hash storage) + key = f"major:{major.id}" + self.redis_client.hset(key, mapping=major.dict()) + ingested_count += 1 + progress.update(task, advance=1) + except Exception as e: + console.print(f"[red]❌ Failed to ingest major {major_data.get('name', 'unknown')}: {e}[/red]") + + return ingested_count + + def clear_existing_data(self): + """Clear existing course and major data from Redis.""" + console.print("[yellow]🧹 Clearing existing data...[/yellow]") + + # Clear course data + course_keys = self.redis_client.keys(f"{redis_config.vector_index_name}:*") + if course_keys: + self.redis_client.delete(*course_keys) + console.print(f" Cleared {len(course_keys)} course records") + + # Clear major data + major_keys = self.redis_client.keys("major:*") + if major_keys: + self.redis_client.delete(*major_keys) + console.print(f" Cleared {len(major_keys)} major records") + + console.print("[green]✅ Data cleared successfully[/green]") + + def verify_ingestion(self) -> Dict[str, int]: + """Verify the ingestion by counting stored records.""" + course_count = len(self.redis_client.keys(f"{redis_config.vector_index_name}:*")) + major_count = len(self.redis_client.keys("major:*")) + + return { + "courses": course_count, + "majors": major_count + } + + async def run_ingestion(self, catalog_file: str, clear_existing: bool = False): + """Run the complete ingestion pipeline.""" + console.print("[bold blue]🚀 Starting Course Catalog Ingestion[/bold blue]") + + # Check Redis connection + if not redis_config.health_check(): + console.print("[red]❌ Redis connection failed. Please check your Redis server.[/red]") + return False + + console.print("[green]✅ Redis connection successful[/green]") + + # Clear existing data if requested + if clear_existing: + self.clear_existing_data() + + # Load catalog data + try: + catalog_data = self.load_catalog_from_json(catalog_file) + except Exception: + return False + + # Ingest majors + majors_data = catalog_data.get('majors', []) + if majors_data: + major_count = self.ingest_majors(majors_data) + console.print(f"[green]✅ Ingested {major_count} majors[/green]") + + # Ingest courses + courses_data = catalog_data.get('courses', []) + if courses_data: + course_count = await self.ingest_courses(courses_data) + console.print(f"[green]✅ Ingested {course_count} courses[/green]") + + # Verify ingestion + verification = self.verify_ingestion() + console.print(f"[blue]📊 Verification - Courses: {verification['courses']}, Majors: {verification['majors']}[/blue]") + + console.print("[bold green]🎉 Ingestion completed successfully![/bold green]") + return True + + +@click.command() +@click.option('--catalog', '-c', default='course_catalog.json', help='Course catalog JSON file') +@click.option('--clear', is_flag=True, help='Clear existing data before ingestion') +@click.option('--redis-url', help='Redis connection URL') +def main(catalog: str, clear: bool, redis_url: str): + """Ingest course catalog data into Redis for the Class Agent.""" + + # Set Redis URL if provided + if redis_url: + os.environ['REDIS_URL'] = redis_url + + # Check for required environment variables + if not os.getenv('OPENAI_API_KEY'): + console.print("[red]❌ OPENAI_API_KEY environment variable is required[/red]") + console.print("[yellow]Please set your OpenAI API key for embedding generation[/yellow]") + sys.exit(1) + + # Run ingestion + pipeline = CourseIngestionPipeline() + + try: + success = asyncio.run(pipeline.run_ingestion(catalog, clear)) + if not success: + sys.exit(1) + except KeyboardInterrupt: + console.print("\n[yellow]Ingestion interrupted by user[/yellow]") + sys.exit(1) + except Exception as e: + console.print(f"[red]❌ Ingestion failed: {e}[/red]") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/python-recipes/context-engineering/reference-agent/redis_context_course/tools.py b/python-recipes/context-engineering/reference-agent/redis_context_course/tools.py new file mode 100644 index 00000000..ac8ac948 --- /dev/null +++ b/python-recipes/context-engineering/reference-agent/redis_context_course/tools.py @@ -0,0 +1,220 @@ +""" +Tools for the Redis University Class Agent. + +This module defines the tools that the agent can use to interact with +the course catalog and student data. These tools are used in the notebooks +throughout the course. +""" + +from typing import List, Optional +from langchain_core.tools import tool +from pydantic import BaseModel, Field + +from .course_manager import CourseManager +from agent_memory_client import MemoryAPIClient + + +# Tool Input Schemas +class SearchCoursesInput(BaseModel): + """Input schema for searching courses.""" + query: str = Field( + description="Natural language search query. Can be topics (e.g., 'machine learning'), " + "characteristics (e.g., 'online courses'), or general questions " + "(e.g., 'beginner programming courses')" + ) + limit: int = Field( + default=5, + description="Maximum number of results to return. Default is 5. " + "Use 3 for quick answers, 10 for comprehensive results." + ) + + +class GetCourseDetailsInput(BaseModel): + """Input schema for getting course details.""" + course_code: str = Field( + description="Specific course code like 'CS101' or 'MATH201'" + ) + + +class CheckPrerequisitesInput(BaseModel): + """Input schema for checking prerequisites.""" + course_code: str = Field( + description="Course code to check prerequisites for" + ) + completed_courses: List[str] = Field( + description="List of course codes the student has completed" + ) + + +# Course Tools +def create_course_tools(course_manager: CourseManager): + """ + Create course-related tools. + + These tools are demonstrated in Section 2 notebooks. + """ + + @tool(args_schema=SearchCoursesInput) + async def search_courses(query: str, limit: int = 5) -> str: + """ + Search for courses using semantic search based on topics, descriptions, or characteristics. + + Use this tool when students ask about: + - Topics or subjects: "machine learning courses", "database courses" + - Course characteristics: "online courses", "beginner courses", "3-credit courses" + - General exploration: "what courses are available in AI?" + + Do NOT use this tool when: + - Student asks about a specific course code (use get_course_details instead) + - Student wants all courses in a department (use a filter instead) + + The search uses semantic matching, so natural language queries work well. + + Examples: + - "machine learning courses" → finds CS401, CS402, etc. + - "beginner programming" → finds CS101, CS102, etc. + - "online data science courses" → finds online courses about data science + """ + results = await course_manager.search_courses(query, limit=limit) + + if not results: + return "No courses found matching your query." + + output = [] + for course in results: + output.append( + f"{course.course_code}: {course.title}\n" + f" Credits: {course.credits} | {course.format.value} | {course.difficulty_level.value}\n" + f" {course.description[:150]}..." + ) + + return "\n\n".join(output) + + @tool(args_schema=GetCourseDetailsInput) + async def get_course_details(course_code: str) -> str: + """ + Get detailed information about a specific course by its course code. + + Use this tool when: + - Student asks about a specific course (e.g., "Tell me about CS101") + - You need prerequisites for a course + - You need full course details (schedule, instructor, etc.) + + Returns complete course information including description, prerequisites, + schedule, credits, and learning objectives. + """ + course = await course_manager.get_course(course_code) + + if not course: + return f"Course {course_code} not found." + + prereqs = "None" if not course.prerequisites else ", ".join( + [f"{p.course_code} (min grade: {p.min_grade})" for p in course.prerequisites] + ) + + return f""" +{course.course_code}: {course.title} + +Description: {course.description} + +Details: +- Credits: {course.credits} +- Department: {course.department} +- Major: {course.major} +- Difficulty: {course.difficulty_level.value} +- Format: {course.format.value} +- Prerequisites: {prereqs} + +Learning Objectives: +""" + "\n".join([f"- {obj}" for obj in course.learning_objectives]) + + @tool(args_schema=CheckPrerequisitesInput) + async def check_prerequisites(course_code: str, completed_courses: List[str]) -> str: + """ + Check if a student meets the prerequisites for a specific course. + + Use this tool when: + - Student asks "Can I take [course]?" + - Student asks about prerequisites + - You need to verify eligibility before recommending a course + + Returns whether the student is eligible and which prerequisites are missing (if any). + """ + course = await course_manager.get_course(course_code) + + if not course: + return f"Course {course_code} not found." + + if not course.prerequisites: + return f"✅ {course_code} has no prerequisites. You can take this course!" + + missing = [] + for prereq in course.prerequisites: + if prereq.course_code not in completed_courses: + missing.append(f"{prereq.course_code} (min grade: {prereq.min_grade})") + + if not missing: + return f"✅ You meet all prerequisites for {course_code}!" + + return f"""❌ You're missing prerequisites for {course_code}: + +Missing: +""" + "\n".join([f"- {p}" for p in missing]) + + return [search_courses, get_course_details, check_prerequisites] + + +# Memory Tools +def create_memory_tools(memory_client: MemoryAPIClient, session_id: str, user_id: str): + """ + Create memory-related tools using the memory client's built-in LangChain integration. + + These tools are demonstrated in Section 3, notebook 04_memory_tools.ipynb. + They give the LLM explicit control over memory operations. + + Args: + memory_client: The memory client instance + session_id: Session ID for the conversation + user_id: User ID for the student + + Returns: + List of LangChain StructuredTool objects for memory operations + """ + from agent_memory_client.integrations.langchain import get_memory_tools + + return get_memory_tools( + memory_client=memory_client, + session_id=session_id, + user_id=user_id + ) + + +# Tool Selection Helpers (from Section 4, notebook 04_tool_optimization.ipynb) +def select_tools_by_keywords(query: str, all_tools: dict) -> List: + """ + Select relevant tools based on query keywords. + + This is a simple tool filtering strategy demonstrated in Section 4. + For production, consider using intent classification or hierarchical tools. + + Args: + query: User's query + all_tools: Dictionary mapping categories to tool lists + + Returns: + List of relevant tools + """ + query_lower = query.lower() + + # Search-related keywords + if any(word in query_lower for word in ['search', 'find', 'show', 'what', 'which', 'tell me about']): + return all_tools.get("search", []) + + # Memory-related keywords + elif any(word in query_lower for word in ['remember', 'recall', 'know about me', 'preferences']): + return all_tools.get("memory", []) + + # Default: return search tools + else: + return all_tools.get("search", []) + diff --git a/python-recipes/context-engineering/reference-agent/requirements.txt b/python-recipes/context-engineering/reference-agent/requirements.txt new file mode 100644 index 00000000..faaf8e68 --- /dev/null +++ b/python-recipes/context-engineering/reference-agent/requirements.txt @@ -0,0 +1,38 @@ +# Core LangGraph and Redis dependencies +langgraph>=0.2.0,<0.3.0 +langgraph-checkpoint>=1.0.0 +langgraph-checkpoint-redis>=0.1.0 + +# Redis Agent Memory Server +agent-memory-client>=0.12.6 + +# Redis and vector storage +redis>=6.0.0 +redisvl>=0.8.0 + +# OpenAI and language models +openai>=1.0.0 +langchain>=0.2.0 +langchain-openai>=0.1.0 +langchain-core>=0.2.0 +langchain-community>=0.2.0 + +# Data processing and utilities +pydantic>=1.8.0,<3.0.0 +python-dotenv>=1.0.0 +click>=8.0.0 +rich>=13.0.0 +faker>=20.0.0 +pandas>=2.0.0 +numpy>=1.24.0 + +# Testing and development +pytest>=7.0.0 +pytest-asyncio>=0.21.0 +black>=23.0.0 +isort>=5.12.0 +mypy>=1.5.0 + +# Optional: For enhanced functionality +tiktoken>=0.5.0 +python-ulid>=3.0.0 diff --git a/python-recipes/context-engineering/reference-agent/setup.py b/python-recipes/context-engineering/reference-agent/setup.py new file mode 100644 index 00000000..dc75259f --- /dev/null +++ b/python-recipes/context-engineering/reference-agent/setup.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +""" +Setup script for the Redis Context Course package. + +This package provides a complete reference implementation of a context-aware +AI agent for university course recommendations, demonstrating context engineering +principles using Redis, LangGraph, and OpenAI. +""" + +from setuptools import setup, find_packages +from pathlib import Path + +# Read the README file +this_directory = Path(__file__).parent +long_description = (this_directory / "README.md").read_text() + +# Read requirements +requirements = [] +with open("requirements.txt", "r") as f: + requirements = [line.strip() for line in f if line.strip() and not line.startswith("#")] + +setup( + name="redis-context-course", + version="1.0.0", + author="Redis AI Resources Team", + author_email="redis-ai@redis.com", + description="Context Engineering with Redis - University Class Agent Reference Implementation", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/redis-developer/redis-ai-resources", + project_urls={ + "Bug Reports": "https://github.com/redis-developer/redis-ai-resources/issues", + "Source": "https://github.com/redis-developer/redis-ai-resources/tree/main/python-recipes/context-engineering", + "Documentation": "https://github.com/redis-developer/redis-ai-resources/blob/main/python-recipes/context-engineering/README.md", + }, + packages=find_packages(), + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: Education", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Database", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + ], + python_requires=">=3.8", + install_requires=requirements, + extras_require={ + "dev": [ + "pytest>=7.0.0", + "pytest-asyncio>=0.21.0", + "black>=23.0.0", + "isort>=5.12.0", + "mypy>=1.5.0", + "flake8>=6.0.0", + ], + "docs": [ + "sphinx>=5.0.0", + "sphinx-rtd-theme>=1.0.0", + "myst-parser>=0.18.0", + ], + }, + entry_points={ + "console_scripts": [ + "redis-class-agent=redis_context_course.cli:main", + "generate-courses=redis_context_course.scripts.generate_courses:main", + "ingest-courses=redis_context_course.scripts.ingest_courses:main", + ], + }, + include_package_data=True, + package_data={ + "redis_context_course": [ + "data/*.json", + "templates/*.txt", + ], + }, + keywords=[ + "redis", + "ai", + "context-engineering", + "langraph", + "openai", + "vector-database", + "semantic-search", + "memory-management", + "chatbot", + "recommendation-system", + ], + zip_safe=False, +) diff --git a/python-recipes/context-engineering/reference-agent/tests/__init__.py b/python-recipes/context-engineering/reference-agent/tests/__init__.py new file mode 100644 index 00000000..394ceec4 --- /dev/null +++ b/python-recipes/context-engineering/reference-agent/tests/__init__.py @@ -0,0 +1,3 @@ +""" +Tests for the Redis Context Course package. +""" diff --git a/python-recipes/context-engineering/reference-agent/tests/test_package.py b/python-recipes/context-engineering/reference-agent/tests/test_package.py new file mode 100644 index 00000000..de9e1297 --- /dev/null +++ b/python-recipes/context-engineering/reference-agent/tests/test_package.py @@ -0,0 +1,166 @@ +""" +Basic tests to verify the package structure and imports work correctly. +""" + +import pytest + + +def test_package_imports(): + """Test that the main package imports work correctly.""" + try: + import redis_context_course + assert redis_context_course.__version__ == "1.0.0" + assert redis_context_course.__author__ == "Redis AI Resources Team" + except ImportError as e: + pytest.fail(f"Failed to import redis_context_course: {e}") + + +def test_model_imports(): + """Test that model imports work correctly.""" + try: + from redis_context_course.models import ( + Course, StudentProfile, DifficultyLevel, CourseFormat + ) + + # Test enum values + assert DifficultyLevel.BEGINNER == "beginner" + assert CourseFormat.ONLINE == "online" + + except ImportError as e: + pytest.fail(f"Failed to import models: {e}") + + +def test_manager_imports(): + """Test that manager imports work correctly.""" + try: + from redis_context_course import MemoryClient, MemoryClientConfig + from redis_context_course.course_manager import CourseManager + from redis_context_course.redis_config import RedisConfig + + # Test that classes can be instantiated (without Redis connection) + assert MemoryClient is not None + assert MemoryClientConfig is not None + assert CourseManager is not None + assert RedisConfig is not None + + except ImportError as e: + pytest.fail(f"Failed to import managers: {e}") + + +def test_agent_imports(): + """Test that agent imports work correctly.""" + try: + from redis_context_course.agent import ClassAgent, AgentState + + assert ClassAgent is not None + assert AgentState is not None + + except ImportError as e: + pytest.fail(f"Failed to import agent: {e}") + + +def test_scripts_imports(): + """Test that script imports work correctly.""" + try: + from redis_context_course.scripts import generate_courses, ingest_courses + + assert generate_courses is not None + assert ingest_courses is not None + + except ImportError as e: + pytest.fail(f"Failed to import scripts: {e}") + + +def test_cli_imports(): + """Test that CLI imports work correctly.""" + try: + from redis_context_course import cli + + assert cli is not None + assert hasattr(cli, 'main') + + except ImportError as e: + pytest.fail(f"Failed to import CLI: {e}") + + +def test_tools_imports(): + """Test that tools module imports work correctly.""" + try: + from redis_context_course.tools import ( + create_course_tools, + create_memory_tools, + select_tools_by_keywords + ) + + assert create_course_tools is not None + assert create_memory_tools is not None + assert select_tools_by_keywords is not None + + except ImportError as e: + pytest.fail(f"Failed to import tools: {e}") + + +def test_optimization_helpers_imports(): + """Test that optimization helpers import work correctly.""" + try: + from redis_context_course.optimization_helpers import ( + count_tokens, + estimate_token_budget, + hybrid_retrieval, + create_summary_view, + filter_tools_by_intent, + format_context_for_llm + ) + + assert count_tokens is not None + assert estimate_token_budget is not None + assert hybrid_retrieval is not None + assert create_summary_view is not None + assert filter_tools_by_intent is not None + assert format_context_for_llm is not None + + except ImportError as e: + pytest.fail(f"Failed to import optimization helpers: {e}") + + +def test_count_tokens_basic(): + """Test basic token counting functionality.""" + try: + from redis_context_course.optimization_helpers import count_tokens + + # Test with simple text + text = "Hello, world!" + tokens = count_tokens(text) + + assert isinstance(tokens, int) + assert tokens > 0 + + except Exception as e: + pytest.fail(f"Token counting failed: {e}") + + +def test_filter_tools_by_intent_basic(): + """Test basic tool filtering functionality.""" + try: + from redis_context_course.optimization_helpers import filter_tools_by_intent + + # Mock tool groups + tool_groups = { + "search": ["search_tool"], + "memory": ["memory_tool"], + } + + # Test search intent + result = filter_tools_by_intent("find courses", tool_groups) + assert result == ["search_tool"] + + # Test memory intent + result = filter_tools_by_intent("remember this", tool_groups) + assert result == ["memory_tool"] + + except Exception as e: + pytest.fail(f"Tool filtering failed: {e}") + + +if __name__ == "__main__": + pytest.main([__file__]) diff --git a/python-recipes/context-engineering/requirements.txt b/python-recipes/context-engineering/requirements.txt new file mode 100644 index 00000000..8f9f994a --- /dev/null +++ b/python-recipes/context-engineering/requirements.txt @@ -0,0 +1,7 @@ +# Core dependencies for Context Engineering notebooks +jupyter>=1.0.0 +python-dotenv>=1.0.0 + +# The reference agent package should be installed separately with: +# pip install -e reference-agent/ +