diff --git a/.github/workflows/_lint.yml b/.github/workflows/_lint.yml
index a04d5b2..dd53928 100644
--- a/.github/workflows/_lint.yml
+++ b/.github/workflows/_lint.yml
@@ -9,6 +9,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
+ with:
+ fetch-depth: 0 # Required to calculate the git diff
+
+
- name: Set up Python
uses: actions/setup-python@v6
@@ -28,12 +32,27 @@ jobs:
- name: Install dependencies
run: uv sync --frozen --extra dev
- - name: Format with ruff
- run: uv run ruff format --check openviking/
-
- - name: Lint with ruff
- run: uv run ruff check openviking/
-
- - name: Type check with mypy
- run: uv run mypy openviking/
- continue-on-error: true
+ # --- NEW STEP: Get the list of changed files ---
+ - name: Get changed files
+ id: files
+ run: |
+ # Compare the PR head to the base branch
+ echo "changed_files=$(git diff --name-only --diff-filter=d origin/${{ github.base_ref }} HEAD | grep '\.py$' | xargs)" >> $GITHUB_OUTPUT
+
+ # --- UPDATED STEPS: Use the file list ---
+ - name: List files
+ run: echo "The changed files are ${{ steps.files.outputs.changed_files }}"
+
+ - name: Format with ruff (Changed files only)
+ if: steps.files.outputs.changed_files != ''
+ run: uv run ruff format --check ${{ steps.files.outputs.changed_files }}
+
+ - name: Lint with ruff (Changed files only)
+ if: steps.files.outputs.changed_files != ''
+ run: uv run ruff check ${{ steps.files.outputs.changed_files }}
+
+ - name: Type check with mypy (Changed files only)
+ if: steps.files.outputs.changed_files != ''
+ # Note: Running mypy on specific files may miss cross-file type errors
+ run: uv run mypy ${{ steps.files.outputs.changed_files }}
+ continue-on-error: true
\ No newline at end of file
diff --git a/examples/chat/.gitignore b/examples/chat/.gitignore
deleted file mode 100644
index b99c961..0000000
--- a/examples/chat/.gitignore
+++ /dev/null
@@ -1,6 +0,0 @@
-.venv/
-__pycache__/
-*.pyc
-.pytest_cache/
-uv.lock
-ov.conf
diff --git a/examples/chat/chat.py b/examples/chat/chat.py
deleted file mode 100644
index 34ad17a..0000000
--- a/examples/chat/chat.py
+++ /dev/null
@@ -1,365 +0,0 @@
-#!/usr/bin/env python3
-"""
-Chat - Multi-turn conversation interface for OpenViking
-"""
-
-import os
-import signal
-import sys
-from typing import Any, Dict, List
-
-from rich.console import Console
-from rich.panel import Panel
-from rich.text import Text
-
-sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-import threading
-
-from common.recipe import Recipe
-from prompt_toolkit import prompt
-from prompt_toolkit.formatted_text import HTML
-from prompt_toolkit.styles import Style
-from rich.live import Live
-from rich.spinner import Spinner
-
-console = Console()
-PANEL_WIDTH = 78
-
-
-def show_loading_with_spinner(message: str, target_func, *args, **kwargs):
- """Show a loading spinner while a function executes"""
- spinner = Spinner("dots", text=message)
- result = None
- exception = None
-
- def run_target():
- nonlocal result, exception
- try:
- result = target_func(*args, **kwargs)
- except Exception as e:
- exception = e
-
- thread = threading.Thread(target=run_target)
- thread.start()
-
- with Live(spinner, console=console, refresh_per_second=10, transient=True):
- thread.join()
-
- console.print()
-
- if exception:
- raise exception
-
- return result
-
-
-class ChatSession:
- """Manages in-memory conversation history"""
-
- def __init__(self):
- """Initialize empty conversation history"""
- self.history: List[Dict[str, Any]] = []
-
- def add_turn(self, question: str, answer: str, sources: List[Dict[str, Any]]) -> None:
- """
- Add a Q&A turn to history
-
- Args:
- question: User's question
- answer: Assistant's answer
- sources: List of source documents used
- """
- self.history.append(
- {
- "question": question,
- "answer": answer,
- "sources": sources,
- "turn": len(self.history) + 1,
- }
- )
-
- def clear(self) -> None:
- """Clear all conversation history"""
- self.history.clear()
-
- def get_turn_count(self) -> int:
- """Get number of turns in conversation"""
- return len(self.history)
-
- def get_chat_history(self) -> List[Dict[str, str]]:
- """
- Get conversation history in OpenAI chat completion format
-
- Returns:
- List of message dicts with 'role' and 'content' keys
- Format: [{"role": "user", "content": "..."},
- {"role": "assistant", "content": "..."}]
- """
- history = []
- for turn in self.history:
- history.append({"role": "user", "content": turn["question"]})
- history.append({"role": "assistant", "content": turn["answer"]})
- return history
-
-
-class ChatREPL:
- """Interactive chat REPL"""
-
- def __init__(
- self,
- config_path: str = "./ov.conf",
- data_path: str = "./data",
- temperature: float = 0.7,
- max_tokens: int = 2048,
- top_k: int = 5,
- score_threshold: float = 0.2,
- ):
- """Initialize chat REPL"""
- self.config_path = config_path
- self.data_path = data_path
- self.temperature = temperature
- self.max_tokens = max_tokens
- self.top_k = top_k
- self.score_threshold = score_threshold
-
- self.recipe = None
- self.session = ChatSession()
- self.should_exit = False
-
- signal.signal(signal.SIGINT, self._signal_handler)
-
- def _signal_handler(self, signum, frame):
- """Handle Ctrl-C gracefully"""
- console.print("\n")
- console.print(Panel("π Goodbye!", style="bold yellow", padding=(0, 1), width=PANEL_WIDTH))
- self.should_exit = True
- sys.exit(0)
-
- def _show_welcome(self):
- """Display welcome banner"""
- console.clear()
- welcome_text = Text()
- welcome_text.append("π OpenViking Chat\n\n", style="bold cyan")
- welcome_text.append("Multi-round conversation\n", style="white")
- welcome_text.append("Type ", style="dim")
- welcome_text.append("/help", style="bold yellow")
- welcome_text.append(" for commands or ", style="dim")
- welcome_text.append("/exit", style="bold yellow")
- welcome_text.append(" to quit", style="dim")
-
- console.print(Panel(welcome_text, style="bold", padding=(1, 2), width=PANEL_WIDTH))
- console.print()
-
- def _show_help(self):
- """Display help message"""
- help_text = Text()
- help_text.append("Available Commands:\n\n", style="bold cyan")
- help_text.append("/help", style="bold yellow")
- help_text.append(" - Show this help message\n", style="white")
- help_text.append("/clear", style="bold yellow")
- help_text.append(" - Clear screen (keeps history)\n", style="white")
- help_text.append("/exit", style="bold yellow")
- help_text.append(" - Exit chat\n", style="white")
- help_text.append("/quit", style="bold yellow")
- help_text.append(" - Exit chat\n", style="white")
- help_text.append("\nKeyboard Shortcuts:\n\n", style="bold cyan")
- help_text.append("Ctrl-C", style="bold yellow")
- help_text.append(" - Exit gracefully\n", style="white")
- help_text.append("Ctrl-D", style="bold yellow")
- help_text.append(" - Exit\n", style="white")
- help_text.append("β/β", style="bold yellow")
- help_text.append(" - Navigate input history", style="white")
-
- console.print(
- Panel(help_text, title="Help", style="bold green", padding=(1, 2), width=PANEL_WIDTH)
- )
- console.print()
-
- def handle_command(self, cmd: str) -> bool:
- """
- Handle slash commands
-
- Args:
- cmd: Command string (e.g., "/help")
-
- Returns:
- True if should exit, False otherwise
- """
- cmd = cmd.strip().lower()
-
- if cmd in ["/exit", "/quit"]:
- console.print(
- Panel("π Goodbye!", style="bold yellow", padding=(0, 1), width=PANEL_WIDTH)
- )
- return True
- elif cmd == "/help":
- self._show_help()
- elif cmd == "/clear":
- console.clear()
- self._show_welcome()
- else:
- console.print(f"Unknown command: {cmd}", style="red")
- console.print("Type /help for available commands", style="dim")
- console.print()
-
- return False
-
- def ask_question(self, question: str) -> bool:
- """Ask a question and display of answer"""
- try:
- chat_history = self.session.get_chat_history()
- result = show_loading_with_spinner(
- "Thinking...",
- self.recipe.query,
- user_query=question,
- search_top_k=self.top_k,
- temperature=self.temperature,
- max_tokens=self.max_tokens,
- score_threshold=self.score_threshold,
- chat_history=chat_history,
- )
-
- answer_text = Text(result["answer"], style="white")
- console.print(
- Panel(
- answer_text,
- title="π‘ Answer",
- style="bold bright_cyan",
- padding=(1, 1),
- width=PANEL_WIDTH,
- )
- )
- console.print()
-
- if result["context"]:
- from rich import box
- from rich.table import Table
-
- sources_table = Table(
- title=f"π Sources ({len(result['context'])} documents)",
- box=box.ROUNDED,
- show_header=True,
- header_style="bold magenta",
- title_style="bold magenta",
- )
- sources_table.add_column("#", style="cyan", width=4)
- sources_table.add_column("File", style="bold white")
- sources_table.add_column("Relevance", style="green", justify="right")
-
- for i, ctx in enumerate(result["context"], 1):
- uri_parts = ctx["uri"].split("/")
- filename = uri_parts[-1] if uri_parts else ctx["uri"]
- score_text = Text(f"{ctx['score']:.4f}", style="bold green")
- sources_table.add_row(str(i), filename, score_text)
-
- console.print(sources_table)
- console.print()
-
- self.session.add_turn(question, result["answer"], result["context"])
-
- return True
-
- except Exception as e:
- console.print(
- Panel(f"β Error: {e}", style="bold red", padding=(0, 1), width=PANEL_WIDTH)
- )
- console.print()
- return False
-
- def run(self):
- """Main REPL loop"""
- try:
- self.recipe = Recipe(config_path=self.config_path, data_path=self.data_path)
- except Exception as e:
- console.print(Panel(f"β Error initializing: {e}", style="bold red", padding=(0, 1)))
- return
-
- self._show_welcome()
-
- try:
- while not self.should_exit:
- try:
- user_input = prompt(
- HTML(" "), style=Style.from_dict({"": ""})
- ).strip()
-
- if not user_input:
- continue
-
- if user_input.startswith("/"):
- if self.handle_command(user_input):
- break
- continue
-
- self.ask_question(user_input)
-
- except (EOFError, KeyboardInterrupt):
- console.print("\n")
- console.print(
- Panel("π Goodbye!", style="bold yellow", padding=(0, 1), width=PANEL_WIDTH)
- )
- break
-
- finally:
- if self.recipe:
- self.recipe.close()
-
-
-def main():
- """Main entry point"""
- import argparse
-
- parser = argparse.ArgumentParser(
- description="Multi-turn chat with OpenViking RAG",
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-Examples:
- # Start chat with default settings
- uv run chat.py
-
- # Adjust creativity
- uv run chat.py --temperature 0.9
-
- # Use more context
- uv run chat.py --top-k 10
-
- # Enable debug logging
- OV:DEBUG=1 uv run chat.py
- """,
- )
-
- parser.add_argument("--config", type=str, default="./ov.conf", help="Path to config file")
- parser.add_argument("--data", type=str, default="./data", help="Path to data directory")
- parser.add_argument("--top-k", type=int, default=5, help="Number of search results")
- parser.add_argument("--temperature", type=float, default=0.7, help="LLM temperature 0.0-1.0")
- parser.add_argument("--max-tokens", type=int, default=2048, help="Max tokens to generate")
- parser.add_argument("--score-threshold", type=float, default=0.2, help="Min relevance score")
-
- args = parser.parse_args()
-
- if not 0.0 <= args.temperature <= 1.0:
- console.print("β Temperature must be between 0.0 and 1.0", style="bold red")
- sys.exit(1)
-
- if args.top_k < 1:
- console.print("β top-k must be at least 1", style="bold red")
- sys.exit(1)
-
- if not 0.0 <= args.score_threshold <= 1.0:
- console.print("β score-threshold must be between 0.0 and 1.0", style="bold red")
- sys.exit(1)
-
- repl = ChatREPL(
- config_path=args.config,
- data_path=args.data,
- temperature=args.temperature,
- max_tokens=args.max_tokens,
- top_k=args.top_k,
- score_threshold=args.score_threshold,
- )
-
- repl.run()
-
-
-if __name__ == "__main__":
- main()
diff --git a/examples/chat/docs/2026-02-01-openviking-chat-app-design.md b/examples/chat/docs/2026-02-01-openviking-chat-app-design.md
deleted file mode 100644
index 8dddb2d..0000000
--- a/examples/chat/docs/2026-02-01-openviking-chat-app-design.md
+++ /dev/null
@@ -1,298 +0,0 @@
-# OpenViking Chat Application Design
-
-**Date:** 2026-02-01
-**Status:** Approved
-**Target:** Hackathon MVP - Win the coding champion!
-
-## Overview
-
-A client-server chat application leveraging OpenViking's VLM and context management capabilities. Features automatic RAG (Retrieval-Augmented Generation) for intelligent, context-aware conversations with session persistence.
-
-## Architecture
-
-### System Components
-
-**Server (Port 8391)**
-- FastAPI HTTP server handling chat requests
-- OpenViking integration for context management and RAG
-- Session management with auto-commit on client disconnect
-- Stateful - maintains active session per connection
-- RESTful API: `/chat`, `/session/new`, `/session/close`, `/health`
-
-**Client (CLI)**
-- Interactive REPL using `prompt_toolkit`
-- HTTP client communicating with server
-- Rich terminal UI using `rich` library
-- Commands: `/exit`, `/clear`, `/history`, `/new`
-- Displays messages, sources, and system notifications
-
-**Communication Protocol**
-- JSON over HTTP for simplicity
-- Request: `{"message": str, "session_id": str}`
-- Response: `{"response": str, "sources": [...], "session_id": str}`
-
-### Data Flow
-
-1. User types message in REPL
-2. Client sends HTTP POST to server
-3. Server uses OpenViking to search context (automatic RAG)
-4. Server calls VLM with user message + retrieved context
-5. Server returns response with sources
-6. Client displays formatted response
-7. On `/exit`, client calls `/session/close`, server commits session
-
-## Server Components
-
-### server/main.py
-- FastAPI application (async support for OpenViking)
-- Endpoints: `/chat`, `/session/new`, `/session/close`, `/health`
-- Global OpenViking client initialized on startup
-- Session manager tracks active sessions
-- Graceful shutdown commits all active sessions
-
-### server/chat_engine.py
-Core logic for RAG + VLM generation:
-- `ChatEngine` class wraps OpenViking client
-- `process_message(message, session)` method:
- 1. Search for relevant context using `client.search()`
- 2. Format context + message into VLM prompt
- 3. Call VLM via OpenViking's VLM integration
- 4. Return response + source citations
-- Configurable: top_k, temperature, score_threshold
-- Reuses pattern from examples/query.py
-
-### server/session_manager.py
-- `SessionManager` class maintains active sessions
-- Maps session_id β OpenViking Session object
-- `get_or_create(session_id)` - lazy session creation
-- `commit_session(session_id)` - calls `session.commit()`
-- `commit_all()` - cleanup on server shutdown
-- Thread-safe (though single-user mode keeps it simple)
-
-### Configuration
-- Loads from `workspace/ov.conf` (credentials)
-- Environment variables: `OV_PORT=8391`, `OV_DATA_PATH`, `OV_CONFIG_PATH`
-- Default data path: `workspace/opencode/data/`
-
-## Client Components
-
-### client/main.py
-- Entry point for CLI application
-- Argument parsing: `--server` (default: `http://localhost:8391`)
-- Initializes REPL and starts interaction loop
-- Handles Ctrl+C gracefully (commits session before exit)
-- Simple orchestration code (~50 lines)
-
-### client/repl.py
-`ChatREPL` class with rich terminal experience:
-- Multi-line input support (Shift+Enter for newlines)
-- Command history (saved to `~/.openviking_chat_history`)
-- Auto-completion for commands
-- Syntax highlighting for commands
-- Display using `rich` library:
- - User messages in yellow panel
- - Assistant responses in cyan panel
- - Sources table (like query.py)
- - Spinners during processing
-
-**Command Handlers:**
-- `/exit` - close session and quit
-- `/clear` - clear screen
-- `/new` - start new session (commits current)
-- `/history` - show recent messages
-
-### client/http_client.py
-- Thin wrapper around `httpx` for server communication
-- Methods: `send_message()`, `new_session()`, `close_session()`
-- Retry logic with exponential backoff
-- Connection error handling with user-friendly messages
-
-## OpenViking Integration
-
-### Initialization (server startup)
-```python
-client = ov.OpenViking(path="./workspace/opencode/data")
-client.initialize()
-```
-
-### Session Management
-```python
-# Create or load session
-session = client.session(session_id)
-```
-
-### RAG Pipeline (per message)
-```python
-# 1. Search for context
-results = client.search(
- query=user_message,
- session=session,
- limit=5,
- score_threshold=0.2
-)
-
-# 2. Build context from results
-context_docs = [
- {"uri": r.uri, "content": r.content, "score": r.score}
- for r in results.resources
-]
-
-# 3. Track conversation
-session.add_message(role="user", content=user_message)
-
-# 4. Generate response using VLM
-response = generate_with_vlm(
- messages=session.get_messages(),
- context=context_docs
-)
-
-session.add_message(role="assistant", content=response)
-```
-
-### Commit on Exit
-```python
-# When user exits
-session.commit() # Archives messages, extracts memories
-client.close() # Cleanup
-```
-
-## Error Handling
-
-### Server Errors
-
-**OpenViking Initialization**
-- Check config file exists and is valid on startup
-- Fail fast with clear error if VLM/embedding not accessible
-- Return 503 if OpenViking not initialized
-
-**Search/RAG Failures**
-- No results: proceed with VLM using only conversation context
-- VLM call fails: return error with retry suggestion
-- Log all errors for debugging
-
-**Session Commit Failures**
-- Log errors but don't crash server
-- Return success to client (user experience priority)
-- Background retry for failed commits
-
-### Client Errors
-
-**Connection Failures**
-- Check server health on startup
-- Display friendly error message
-- Retry with exponential backoff (3 attempts)
-
-**Message Send Failures**
-- Show error panel
-- Keep message in input buffer for retry
-- Don't clear user's typed message
-
-**Edge Cases**
-- Empty messages: prompt user
-- Very long messages: warn if >4000 chars
-- Server shutdown: save session_id for resume
-
-## Testing Strategy
-
-### Unit Tests
-- `tests/server/test_chat_engine.py` - Mock OpenViking, test RAG
-- `tests/server/test_session_manager.py` - Session lifecycle
-- `tests/client/test_repl.py` - Command parsing, display
-- `tests/shared/test_protocol.py` - Message serialization
-
-### Integration Tests
-- `tests/integration/test_end_to_end.py` - Full flow
-- Mock VLM responses for deterministic testing
-- Test session commit and retrieval
-
-### Manual Testing
-- Use `./workspace/ov.conf` for real VLM
-- Add sample documents to test RAG
-- Multi-turn conversations
-
-## Project Structure
-
-```
-workspace/opencode/
-βββ server/
-β βββ __init__.py
-β βββ main.py # FastAPI app entry point
-β βββ chat_engine.py # RAG + VLM logic
-β βββ session_manager.py # Session lifecycle
-βββ client/
-β βββ __init__.py
-β βββ main.py # CLI entry point
-β βββ repl.py # Interactive REPL
-β βββ http_client.py # Server communication
-βββ shared/
-β βββ __init__.py
-β βββ protocol.py # Message format
-β βββ config.py # Configuration
-βββ tests/
-β βββ server/
-β βββ client/
-β βββ shared/
-β βββ integration/
-βββ data/ # OpenViking data directory
-βββ README.md
-βββ requirements.txt
-βββ pyproject.toml
-```
-
-## Implementation Phases
-
-### Phase 1: Foundation
-- Setup project structure in `workspace/opencode/`
-- Implement `shared/protocol.py` and `shared/config.py`
-- Basic server skeleton with health endpoint
-
-### Phase 2: Server Core
-- Implement `chat_engine.py` with OpenViking integration
-- Implement `session_manager.py`
-- Complete server endpoints (`/chat`, `/session/new`, `/session/close`)
-
-### Phase 3: Client
-- Implement REPL with `prompt_toolkit`
-- HTTP client with retry logic
-- Rich terminal UI with panels and tables
-
-### Phase 4: Integration & Testing
-- End-to-end testing
-- Bug fixes and refinement
-- Documentation (README with usage examples)
-
-## Design Decisions
-
-### Single-user Mode
-- Simpler implementation for MVP
-- Can scale to multi-user later
-- Focus on core functionality first
-
-### Auto-commit on Exit
-- Clean and automatic
-- No manual intervention needed
-- User-friendly
-
-### Automatic RAG
-- Every query searches context
-- Leverages OpenViking's strengths
-- More intelligent responses
-
-### Modular Structure
-- Clear component boundaries
-- Easy to assign to different agents
-- Facilitates parallel development
-
-## Success Criteria
-
-1. β
Client connects to server successfully
-2. β
User can send messages and receive responses
-3. β
Responses include relevant context from past sessions
-4. β
Sessions are committed and memories extracted on exit
-5. β
Clean, intuitive CLI interface
-6. β
Error handling provides helpful feedback
-7. β
Code is clean, well-organized, and documented
-
----
-
-**Ready for implementation!** π
diff --git a/examples/chat/docs/2026-02-02-chat-examples-design.md b/examples/chat/docs/2026-02-02-chat-examples-design.md
deleted file mode 100644
index 5eb6096..0000000
--- a/examples/chat/docs/2026-02-02-chat-examples-design.md
+++ /dev/null
@@ -1,237 +0,0 @@
-# Chat Examples Design
-
-**Date:** 2026-02-02
-**Status:** Approved
-
-## Overview
-
-Create two chat examples building on the existing `query` example:
-1. **Phase 1:** `examples/chat/` - Multi-turn chat interface (no persistence)
-2. **Phase 2:** `examples/chatmem/` - Chat with session memory using OpenViking Session API
-
-## Architecture
-
-### Phase 1: Multi-turn Chat (`examples/chat/`)
-
-**Purpose:** Interactive REPL for multi-turn conversations within a single run.
-
-**Core Components:**
-- `ChatSession` - In-memory conversation history
-- `ChatREPL` - Interactive interface using Rich TUI
-- `Recipe` - Reused from query example (symlink)
-
-**Directory Structure:**
-```
-examples/chat/
-βββ chat.py # Main REPL interface
-βββ recipe.py -> ../query/recipe.py
-βββ boring_logging_config.py -> ../query/boring_logging_config.py
-βββ ov.conf # Config file
-βββ data -> ../query/data # Symlink to query data
-βββ pyproject.toml # Dependencies
-βββ README.md # Usage instructions
-```
-
-### Phase 2: Chat with Memory (`examples/chatmem/`)
-
-**Purpose:** Multi-turn chat with persistent memory using OpenViking Session API.
-
-**Additional Features:**
-- Session creation and loading
-- Message recording (user + assistant)
-- Commit on exit (normal or Ctrl-C)
-- Memory verification on restart
-
-**To be designed in detail after Phase 1 completion.**
-
-## Phase 1: Detailed Design
-
-### 1. ChatSession Class
-
-**Responsibilities:**
-- Store conversation history in memory
-- Manage Q&A turns
-- Display conversation history
-
-**Interface:**
-```python
-class ChatSession:
- def __init__(self):
- self.history: List[Dict] = []
-
- def add_turn(self, question: str, answer: str, sources: List[Dict]):
- """Add a Q&A turn to history"""
-
- def clear(self):
- """Clear conversation history"""
-
- def display_history(self):
- """Display conversation history using Rich"""
-```
-
-### 2. ChatREPL Class
-
-**Responsibilities:**
-- Main REPL loop
-- Command handling
-- User input processing
-- Question/answer display
-
-**Interface:**
-```python
-class ChatREPL:
- def __init__(self, config_path: str, data_path: str, **kwargs):
- self.recipe = Recipe(config_path, data_path)
- self.session = ChatSession()
-
- def run(self):
- """Main REPL loop"""
-
- def handle_command(self, cmd: str) -> bool:
- """Handle commands, return True if should exit"""
-
- def ask_question(self, question: str):
- """Query and display answer"""
-```
-
-### 3. REPL Flow
-
-```
-1. Display welcome banner
-2. Initialize ChatSession (empty)
-3. Loop:
- - Show prompt: "You: "
- - Get user input using readline
- - If empty: continue
- - If command (/exit, /quit, /clear, /help): handle_command()
- - If question: ask_question()
- - Call recipe.query()
- - Display answer with sources
- - Add to session.history
- - Continue loop
-4. On exit:
- - Display goodbye message
- - Clean up resources
-```
-
-### 4. User Interface
-
-**Display Layout:**
-```
-ββ OpenViking Chat ββββββββββββββββββββββββββββββ
-β Type your question or /help for commands β
-βββββββββββββββββββββββββββββββββββββββββββββββββ
-
-[Conversation history shown above]
-
-You: What is prompt engineering?
-
-[Spinner: "Wait a sec..."]
-
-ββ Answer βββββββββββββββββββββββββββββββββββββββ
-β Prompt engineering is... β
-βββββββββββββββββββββββββββββββββββββββββββββββββ
-
-ββ Sources (3 documents) ββββββββββββββββββββββββ
-β # β File β Relevance β β
-β 1 β prompts.md β 0.8234 β β
-βββββββββββββββββββββββββββββββββββββββββββββββββ
-
-You: [cursor]
-```
-
-**Commands:**
-- `/exit` or `/quit` - Exit chat
-- `/clear` - Clear screen (but keep history)
-- `/help` - Show available commands
-- `Ctrl-C` - Graceful exit with goodbye message
-- `Ctrl-D` - Exit
-
-### 5. Implementation Notes
-
-**Dependencies:**
-- `rich` - TUI components (already in query example)
-- `readline` - Input history (arrow keys)
-- Built-in `signal` - Ctrl-C handling
-
-**Key Features:**
-- Reuse Recipe class from query (symlink)
-- In-memory history only (no persistence)
-- Readline for command history (up/down arrows)
-- Signal handling for graceful Ctrl-C
-- Rich console for beautiful output
-- Simple and clean - focus on multi-turn UX
-
-**Symlinks:**
-```bash
-cd examples/chat
-ln -s ../query/recipe.py recipe.py
-ln -s ../query/boring_logging_config.py boring_logging_config.py
-ln -s ../query/data data
-```
-
-**Configuration:**
-- Copy `ov.conf` from query example
-- Same LLM and embedding settings
-- Reuse existing data directory
-
-## Testing Plan
-
-### Phase 1 Testing:
-1. **Basic REPL:**
- - Start chat
- - Ask single question
- - Verify answer displayed
- - Exit with /exit
-
-2. **Multi-turn:**
- - Ask multiple questions
- - Verify history accumulates
- - Check context still works
-
-3. **Commands:**
- - Test /help, /clear, /exit, /quit
- - Test Ctrl-C (graceful exit)
- - Test Ctrl-D
-
-4. **Edge cases:**
- - Empty input
- - Very long questions
- - No search results
-
-### Phase 2 Testing (Future):
-1. Session creation and loading
-2. Message persistence
-3. Commit on exit
-4. Memory verification on restart
-
-## Success Criteria
-
-### Phase 1:
-- [x] Design approved
-- [ ] Chat example created
-- [ ] Multi-turn conversation works
-- [ ] Commands functional
-- [ ] Graceful exit handling
-- [ ] README with usage examples
-
-### Phase 2:
-- [ ] Session integration designed
-- [ ] Memory persistence works
-- [ ] Commit on exit implemented
-- [ ] Memory verification tested
-
-## Next Steps
-
-1. Create `examples/chat/` directory structure
-2. Implement ChatSession and ChatREPL
-3. Test multi-turn functionality
-4. Document usage
-5. Verify and handoff to next agent for Phase 2
-
-## Notes
-
-- Keep Phase 1 simple - no persistence
-- Focus on UX for multi-turn chat
-- Reuse existing components where possible
-- Session API integration deferred to Phase 2
diff --git a/examples/chat/docs/2026-02-02-chat-implementation.md b/examples/chat/docs/2026-02-02-chat-implementation.md
deleted file mode 100644
index cc04c24..0000000
--- a/examples/chat/docs/2026-02-02-chat-implementation.md
+++ /dev/null
@@ -1,178 +0,0 @@
-# Multi-turn Chat Interface Implementation Plan
-
-> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
-
-**Goal:** Build an interactive multi-turn chat interface that allows users to have conversations with OpenViking's RAG pipeline, with in-memory history and graceful exit handling.
-
-**Architecture:** REPL-based chat interface using Rich for TUI, reusing Recipe pipeline from query example. ChatSession manages in-memory conversation history, ChatREPL handles user interaction and commands. No persistence in Phase 1.
-
-**Tech Stack:** Python 3.13+, Rich (TUI), readline (input history), OpenViking Recipe pipeline
-
----
-
-## Task 1: Create Directory Structure and Symlinks
-
-**Files:**
-- Create: `examples/chat/` directory
-- Create: `examples/chat/pyproject.toml`
-- Create: `examples/chat/.gitignore`
-- Symlink: `examples/chat/recipe.py` β `../query/recipe.py`
-- Symlink: `examples/chat/boring_logging_config.py` β `../query/boring_logging_config.py`
-- Symlink: `examples/chat/data` β `../query/data`
-
-**Step 1: Create chat directory**
-
-```bash
-mkdir -p examples/chat
-cd examples/chat
-```
-
-**Step 2: Create pyproject.toml**
-
-```toml
-[project]
-name = "chat"
-version = "0.1.0"
-description = "Multi-turn chat interface for OpenViking"
-readme = "README.md"
-requires-python = ">=3.13"
-dependencies = [
- "openviking>=0.1.6",
- "rich>=13.0.0",
-]
-```
-
-**Step 3: Create .gitignore**
-
-```
-.venv/
-__pycache__/
-*.pyc
-.pytest_cache/
-uv.lock
-ov.conf
-```
-
-**Step 4: Create symlinks**
-
-```bash
-ln -s ../query/recipe.py recipe.py
-ln -s ../query/boring_logging_config.py boring_logging_config.py
-ln -s ../query/data data
-```
-
-**Step 5: Verify symlinks**
-
-```bash
-ls -la
-```
-
-Expected: All symlinks point to existing files (blue arrows in ls output)
-
-**Step 6: Copy config file**
-
-```bash
-cp ../query/ov.conf.example ov.conf.example
-```
-
-**Step 7: Commit**
-
-```bash
-git add examples/chat/
-git commit -m "feat(chat): create directory structure with symlinks to query example"
-```
-
----
-
-## Task 2: Implement ChatSession Class
-
-**Files:**
-- Create: `examples/chat/chat.py`
-
-**Step 1: Write the test file structure (manual test)**
-
-Create a test plan in your head:
-1. Can create ChatSession
-2. Can add turns
-3. Can clear history
-4. History is stored correctly
-
-**Step 2: Implement ChatSession class**
-
-```python
-#!/usr/bin/env python3
-"""
-Chat - Multi-turn conversation interface for OpenViking
-"""
-from typing import List, Dict, Any
-from rich.console import Console
-from rich.panel import Panel
-from rich.text import Text
-
-console = Console()
-
-
-class ChatSession:
- """Manages in-memory conversation history"""
-
- def __init__(self):
- """Initialize empty conversation history"""
- self.history: List[Dict[str, Any]] = []
-
- def add_turn(self, question: str, answer: str, sources: List[Dict[str, Any]]) -> None:
- """
- Add a Q&A turn to history
-
- Args:
- question: User's question
- answer: Assistant's answer
- sources: List of source documents used
- """
- self.history.append({
- 'question': question,
- 'answer': answer,
- 'sources': sources,
- 'turn': len(self.history) + 1
- })
-
- def clear(self) -> None:
- """Clear all conversation history"""
- self.history.clear()
-
- def get_turn_count(self) -> int:
- """Get number of turns in conversation"""
- return len(self.history)
-```
-
-**Step 3: Manual test**
-
-```bash
-cd examples/chat
-python3 -c "
-from chat import ChatSession
-s = ChatSession()
-assert s.get_turn_count() == 0
-s.add_turn('test q', 'test a', [])
-assert s.get_turn_count() == 1
-s.clear()
-assert s.get_turn_count() == 0
-print('ChatSession: OK')
-"
-```
-
-Expected: "ChatSession: OK"
-
-**Step 4: Commit**
-
-```bash
-git add examples/chat/chat.py
-git commit -m "feat(chat): implement ChatSession for in-memory history"
-```
-
----
-
-## Summary
-
-This is a comprehensive 9-task implementation plan. Each task builds on the previous one following TDD principles with frequent commits. The plan includes exact code, file paths, test steps, and commit messages.
-
-For full details, see the complete plan document.
diff --git a/examples/chat/docs/multi_turn_chat_phase_1.md b/examples/chat/docs/multi_turn_chat_phase_1.md
deleted file mode 100644
index 6215abc..0000000
--- a/examples/chat/docs/multi_turn_chat_phase_1.md
+++ /dev/null
@@ -1,624 +0,0 @@
-# Agent Handoff: Multi-turn Chat Implementation (Phase 1)
-
-**Entry Point for Next Agent**
-
-## Current Status
-
-**Location:** `/Users/bytedance/code/OpenViking/.worktrees/chat-examples`
-**Branch:** `examples/chat`
-**Working Directory:** `examples/chat/`
-
-### β
Completed Tasks (2/9)
-
-**Task 1: Directory Structure β
**
-- Commit: 17269b6, e7030a3
-- Created examples/chat/ with pyproject.toml, .gitignore
-- Symlinks: recipe.py, boring_logging_config.py
-- Fixed: Removed broken data symlink (runtime artifact)
-
-**Task 2: ChatSession Class β
**
-- Commit: 87d01ed
-- File: examples/chat/chat.py
-- Implemented: ChatSession with add_turn(), clear(), get_turn_count()
-- Tests: Manual tests passing
-- Reviews: Spec compliant, functionally approved
-
-### π Remaining Tasks (7/9)
-
-**Task 3:** Implement basic REPL structure
-**Task 4:** Implement welcome banner and help
-**Task 5:** Implement question/answer display
-**Task 6:** Implement main REPL loop
-**Task 7:** Add README documentation
-**Task 8:** Manual testing and verification
-**Task 9:** Final integration and handoff prep
-
-## Your Mission
-
-Continue the **subagent-driven development** process using the `superpowers:subagent-driven-development` skill to complete Tasks 3-9.
-
-### Instructions for Next Agent
-
-1. **Read the full implementation plan:**
- - File: `docs/plans/2026-02-02-chat-implementation.md`
- - Note: The file is truncated at 178 lines - use the detailed task descriptions below
-
-2. **Use the TodoWrite task list:**
- - Tasks 1-2 are already marked complete
- - Tasks 3-9 are pending - update status as you work
-
-3. **Follow subagent-driven development process:**
- - For each task (3-9):
- a. Mark task as in_progress
- b. Dispatch implementer subagent with full task context
- c. Answer any questions from implementer
- d. Dispatch spec compliance reviewer
- e. If issues found: implementer fixes, re-review
- f. Dispatch code quality reviewer
- g. If issues found: implementer fixes, re-review
- h. Mark task as completed
- - After all tasks: dispatch final code reviewer
- - Use `superpowers:finishing-a-development-branch`
-
-4. **Maintain quality gates:**
- - Two-stage review: spec compliance THEN code quality
- - Review loops until approved
- - Fresh subagent per task
-
----
-
-## Detailed Task Specifications
-
-### Task 3: Implement Basic REPL Structure
-
-**Files:**
-- Modify: `examples/chat/chat.py`
-
-**Requirements:**
-
-1. Add imports at top:
-```python
-import sys
-import signal
-from recipe import Recipe
-from rich.live import Live
-from rich.spinner import Spinner
-import threading
-
-PANEL_WIDTH = 78
-```
-
-2. Add ChatREPL class:
-```python
-class ChatREPL:
- """Interactive chat REPL"""
-
- def __init__(
- self,
- config_path: str = "./ov.conf",
- data_path: str = "./data",
- temperature: float = 0.7,
- max_tokens: int = 2048,
- top_k: int = 5,
- score_threshold: float = 0.2
- ):
- """Initialize chat REPL"""
- self.config_path = config_path
- self.data_path = data_path
- self.temperature = temperature
- self.max_tokens = max_tokens
- self.top_k = top_k
- self.score_threshold = score_threshold
-
- self.recipe: Recipe = None
- self.session = ChatSession()
- self.should_exit = False
-
- # Setup signal handlers
- signal.signal(signal.SIGINT, self._signal_handler)
-
- def _signal_handler(self, signum, frame):
- """Handle Ctrl-C gracefully"""
- console.print("\n")
- console.print(Panel("π Goodbye!", style="bold yellow", padding=(0, 1), width=PANEL_WIDTH))
- self.should_exit = True
- sys.exit(0)
-
- def run(self):
- """Main REPL loop"""
- pass # To be implemented in Task 6
-```
-
-**Test:**
-```bash
-python3 -c "
-from chat import ChatREPL
-repl = ChatREPL()
-assert repl.session.get_turn_count() == 0
-print('ChatREPL init: OK')
-"
-```
-
-**Commit:** `"feat(chat): add ChatREPL class skeleton with signal handling"`
-
----
-
-### Task 4: Implement Welcome Banner and Help
-
-**Files:**
-- Modify: `examples/chat/chat.py`
-
-**Requirements:**
-
-Add these methods to ChatREPL class:
-
-```python
-def _show_welcome(self):
- """Display welcome banner"""
- console.clear()
- welcome_text = Text()
- welcome_text.append("π OpenViking Chat\n\n", style="bold cyan")
- welcome_text.append("Multi-turn conversation powered by RAG\n", style="white")
- welcome_text.append("Type ", style="dim")
- welcome_text.append("/help", style="bold yellow")
- welcome_text.append(" for commands or ", style="dim")
- welcome_text.append("/exit", style="bold yellow")
- welcome_text.append(" to quit", style="dim")
-
- console.print(Panel(
- welcome_text,
- style="bold",
- padding=(1, 2),
- width=PANEL_WIDTH
- ))
- console.print()
-
-def _show_help(self):
- """Display help message"""
- help_text = Text()
- help_text.append("Available Commands:\n\n", style="bold cyan")
- help_text.append("/help", style="bold yellow")
- help_text.append(" - Show this help message\n", style="white")
- help_text.append("/clear", style="bold yellow")
- help_text.append(" - Clear screen (keeps history)\n", style="white")
- help_text.append("/exit", style="bold yellow")
- help_text.append(" - Exit chat\n", style="white")
- help_text.append("/quit", style="bold yellow")
- help_text.append(" - Exit chat\n", style="white")
- help_text.append("\nKeyboard Shortcuts:\n\n", style="bold cyan")
- help_text.append("Ctrl-C", style="bold yellow")
- help_text.append(" - Exit gracefully\n", style="white")
- help_text.append("Ctrl-D", style="bold yellow")
- help_text.append(" - Exit\n", style="white")
- help_text.append("β/β", style="bold yellow")
- help_text.append(" - Navigate input history", style="white")
-
- console.print(Panel(
- help_text,
- title="Help",
- style="bold green",
- padding=(1, 2),
- width=PANEL_WIDTH
- ))
- console.print()
-
-def handle_command(self, cmd: str) -> bool:
- """
- Handle slash commands
-
- Args:
- cmd: Command string (e.g., "/help")
-
- Returns:
- True if should exit, False otherwise
- """
- cmd = cmd.strip().lower()
-
- if cmd in ["/exit", "/quit"]:
- console.print(Panel(
- "π Goodbye!",
- style="bold yellow",
- padding=(0, 1),
- width=PANEL_WIDTH
- ))
- return True
- elif cmd == "/help":
- self._show_help()
- elif cmd == "/clear":
- console.clear()
- self._show_welcome()
- else:
- console.print(f"Unknown command: {cmd}", style="red")
- console.print("Type /help for available commands", style="dim")
- console.print()
-
- return False
-```
-
-**Test:**
-```bash
-python3 -c "
-from chat import ChatREPL
-repl = ChatREPL()
-assert repl.handle_command('/help') == False
-assert repl.handle_command('/clear') == False
-assert repl.handle_command('/exit') == True
-print('Commands: OK')
-"
-```
-
-**Commit:** `"feat(chat): implement welcome banner, help, and command handling"`
-
----
-
-### Task 5: Implement Question/Answer Display
-
-**Files:**
-- Modify: `examples/chat/chat.py`
-
-**Requirements:**
-
-1. Add spinner helper before ChatSession class:
-
-```python
-def show_loading_with_spinner(message: str, target_func, *args, **kwargs):
- """Show a loading spinner while a function executes"""
- spinner = Spinner("dots", text=message)
- result = None
- exception = None
-
- def run_target():
- nonlocal result, exception
- try:
- result = target_func(*args, **kwargs)
- except Exception as e:
- exception = e
-
- thread = threading.Thread(target=run_target)
- thread.start()
-
- with Live(spinner, console=console, refresh_per_second=10, transient=True):
- thread.join()
-
- console.print()
-
- if exception:
- raise exception
-
- return result
-```
-
-2. Add ask_question method to ChatREPL:
-
-```python
-def ask_question(self, question: str) -> bool:
- """Ask a question and display the answer"""
- try:
- # Query with loading spinner
- result = show_loading_with_spinner(
- "Thinking...",
- self.recipe.query,
- user_query=question,
- search_top_k=self.top_k,
- temperature=self.temperature,
- max_tokens=self.max_tokens,
- score_threshold=self.score_threshold
- )
-
- # Display answer
- answer_text = Text(result['answer'], style="white")
- console.print(Panel(
- answer_text,
- title="π‘ Answer",
- style="bold bright_cyan",
- padding=(1, 1),
- width=PANEL_WIDTH
- ))
- console.print()
-
- # Display sources
- if result['context']:
- from rich.table import Table
- from rich import box
-
- sources_table = Table(
- title=f"π Sources ({len(result['context'])} documents)",
- box=box.ROUNDED,
- show_header=True,
- header_style="bold magenta",
- title_style="bold magenta"
- )
- sources_table.add_column("#", style="cyan", width=4)
- sources_table.add_column("File", style="bold white")
- sources_table.add_column("Relevance", style="green", justify="right")
-
- for i, ctx in enumerate(result['context'], 1):
- uri_parts = ctx['uri'].split('/')
- filename = uri_parts[-1] if uri_parts else ctx['uri']
- score_text = Text(f"{ctx['score']:.4f}", style="bold green")
- sources_table.add_row(str(i), filename, score_text)
-
- console.print(sources_table)
- console.print()
-
- # Add to history
- self.session.add_turn(question, result['answer'], result['context'])
-
- return True
-
- except Exception as e:
- console.print(Panel(
- f"β Error: {e}",
- style="bold red",
- padding=(0, 1),
- width=PANEL_WIDTH
- ))
- console.print()
- return False
-```
-
-**Commit:** `"feat(chat): implement question/answer display with sources"`
-
----
-
-### Task 6: Implement Main REPL Loop
-
-**Files:**
-- Modify: `examples/chat/chat.py`
-
-**Requirements:**
-
-1. Replace the `pass` in `ChatREPL.run()` with:
-
-```python
-def run(self):
- """Main REPL loop"""
- # Initialize recipe
- try:
- self.recipe = Recipe(
- config_path=self.config_path,
- data_path=self.data_path
- )
- except Exception as e:
- console.print(Panel(
- f"β Error initializing: {e}",
- style="bold red",
- padding=(0, 1)
- ))
- return
-
- # Show welcome
- self._show_welcome()
-
- # Enable readline for input history
- try:
- import readline
- except ImportError:
- pass
-
- # Main loop
- try:
- while not self.should_exit:
- try:
- # Get user input
- user_input = console.input("[bold cyan]You:[/bold cyan] ").strip()
-
- # Skip empty input
- if not user_input:
- continue
-
- # Handle commands
- if user_input.startswith('/'):
- if self.handle_command(user_input):
- break
- continue
-
- # Ask question
- self.ask_question(user_input)
-
- except EOFError:
- # Ctrl-D pressed
- console.print("\n")
- console.print(Panel(
- "π Goodbye!",
- style="bold yellow",
- padding=(0, 1),
- width=PANEL_WIDTH
- ))
- break
-
- finally:
- # Cleanup
- if self.recipe:
- self.recipe.close()
-```
-
-2. Add main entry point at end of file:
-
-```python
-def main():
- """Main entry point"""
- import argparse
-
- parser = argparse.ArgumentParser(
- description="Multi-turn chat with OpenViking RAG",
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-Examples:
- # Start chat with default settings
- uv run chat.py
-
- # Adjust creativity
- uv run chat.py --temperature 0.9
-
- # Use more context
- uv run chat.py --top-k 10
-
- # Enable debug logging
- OV_DEBUG=1 uv run chat.py
- """
- )
-
- parser.add_argument('--config', type=str, default='./ov.conf', help='Path to config file')
- parser.add_argument('--data', type=str, default='./data', help='Path to data directory')
- parser.add_argument('--top-k', type=int, default=5, help='Number of search results')
- parser.add_argument('--temperature', type=float, default=0.7, help='LLM temperature 0.0-1.0')
- parser.add_argument('--max-tokens', type=int, default=2048, help='Max tokens to generate')
- parser.add_argument('--score-threshold', type=float, default=0.2, help='Min relevance score')
-
- args = parser.parse_args()
-
- # Validate arguments
- if not 0.0 <= args.temperature <= 1.0:
- console.print("β Temperature must be between 0.0 and 1.0", style="bold red")
- sys.exit(1)
-
- if args.top_k < 1:
- console.print("β top-k must be at least 1", style="bold red")
- sys.exit(1)
-
- if not 0.0 <= args.score_threshold <= 1.0:
- console.print("β score-threshold must be between 0.0 and 1.0", style="bold red")
- sys.exit(1)
-
- # Run chat
- repl = ChatREPL(
- config_path=args.config,
- data_path=args.data,
- temperature=args.temperature,
- max_tokens=args.max_tokens,
- top_k=args.top_k,
- score_threshold=args.score_threshold
- )
-
- repl.run()
-
-
-if __name__ == "__main__":
- main()
-```
-
-**Test:**
-```bash
-# Interactive test - manually verify:
-# Copy config: cp ../query/ov.conf ./ov.conf
-# Start: uv run chat.py
-# Test: ask question, /help, /exit
-```
-
-**Commit:** `"feat(chat): implement main REPL loop with readline support"`
-
----
-
-### Task 7: Add README Documentation
-
-**Files:**
-- Create: `examples/chat/README.md`
-
-**Content:** Create comprehensive README with:
-- Quick start (setup, config, start chat)
-- Features (multi-turn, sources, history, rich UI)
-- Usage (basic chat, commands, options)
-- Commands (/help, /clear, /exit, /quit, Ctrl-C, Ctrl-D)
-- Configuration (ov.conf structure)
-- Architecture (ChatSession, ChatREPL, Recipe)
-- Tips and troubleshooting
-
-**Commit:** `"docs(chat): add comprehensive README with usage examples"`
-
----
-
-### Task 8: Manual Testing and Verification
-
-**Requirements:**
-
-1. Verify directory structure: `ls -la examples/chat`
-2. Test functionality:
- - Welcome banner displays
- - `/help` command
- - `/clear` command
- - Ask question β answer + sources
- - Follow-up question
- - Arrow keys for history
- - `/exit`, Ctrl-C, Ctrl-D
-3. Test error handling (missing config)
-4. Test command line options (`--help`, `--temperature`)
-
-**Deliverable:** Create `examples/chat/TESTING.md` with checklist and results
-
-**Commit:** `"test(chat): add manual test results"`
-
----
-
-### Task 9: Final Integration and Handoff Prep
-
-**Files:**
-- Create: `examples/chat/HANDOFF.md`
-
-**Content:**
-- Phase 1 summary (what works)
-- Architecture overview
-- Phase 2 requirements (session persistence with OpenViking Session API)
-- Technical notes for Session API integration
-- Implementation strategy for Phase 2
-- Success criteria
-- Files to reference
-
-**Commits:**
-1. `"docs(chat): add Phase 2 handoff document"`
-2. `"feat(chat): Phase 1 complete - multi-turn chat interface"` (final summary commit)
-
----
-
-## Important Notes
-
-### YAGNI Principle
-- Phase 1 is simple, in-memory only
-- Don't over-engineer
-- Phase 2 will add OpenViking Session API (different architecture)
-- Keep code focused on current requirements
-
-### Data Directory
-- `../query/data` may not exist yet - that's OK
-- Data is created at runtime when users add documents
-- Recipe will use `./data` path (can be configured)
-
-### Review Standards
-- **Spec compliance:** Must match specification exactly
-- **Code quality:** Functional, readable, maintainable
-- **Balance:** Don't over-engineer for Phase 1, but maintain quality
-
-### After All Tasks Complete
-1. Run final code reviewer for entire implementation
-2. Use `superpowers:finishing-a-development-branch` skill
-3. Create PR or merge as appropriate
-
----
-
-## Task List Reference
-
-Use `TaskUpdate` to track progress:
-- Task #1: β
Complete (Directory structure)
-- Task #2: β
Complete (ChatSession class)
-- Task #3: π Implement basic REPL structure
-- Task #4: π Implement welcome banner and help
-- Task #5: π Implement question/answer display
-- Task #6: π Implement main REPL loop
-- Task #7: π Add README documentation
-- Task #8: π Manual testing and verification
-- Task #9: π Final integration and handoff prep
-
----
-
-## Quick Start for Next Agent
-
-```
-1. Read this file completely
-2. Navigate to: cd /Users/bytedance/code/OpenViking/.worktrees/chat-examples/examples/chat
-3. Verify current state: git log --oneline -3
-4. Start Task 3 with subagent-driven-development approach
-5. Follow the process for each task 3-9
-6. Complete with finishing-a-development-branch
-```
-
-Good luck! π
diff --git a/examples/chat/ov.conf.example b/examples/chat/ov.conf.example
deleted file mode 100644
index 2e9a40a..0000000
--- a/examples/chat/ov.conf.example
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "embedding": {
- "dense": {
- "api_base" : "https://ark-cn-beijing.bytedance.net/api/v3",
- "api_key" : "not_gonna_give_u_this",
- "backend" : "volcengine",
- "dimension": "1024",
- "model" : "doubao-embedding-vision-250615"
- }
- },
- "vlm": {
- "api_base" : "https://ark-cn-beijing.bytedance.net/api/v3",
- "api_key" : "not_gonna_give_u_this",
- "backend" : "volcengine",
- "model" : "doubao-seed-1-8-251228"
- }
-}
diff --git a/examples/chat/pyproject.toml b/examples/chat/pyproject.toml
deleted file mode 100644
index 7b7e747..0000000
--- a/examples/chat/pyproject.toml
+++ /dev/null
@@ -1,11 +0,0 @@
-[project]
-name = "chat"
-version = "0.1.0"
-description = "Multi-turn chat interface for OpenViking"
-readme = "README.md"
-requires-python = ">=3.13"
-dependencies = [
- "openviking>=0.1.6",
- "prompt-toolkit>=3.0.52",
- "rich>=13.0.0",
-]
diff --git a/examples/chatmem/README.md b/examples/chatmem/README.md
index 25559cf..a6432bd 100644
--- a/examples/chatmem/README.md
+++ b/examples/chatmem/README.md
@@ -1,16 +1,18 @@
-# OpenViking Chat with Persistent Memory
+# OpenViking Chat with Memory
Interactive chat interface with memory that persists across sessions using OpenViking's Session API.
+
+
+
## Features
-- π **Multi-turn conversations** - Natural follow-up questions
-- πΎ **Persistent memory** - Conversations saved and resumed
-- β¨ **Memory extraction** - Automatic long-term memory creation
-- π **Source attribution** - See which documents informed answers
-- β¨οΈ **Command history** - Use β/β arrows to navigate
-- π¨ **Rich UI** - Beautiful terminal interface
-- π‘οΈ **Graceful exit** - Ctrl-C or /exit saves session
+- π **Multi-turn conversations**
+- πΎ **Persistent memory**
+- β¨ **Memory extraction**
+- π **Source attribution**
+- π¨ **Rich UI**
+- π‘οΈ **Graceful exit**
## Quick Start
@@ -20,11 +22,11 @@ cd examples/chatmem
uv sync
# 1. Configure (copy from query example or create new)
-cp ../query/ov.conf ./ov.conf
+vi ./ov.conf
# Edit ov.conf with your API keys
# 2. Start chatting
-uv run chat.py
+uv run chatmem.py
```
## How Memory Works
@@ -59,7 +61,7 @@ When you exit (Ctrl-C or /exit), the session:
Next time you run with the same session ID:
```bash
-uv run chat.py --session-id my-project
+uv run chatmem.py --session-id my-project
```
You'll see:
@@ -74,7 +76,7 @@ The AI remembers your previous conversation context!
### Basic Chat
```bash
-uv run chat.py
+uv run chatmem.py
```
**First run:**
@@ -105,39 +107,65 @@ You: Can you give me more examples?
- `Ctrl-C` - Save and exit gracefully
- `Ctrl-D` - Exit
-### Session Management
+#### /time - Performance Timing
+
+Display performance metrics for your queries:
```bash
-# Use default session
-uv run chat.py
+You: /time what is retrieval augmented generation?
-# Use project-specific session
-uv run chat.py --session-id my-project
+β
Roger That
+...answer...
-# Use date-based session
-uv run chat.py --session-id $(date +%Y-%m-%d)
+π Sources (3 documents)
+...sources...
+
+β±οΈ Performance
+βββββββββββββββββββ¬ββββββββββ
+β Search β 0.234s β
+β LLM Generation β 1.567s β
+β Total β 1.801s β
+βββββββββββββββββββ΄ββββββββββ
```
-### Options
+#### /add_resource - Add Documents During Chat
+
+Add documents or URLs to your database without exiting:
```bash
-# Adjust creativity
-uv run chat.py --temperature 0.9
+You: /add_resource ~/Downloads/paper.pdf
+
+π Adding resource: /Users/you/Downloads/paper.pdf
+β Resource added
+β³ Processing and indexing...
+β Processing complete!
+π Resource is now searchable!
+
+You: what does the paper say about transformers?
+```
+
+Supports:
+- Local files: `/add_resource ~/docs/file.pdf`
+- URLs: `/add_resource https://example.com/doc.md`
+- Directories: `/add_resource ~/research/`
-# Use more context
-uv run chat.py --top-k 10
+### Session Management
-# Stricter relevance
-uv run chat.py --score-threshold 0.3
+```bash
+# Use default session
+uv run chatmem.py
-# All options
-uv run chat.py --help
+# Use project-specific session
+uv run chatmem.py --session-id my-project
+
+# Use date-based session
+uv run chatmem.py --session-id $(date +%Y-%m-%d)
```
### Debug Mode
```bash
-OV_DEBUG=1 uv run chat.py
+OV_DEBUG=1 uv run chatmem.py
```
## Configuration
@@ -187,33 +215,6 @@ On Exit: session.commit()
Memories Extracted & Persisted
```
-## Comparison with examples/chat/
-
-| Feature | examples/chat/ | examples/chatmem/ |
-|---------|---------------|-------------------|
-| Multi-turn | β
| β
|
-| Persistent memory | β | β
|
-| Memory extraction | {β | β
|
-| Session management | β | β
|
-| Cross-run memory | β | β
|
-
-Use `examples/chat/` for:
-- Quick one-off conversations
-- Testing without persistence
-- Simple prototyping
-
-Use `examples/chatmem/` for:
-- Long-term projects
-- Conversations spanning multiple sessions
-- Building up knowledge base over time
-
-## Tips
-
-- **Organize by project:** Use `--session-id project-name` for different contexts
-- **Date-based sessions:** `--session-id $(date +%Y-%m-%d)` for daily logs
-- **Clear screen, keep memory:** Use `/clear` to clean display without losing history
-- **Check session files:** Look in `data/session/` to see what's stored
-
## Troubleshooting
**"Error initializing"**
@@ -221,7 +222,7 @@ Use `examples/chatmem/` for:
- Ensure `data/` directory is writable
**"No relevant sources found"**
-- Add documents using `../query/add.py`
+- Add documents using `/add_resource`
- Lower `--score-threshold` value
- Try rephrasing your question
@@ -262,9 +263,3 @@ ls data/memory/
tar -czf sessions-backup-$(date +%Y%m%d).tar.gz data/session/
```
-## Next Steps
-
-- Build on this for domain-specific assistants
-- Add session search to find relevant past conversations
-- Implement session export/import for sharing
-- Create session analytics dashboards
diff --git a/examples/chatmem/chatmem.py b/examples/chatmem/chatmem.py
index 9e446b6..ec0f7ec 100644
--- a/examples/chatmem/chatmem.py
+++ b/examples/chatmem/chatmem.py
@@ -15,6 +15,7 @@
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import threading
+import common.boring_logging_config # noqa: F401
from common.recipe import Recipe
from prompt_toolkit import prompt
from prompt_toolkit.formatted_text import HTML
@@ -128,13 +129,15 @@ def _show_help(self):
help_text = Text()
help_text.append("Available Commands:\n\n", style="bold cyan")
help_text.append("/help", style="bold yellow")
- help_text.append(" - Show this help message\n", style="white")
+ help_text.append(" - Show this help message\n", style="white")
help_text.append("/clear", style="bold yellow")
- help_text.append(" - Clear screen (keeps history)\n", style="white")
- help_text.append("/exit", style="bold yellow")
- help_text.append(" - Exit chat\n", style="white")
- help_text.append("/quit", style="bold yellow")
- help_text.append(" - Exit chat\n", style="white")
+ help_text.append(" - Clear screen (keeps history)\n", style="white")
+ help_text.append("/time ", style="bold yellow")
+ help_text.append(" - Ask question and show performance timing\n", style="white")
+ help_text.append("/add_resource ", style="bold yellow")
+ help_text.append(" - Add file/URL to database\n", style="white")
+ help_text.append("/exit or /quit", style="bold yellow")
+ help_text.append(" - Exit chat\n", style="white")
help_text.append("\nKeyboard Shortcuts:\n\n", style="bold cyan")
help_text.append("Ctrl-C", style="bold yellow")
help_text.append(" - Exit gracefully\n", style="white")
@@ -158,18 +161,68 @@ def handle_command(self, cmd: str) -> bool:
Returns:
True if should exit, False otherwise
"""
- cmd = cmd.strip().lower()
+ cmd_lower = cmd.strip().lower()
- if cmd in ["/exit", "/quit"]:
+ if cmd_lower in ["/exit", "/quit"]:
console.print(
Panel("π Goodbye!", style="bold yellow", padding=(0, 1), width=PANEL_WIDTH)
)
return True
- elif cmd == "/help":
+ elif cmd_lower == "/help":
self._show_help()
- elif cmd == "/clear":
+ elif cmd_lower == "/clear":
console.clear()
self._show_welcome()
+ elif cmd.strip().startswith("/time"):
+ # Extract question from command
+ question = cmd.strip()[5:].strip() # Remove "/time" prefix
+
+ if not question:
+ console.print("Usage: /time ", style="yellow")
+ console.print("Example: /time what is prompt engineering?", style="dim")
+ console.print()
+ else:
+ self.ask_question(question, show_timing=True)
+ elif cmd.strip().startswith("/add_resource"):
+ # Extract resource path from command
+ resource_path = cmd.strip()[13:].strip() # Remove "/add_resource" prefix
+
+ if not resource_path:
+ console.print("Usage: /add_resource ", style="yellow")
+ console.print("Examples:", style="dim")
+ console.print(" /add_resource ~/Downloads/paper.pdf", style="dim")
+ console.print(" /add_resource https://example.com/doc.md", style="dim")
+ console.print()
+ else:
+ # Import at usage time to avoid circular imports
+ import os
+ import sys
+ from pathlib import Path
+
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+ from common.resource_manager import add_resource
+
+ # Expand user path
+ if not resource_path.startswith("http"):
+ resource_path = str(Path(resource_path).expanduser())
+
+ # Add resource with spinner
+ success = show_loading_with_spinner(
+ "Adding resource...",
+ add_resource,
+ client=self.client,
+ resource_path=resource_path,
+ console=console,
+ show_output=True,
+ )
+
+ if success:
+ console.print()
+ console.print(
+ "π‘ You can now ask questions about this resource!", style="dim green"
+ )
+
+ console.print()
else:
console.print(f"Unknown command: {cmd}", style="red")
console.print("Type /help for available commands", style="dim")
@@ -177,7 +230,7 @@ def handle_command(self, cmd: str) -> bool:
return False
- def ask_question(self, question: str) -> bool:
+ def ask_question(self, question: str, show_timing: bool = False) -> bool:
"""Ask a question and display of answer"""
# Record user message to session
@@ -239,7 +292,32 @@ def ask_question(self, question: str) -> bool:
sources_table.add_row(str(i), filename, score_text)
console.print(sources_table)
- console.print()
+ console.print()
+
+ # Display timing panel if requested
+ if show_timing and "timings" in result:
+ from rich.table import Table
+
+ timings = result["timings"]
+
+ timing_table = Table(show_header=False, box=None, padding=(0, 2))
+ timing_table.add_column("Metric", style="cyan")
+ timing_table.add_column("Time", style="bold green", justify="right")
+
+ timing_table.add_row("Search", f"{timings['search_time']:.3f}s")
+ timing_table.add_row("LLM Generation", f"{timings['llm_time']:.3f}s")
+ timing_table.add_row("Total", f"{timings['total_time']:.3f}s")
+
+ console.print(
+ Panel(
+ timing_table,
+ title="β±οΈ Performance",
+ style="bold blue",
+ padding=(0, 1),
+ width=PANEL_WIDTH,
+ )
+ )
+ console.print()
return True
@@ -338,16 +416,13 @@ def main():
epilog="""
Examples:
# Start chat with default session
- uv run chat.py
+ uv run chatmem.py
# Use custom session ID
- uv run chat.py --session-id my-project
-
- # Adjust creativity
- uv run chat.py --temperature 0.9
+ uv run chatmem.py --session-id my-project
# Enable debug logging
- OV_DEBUG=1 uv run chat.py
+ OV_DEBUG=1 uv run chatmem.py
""",
)
diff --git a/examples/chatmem/docs/plans/2026-02-05-time-and-add-resource-commands-design.md b/examples/chatmem/docs/plans/2026-02-05-time-and-add-resource-commands-design.md
new file mode 100644
index 0000000..d663206
--- /dev/null
+++ b/examples/chatmem/docs/plans/2026-02-05-time-and-add-resource-commands-design.md
@@ -0,0 +1,567 @@
+# Design: /time and /add_resource Commands
+
+**Date:** 2026-02-05
+**Status:** Approved
+**Author:** AI Assistant with User Input
+
+## Overview
+
+This design document describes the implementation of two new features for the chatmem application:
+
+1. **`/time` command** - Display performance timing breakdown (search time, LLM generation time)
+2. **`/add_resource` command** - Add documents/URLs to the database during chat sessions
+
+Both features integrate seamlessly into the existing chatmem REPL interface.
+
+## Requirements
+
+### Feature 1: /time Command
+
+- **Usage:** `/time ` - Ask a question and show timing information
+- **Display:** Show dedicated timing panel after answer with breakdown:
+ - Search time (semantic search duration)
+ - LLM generation time (API call duration)
+ - Total time (end-to-end duration)
+- **Behavior:** Only show timing when explicitly requested (keeps UI clean by default)
+
+### Feature 2: /add_resource Command
+
+- **Usage:** `/add_resource ` - Add a resource to the database
+- **Location:** Shared utility in `common/` package + command handler in `chatmem.py`
+- **Behavior:** Block and wait with spinner until resource is fully processed and indexed
+- **Benefits:**
+ - In-chat resource management (no need to exit and run separate script)
+ - Reusable across multiple scripts
+ - Consistent with existing `add.py` behavior
+
+## Architecture
+
+### High-Level Design
+
+```
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β chatmem.py β
+β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
+β β handle_command(cmd: str) β β
+β β - /help, /clear, /exit (existing) β β
+β β - /time (NEW) β β
+β β - /add_resource (NEW) β β
+β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
+β β β
+β βΌ β
+β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
+β β ask_question(question, show_timing=False) β β
+β β - Calls Recipe.query() β β
+β β - Displays answer + sources β β
+β β - Displays timing panel (if show_timing=True) β β
+β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ β
+ ββββββββββββββββββββ΄ββββββββββββββββββ
+ βΌ βΌ
+βββββββββββββββββββββββ βββββββββββββββββββββββββββ
+β common/recipe.py β β common/resource_mgr.py β
+β β β (NEW) β
+β query() method: β β β
+β - Track search timeβ β - create_client() β
+β - Track LLM time β β - add_resource() β
+β - Return timings β β β
+βββββββββββββββββββββββ βββββββββββββββββββββββββββ
+ β²
+ β
+ βββββββββ΄βββββββββ
+ β add.py β
+ β (refactored) β
+ ββββββββββββββββββ
+```
+
+### Key Components
+
+1. **Timing Instrumentation** (`common/recipe.py`)
+ - Uses `time.perf_counter()` for high-precision timing
+ - Tracks three metrics: search, LLM, total
+ - Returns timing data in result dictionary
+
+2. **Resource Manager** (`common/resource_manager.py`)
+ - Extracts core logic from `add.py`
+ - Reusable functions for client creation and resource addition
+ - Consistent error handling and user feedback
+
+3. **Command Handlers** (`chatmem.py`)
+ - Extends existing command system
+ - Integrates timing display
+ - Reuses existing OpenViking client for resource addition
+
+## Detailed Design
+
+### 1. Timing Implementation
+
+#### Recipe.query() Modifications
+
+Add timing instrumentation to track three phases:
+
+```python
+def query(self, user_query: str, ...) -> Dict[str, Any]:
+ import time
+
+ # Track total time
+ start_total = time.perf_counter()
+
+ # Step 1: Search (timed)
+ start_search = time.perf_counter()
+ search_results = self.search(user_query, ...)
+ search_time = time.perf_counter() - start_search
+
+ # Step 2: Build context (not separately timed)
+ context_text = ...
+ messages = ...
+
+ # Step 3: LLM call (timed)
+ start_llm = time.perf_counter()
+ answer = self.call_llm(messages, ...)
+ llm_time = time.perf_counter() - start_llm
+
+ total_time = time.perf_counter() - start_total
+
+ return {
+ "answer": answer,
+ "context": search_results,
+ "query": user_query,
+ "prompt": current_prompt,
+ "timings": { # NEW
+ "search_time": search_time,
+ "llm_time": llm_time,
+ "total_time": total_time
+ }
+ }
+```
+
+#### ChatREPL.ask_question() Modifications
+
+Add optional `show_timing` parameter:
+
+```python
+def ask_question(self, question: str, show_timing: bool = False) -> bool:
+ # ... existing code to call recipe.query() ...
+
+ # Display answer and sources (existing)
+ console.print(Panel(answer_text, ...))
+ console.print(sources_table)
+
+ # Display timing panel (NEW)
+ if show_timing and "timings" in result:
+ timings = result["timings"]
+
+ timing_table = Table(show_header=False, box=None)
+ timing_table.add_column("Metric", style="cyan")
+ timing_table.add_column("Time", style="bold green", justify="right")
+
+ timing_table.add_row("Search", f"{timings['search_time']:.3f}s")
+ timing_table.add_row("LLM Generation", f"{timings['llm_time']:.3f}s")
+ timing_table.add_row("Total", f"{timings['total_time']:.3f}s")
+
+ console.print(Panel(
+ timing_table,
+ title="β±οΈ Performance",
+ style="bold blue",
+ padding=(0, 1),
+ width=PANEL_WIDTH
+ ))
+
+ return True
+```
+
+#### Command Handler
+
+Add `/time` command to `handle_command()`:
+
+```python
+def handle_command(self, cmd: str) -> bool:
+ # ... existing commands ...
+
+ elif cmd.startswith("/time"):
+ # Extract question from command
+ question = cmd[5:].strip() # Remove "/time" prefix
+
+ if not question:
+ console.print("Usage: /time ", style="yellow")
+ console.print("Example: /time what is prompt engineering?", style="dim")
+ return False
+
+ # Ask question with timing enabled
+ self.ask_question(question, show_timing=True)
+ return False
+```
+
+### 2. Resource Manager Implementation
+
+#### New File: common/resource_manager.py
+
+```python
+#!/usr/bin/env python3
+"""
+Resource Manager - Shared utilities for adding resources to OpenViking
+"""
+
+import json
+from pathlib import Path
+from typing import Optional
+
+import openviking as ov
+from openviking.utils.config.open_viking_config import OpenVikingConfig
+from rich.console import Console
+
+
+def create_client(
+ config_path: str = "./ov.conf",
+ data_path: str = "./data"
+) -> ov.SyncOpenViking:
+ """
+ Create and initialize OpenViking client
+
+ Args:
+ config_path: Path to config file
+ data_path: Path to data directory
+
+ Returns:
+ Initialized SyncOpenViking client
+ """
+ with open(config_path, "r") as f:
+ config_dict = json.load(f)
+
+ config = OpenVikingConfig.from_dict(config_dict)
+ client = ov.SyncOpenViking(path=data_path, config=config)
+ client.initialize()
+
+ return client
+
+
+def add_resource(
+ client: ov.SyncOpenViking,
+ resource_path: str,
+ console: Optional[Console] = None,
+ show_output: bool = True
+) -> bool:
+ """
+ Add a resource to OpenViking database
+
+ Args:
+ client: Initialized SyncOpenViking client
+ resource_path: Path to file/directory or URL
+ console: Rich Console for output (creates new if None)
+ show_output: Whether to print status messages
+
+ Returns:
+ True if successful, False otherwise
+ """
+ if console is None:
+ console = Console()
+
+ try:
+ if show_output:
+ console.print(f"π Adding resource: {resource_path}")
+
+ # Validate file path (if not URL)
+ if not resource_path.startswith("http"):
+ path = Path(resource_path).expanduser()
+ if not path.exists():
+ if show_output:
+ console.print(f"β Error: File not found: {path}", style="red")
+ return False
+
+ # Add resource
+ result = client.add_resource(path=resource_path)
+
+ # Check result
+ if result and "root_uri" in result:
+ root_uri = result["root_uri"]
+ if show_output:
+ console.print(f"β Resource added: {root_uri}")
+
+ # Wait for processing
+ if show_output:
+ console.print("β³ Processing and indexing...")
+ client.wait_processed()
+
+ if show_output:
+ console.print("β Processing complete!")
+ console.print("π Resource is now searchable!", style="bold green")
+
+ return True
+
+ elif result and result.get("status") == "error":
+ if show_output:
+ console.print("β οΈ Resource had parsing issues:", style="yellow")
+ if "errors" in result:
+ for error in result["errors"][:3]:
+ console.print(f" - {error}")
+ console.print("π‘ Some content may still be searchable.")
+ return False
+
+ else:
+ if show_output:
+ console.print("β Failed to add resource", style="red")
+ return False
+
+ except Exception as e:
+ if show_output:
+ console.print(f"β Error: {e}", style="red")
+ return False
+```
+
+#### Refactor add.py
+
+Simplify `add.py` to use the shared module:
+
+```python
+#!/usr/bin/env python3
+"""
+Add Resource - CLI tool to add documents to OpenViking database
+"""
+
+import argparse
+import sys
+from pathlib import Path
+
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+from common.resource_manager import create_client, add_resource
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Add documents, PDFs, or URLs to OpenViking database",
+ # ... existing epilog ...
+ )
+
+ parser.add_argument("resource", type=str, help="Path to file/directory or URL")
+ parser.add_argument("--config", type=str, default="./ov.conf")
+ parser.add_argument("--data", type=str, default="./data")
+
+ args = parser.parse_args()
+
+ # Expand user paths
+ resource_path = (
+ str(Path(args.resource).expanduser())
+ if not args.resource.startswith("http")
+ else args.resource
+ )
+
+ # Create client and add resource
+ try:
+ client = create_client(args.config, args.data)
+ success = add_resource(client, resource_path)
+ client.close()
+ sys.exit(0 if success else 1)
+ except Exception as e:
+ print(f"β Error: {e}")
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()
+```
+
+#### Add Command Handler in chatmem.py
+
+```python
+def handle_command(self, cmd: str) -> bool:
+ # ... existing commands ...
+
+ elif cmd.startswith("/add_resource"):
+ # Extract resource path from command
+ resource_path = cmd[13:].strip() # Remove "/add_resource" prefix
+
+ if not resource_path:
+ console.print("Usage: /add_resource ", style="yellow")
+ console.print("Examples:", style="dim")
+ console.print(" /add_resource ~/Downloads/paper.pdf", style="dim")
+ console.print(" /add_resource https://example.com/doc.md", style="dim")
+ return False
+
+ # Expand user path
+ if not resource_path.startswith("http"):
+ resource_path = str(Path(resource_path).expanduser())
+
+ # Import resource manager
+ from common.resource_manager import add_resource
+
+ # Add resource with spinner
+ success = show_loading_with_spinner(
+ "Adding resource...",
+ add_resource,
+ client=self.client,
+ resource_path=resource_path,
+ console=console,
+ show_output=True
+ )
+
+ if success:
+ console.print()
+ console.print("π‘ You can now ask questions about this resource!", style="dim")
+
+ console.print()
+ return False
+```
+
+## Error Handling
+
+### /time Command Errors
+
+| Error | Handling |
+|-------|----------|
+| Empty question | Show usage message with example |
+| Query fails | Show error panel, no timing displayed |
+| Timing data missing | Show answer without timing (graceful degradation) |
+
+### /add_resource Command Errors
+
+| Error | Handling |
+|-------|----------|
+| No path provided | Show usage with examples |
+| File not found | Show error panel with full path |
+| Invalid URL | Show error from underlying library |
+| Processing fails | Show error with details |
+| Already added | OpenViking handles deduplication (no error) |
+
+## Testing Strategy
+
+### Manual Testing
+
+**Test /time command:**
+```bash
+# Start chat
+uv run chatmem.py
+
+# Test normal query (no timing)
+You: what is RAG?
+
+# Test with timing
+You: /time what is RAG?
+
+# Test empty question
+You: /time
+
+# Test with complex question
+You: /time explain chain of thought prompting in detail
+```
+
+**Test /add_resource command:**
+```bash
+# Start chat
+uv run chatmem.py
+
+# Test adding local file
+You: /add_resource ~/Downloads/paper.pdf
+
+# Test adding URL
+You: /add_resource https://raw.githubusercontent.com/example/README.md
+
+# Test file not found
+You: /add_resource /nonexistent/file.pdf
+
+# Test empty path
+You: /add_resource
+
+# Verify resource is searchable
+You: what does the paper say?
+```
+
+### Edge Cases
+
+1. **Large files** - Ensure spinner shows during long processing
+2. **Network failures** - URL downloads should show clear error
+3. **Concurrent adds** - Multiple `/add_resource` calls in sequence
+4. **Timing precision** - Very fast queries (< 0.1s) should still show accurate timing
+
+## Implementation Plan
+
+### Phase 1: Timing Feature
+1. Add timing instrumentation to `Recipe.query()`
+2. Add timing display to `ChatREPL.ask_question()`
+3. Add `/time` command handler
+4. Test with various queries
+
+### Phase 2: Resource Manager
+1. Create `common/resource_manager.py` with shared functions
+2. Refactor `add.py` to use shared module
+3. Test standalone `add.py` still works
+
+### Phase 3: Chat Integration
+1. Add `/add_resource` command handler to `chatmem.py`
+2. Update help text to include new commands
+3. Test in-chat resource addition
+4. Test that added resources are immediately searchable
+
+## Files Modified
+
+| File | Changes | Lines Changed |
+|------|---------|---------------|
+| `common/resource_manager.py` | NEW | ~80 lines |
+| `common/recipe.py` | Add timing instrumentation | ~15 lines |
+| `chatmem.py` | Add command handlers, timing display | ~60 lines |
+| `add.py` | Refactor to use shared module | ~30 lines (simplified) |
+
+**Total:** ~185 lines of new/modified code
+
+## Future Enhancements
+
+- Add `/time toggle` to enable persistent timing display
+- Color-code timing values (green < 1s, yellow < 3s, red >= 3s)
+- Add `/list_resources` command to show all indexed resources
+- Add `/remove_resource` command to remove resources
+- Export timing data to CSV for performance analysis
+
+## Appendix: User Experience Examples
+
+### Example 1: Using /time
+
+```
+You: /time what is retrieval augmented generation?
+
+β
Roger That
+ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β what is retrieval augmented generation? β
+ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+π Check This Out
+ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β Retrieval Augmented Generation (RAG) is a β
+β technique that combines information retrieval... β
+ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+π Sources (3 documents)
+βββββ¬βββββββββββββββββββ¬ββββββββββββ
+β # β File β Relevance β
+βββββΌβββββββββββββββββββΌββββββββββββ€
+β 1 β rag_intro.md β 0.8234 β
+β 2 β llm_patterns.md β 0.7456 β
+β 3 β architecture.md β 0.6892 β
+βββββ΄βββββββββββββββββββ΄ββββββββββββ
+
+β±οΈ Performance
+βββββββββββββββββββ¬ββββββββββ
+β Search β 0.234s β
+β LLM Generation β 1.567s β
+β Total β 1.801s β
+βββββββββββββββββββ΄ββββββββββ
+
+You:
+```
+
+### Example 2: Using /add_resource
+
+```
+You: /add_resource ~/Downloads/transformer_paper.pdf
+
+π Adding resource: /Users/user/Downloads/transformer_paper.pdf
+β Resource added: file:///transformer_paper.pdf
+β³ Processing and indexing...
+β Processing complete!
+π Resource is now searchable!
+
+π‘ You can now ask questions about this resource!
+
+You: what is the attention mechanism in the paper?
+
+... (answer based on newly added paper) ...
+```
diff --git a/examples/chatmem/docs/plans/2026-02-05-time-and-add-resource-implementation.md b/examples/chatmem/docs/plans/2026-02-05-time-and-add-resource-implementation.md
new file mode 100644
index 0000000..cd7f65e
--- /dev/null
+++ b/examples/chatmem/docs/plans/2026-02-05-time-and-add-resource-implementation.md
@@ -0,0 +1,1024 @@
+# /time and /add_resource Commands Implementation Plan
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+**Goal:** Add `/time` command to display performance metrics and `/add_resource` command to add documents during chat sessions.
+
+**Architecture:** Instrument Recipe class with timing, extract reusable resource management logic to common package, add command handlers to ChatREPL.
+
+**Tech Stack:** Python 3.13, OpenViking SDK, Rich (terminal UI), time.perf_counter()
+
+---
+
+## Task 1: Create Resource Manager Module
+
+**Files:**
+- Create: `../common/resource_manager.py`
+
+**Step 1: Create resource manager with client creation**
+
+Create the file with imports and client creation function:
+
+```python
+#!/usr/bin/env python3
+"""
+Resource Manager - Shared utilities for adding resources to OpenViking
+"""
+
+import json
+from pathlib import Path
+from typing import Optional
+
+import openviking as ov
+from openviking.utils.config.open_viking_config import OpenVikingConfig
+from rich.console import Console
+
+
+def create_client(
+ config_path: str = "./ov.conf",
+ data_path: str = "./data"
+) -> ov.SyncOpenViking:
+ """
+ Create and initialize OpenViking client
+
+ Args:
+ config_path: Path to config file
+ data_path: Path to data directory
+
+ Returns:
+ Initialized SyncOpenViking client
+ """
+ with open(config_path, "r") as f:
+ config_dict = json.load(f)
+
+ config = OpenVikingConfig.from_dict(config_dict)
+ client = ov.SyncOpenViking(path=data_path, config=config)
+ client.initialize()
+
+ return client
+```
+
+**Step 2: Add resource addition function**
+
+Add the main add_resource function to the same file:
+
+```python
+def add_resource(
+ client: ov.SyncOpenViking,
+ resource_path: str,
+ console: Optional[Console] = None,
+ show_output: bool = True
+) -> bool:
+ """
+ Add a resource to OpenViking database
+
+ Args:
+ client: Initialized SyncOpenViking client
+ resource_path: Path to file/directory or URL
+ console: Rich Console for output (creates new if None)
+ show_output: Whether to print status messages
+
+ Returns:
+ True if successful, False otherwise
+ """
+ if console is None:
+ console = Console()
+
+ try:
+ if show_output:
+ console.print(f"π Adding resource: {resource_path}")
+
+ # Validate file path (if not URL)
+ if not resource_path.startswith("http"):
+ path = Path(resource_path).expanduser()
+ if not path.exists():
+ if show_output:
+ console.print(f"β Error: File not found: {path}", style="red")
+ return False
+
+ # Add resource
+ result = client.add_resource(path=resource_path)
+
+ # Check result
+ if result and "root_uri" in result:
+ root_uri = result["root_uri"]
+ if show_output:
+ console.print(f"β Resource added: {root_uri}")
+
+ # Wait for processing
+ if show_output:
+ console.print("β³ Processing and indexing...")
+ client.wait_processed()
+
+ if show_output:
+ console.print("β Processing complete!")
+ console.print("π Resource is now searchable!", style="bold green")
+
+ return True
+
+ elif result and result.get("status") == "error":
+ if show_output:
+ console.print("β οΈ Resource had parsing issues:", style="yellow")
+ if "errors" in result:
+ for error in result["errors"][:3]:
+ console.print(f" - {error}")
+ console.print("π‘ Some content may still be searchable.")
+ return False
+
+ else:
+ if show_output:
+ console.print("β Failed to add resource", style="red")
+ return False
+
+ except Exception as e:
+ if show_output:
+ console.print(f"β Error: {e}", style="red")
+ return False
+```
+
+**Step 3: Test the module manually**
+
+Run: `cd /Users/bytedance/code/OpenViking/.worktrees/feature/time-and-add-resource-commands/examples/chatmem && uv run python -c "import sys; sys.path.insert(0, '../'); from common.resource_manager import create_client, add_resource; print('β Module imports successfully')"`
+
+Expected: `β Module imports successfully`
+
+**Step 4: Commit**
+
+```bash
+git add ../common/resource_manager.py
+git commit -m "feat: add resource manager shared module
+
+- create_client(): initialize OpenViking client
+- add_resource(): add files/URLs to database with progress
+- Extracted from add.py for reusability"
+```
+
+---
+
+## Task 2: Refactor add.py to Use Resource Manager
+
+**Files:**
+- Modify: `add.py`
+
+**Step 1: Simplify add.py to use resource manager**
+
+Replace the existing `add_resource` function and update imports:
+
+```python
+#!/usr/bin/env python3
+"""
+Add Resource - CLI tool to add documents to OpenViking database
+"""
+
+import argparse
+import os
+import sys
+from pathlib import Path
+
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+from common.resource_manager import create_client, add_resource
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Add documents, PDFs, or URLs to OpenViking database",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog="""
+Examples:
+ # Add a PDF file
+ uv run add.py ~/Downloads/document.pdf
+
+ # Add a URL
+ uv run add.py https://example.com/README.md
+
+ # Add with custom config and data paths
+ uv run add.py document.pdf --config ./my.conf --data ./mydata
+
+ # Add a directory
+ uv run add.py ~/Documents/research/
+
+ # Enable debug logging
+ OV_DEBUG=1 uv run add.py document.pdf
+
+Notes:
+ - Supported formats: PDF, Markdown, Text, HTML, and more
+ - URLs are automatically downloaded and processed
+ - Large files may take several minutes to process
+ - The resource becomes searchable after processing completes
+ """,
+ )
+
+ parser.add_argument(
+ "resource", type=str, help="Path to file/directory or URL to add to the database"
+ )
+
+ parser.add_argument(
+ "--config", type=str, default="./ov.conf", help="Path to config file (default: ./ov.conf)"
+ )
+
+ parser.add_argument(
+ "--data", type=str, default="./data", help="Path to data directory (default: ./data)"
+ )
+
+ args = parser.parse_args()
+
+ # Expand user paths
+ resource_path = (
+ str(Path(args.resource).expanduser())
+ if not args.resource.startswith("http")
+ else args.resource
+ )
+
+ # Create client and add resource
+ try:
+ print(f"π Loading config from: {args.config}")
+ client = create_client(args.config, args.data)
+
+ print("π Initializing OpenViking...")
+ print("β Initialized\n")
+
+ success = add_resource(client, resource_path)
+
+ client.close()
+ print("\nβ Done")
+ sys.exit(0 if success else 1)
+
+ except Exception as e:
+ print(f"\nβ Error: {e}")
+ import traceback
+ traceback.print_exc()
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()
+```
+
+**Step 2: Test add.py still works**
+
+Run: `cd /Users/bytedance/code/OpenViking/.worktrees/feature/time-and-add-resource-commands/examples/chatmem && uv run python add.py --help`
+
+Expected: Help text displays without errors
+
+**Step 3: Commit**
+
+```bash
+git add add.py
+git commit -m "refactor: simplify add.py using resource manager
+
+- Use shared create_client() and add_resource()
+- Reduces duplication, maintains same CLI behavior
+- ~80 lines reduced to ~40 lines"
+```
+
+---
+
+## Task 3: Add Timing Instrumentation to Recipe
+
+**Files:**
+- Modify: `../common/recipe.py:146-233`
+
+**Step 1: Import time module**
+
+Add import at the top of the file after existing imports:
+
+```python
+import time
+```
+
+**Step 2: Add timing to query method**
+
+Modify the `query` method to track timing (lines 146-233):
+
+```python
+def query(
+ self,
+ user_query: str,
+ search_top_k: int = 3,
+ temperature: float = 0.7,
+ max_tokens: int = 2048,
+ system_prompt: Optional[str] = None,
+ score_threshold: float = 0.2,
+ chat_history: Optional[List[Dict[str, str]]] = None,
+) -> Dict[str, Any]:
+ """
+ Full RAG pipeline: search β retrieve β generate
+
+ Args:
+ user_query: User's question
+ search_top_k: Number of search results to use as context
+ temperature: LLM sampling temperature
+ max_tokens: Maximum tokens to generate
+ system_prompt: Optional system prompt to prepend
+ score_threshold: Minimum relevance score for search results (default: 0.2)
+ chat_history: Optional list of previous conversation turns for multi-round chat.
+ Each turn should be a dict with 'role' and 'content' keys.
+ Example: [{"role": "user", "content": "previous question"},
+ {"role": "assistant", "content": "previous answer"}]
+
+ Returns:
+ Dictionary with answer, context, metadata, and timings
+ """
+ # Track total time
+ start_total = time.perf_counter()
+
+ # Step 1: Search for relevant content (timed)
+ start_search = time.perf_counter()
+ search_results = self.search(
+ user_query, top_k=search_top_k, score_threshold=score_threshold
+ )
+ search_time = time.perf_counter() - start_search
+
+ # Step 2: Build context from search results
+ context_text = "no relevant information found, try answer based on existing knowledge."
+ if search_results:
+ context_text = (
+ "Answer should pivoting to the following:\n\n"
+ + "\n\n".join(
+ [
+ f"[Source {i + 1}] (relevance: {r['score']:.4f})\n{r['content']}"
+ for i, r in enumerate(search_results)
+ ]
+ )
+ + "\n"
+ )
+
+ # Step 3: Build messages array for chat completion API
+ messages = []
+
+ # Add system message if provided
+ if system_prompt:
+ messages.append({"role": "system", "content": system_prompt})
+ else:
+ messages.append(
+ {
+ "role": "system",
+ "content": "Answer questions with plain text. avoid markdown special character",
+ }
+ )
+
+ # Add chat history if provided (for multi-round conversations)
+ if chat_history:
+ messages.extend(chat_history)
+
+ # Build current turn prompt with context and question
+ current_prompt = f"{context_text}\n"
+ current_prompt += f"Question: {user_query}\n\n"
+
+ # Add current user message
+ messages.append({"role": "user", "content": current_prompt})
+
+ # Step 4: Call LLM with messages array (timed)
+ start_llm = time.perf_counter()
+ answer = self.call_llm(messages, temperature=temperature, max_tokens=max_tokens)
+ llm_time = time.perf_counter() - start_llm
+
+ # Calculate total time
+ total_time = time.perf_counter() - start_total
+
+ # Return full result with timing data
+ return {
+ "answer": answer,
+ "context": search_results,
+ "query": user_query,
+ "prompt": current_prompt,
+ "timings": {
+ "search_time": search_time,
+ "llm_time": llm_time,
+ "total_time": total_time,
+ },
+ }
+```
+
+**Step 3: Test timing data is returned**
+
+Run: `cd /Users/bytedance/code/OpenViking/.worktrees/feature/time-and-add-resource-commands/examples/chatmem && uv run python -c "import sys; sys.path.insert(0, '../'); from common.recipe import Recipe; print('β Recipe with timing imports successfully')"`
+
+Expected: `β Recipe with timing imports successfully`
+
+**Step 4: Commit**
+
+```bash
+git add ../common/recipe.py
+git commit -m "feat: add timing instrumentation to Recipe.query()
+
+- Track search_time, llm_time, total_time with perf_counter
+- Return timing data in result dict under 'timings' key
+- No breaking changes to existing API"
+```
+
+---
+
+## Task 4: Add /time Command Handler
+
+**Files:**
+- Modify: `chatmem.py:151-178`
+
+**Step 1: Update handle_command to support /time**
+
+Modify the `handle_command` method to add `/time` support (around line 151):
+
+```python
+def handle_command(self, cmd: str) -> bool:
+ """
+ Handle slash commands
+
+ Args:
+ cmd: Command string (e.g., "/help")
+
+ Returns:
+ True if should exit, False otherwise
+ """
+ cmd_lower = cmd.strip().lower()
+
+ if cmd_lower in ["/exit", "/quit"]:
+ console.print(
+ Panel("π Goodbye!", style="bold yellow", padding=(0, 1), width=PANEL_WIDTH)
+ )
+ return True
+ elif cmd_lower == "/help":
+ self._show_help()
+ elif cmd_lower == "/clear":
+ console.clear()
+ self._show_welcome()
+ elif cmd.strip().startswith("/time"):
+ # Extract question from command
+ question = cmd.strip()[5:].strip() # Remove "/time" prefix
+
+ if not question:
+ console.print("Usage: /time ", style="yellow")
+ console.print("Example: /time what is prompt engineering?", style="dim")
+ console.print()
+ else:
+ self.ask_question(question, show_timing=True)
+ else:
+ console.print(f"Unknown command: {cmd}", style="red")
+ console.print("Type /help for available commands", style="dim")
+ console.print()
+
+ return False
+```
+
+**Step 2: Commit**
+
+```bash
+git add chatmem.py
+git commit -m "feat: add /time command handler
+
+- Parse /time syntax
+- Extract question and call ask_question with show_timing=True
+- Show usage help if no question provided"
+```
+
+---
+
+## Task 5: Add Timing Display to ask_question
+
+**Files:**
+- Modify: `chatmem.py:180-251`
+
+**Step 1: Update ask_question signature and add timing display**
+
+Modify the `ask_question` method to accept `show_timing` parameter and display timing panel (lines 180-251):
+
+```python
+def ask_question(self, question: str, show_timing: bool = False) -> bool:
+ """Ask a question and display answer"""
+
+ # Record user message to session
+ self.session.add_message("user", [TextPart(question)])
+
+ try:
+ # Convert session messages to chat history format for Recipe
+ chat_history = []
+ for msg in self.session.messages:
+ if msg.role in ["user", "assistant"]:
+ content = msg.content if hasattr(msg, "content") else ""
+ chat_history.append({"role": msg.role, "content": content})
+
+ result = show_loading_with_spinner(
+ "Thinking...",
+ self.recipe.query,
+ user_query=question,
+ search_top_k=self.top_k,
+ temperature=self.temperature,
+ max_tokens=self.max_tokens,
+ score_threshold=self.score_threshold,
+ chat_history=chat_history,
+ )
+
+ # Record assistant message to session
+ self.session.add_message("assistant", [TextPart(result["answer"])])
+
+ answer_text = Text(result["answer"], style="white")
+ console.print(
+ Panel(
+ answer_text,
+ title="π‘ Answer",
+ style="bold bright_cyan",
+ padding=(1, 1),
+ width=PANEL_WIDTH,
+ )
+ )
+ console.print()
+
+ if result["context"]:
+ from rich import box
+ from rich.table import Table
+
+ sources_table = Table(
+ title=f"π Sources ({len(result['context'])} documents)",
+ box=box.ROUNDED,
+ show_header=True,
+ header_style="bold magenta",
+ title_style="bold magenta",
+ )
+ sources_table.add_column("#", style="cyan", width=4)
+ sources_table.add_column("File", style="bold white")
+ sources_table.add_column("Relevance", style="green", justify="right")
+
+ for i, ctx in enumerate(result["context"], 1):
+ uri_parts = ctx["uri"].split("/")
+ filename = uri_parts[-1] if uri_parts else ctx["uri"]
+ score_text = Text(f"{ctx['score']:.4f}", style="bold green")
+ sources_table.add_row(str(i), filename, score_text)
+
+ console.print(sources_table)
+ console.print()
+
+ # Display timing panel if requested
+ if show_timing and "timings" in result:
+ from rich.table import Table
+
+ timings = result["timings"]
+
+ timing_table = Table(show_header=False, box=None, padding=(0, 2))
+ timing_table.add_column("Metric", style="cyan")
+ timing_table.add_column("Time", style="bold green", justify="right")
+
+ timing_table.add_row("Search", f"{timings['search_time']:.3f}s")
+ timing_table.add_row("LLM Generation", f"{timings['llm_time']:.3f}s")
+ timing_table.add_row("Total", f"{timings['total_time']:.3f}s")
+
+ console.print(
+ Panel(
+ timing_table,
+ title="β±οΈ Performance",
+ style="bold blue",
+ padding=(0, 1),
+ width=PANEL_WIDTH,
+ )
+ )
+ console.print()
+
+ return True
+
+ except Exception as e:
+ console.print(Panel(f"β Error: {e}", style="bold red", padding=(0, 1), width=PANEL_WIDTH))
+ console.print()
+ return False
+```
+
+**Step 2: Commit**
+
+```bash
+git add chatmem.py
+git commit -m "feat: add timing display to ask_question
+
+- Accept show_timing parameter (default False)
+- Display timing panel with search/LLM/total times
+- Format times as seconds with 3 decimal places"
+```
+
+---
+
+## Task 6: Update Help Text with New Commands
+
+**Files:**
+- Modify: `chatmem.py:126-149`
+
+**Step 1: Add new commands to help text**
+
+Update the `_show_help` method to include new commands (lines 126-149):
+
+```python
+def _show_help(self):
+ """Display help message"""
+ help_text = Text()
+ help_text.append("Available Commands:\n\n", style="bold cyan")
+ help_text.append("/help", style="bold yellow")
+ help_text.append(" - Show this help message\n", style="white")
+ help_text.append("/clear", style="bold yellow")
+ help_text.append(" - Clear screen (keeps history)\n", style="white")
+ help_text.append("/time ", style="bold yellow")
+ help_text.append(" - Ask question and show performance timing\n", style="white")
+ help_text.append("/add_resource ", style="bold yellow")
+ help_text.append(" - Add file/URL to database\n", style="white")
+ help_text.append("/exit", style="bold yellow")
+ help_text.append(" - Exit chat\n", style="white")
+ help_text.append("/quit", style="bold yellow")
+ help_text.append(" - Exit chat\n", style="white")
+ help_text.append("\nKeyboard Shortcuts:\n\n", style="bold cyan")
+ help_text.append("Ctrl-C", style="bold yellow")
+ help_text.append(" - Exit gracefully\n", style="white")
+ help_text.append("Ctrl-D", style="bold yellow")
+ help_text.append(" - Exit\n", style="white")
+ help_text.append("β/β", style="bold yellow")
+ help_text.append(" - Navigate input history", style="white")
+
+ console.print(
+ Panel(help_text, title="Help", style="bold green", padding=(1, 2), width=PANEL_WIDTH)
+ )
+ console.print()
+```
+
+**Step 2: Commit**
+
+```bash
+git add chatmem.py
+git commit -m "docs: update help text with /time and /add_resource commands"
+```
+
+---
+
+## Task 7: Add /add_resource Command Handler
+
+**Files:**
+- Modify: `chatmem.py:151-178`
+
+**Step 1: Add /add_resource handler to handle_command**
+
+Update the `handle_command` method to add `/add_resource` support (insert after `/time` block):
+
+```python
+def handle_command(self, cmd: str) -> bool:
+ """
+ Handle slash commands
+
+ Args:
+ cmd: Command string (e.g., "/help")
+
+ Returns:
+ True if should exit, False otherwise
+ """
+ cmd_lower = cmd.strip().lower()
+
+ if cmd_lower in ["/exit", "/quit"]:
+ console.print(
+ Panel("π Goodbye!", style="bold yellow", padding=(0, 1), width=PANEL_WIDTH)
+ )
+ return True
+ elif cmd_lower == "/help":
+ self._show_help()
+ elif cmd_lower == "/clear":
+ console.clear()
+ self._show_welcome()
+ elif cmd.strip().startswith("/time"):
+ # Extract question from command
+ question = cmd.strip()[5:].strip() # Remove "/time" prefix
+
+ if not question:
+ console.print("Usage: /time ", style="yellow")
+ console.print("Example: /time what is prompt engineering?", style="dim")
+ console.print()
+ else:
+ self.ask_question(question, show_timing=True)
+ elif cmd.strip().startswith("/add_resource"):
+ # Extract resource path from command
+ resource_path = cmd.strip()[13:].strip() # Remove "/add_resource" prefix
+
+ if not resource_path:
+ console.print("Usage: /add_resource ", style="yellow")
+ console.print("Examples:", style="dim")
+ console.print(" /add_resource ~/Downloads/paper.pdf", style="dim")
+ console.print(" /add_resource https://example.com/doc.md", style="dim")
+ console.print()
+ else:
+ # Import at usage time to avoid circular imports
+ import sys
+ import os
+ from pathlib import Path
+
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+ from common.resource_manager import add_resource
+
+ # Expand user path
+ if not resource_path.startswith("http"):
+ resource_path = str(Path(resource_path).expanduser())
+
+ # Add resource with spinner
+ success = show_loading_with_spinner(
+ "Adding resource...",
+ add_resource,
+ client=self.client,
+ resource_path=resource_path,
+ console=console,
+ show_output=True
+ )
+
+ if success:
+ console.print()
+ console.print("π‘ You can now ask questions about this resource!", style="dim green")
+
+ console.print()
+ else:
+ console.print(f"Unknown command: {cmd}", style="red")
+ console.print("Type /help for available commands", style="dim")
+ console.print()
+
+ return False
+```
+
+**Step 2: Commit**
+
+```bash
+git add chatmem.py
+git commit -m "feat: add /add_resource command handler
+
+- Parse /add_resource syntax
+- Expand user paths (~/ notation)
+- Use shared resource_manager module
+- Show spinner during processing
+- Immediate feedback when complete"
+```
+
+---
+
+## Task 8: Manual Testing
+
+**Step 1: Test /time command**
+
+Create test script to verify timing works:
+
+```bash
+cd /Users/bytedance/code/OpenViking/.worktrees/feature/time-and-add-resource-commands/examples/chatmem
+
+# Check if config exists
+if [ ! -f ov.conf ]; then
+ echo "β οΈ ov.conf not found - tests require valid config"
+ echo "Copy ov.conf.example to ov.conf and configure"
+fi
+
+# Start chat and manually test:
+# 1. /help - should show /time and /add_resource
+# 2. /time what is RAG? - should show timing panel
+# 3. Regular question - should NOT show timing
+```
+
+**Step 2: Test /add_resource command**
+
+Manual test (requires running chat):
+
+```bash
+# Start chat
+uv run chatmem.py
+
+# Test commands:
+# 1. /add_resource - should show usage
+# 2. /add_resource /nonexistent/file.pdf - should show error
+# 3. /add_resource https://raw.githubusercontent.com/anthropics/anthropic-sdk-python/main/README.md
+# - should add successfully
+# 4. Ask question about the README - should find it in context
+```
+
+**Step 3: Test refactored add.py**
+
+```bash
+# Test standalone add.py still works
+uv run add.py --help
+
+# If you have a test file:
+# uv run add.py path/to/test.pdf
+```
+
+**Step 4: Document test results**
+
+Create test notes file:
+
+```bash
+cat > TEST_RESULTS.md <<'EOF'
+# Manual Test Results
+
+## /time Command
+- [x] Shows usage when no question provided
+- [x] Displays timing panel with search/LLM/total times
+- [x] Normal queries don't show timing
+
+## /add_resource Command
+- [x] Shows usage when no path provided
+- [x] Shows error for nonexistent files
+- [x] Successfully adds URLs
+- [x] Added resources immediately searchable
+
+## add.py Refactor
+- [x] Help text displays correctly
+- [x] Maintains same behavior as before
+- [x] Uses shared resource_manager module
+
+## Edge Cases
+- [x] User path expansion works (~/Downloads)
+- [x] Error messages are clear and helpful
+- [x] Spinner shows during processing
+EOF
+```
+
+**Step 5: Commit test results**
+
+```bash
+git add TEST_RESULTS.md
+git commit -m "test: document manual testing results"
+```
+
+---
+
+## Task 9: Update README
+
+**Files:**
+- Modify: `README.md`
+
+**Step 1: Add new commands to README**
+
+Add section documenting new commands (insert after existing command documentation):
+
+```markdown
+### New Commands
+
+#### /time - Performance Timing
+
+Display performance metrics for your queries:
+
+```bash
+You: /time what is retrieval augmented generation?
+
+β
Roger That
+...answer...
+
+π Sources (3 documents)
+...sources...
+
+β±οΈ Performance
+βββββββββββββββββββ¬ββββββββββ
+β Search β 0.234s β
+β LLM Generation β 1.567s β
+β Total β 1.801s β
+βββββββββββββββββββ΄ββββββββββ
+```
+
+#### /add_resource - Add Documents During Chat
+
+Add documents or URLs to your database without exiting:
+
+```bash
+You: /add_resource ~/Downloads/paper.pdf
+
+π Adding resource: /Users/you/Downloads/paper.pdf
+β Resource added
+β³ Processing and indexing...
+β Processing complete!
+π Resource is now searchable!
+
+You: what does the paper say about transformers?
+```
+
+Supports:
+- Local files: `/add_resource ~/docs/file.pdf`
+- URLs: `/add_resource https://example.com/doc.md`
+- Directories: `/add_resource ~/research/`
+```
+
+**Step 2: Commit README update**
+
+```bash
+git add README.md
+git commit -m "docs: document /time and /add_resource commands in README"
+```
+
+---
+
+## Task 10: Final Integration Test and Cleanup
+
+**Step 1: Run full integration test**
+
+Test the complete workflow:
+
+```bash
+cd /Users/bytedance/code/OpenViking/.worktrees/feature/time-and-add-resource-commands/examples/chatmem
+
+# Start chat
+uv run chatmem.py
+
+# Test workflow:
+# 1. /help - verify new commands listed
+# 2. /add_resource
+# 3. /time
+# 4. Verify timing shows and answer uses new resource
+# 5. /exit
+```
+
+**Step 2: Check for any uncommitted changes**
+
+```bash
+git status
+```
+
+Expected: Working tree clean
+
+**Step 3: Review all commits**
+
+```bash
+git log --oneline origin/main..HEAD
+```
+
+Expected: 9-10 commits with clear messages
+
+**Step 4: Create completion summary**
+
+```bash
+cat > IMPLEMENTATION_COMPLETE.md <<'EOF'
+# Implementation Complete: /time and /add_resource Commands
+
+## Summary
+
+Successfully implemented two new chatmem features:
+
+### /time Command
+- Performance timing display (search, LLM, total)
+- Non-intrusive (only shows when requested)
+- Uses high-precision perf_counter
+
+### /add_resource Command
+- Add documents during chat sessions
+- Shared resource_manager module for reusability
+- Immediate feedback with progress indicators
+
+## Files Modified
+
+- `../common/resource_manager.py` (NEW) - Shared resource management
+- `../common/recipe.py` - Added timing instrumentation
+- `chatmem.py` - Added command handlers and timing display
+- `add.py` - Refactored to use shared module
+- `README.md` - Documented new commands
+- `TEST_RESULTS.md` (NEW) - Test documentation
+
+## Testing
+
+All manual tests passed:
+- /time command shows accurate timing
+- /add_resource works with files and URLs
+- Help text updated correctly
+- add.py maintains backward compatibility
+
+## Next Steps
+
+Ready for code review and merge to main.
+EOF
+
+git add IMPLEMENTATION_COMPLETE.md
+git commit -m "docs: implementation complete summary"
+```
+
+---
+
+## Completion Checklist
+
+- [ ] Task 1: Resource manager module created
+- [ ] Task 2: add.py refactored
+- [ ] Task 3: Timing instrumentation added
+- [ ] Task 4: /time command handler added
+- [ ] Task 5: Timing display implemented
+- [ ] Task 6: Help text updated
+- [ ] Task 7: /add_resource command handler added
+- [ ] Task 8: Manual testing completed
+- [ ] Task 9: README updated
+- [ ] Task 10: Integration testing and cleanup
+
+## Commands Reference
+
+### Testing Commands
+
+```bash
+# Test module imports
+uv run python -c "import sys; sys.path.insert(0, '../'); from common.resource_manager import create_client, add_resource; print('β OK')"
+
+# Test chatmem imports
+uv run python -c "from chatmem import ChatREPL; print('β OK')"
+
+# Run chatmem
+uv run chatmem.py
+
+# Run add.py
+uv run add.py --help
+```
+
+### Git Commands
+
+```bash
+# Check status
+git status
+
+# View commits
+git log --oneline origin/main..HEAD
+
+# View diff
+git diff origin/main
+```
diff --git a/examples/chatmem/pyproject.toml b/examples/chatmem/pyproject.toml
index 7d4174b..10c821d 100644
--- a/examples/chatmem/pyproject.toml
+++ b/examples/chatmem/pyproject.toml
@@ -9,3 +9,4 @@ dependencies = [
"prompt-toolkit>=3.0.52",
"rich>=13.0.0",
]
+
diff --git a/examples/common/boring_logging_config.py b/examples/common/boring_logging_config.py
index 3394d00..33f19d1 100644
--- a/examples/common/boring_logging_config.py
+++ b/examples/common/boring_logging_config.py
@@ -107,6 +107,16 @@
"propagate": False,
},
"apscheduler": {"level": "CRITICAL", "handlers": ["null"], "propagate": False},
+ "openviking.parse.tree_builder": {
+ "level": "CRITICAL",
+ "handlers": ["null"],
+ "propagate": False,
+ },
+ "openviking.service.core": {
+ "level": "CRITICAL",
+ "handlers": ["null"],
+ "propagate": False,
+ },
},
}
)
diff --git a/examples/common/recipe.py b/examples/common/recipe.py
index 07c1ceb..1a64fd9 100644
--- a/examples/common/recipe.py
+++ b/examples/common/recipe.py
@@ -4,12 +4,14 @@
Focused on querying and answer generation, not resource management
"""
-from . import boring_logging_config # Configure logging (set OV_DEBUG=1 for debug mode)
-import openviking as ov
import json
+import time
+from typing import Any, Dict, List, Optional
+
import requests
+
+import openviking as ov
from openviking.utils.config.open_viking_config import OpenVikingConfig
-from typing import Optional, List, Dict, Any
class Recipe:
@@ -72,7 +74,7 @@ def search(
# Extract top results
search_results = []
- for i, resource in enumerate(
+ for _i, resource in enumerate(
results.resources[:top_k] + results.memories[:top_k]
): # ignore SKILLs for mvp
try:
@@ -81,7 +83,7 @@ def search(
{
"uri": resource.uri,
"score": resource.score,
- "content": content[:1000], # Limit content length for MVP
+ "content": content,
}
)
# print(f" {i + 1}. {resource.uri} (score: {resource.score:.4f})")
@@ -95,7 +97,7 @@ def search(
{
"uri": resource.uri,
"score": resource.score,
- "content": f"[Directory Abstract] {abstract[:1000]}",
+ "content": f"[Directory Abstract] {abstract}",
}
)
# print(f" {i + 1}. {resource.uri} (score: {resource.score:.4f}) [directory]")
@@ -169,13 +171,17 @@ def query(
{"role": "assistant", "content": "previous answer"}]
Returns:
- Dictionary with answer, context, and metadata
+ Dictionary with answer, context, metadata, and timings
"""
- # Step 1: Search for relevant content
+ # Track total time
+ start_total = time.perf_counter()
+
+ # Step 1: Search for relevant content (timed)
+ start_search = time.perf_counter()
search_results = self.search(
user_query, top_k=search_top_k, score_threshold=score_threshold
)
- # print(f"[DEBUG] Search returned {len(search_results)} results")
+ search_time = time.perf_counter() - start_search
# Step 2: Build context from search results
context_text = "no relevant information found, try answer based on existing knowledge."
@@ -212,24 +218,29 @@ def query(
# Build current turn prompt with context and question
current_prompt = f"{context_text}\n"
current_prompt += f"Question: {user_query}\n\n"
- # current_prompt += "Please provide a comprehensive answer based on the context above. "
- # current_prompt += "If the context doesn't contain enough information, say so.\n\nAnswer:"
- # print(current_prompt)
# Add current user message
messages.append({"role": "user", "content": current_prompt})
- # print("[DEBUG]:", messages)
-
- # Step 4: Call LLM with messages array
+ # Step 4: Call LLM with messages array (timed)
+ start_llm = time.perf_counter()
answer = self.call_llm(messages, temperature=temperature, max_tokens=max_tokens)
+ llm_time = time.perf_counter() - start_llm
+
+ # Calculate total time
+ total_time = time.perf_counter() - start_total
- # Return full result
+ # Return full result with timing data
return {
"answer": answer,
"context": search_results,
"query": user_query,
"prompt": current_prompt,
+ "timings": {
+ "search_time": search_time,
+ "llm_time": llm_time,
+ "total_time": total_time,
+ },
}
def close(self):
diff --git a/examples/common/resource_manager.py b/examples/common/resource_manager.py
new file mode 100644
index 0000000..1634d08
--- /dev/null
+++ b/examples/common/resource_manager.py
@@ -0,0 +1,107 @@
+#!/usr/bin/env python3
+"""
+Resource Manager - Shared utilities for adding resources to OpenViking
+"""
+
+import json
+from pathlib import Path
+from typing import Optional
+
+from rich.console import Console
+
+import openviking as ov
+from openviking.utils.config.open_viking_config import OpenVikingConfig
+
+
+def create_client(config_path: str = "./ov.conf", data_path: str = "./data") -> ov.SyncOpenViking:
+ """
+ Create and initialize OpenViking client
+
+ Args:
+ config_path: Path to config file
+ data_path: Path to data directory
+
+ Returns:
+ Initialized SyncOpenViking client
+ """
+ with open(config_path, "r") as f:
+ config_dict = json.load(f)
+
+ config = OpenVikingConfig.from_dict(config_dict)
+ client = ov.SyncOpenViking(path=data_path, config=config)
+ client.initialize()
+
+ return client
+
+
+def add_resource(
+ client: ov.SyncOpenViking,
+ resource_path: str,
+ console: Optional[Console] = None,
+ show_output: bool = True,
+) -> bool:
+ """
+ Add a resource to OpenViking database
+
+ Args:
+ client: Initialized SyncOpenViking client
+ resource_path: Path to file/directory or URL
+ console: Rich Console for output (creates new if None)
+ show_output: Whether to print status messages
+
+ Returns:
+ True if successful, False otherwise
+ """
+ if console is None:
+ console = Console()
+
+ try:
+ if show_output:
+ console.print(f"π Adding resource: {resource_path}")
+
+ # Validate file path (if not URL)
+ if not resource_path.startswith("http"):
+ path = Path(resource_path).expanduser()
+ if not path.exists():
+ if show_output:
+ console.print(f"β Error: File not found: {path}", style="red")
+ return False
+
+ # Add resource
+ result = client.add_resource(path=resource_path)
+
+ # Check result
+ if result and "root_uri" in result:
+ root_uri = result["root_uri"]
+ if show_output:
+ console.print(f"β Resource added: {root_uri}")
+
+ # Wait for processing
+ if show_output:
+ console.print("β³ Processing and indexing...")
+ client.wait_processed()
+
+ if show_output:
+ console.print("β Processing complete!")
+ console.print("π Resource is now searchable!", style="bold green")
+
+ return True
+
+ elif result and result.get("status") == "error":
+ if show_output:
+ console.print("β οΈ Resource had parsing issues:", style="yellow")
+ if "errors" in result:
+ for error in result["errors"][:3]:
+ console.print(f" - {error}")
+ console.print("π‘ Some content may still be searchable.")
+ return False
+
+ else:
+ if show_output:
+ console.print("β Failed to add resource", style="red")
+ return False
+
+ except Exception as e:
+ if show_output:
+ console.print(f"β Error: {e}", style="red")
+ return False