Add MkDocs Material documentation site with 40 pages and auto-generated API reference

Sets up a complete documentation website with 7 navigable sections (Home, Getting
Started, User Guide, Architecture, API Reference, Deployment, Development), light/dark
mode, search, code copy, and Mermaid diagram support. API reference pages use
mkdocstrings to auto-generate docs from source docstrings. GitHub Actions workflow
deploys to GitHub Pages on push to main.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jon Saad-Falcon
2026-02-21 06:09:36 +00:00
co-authored by Claude Opus 4.6
parent 990d7d8a79
commit f75afefcfb
45 changed files with 10822 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
name: Deploy Documentation
on:
push:
branches: [main]
paths:
- "docs/**"
- "mkdocs.yml"
- "src/openjarvis/**"
pull_request:
branches: [main]
paths:
- "docs/**"
- "mkdocs.yml"
- "src/openjarvis/**"
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install uv
uses: astral-sh/setup-uv@v4
- name: Install dependencies
run: uv sync --extra docs
- name: Build documentation
run: uv run mkdocs build --strict
- name: Upload artifact
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
uses: actions/upload-pages-artifact@v3
with:
path: site/
deploy:
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
+8
View File
@@ -36,3 +36,11 @@ Thumbs.db
# Project
*.sqlite
*.db
# MkDocs build output
site/
# Frontend
frontend/node_modules/
frontend/dist/
src/openjarvis/server/static/
+150
View File
@@ -0,0 +1,150 @@
# Agents Module
The agents module implements the agentic logic pillar. All agents implement
the `BaseAgent` ABC with a `run()` method. Agents handle queries by
coordinating tool calls, memory retrieval, and inference engine interactions.
The module also includes the OpenClaw infrastructure for interoperating with
external agent frameworks via HTTP or subprocess transport.
## Abstract Base Class and Context
### BaseAgent
::: openjarvis.agents._stubs.BaseAgent
options:
show_source: true
members_order: source
### AgentContext
::: openjarvis.agents._stubs.AgentContext
options:
show_source: true
members_order: source
### AgentResult
::: openjarvis.agents._stubs.AgentResult
options:
show_source: true
members_order: source
---
## Agent Implementations
### SimpleAgent
::: openjarvis.agents.simple.SimpleAgent
options:
show_source: true
members_order: source
### OrchestratorAgent
::: openjarvis.agents.orchestrator.OrchestratorAgent
options:
show_source: true
members_order: source
### OpenClawAgent
::: openjarvis.agents.openclaw.OpenClawAgent
options:
show_source: true
members_order: source
### CustomAgent
::: openjarvis.agents.custom.CustomAgent
options:
show_source: true
members_order: source
---
## OpenClaw Protocol
JSON-line wire protocol for communication between OpenJarvis and external
OpenClaw agent processes.
### MessageType
::: openjarvis.agents.openclaw_protocol.MessageType
options:
show_source: true
members_order: source
### ProtocolMessage
::: openjarvis.agents.openclaw_protocol.ProtocolMessage
options:
show_source: true
members_order: source
### serialize
::: openjarvis.agents.openclaw_protocol.serialize
options:
show_source: true
### deserialize
::: openjarvis.agents.openclaw_protocol.deserialize
options:
show_source: true
---
## OpenClaw Transport
Transport abstraction for communicating with OpenClaw agent processes
via HTTP or subprocess stdin/stdout.
### OpenClawTransport
::: openjarvis.agents.openclaw_transport.OpenClawTransport
options:
show_source: true
members_order: source
### HttpTransport
::: openjarvis.agents.openclaw_transport.HttpTransport
options:
show_source: true
members_order: source
### SubprocessTransport
::: openjarvis.agents.openclaw_transport.SubprocessTransport
options:
show_source: true
members_order: source
---
## OpenClaw Plugin
Plugin layer that wraps OpenJarvis engine and memory as an OpenClaw-compatible
provider and search manager.
### ProviderPlugin
::: openjarvis.agents.openclaw_plugin.ProviderPlugin
options:
show_source: true
members_order: source
### MemorySearchManager
::: openjarvis.agents.openclaw_plugin.MemorySearchManager
options:
show_source: true
members_order: source
### register
::: openjarvis.agents.openclaw_plugin.register
options:
show_source: true
+48
View File
@@ -0,0 +1,48 @@
# Benchmarks Module
The benchmarks module provides a framework for measuring inference engine
performance. All benchmarks implement the `BaseBenchmark` ABC and are
registered via `BenchmarkRegistry`. The `BenchmarkSuite` runner executes
a collection of benchmarks and aggregates results into JSONL or summary
format.
## Abstract Base Class and Runner
### BaseBenchmark
::: openjarvis.bench._stubs.BaseBenchmark
options:
show_source: true
members_order: source
### BenchmarkResult
::: openjarvis.bench._stubs.BenchmarkResult
options:
show_source: true
members_order: source
### BenchmarkSuite
::: openjarvis.bench._stubs.BenchmarkSuite
options:
show_source: true
members_order: source
---
## Benchmark Implementations
### LatencyBenchmark
::: openjarvis.bench.latency.LatencyBenchmark
options:
show_source: true
members_order: source
### ThroughputBenchmark
::: openjarvis.bench.throughput.ThroughputBenchmark
options:
show_source: true
members_order: source
+290
View File
@@ -0,0 +1,290 @@
# Core Module
The core module contains the foundational abstractions shared across all
OpenJarvis pillars: the decorator-based registry system for runtime discovery,
canonical data types, configuration loading with hardware detection, and the
pub/sub event bus for inter-pillar communication.
## Registry System
The registry pattern is the primary extension mechanism. Each pillar has its
own typed registry subclass. New implementations are added by decorating a
class with `@XRegistry.register("name")`.
### RegistryBase
::: openjarvis.core.registry.RegistryBase
options:
show_source: true
members_order: source
### ModelRegistry
::: openjarvis.core.registry.ModelRegistry
options:
show_source: true
members_order: source
### EngineRegistry
::: openjarvis.core.registry.EngineRegistry
options:
show_source: true
members_order: source
### MemoryRegistry
::: openjarvis.core.registry.MemoryRegistry
options:
show_source: true
members_order: source
### AgentRegistry
::: openjarvis.core.registry.AgentRegistry
options:
show_source: true
members_order: source
### ToolRegistry
::: openjarvis.core.registry.ToolRegistry
options:
show_source: true
members_order: source
### RouterPolicyRegistry
::: openjarvis.core.registry.RouterPolicyRegistry
options:
show_source: true
members_order: source
### BenchmarkRegistry
::: openjarvis.core.registry.BenchmarkRegistry
options:
show_source: true
members_order: source
---
## Types
Canonical data types shared across all OpenJarvis pillars, including message
structures, model specifications, telemetry records, and trace objects.
### Role
::: openjarvis.core.types.Role
options:
show_source: true
members_order: source
### Quantization
::: openjarvis.core.types.Quantization
options:
show_source: true
members_order: source
### StepType
::: openjarvis.core.types.StepType
options:
show_source: true
members_order: source
### ToolCall
::: openjarvis.core.types.ToolCall
options:
show_source: true
members_order: source
### Message
::: openjarvis.core.types.Message
options:
show_source: true
members_order: source
### Conversation
::: openjarvis.core.types.Conversation
options:
show_source: true
members_order: source
### ModelSpec
::: openjarvis.core.types.ModelSpec
options:
show_source: true
members_order: source
### ToolResult
::: openjarvis.core.types.ToolResult
options:
show_source: true
members_order: source
### TelemetryRecord
::: openjarvis.core.types.TelemetryRecord
options:
show_source: true
members_order: source
### TraceStep
::: openjarvis.core.types.TraceStep
options:
show_source: true
members_order: source
### Trace
::: openjarvis.core.types.Trace
options:
show_source: true
members_order: source
---
## Configuration
Configuration loading, hardware detection, and engine recommendation. User
configuration lives at `~/.openjarvis/config.toml`. The `load_config()`
function detects hardware, fills sensible defaults, then overlays any user
overrides from the TOML file.
### JarvisConfig
::: openjarvis.core.config.JarvisConfig
options:
show_source: true
members_order: source
### EngineConfig
::: openjarvis.core.config.EngineConfig
options:
show_source: true
members_order: source
### IntelligenceConfig
::: openjarvis.core.config.IntelligenceConfig
options:
show_source: true
members_order: source
### LearningConfig
::: openjarvis.core.config.LearningConfig
options:
show_source: true
members_order: source
### MemoryConfig
::: openjarvis.core.config.MemoryConfig
options:
show_source: true
members_order: source
### AgentConfig
::: openjarvis.core.config.AgentConfig
options:
show_source: true
members_order: source
### ServerConfig
::: openjarvis.core.config.ServerConfig
options:
show_source: true
members_order: source
### TelemetryConfig
::: openjarvis.core.config.TelemetryConfig
options:
show_source: true
members_order: source
### HardwareInfo
::: openjarvis.core.config.HardwareInfo
options:
show_source: true
members_order: source
### GpuInfo
::: openjarvis.core.config.GpuInfo
options:
show_source: true
members_order: source
### detect_hardware
::: openjarvis.core.config.detect_hardware
options:
show_source: true
### load_config
::: openjarvis.core.config.load_config
options:
show_source: true
### recommend_engine
::: openjarvis.core.config.recommend_engine
options:
show_source: true
---
## Event Bus
Thread-safe pub/sub event bus for inter-pillar telemetry. Any pillar can emit
events (e.g. `INFERENCE_END`) and any other pillar can react without direct
coupling.
### EventType
::: openjarvis.core.events.EventType
options:
show_source: true
members_order: source
### Event
::: openjarvis.core.events.Event
options:
show_source: true
members_order: source
### EventBus
::: openjarvis.core.events.EventBus
options:
show_source: true
members_order: source
### get_event_bus
::: openjarvis.core.events.get_event_bus
options:
show_source: true
### reset_event_bus
::: openjarvis.core.events.reset_event_bus
options:
show_source: true
+98
View File
@@ -0,0 +1,98 @@
# Engine Module
The engine module implements the inference runtime pillar. All backends
implement the `InferenceEngine` ABC with `generate()`, `stream()`,
`list_models()`, and `health()` methods. The discovery subsystem probes
running engines and selects the best available backend based on
configuration and health checks.
## Abstract Base Class
### InferenceEngine
::: openjarvis.engine._stubs.InferenceEngine
options:
show_source: true
members_order: source
### EngineConnectionError
::: openjarvis.engine._base.EngineConnectionError
options:
show_source: true
### messages_to_dicts
::: openjarvis.engine._base.messages_to_dicts
options:
show_source: true
---
## Engine Implementations
### OllamaEngine
::: openjarvis.engine.ollama.OllamaEngine
options:
show_source: true
members_order: source
### VLLMEngine
::: openjarvis.engine.vllm.VLLMEngine
options:
show_source: true
members_order: source
### LlamaCppEngine
::: openjarvis.engine.llamacpp.LlamaCppEngine
options:
show_source: true
members_order: source
### SGLangEngine
::: openjarvis.engine.sglang.SGLangEngine
options:
show_source: true
members_order: source
### CloudEngine
::: openjarvis.engine.cloud.CloudEngine
options:
show_source: true
members_order: source
### estimate_cost
::: openjarvis.engine.cloud.estimate_cost
options:
show_source: true
---
## Engine Discovery
Functions for probing running engines, aggregating available models,
and selecting the best engine for a given configuration.
### get_engine
::: openjarvis.engine._discovery.get_engine
options:
show_source: true
### discover_engines
::: openjarvis.engine._discovery.discover_engines
options:
show_source: true
### discover_models
::: openjarvis.engine._discovery.discover_models
options:
show_source: true
+25
View File
@@ -0,0 +1,25 @@
# API Reference
This section contains the auto-generated API reference for the OpenJarvis Python
package. Documentation is extracted directly from the source code docstrings
using [mkdocstrings](https://mkdocstrings.github.io/).
Each page documents a major module of the framework, including abstract base
classes, concrete implementations, and utility functions.
## Modules
| Module | Description |
|--------|-------------|
| [SDK](sdk.md) | High-level `Jarvis` class and `MemoryHandle` proxy |
| [Core](core.md) | Registries, types, configuration, and event bus |
| [Engine](engine.md) | Inference engine ABC and backends (Ollama, vLLM, llama.cpp, SGLang, Cloud) |
| [Agents](agents.md) | Agent ABC and implementations (Simple, Orchestrator, OpenClaw, Custom) |
| [Memory](memory.md) | Memory backend ABC and implementations (SQLite, FAISS, ColBERT, BM25, Hybrid) |
| [Tools](tools.md) | Tool ABC, executor, and built-in tools (Calculator, Think, Retrieval, LLM, FileRead) |
| [Intelligence](intelligence.md) | Heuristic router and model catalog |
| [Learning](learning.md) | Router policies, reward functions, and trace-driven learning |
| [Traces](traces.md) | Trace storage, collection, and analysis |
| [Telemetry](telemetry.md) | Telemetry storage, aggregation, and instrumented wrappers |
| [Benchmarks](bench.md) | Benchmark ABC, suite runner, latency and throughput benchmarks |
| [Server](server.md) | FastAPI application, OpenAI-compatible routes, and Pydantic models |
+47
View File
@@ -0,0 +1,47 @@
# Intelligence Module
The intelligence module handles model management and query routing. It
provides the `HeuristicRouter` which selects models based on query
characteristics (code detection, math detection, query length, urgency),
and a model catalog of well-known local and cloud models with their
specifications.
## Heuristic Router
### HeuristicRouter
::: openjarvis.intelligence.router.HeuristicRouter
options:
show_source: true
members_order: source
### build_routing_context
::: openjarvis.intelligence.router.build_routing_context
options:
show_source: true
---
## Model Catalog
Built-in catalog of well-known model specifications, including local models
(Qwen, Llama, Mistral, DeepSeek) and cloud models (OpenAI, Anthropic, Google).
### BUILTIN_MODELS
::: openjarvis.intelligence.model_catalog.BUILTIN_MODELS
options:
show_source: true
### register_builtin_models
::: openjarvis.intelligence.model_catalog.register_builtin_models
options:
show_source: true
### merge_discovered_models
::: openjarvis.intelligence.model_catalog.merge_discovered_models
options:
show_source: true
+65
View File
@@ -0,0 +1,65 @@
# Learning Module
The learning module implements router policies that determine which model
handles a given query. Policies range from static heuristic rules to
trace-driven learning that improves routing decisions based on historical
interaction outcomes. The module also provides reward functions for scoring
inference results.
## Abstract Base Classes
### RouterPolicy
::: openjarvis.learning._stubs.RouterPolicy
options:
show_source: true
members_order: source
### RoutingContext
::: openjarvis.learning._stubs.RoutingContext
options:
show_source: true
members_order: source
### RewardFunction
::: openjarvis.learning._stubs.RewardFunction
options:
show_source: true
members_order: source
---
## Policy Implementations
### TraceDrivenPolicy
::: openjarvis.learning.trace_policy.TraceDrivenPolicy
options:
show_source: true
members_order: source
### classify_query
::: openjarvis.learning.trace_policy.classify_query
options:
show_source: true
### GRPORouterPolicy
::: openjarvis.learning.grpo_policy.GRPORouterPolicy
options:
show_source: true
members_order: source
---
## Reward Functions
### HeuristicRewardFunction
::: openjarvis.learning.heuristic_reward.HeuristicRewardFunction
options:
show_source: true
members_order: source
+179
View File
@@ -0,0 +1,179 @@
# Memory Module
The memory module implements persistent searchable storage for document
retrieval. All backends implement the `MemoryBackend` ABC with `store()`,
`retrieve()`, `delete()`, and `clear()` methods. The module also includes
a document ingestion pipeline (chunking, file reading) and context injection
for augmenting prompts with retrieved knowledge.
## Abstract Base Class
### MemoryBackend
::: openjarvis.memory._stubs.MemoryBackend
options:
show_source: true
members_order: source
### RetrievalResult
::: openjarvis.memory._stubs.RetrievalResult
options:
show_source: true
members_order: source
---
## Backend Implementations
### SQLiteMemory
::: openjarvis.memory.sqlite.SQLiteMemory
options:
show_source: true
members_order: source
### FAISSMemory
::: openjarvis.memory.faiss_backend.FAISSMemory
options:
show_source: true
members_order: source
### ColBERTMemory
::: openjarvis.memory.colbert_backend.ColBERTMemory
options:
show_source: true
members_order: source
### BM25Memory
::: openjarvis.memory.bm25.BM25Memory
options:
show_source: true
members_order: source
### HybridMemory
::: openjarvis.memory.hybrid.HybridMemory
options:
show_source: true
members_order: source
### reciprocal_rank_fusion
::: openjarvis.memory.hybrid.reciprocal_rank_fusion
options:
show_source: true
---
## Document Chunking
Splits text into fixed-size chunks with configurable overlap, respecting
paragraph boundaries.
### ChunkConfig
::: openjarvis.memory.chunking.ChunkConfig
options:
show_source: true
members_order: source
### Chunk
::: openjarvis.memory.chunking.Chunk
options:
show_source: true
members_order: source
### chunk_text
::: openjarvis.memory.chunking.chunk_text
options:
show_source: true
---
## Document Ingestion
File reading, type detection, and directory walking for the ingestion
pipeline.
### DocumentMeta
::: openjarvis.memory.ingest.DocumentMeta
options:
show_source: true
members_order: source
### detect_file_type
::: openjarvis.memory.ingest.detect_file_type
options:
show_source: true
### read_document
::: openjarvis.memory.ingest.read_document
options:
show_source: true
### ingest_path
::: openjarvis.memory.ingest.ingest_path
options:
show_source: true
---
## Context Injection
Retrieves relevant memory and injects it into prompts as system messages
with source attribution.
### ContextConfig
::: openjarvis.memory.context.ContextConfig
options:
show_source: true
members_order: source
### inject_context
::: openjarvis.memory.context.inject_context
options:
show_source: true
### format_context
::: openjarvis.memory.context.format_context
options:
show_source: true
### build_context_message
::: openjarvis.memory.context.build_context_message
options:
show_source: true
---
## Embeddings
Abstraction layer for text embedding models used by dense retrieval backends.
### Embedder
::: openjarvis.memory.embeddings.Embedder
options:
show_source: true
members_order: source
### SentenceTransformerEmbedder
::: openjarvis.memory.embeddings.SentenceTransformerEmbedder
options:
show_source: true
members_order: source
+20
View File
@@ -0,0 +1,20 @@
# SDK
The SDK module provides the high-level Python interface for OpenJarvis. The
`Jarvis` class wraps engine discovery, agent dispatch, memory operations, and
telemetry into a single convenient entry point. `MemoryHandle` acts as a lazy
proxy for memory backend operations.
## Jarvis
::: openjarvis.sdk.Jarvis
options:
show_source: true
members_order: source
## MemoryHandle
::: openjarvis.sdk.MemoryHandle
options:
show_source: true
members_order: source
+117
View File
@@ -0,0 +1,117 @@
# Server Module
The server module provides an OpenAI-compatible API server built on FastAPI
and uvicorn. It exposes `POST /v1/chat/completions` (with streaming via SSE),
`GET /v1/models`, and `GET /health` endpoints. Pydantic models ensure
request/response validation matching the OpenAI API format.
## Application Factory
### create_app
::: openjarvis.server.app.create_app
options:
show_source: true
---
## Route Handlers
### chat_completions
::: openjarvis.server.routes.chat_completions
options:
show_source: true
### list_models
::: openjarvis.server.routes.list_models
options:
show_source: true
### health
::: openjarvis.server.routes.health
options:
show_source: true
---
## Pydantic Request/Response Models
### ChatMessage
::: openjarvis.server.models.ChatMessage
options:
show_source: true
members_order: source
### ChatCompletionRequest
::: openjarvis.server.models.ChatCompletionRequest
options:
show_source: true
members_order: source
### ChatCompletionResponse
::: openjarvis.server.models.ChatCompletionResponse
options:
show_source: true
members_order: source
### Choice
::: openjarvis.server.models.Choice
options:
show_source: true
members_order: source
### ChoiceMessage
::: openjarvis.server.models.ChoiceMessage
options:
show_source: true
members_order: source
### UsageInfo
::: openjarvis.server.models.UsageInfo
options:
show_source: true
members_order: source
### ChatCompletionChunk
::: openjarvis.server.models.ChatCompletionChunk
options:
show_source: true
members_order: source
### DeltaMessage
::: openjarvis.server.models.DeltaMessage
options:
show_source: true
members_order: source
### StreamChoice
::: openjarvis.server.models.StreamChoice
options:
show_source: true
members_order: source
### ModelObject
::: openjarvis.server.models.ModelObject
options:
show_source: true
members_order: source
### ModelListResponse
::: openjarvis.server.models.ModelListResponse
options:
show_source: true
members_order: source
+53
View File
@@ -0,0 +1,53 @@
# Telemetry Module
The telemetry module provides append-only recording and read-only aggregation
of inference metrics. Every engine call records timing, token counts, energy
usage, and cost to SQLite via the event bus. The `TelemetryAggregator`
provides per-model and per-engine statistics with time-range filtering.
## TelemetryStore
::: openjarvis.telemetry.store.TelemetryStore
options:
show_source: true
members_order: source
---
## TelemetryAggregator
::: openjarvis.telemetry.aggregator.TelemetryAggregator
options:
show_source: true
members_order: source
### ModelStats
::: openjarvis.telemetry.aggregator.ModelStats
options:
show_source: true
members_order: source
### EngineStats
::: openjarvis.telemetry.aggregator.EngineStats
options:
show_source: true
members_order: source
### AggregatedStats
::: openjarvis.telemetry.aggregator.AggregatedStats
options:
show_source: true
members_order: source
---
## Instrumented Wrapper
### instrumented_generate
::: openjarvis.telemetry.wrapper.instrumented_generate
options:
show_source: true
+75
View File
@@ -0,0 +1,75 @@
# Tools Module
The tools module implements the tool system used by agents for executing
actions such as calculations, memory search, file reading, and sub-model
queries. Each tool implements the `BaseTool` ABC and is registered via
`@ToolRegistry.register("name")`. The `ToolExecutor` dispatches tool calls
with JSON argument parsing, latency tracking, and event bus integration.
## Abstract Base Class
### BaseTool
::: openjarvis.tools._stubs.BaseTool
options:
show_source: true
members_order: source
### ToolSpec
::: openjarvis.tools._stubs.ToolSpec
options:
show_source: true
members_order: source
### ToolExecutor
::: openjarvis.tools._stubs.ToolExecutor
options:
show_source: true
members_order: source
---
## Built-in Tools
### CalculatorTool
::: openjarvis.tools.calculator.CalculatorTool
options:
show_source: true
members_order: source
### safe_eval
::: openjarvis.tools.calculator.safe_eval
options:
show_source: true
### ThinkTool
::: openjarvis.tools.think.ThinkTool
options:
show_source: true
members_order: source
### RetrievalTool
::: openjarvis.tools.retrieval.RetrievalTool
options:
show_source: true
members_order: source
### LLMTool
::: openjarvis.tools.llm_tool.LLMTool
options:
show_source: true
members_order: source
### FileReadTool
::: openjarvis.tools.file_read.FileReadTool
options:
show_source: true
members_order: source
+53
View File
@@ -0,0 +1,53 @@
# Traces Module
The traces module provides full interaction-level recording for the learning
system. Every agent interaction can produce a `Trace` capturing the sequence
of steps (route, retrieve, generate, tool_call, respond) with timing, inputs,
outputs, and outcomes. The `TraceCollector` wraps any agent to record traces
automatically, while `TraceAnalyzer` provides aggregated statistics.
## TraceStore
::: openjarvis.traces.store.TraceStore
options:
show_source: true
members_order: source
---
## TraceCollector
::: openjarvis.traces.collector.TraceCollector
options:
show_source: true
members_order: source
---
## TraceAnalyzer
::: openjarvis.traces.analyzer.TraceAnalyzer
options:
show_source: true
members_order: source
### RouteStats
::: openjarvis.traces.analyzer.RouteStats
options:
show_source: true
members_order: source
### ToolStats
::: openjarvis.traces.analyzer.ToolStats
options:
show_source: true
members_order: source
### TraceSummary
::: openjarvis.traces.analyzer.TraceSummary
options:
show_source: true
members_order: source
+355
View File
@@ -0,0 +1,355 @@
# Agentic Logic Pillar
The Agentic Logic pillar provides **pluggable agents** that handle queries with varying levels of sophistication -- from simple single-turn responses to multi-turn tool-calling loops and external agent communication.
---
## BaseAgent ABC
All agents implement the `BaseAgent` abstract base class:
```python
class BaseAgent(ABC):
agent_id: str
@abstractmethod
def run(
self,
input: str,
context: Optional[AgentContext] = None,
**kwargs: Any,
) -> AgentResult:
"""Execute the agent on *input* and return an AgentResult."""
```
### The `run()` Contract
The `run()` method is the single entry point for all agent implementations. It receives:
- **`input`** -- The user's query text
- **`context`** -- An optional `AgentContext` with conversation history, tool names, and memory results
- **`**kwargs`** -- Additional implementation-specific parameters
It returns an `AgentResult` containing the response content, any tool results, the number of turns taken, and metadata.
### Supporting Dataclasses
```python
@dataclass(slots=True)
class AgentContext:
conversation: Conversation # Prior messages for multi-turn context
tools: List[str] # Available tool names
memory_results: List[Any] # Pre-fetched memory search results
metadata: Dict[str, Any] # Arbitrary key-value pairs
@dataclass(slots=True)
class AgentResult:
content: str # The agent's response text
tool_results: List[ToolResult] # Results from tool invocations
turns: int # Number of inference turns taken
metadata: Dict[str, Any] # Arbitrary metadata
```
---
## Agent Implementations
### SimpleAgent
**Registry key:** `simple`
The simplest agent implementation -- a single-turn, no-tool query-to-response pipeline.
```mermaid
graph LR
Q["User Query"] --> M["Build Messages"]
M --> E["Engine.generate()"]
E --> R["AgentResult"]
```
How it works:
1. Publishes `AGENT_TURN_START` on the event bus
2. Builds a message list from any existing conversation context plus the user's input
3. Calls `instrumented_generate()` (if bus is available) or `engine.generate()` directly
4. Publishes `AGENT_TURN_END` and returns an `AgentResult` with `turns=1`
```python
from openjarvis.agents.simple import SimpleAgent
agent = SimpleAgent(engine, model="qwen3:8b", bus=bus)
result = agent.run("What is the capital of France?")
print(result.content) # "The capital of France is Paris."
```
### OrchestratorAgent
**Registry key:** `orchestrator`
A multi-turn agent that implements a **tool-calling loop**. The LLM can request tool invocations, and the results are fed back for further processing until the model produces a final text response.
```mermaid
graph TD
Q["User Query"] --> BUILD["Build messages +<br/>tool definitions"]
BUILD --> GEN["Engine.generate()<br/>with tools"]
GEN --> CHECK{"Tool calls<br/>in response?"}
CHECK -->|No| DONE["Return final answer"]
CHECK -->|Yes| EXEC["Execute each tool<br/>via ToolExecutor"]
EXEC --> APPEND["Append tool results<br/>to messages"]
APPEND --> MAXCHECK{"Max turns<br/>exceeded?"}
MAXCHECK -->|No| GEN
MAXCHECK -->|Yes| TIMEOUT["Return with<br/>max_turns_exceeded"]
```
How it works:
1. Builds initial messages from context and user input
2. Converts available tools to OpenAI function-calling format via `ToolExecutor.get_openai_tools()`
3. Enters a loop (up to `max_turns` iterations):
- Calls `engine.generate()` with messages and tool definitions
- If the response contains `tool_calls`, executes each tool and appends the results as `TOOL` messages
- If no `tool_calls` are present, returns the content as the final answer
4. If `max_turns` is exceeded, returns the last content or a warning message
```python
from openjarvis.agents.orchestrator import OrchestratorAgent
from openjarvis.tools.calculator import CalculatorTool
from openjarvis.tools.think import ThinkTool
agent = OrchestratorAgent(
engine,
model="qwen3:8b",
tools=[CalculatorTool(), ThinkTool()],
bus=bus,
max_turns=10,
)
result = agent.run("What is 2^10 + 3^5?")
# The agent may call the calculator tool, get "1267", then respond
```
### OpenClawAgent
**Registry key:** `openclaw`
Communicates with an external OpenClaw Pi agent server via HTTP or subprocess transport. This agent delegates query handling to a separate process or service.
```mermaid
graph LR
Q["User Query"] --> MSG["Build ProtocolMessage"]
MSG --> SEND["Transport.send()"]
SEND --> CHECK{"Response type?"}
CHECK -->|TOOL_CALL| EXEC["Execute tool locally"]
EXEC --> RESULT["Send tool result back"]
RESULT --> SEND
CHECK -->|RESPONSE| DONE["Return content"]
CHECK -->|ERROR| ERR["Return error"]
```
How it works:
1. Checks transport health
2. Sends a `QUERY` message through the transport
3. If the response is a `TOOL_CALL`, executes the tool locally via `ToolRegistry` and sends the result back
4. Continues the tool-call loop until a `RESPONSE` or `ERROR` is received
```python
from openjarvis.agents.openclaw import OpenClawAgent
# HTTP mode (connects to OpenClaw server)
agent = OpenClawAgent(engine, model="qwen3:8b", mode="http")
# Subprocess mode (launches Node.js process)
agent = OpenClawAgent(engine, model="qwen3:8b", mode="subprocess")
```
### CustomAgent
**Registry key:** `custom`
A template for user-defined agents. Its `run()` method raises `NotImplementedError` -- users must subclass it and override `run()`:
```python
from openjarvis.agents.custom import CustomAgent
from openjarvis.core.registry import AgentRegistry
@AgentRegistry.register("my-agent")
class MyAgent(CustomAgent):
agent_id = "my-agent"
def run(self, input, context=None, **kwargs):
# Custom logic here
return AgentResult(content="Custom response")
```
---
## Tool System Integration
The `OrchestratorAgent` uses the `ToolExecutor` to dispatch tool calls. The tool system is built on the `BaseTool` ABC:
```python
class BaseTool(ABC):
tool_id: str
@property
@abstractmethod
def spec(self) -> ToolSpec:
"""Return the tool specification."""
@abstractmethod
def execute(self, **params: Any) -> ToolResult:
"""Execute the tool with the given parameters."""
def to_openai_function(self) -> Dict[str, Any]:
"""Convert to OpenAI function-calling format."""
```
### Built-in Tools
| Tool | Registry Key | Description |
|------|-------------|-------------|
| `CalculatorTool` | `calculator` | AST-based safe expression evaluator |
| `ThinkTool` | `think` | Reasoning scratchpad (returns input as-is) |
| `RetrievalTool` | `retrieval` | Memory search via a memory backend |
| `LLMTool` | `llm` | Sub-model calls (query a different model) |
| `FileReadTool` | `file_read` | Safe file reading with path validation |
### ToolExecutor
The `ToolExecutor` handles tool dispatch with JSON argument parsing, latency tracking, and event bus integration:
```python
class ToolExecutor:
def __init__(self, tools: List[BaseTool], bus: Optional[EventBus] = None):
self._tools = {t.spec.name: t for t in tools}
self._bus = bus
def execute(self, tool_call: ToolCall) -> ToolResult:
"""Parse arguments, dispatch to tool, measure latency, emit events."""
def get_openai_tools(self) -> List[Dict[str, Any]]:
"""Return tools in OpenAI function-calling format."""
```
For each tool call:
1. Looks up the tool by name
2. Parses the JSON arguments string
3. Publishes `TOOL_CALL_START` on the event bus
4. Executes the tool with timing
5. Publishes `TOOL_CALL_END` with success status and latency
6. Returns the `ToolResult`
---
## OpenClaw Infrastructure
The OpenClaw infrastructure enables OpenJarvis agents to communicate with external OpenClaw servers through a structured protocol.
### Protocol
The `openclaw_protocol.py` module defines the wire protocol:
**Message Types:**
| Type | Direction | Purpose |
|------|-----------|---------|
| `QUERY` | Client -> Server | Send a user query |
| `RESPONSE` | Server -> Client | Return a response |
| `TOOL_CALL` | Server -> Client | Request tool execution |
| `TOOL_RESULT` | Client -> Server | Return tool execution result |
| `ERROR` | Server -> Client | Report an error |
| `HEALTH` | Client -> Server | Health check request |
| `HEALTH_OK` | Server -> Client | Health check response |
**ProtocolMessage dataclass:**
```python
@dataclass(slots=True)
class ProtocolMessage:
type: MessageType
id: str # UUID, auto-generated
content: str = ""
tool_name: Optional[str] = None
tool_args: Optional[Dict] = None
tool_result: Optional[str] = None
error: Optional[str] = None
metadata: Dict[str, Any] = field(default_factory=dict)
```
Messages are serialized to JSON lines via `serialize()` and deserialized via `deserialize()`.
### Transport
The `openclaw_transport.py` module provides two transport implementations:
**`HttpTransport`** -- Communicates via HTTP POST to an OpenClaw server:
- Default endpoint: `http://localhost:18789`
- Sends messages to `/api/query`
- Health check via `GET /health`
**`SubprocessTransport`** -- Launches a Node.js process and communicates via stdin/stdout:
- Sends JSON lines to the process's stdin
- Reads JSON line responses from stdout
- Auto-starts the process if it is not running
- Terminates the process on `close()`
### Plugin System
The `openclaw_plugin.py` module wraps OpenJarvis as an OpenClaw provider:
- **`ProviderPlugin`** -- Wraps an OpenJarvis engine for OpenClaw's `generate()` and `list_models()` interface
- **`MemorySearchManager`** -- Wraps a memory backend for OpenClaw's `search()`, `sync()`, and `status()` interface
- **`register()`** -- Entry point that returns plugin capabilities for OpenClaw discovery
---
## Event Bus Integration
All agents integrate with the `EventBus` for telemetry and trace collection:
| Event | Published By | When |
|-------|-------------|------|
| `AGENT_TURN_START` | All agents | Before starting query processing |
| `AGENT_TURN_END` | All agents | After producing a response |
| `INFERENCE_START` | OrchestratorAgent | Before each `engine.generate()` call |
| `INFERENCE_END` | OrchestratorAgent | After each `engine.generate()` call |
| `TOOL_CALL_START` | ToolExecutor / OpenClawAgent | Before executing a tool |
| `TOOL_CALL_END` | ToolExecutor / OpenClawAgent | After executing a tool |
These events are consumed by the `TelemetryStore` (for metrics) and `TraceCollector` (for interaction traces).
---
## Agent Registration
Agents are registered via the `@AgentRegistry.register("name")` decorator:
```python
from openjarvis.core.registry import AgentRegistry
from openjarvis.agents._stubs import BaseAgent
@AgentRegistry.register("my-agent")
class MyAgent(BaseAgent):
agent_id = "my-agent"
def run(self, input, context=None, **kwargs):
...
```
To list all registered agents:
```python
from openjarvis.core.registry import AgentRegistry
print(AgentRegistry.keys())
# ("simple", "orchestrator", "openclaw", "custom")
```
To instantiate an agent by key:
```python
agent = AgentRegistry.create("orchestrator", engine, model, tools=tools, bus=bus)
```
+299
View File
@@ -0,0 +1,299 @@
# Design Principles
OpenJarvis follows a set of design principles that guide every architectural decision. These principles ensure the framework remains extensible, portable, and easy to work with.
---
## 1. Pluggable Everything
Every major component in OpenJarvis is defined as an **abstract base class** (ABC) with concrete implementations registered at runtime. This means you can swap, extend, or replace any part of the system without modifying existing code.
```mermaid
graph LR
subgraph "ABC Interface"
ABC["InferenceEngine ABC<br/><code>generate(), stream(),<br/>list_models(), health()</code>"]
end
subgraph "Implementations"
A["OllamaEngine"]
B["VLLMEngine"]
C["SGLangEngine"]
D["LlamaCppEngine"]
E["CloudEngine"]
F["YourCustomEngine"]
end
ABC --> A
ABC --> B
ABC --> C
ABC --> D
ABC --> E
ABC -.->|"extend"| F
```
This pattern applies across all four pillars:
| Pillar | ABC | Implementations |
|--------|-----|----------------|
| Engine | `InferenceEngine` | Ollama, vLLM, SGLang, llama.cpp, Cloud |
| Memory | `MemoryBackend` | SQLite, FAISS, ColBERT, BM25, Hybrid |
| Agents | `BaseAgent` | Simple, Orchestrator, OpenClaw, Custom |
| Learning | `RouterPolicy` | Heuristic, TraceDriven, GRPO |
| Tools | `BaseTool` | Calculator, Think, Retrieval, LLM, FileRead |
Adding a new implementation requires two things: implement the ABC and register it. The rest of the system discovers and uses it automatically.
---
## 2. Registry-Driven
All extensible components use the **`@XRegistry.register("name")` decorator** pattern. Registration happens at import time, and no factory function or configuration file needs modification.
```python
from openjarvis.core.registry import EngineRegistry
from openjarvis.engine._stubs import InferenceEngine
@EngineRegistry.register("my-engine")
class MyEngine(InferenceEngine):
engine_id = "my-engine"
def generate(self, messages, *, model, **kwargs):
...
def stream(self, messages, *, model, **kwargs):
...
def list_models(self):
...
def health(self):
...
```
The `RegistryBase[T]` generic base class provides:
- **Class-specific isolation** -- Each typed subclass (`EngineRegistry`, `MemoryRegistry`, etc.) has its own entry storage, so registrations never leak between registries
- **Duplicate detection** -- Registering the same key twice raises `ValueError`
- **Runtime instantiation** -- `Registry.create(key, *args)` looks up and instantiates in one step
- **Introspection** -- `keys()`, `items()`, `contains()` for discovering available components
!!! info "Why decorators instead of configuration files?"
The decorator pattern means that adding a new component is a single-file change.
There is no central registry file to edit, no YAML to update, and no factory to modify.
The component self-registers simply by being imported.
---
## 3. Offline-First
OpenJarvis is designed to work **entirely without network access**. All core functionality -- inference, memory, agents, tools, telemetry -- operates locally. Cloud APIs are optional extensions, never requirements.
| Feature | Offline Behavior |
|---------|-----------------|
| Inference | Ollama, vLLM, SGLang, llama.cpp all run locally |
| Memory | SQLite/FTS5 uses built-in Python `sqlite3` module |
| Embeddings | `sentence-transformers` models run locally |
| Telemetry | SQLite-based, fully local |
| Traces | SQLite-based, fully local |
| Tools | Calculator, Think, FileRead all local |
| Configuration | TOML file on disk |
Cloud engines (OpenAI, Anthropic, Google) are available through the optional `cloud` backend, but they are:
- Only registered if the corresponding SDK packages are installed
- Only activated if API keys are set as environment variables
- Never required for any core functionality
```python
# This works without any network connection
from openjarvis import Jarvis
j = Jarvis(engine_key="ollama") # Local Ollama server
response = j.ask("Hello")
```
---
## 4. Hardware-Aware
OpenJarvis **auto-detects system hardware** at startup and recommends the optimal inference engine. The `detect_hardware()` function probes:
| Hardware | Detection Method |
|----------|-----------------|
| NVIDIA GPUs | `nvidia-smi` (name, VRAM, count) |
| AMD GPUs | `rocm-smi` (product name) |
| Apple Silicon | `system_profiler SPDisplaysDataType` |
| CPU | `/proc/cpuinfo` or `sysctl` (brand string) |
| RAM | `/proc/meminfo` or `sysctl hw.memsize` |
The `recommend_engine()` function maps hardware to engines:
| Hardware | Recommended Engine |
|----------|-------------------|
| No GPU | `llamacpp` (CPU-optimized) |
| Apple Silicon | `ollama` (Metal acceleration) |
| NVIDIA datacenter (A100, H100, etc.) | `vllm` (high throughput) |
| NVIDIA consumer | `ollama` (easy setup) |
| AMD GPU | `vllm` (ROCm support) |
This recommendation is written to `config.toml` during `jarvis init` and used as the default engine:
```bash
jarvis init --force
# Detects hardware, writes ~/.openjarvis/config.toml with:
# [engine]
# default = "vllm" # (for A100)
```
---
## 5. Telemetry-Native
Every inference call automatically records timing, token counts, energy usage, and cost to a local SQLite database. Telemetry is a **first-class concern**, not an afterthought.
```python
@dataclass(slots=True)
class TelemetryRecord:
timestamp: float
model_id: str
prompt_tokens: int
completion_tokens: int
total_tokens: int
latency_seconds: float
ttft: float # Time to first token
cost_usd: float
energy_joules: float
power_watts: float
engine: str
agent: str
```
The `instrumented_generate()` wrapper handles all telemetry transparently:
1. Records start time
2. Calls the engine's `generate()` method
3. Records end time and extracts token counts
4. Publishes a `TELEMETRY_RECORD` event on the EventBus
5. The `TelemetryStore` (subscribed to the bus) persists the record
The `TelemetryAggregator` provides read-only queries over stored records:
```bash
jarvis telemetry stats # Aggregated statistics
jarvis telemetry export --json # Export all records
```
!!! note "Telemetry is best-effort"
If telemetry setup fails (e.g., database is locked), the system continues
without telemetry rather than raising an error. Telemetry never blocks
the query flow.
---
## 6. Python-First
OpenJarvis provides a **clean Python API** through the `Jarvis` class. There is no framework lock-in -- the SDK is a standard Python package with dataclass-based types and no required web framework.
```python
from openjarvis import Jarvis
j = Jarvis()
response = j.ask("Hello")
# Full control
result = j.ask_full(
"Explain quantum computing",
model="qwen3:8b",
agent="orchestrator",
tools=["think"],
temperature=0.5,
max_tokens=2048,
)
# Memory operations
j.memory.index("./docs/")
results = j.memory.search("quantum computing")
# Resource cleanup
j.close()
```
Design choices that support this principle:
- **Dataclasses** for all structured types (`Message`, `ModelSpec`, `Trace`, etc.)
- **Type hints** throughout the codebase
- **No magic** -- explicit initialization, clear method signatures
- **Optional dependencies** via extras (`openjarvis[server]`, `openjarvis[memory-colbert]`, etc.)
- **Standard packaging** with `hatchling` build backend and `uv` package manager
---
## 7. OpenAI-Compatible
The API server (`jarvis serve`) implements the **OpenAI chat completions API format**, making OpenJarvis a drop-in replacement for OpenAI in existing applications.
Supported endpoints:
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/v1/chat/completions` | POST | Chat completions (streaming and non-streaming) |
| `/v1/models` | GET | List available models |
| `/health` | GET | Health check |
Request and response formats match the OpenAI API specification:
```bash
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qwen3:8b",
"messages": [{"role": "user", "content": "Hello"}],
"temperature": 0.7,
"max_tokens": 1024,
"stream": false
}'
```
Streaming responses use Server-Sent Events (SSE) with `data: [DONE]` termination, matching the OpenAI streaming protocol.
Any OpenAI client library can connect to OpenJarvis:
```python
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed")
response = client.chat.completions.create(
model="qwen3:8b",
messages=[{"role": "user", "content": "Hello"}],
)
```
---
## 8. Standalone
OpenJarvis requires **no external services** for core functionality. Everything needed to run the system is included or uses standard system libraries.
| Component | Dependency |
|-----------|-----------|
| Configuration | TOML file, built-in `tomllib` (Python 3.11+) or `tomli` |
| Memory (default) | Built-in `sqlite3` module |
| Telemetry | Built-in `sqlite3` module |
| Traces | Built-in `sqlite3` module |
| HTTP client | `httpx` (lightweight, pure Python) |
| CLI | `click` + `rich` |
| Event bus | Built-in `threading` module |
The only external requirement is a running inference engine (Ollama, vLLM, etc.), which is the model server itself -- not a dependency of OpenJarvis.
Optional features that require additional packages:
| Feature | Extra | Packages |
|---------|-------|----------|
| FAISS memory | `openjarvis[memory-faiss]` | `faiss-cpu`, `sentence-transformers` |
| ColBERT memory | `openjarvis[memory-colbert]` | `colbert-ai`, `torch` |
| BM25 memory | `openjarvis[memory-bm25]` | `rank-bm25` |
| API server | `openjarvis[server]` | `fastapi`, `uvicorn` |
| Cloud inference | `openjarvis[inference-cloud]` | `openai`, `anthropic`, `google-genai` |
| vLLM engine | `openjarvis[inference-vllm]` | `vllm` |
| PDF ingestion | `openjarvis[memory-pdf]` | `pdfplumber` |
| OpenClaw | `openjarvis[openclaw]` | Node.js 22+ |
This design ensures that a minimal installation (`pip install openjarvis`) gives you a fully functional system with SQLite memory, local inference, and the complete CLI -- no Docker, no external databases, no cloud accounts required.
+287
View File
@@ -0,0 +1,287 @@
# Inference Engine Pillar
The Engine pillar provides the **inference runtime** -- the layer that connects OpenJarvis to language model servers. All backends implement a uniform interface, making it straightforward to swap between local and cloud inference without changing application code.
---
## InferenceEngine ABC
Every engine backend extends the `InferenceEngine` abstract base class:
```python
class InferenceEngine(ABC):
engine_id: str
@abstractmethod
def generate(
self,
messages: Sequence[Message],
*,
model: str,
temperature: float = 0.7,
max_tokens: int = 1024,
**kwargs: Any,
) -> Dict[str, Any]:
"""Synchronous completion -- returns a dict with 'content' and 'usage'."""
@abstractmethod
async def stream(
self,
messages: Sequence[Message],
*,
model: str,
temperature: float = 0.7,
max_tokens: int = 1024,
**kwargs: Any,
) -> AsyncIterator[str]:
"""Yield token strings as they are generated."""
@abstractmethod
def list_models(self) -> List[str]:
"""Return identifiers of models available on this engine."""
@abstractmethod
def health(self) -> bool:
"""Return True when the engine is reachable and healthy."""
def prepare(self, model: str) -> None:
"""Optional warm-up hook called before the first request."""
```
### Return Format
The `generate()` method returns a dictionary with the following structure:
```python
{
"content": "The model's response text",
"usage": {
"prompt_tokens": 42,
"completion_tokens": 128,
"total_tokens": 170,
},
"model": "qwen3:8b",
"finish_reason": "stop",
"tool_calls": [...] # Optional, present if model requested tool calls
}
```
When the model requests tool calls, they are extracted and passed through in OpenAI format:
```python
{
"tool_calls": [
{
"id": "call_abc123",
"name": "calculator",
"arguments": "{\"expression\": \"2 + 2\"}"
}
]
}
```
---
## Backend Comparison
| Backend | Registry Key | Protocol | GPU Required | Best For |
|---------|-------------|----------|-------------|----------|
| **Ollama** | `ollama` | Native HTTP API | No (GPU optional) | Getting started, consumer GPUs, Apple Silicon |
| **vLLM** | `vllm` | OpenAI-compatible | NVIDIA recommended | Datacenter GPUs (A100, H100), high throughput |
| **SGLang** | `sglang` | OpenAI-compatible | NVIDIA recommended | Structured generation, speculative decoding |
| **llama.cpp** | `llamacpp` | OpenAI-compatible | No (CPU-optimized) | CPU-only systems, GGUF models, edge devices |
| **Cloud** | `cloud` | Provider SDKs | No | OpenAI, Anthropic, Google API access |
### Ollama
The Ollama backend communicates via Ollama's native HTTP API at `/api/chat` and `/api/tags`. It is the default engine on Apple Silicon and consumer NVIDIA GPUs.
- **Default host:** `http://localhost:11434`
- **Health check:** `GET /api/tags`
- **Model listing:** `GET /api/tags` (extracts model names)
- **Tool support:** Passes `tools` in the request payload and extracts `tool_calls` from responses
### vLLM
The vLLM backend uses the OpenAI-compatible `/v1/chat/completions` API. It is recommended for datacenter GPUs (A100, H100, L40, A10, A30) and AMD GPUs.
- **Default host:** `http://localhost:8000`
- **Health check:** `GET /v1/models`
- **Tool fallback:** If the server returns HTTP 400 when tools are included, the engine automatically retries without tools
### SGLang
The SGLang backend also uses the OpenAI-compatible API. It shares the same `_OpenAICompatibleEngine` base class as vLLM and llama.cpp.
- **Default host:** `http://localhost:30000`
- **Health check:** `GET /v1/models`
### llama.cpp
The llama.cpp backend connects to a `llama-server` instance via the OpenAI-compatible API. It is recommended for CPU-only systems and GGUF-quantized models.
- **Default host:** `http://localhost:8080`
- **Health check:** `GET /v1/models`
### Cloud
The Cloud backend provides access to OpenAI, Anthropic, and Google models via their respective Python SDKs. It automatically detects the provider based on the model name:
- Models containing `"claude"` route to the **Anthropic** client
- Models containing `"gemini"` route to the **Google** client
- All other models route to the **OpenAI** client
!!! info "API Keys"
Cloud models require API keys set as environment variables:
`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY` (or `GOOGLE_API_KEY`).
The cloud engine is only registered if the corresponding SDK packages are installed.
---
## Hardware Auto-Detection
OpenJarvis automatically detects system hardware to recommend the best engine. Detection runs at config load time via `detect_hardware()`:
| Detection | Method | Information Extracted |
|-----------|--------|---------------------|
| NVIDIA GPU | `nvidia-smi` | GPU name, VRAM (GB), count |
| AMD GPU | `rocm-smi` | GPU name |
| Apple Silicon | `system_profiler SPDisplaysDataType` | Chipset model name |
| CPU | `/proc/cpuinfo` or `sysctl` | Brand string |
| RAM | `/proc/meminfo` or `sysctl hw.memsize` | Total GB |
### Engine Recommendation Logic
The `recommend_engine()` function maps hardware to the best engine:
```mermaid
graph TD
A["detect_hardware()"] --> B{"GPU detected?"}
B -->|No| C["llamacpp"]
B -->|Yes| D{"GPU vendor?"}
D -->|Apple| E["ollama"]
D -->|NVIDIA| F{"Datacenter card?<br/>(A100, H100, H200,<br/>L40, A10, A30)"}
F -->|Yes| G["vllm"]
F -->|No| H["ollama"]
D -->|AMD| I["vllm"]
D -->|Other| J["llamacpp"]
```
---
## Engine Discovery
The `_discovery.py` module provides three functions for finding and instantiating engines at runtime.
### `get_engine(config, engine_key=None)`
Returns a `(key, engine_instance)` tuple for the requested engine, or `None` if unavailable:
1. If `engine_key` is specified, try to instantiate and health-check that specific engine
2. Otherwise, try the default engine from config
3. If the default is unhealthy, fall back to any healthy engine via `discover_engines()`
### `discover_engines(config)`
Probes all registered engines for health and returns a sorted list of healthy `(key, engine)` pairs. The config default engine is sorted first.
```python
from openjarvis.engine import discover_engines
from openjarvis.core.config import load_config
config = load_config()
healthy = discover_engines(config)
# [("ollama", OllamaEngine(...)), ("vllm", VLLMEngine(...))]
```
### `discover_models(engines)`
Calls `list_models()` on each engine and returns a dictionary mapping engine keys to model ID lists:
```python
from openjarvis.engine import discover_engines, discover_models
engines = discover_engines(config)
models = discover_models(engines)
# {"ollama": ["qwen3:8b", "llama3.2:3b"], "vllm": ["mistral:7b"]}
```
---
## OpenAI Compatibility Layer
The `_OpenAICompatibleEngine` base class provides a shared implementation for engines that serve the standard `/v1/chat/completions` endpoint. vLLM, SGLang, and llama.cpp all extend this base class with minimal overrides -- typically just setting `engine_id` and `_default_host`.
```python
class _OpenAICompatibleEngine(InferenceEngine):
engine_id: str = ""
_default_host: str = "http://localhost:8000"
def __init__(self, host: str | None = None, *, timeout: float = 120.0):
self._host = (host or self._default_host).rstrip("/")
self._client = httpx.Client(base_url=self._host, timeout=timeout)
```
Key behaviors:
- **Synchronous generation:** `POST /v1/chat/completions` with `stream=False`
- **Streaming:** `POST /v1/chat/completions` with `stream=True`, parsing SSE `data:` lines
- **Model listing:** `GET /v1/models`, extracting `data[].id`
- **Health check:** `GET /v1/models` with a 2-second timeout
- **Tool call fallback:** On HTTP 400 with tools in the payload, retries without tools (handles engines that do not support function calling)
---
## Configuration
Engine hosts and defaults are configured in `~/.openjarvis/config.toml`:
```toml
[engine]
default = "ollama"
ollama_host = "http://localhost:11434"
vllm_host = "http://localhost:8000"
llamacpp_host = "http://localhost:8080"
sglang_host = "http://localhost:30000"
```
The `EngineConfig` dataclass maps these settings:
| Field | Default | Description |
|-------|---------|-------------|
| `default` | `"ollama"` (hardware-dependent) | Preferred engine backend |
| `ollama_host` | `http://localhost:11434` | Ollama server URL |
| `vllm_host` | `http://localhost:8000` | vLLM server URL |
| `llamacpp_host` | `http://localhost:8080` | llama.cpp server URL |
| `sglang_host` | `http://localhost:30000` | SGLang server URL |
| `llamacpp_path` | `""` | Path to llama.cpp binary (for managed mode) |
---
## Utility Functions
### `messages_to_dicts()`
Converts a sequence of `Message` objects to OpenAI-format dictionaries, handling tool calls and tool call IDs:
```python
from openjarvis.engine._base import messages_to_dicts
from openjarvis.core.types import Message, Role
messages = [Message(role=Role.USER, content="Hello")]
dicts = messages_to_dicts(messages)
# [{"role": "user", "content": "Hello"}]
```
### `EngineConnectionError`
A custom exception raised when an engine is unreachable. All engine backends catch `httpx.ConnectError` and `httpx.TimeoutException` and re-raise as `EngineConnectionError`:
```python
from openjarvis.engine import EngineConnectionError
try:
result = engine.generate(messages, model="qwen3:8b")
except EngineConnectionError as exc:
print(f"Engine unavailable: {exc}")
```
+199
View File
@@ -0,0 +1,199 @@
# Intelligence Pillar
The Intelligence pillar handles **model management and query routing**. It maintains a catalog of known models with detailed metadata and provides a heuristic router that selects the best model for a given query based on its characteristics.
---
## Purpose
When a user sends a query to OpenJarvis, the system needs to decide which model should handle it. A short, simple question like "What time is it?" does not need a 70B parameter model, while a complex multi-step math problem benefits from the largest available model. The Intelligence pillar encapsulates this decision-making logic.
The pillar provides three key capabilities:
1. **Model catalog** -- a registry of well-known models with metadata (parameter count, context length, VRAM requirements, supported engines)
2. **Query routing** -- analyzing query characteristics and selecting the optimal model
3. **Auto-discovery** -- merging models discovered from running engines into the catalog
---
## ModelSpec
Every model in the system is described by a `ModelSpec` dataclass, defined in `core/types.py`:
```python
@dataclass(slots=True)
class ModelSpec:
model_id: str # Unique identifier (e.g., "qwen3:8b")
name: str # Human-readable name
parameter_count_b: float # Total parameters in billions
context_length: int # Maximum context window (tokens)
active_parameter_count_b: Optional[float] # MoE active params (None for dense)
quantization: Quantization # Quantization format (none, fp8, int4, etc.)
min_vram_gb: float # Minimum VRAM required
supported_engines: Sequence[str] # Which engines can run this model
provider: str # Model provider (e.g., "alibaba", "meta")
requires_api_key: bool # Whether cloud API key is needed
metadata: Dict[str, Any] # Additional metadata (pricing, architecture)
```
Models are registered in the `ModelRegistry`:
```python
from openjarvis.core.registry import ModelRegistry
# Register a model
ModelRegistry.register_value("qwen3:8b", ModelSpec(
model_id="qwen3:8b",
name="Qwen3 8B",
parameter_count_b=8.2,
context_length=32768,
supported_engines=("vllm", "ollama", "llamacpp", "sglang"),
provider="alibaba",
))
```
---
## Model Catalog
The built-in model catalog is defined in `intelligence/model_catalog.py` as the `BUILTIN_MODELS` list. It includes models across three categories:
### Local Models -- Dense
| Model ID | Name | Parameters | Context | Supported Engines |
|----------|------|-----------|---------|-------------------|
| `qwen3:8b` | Qwen3 8B | 8.2B | 32K | vLLM, Ollama, llama.cpp, SGLang |
| `qwen3:32b` | Qwen3 32B | 32B | 32K | Ollama, vLLM |
| `llama3.3:70b` | Llama 3.3 70B | 70B | 128K | Ollama, vLLM |
| `llama3.2:3b` | Llama 3.2 3B | 3B | 128K | Ollama, vLLM, llama.cpp |
| `deepseek-coder-v2:16b` | DeepSeek Coder V2 16B | 16B | 128K | Ollama, vLLM |
| `mistral:7b` | Mistral 7B | 7B | 32K | Ollama, vLLM, llama.cpp |
### Local Models -- Mixture of Experts (MoE)
| Model ID | Name | Total / Active Params | Context | Min VRAM |
|----------|------|----------------------|---------|----------|
| `gpt-oss:120b` | GPT-OSS 120B | 117B / 5.1B | 128K | 12 GB |
| `glm-4.7-flash` | GLM 4.7 Flash | 30B / 3B | 128K | 8 GB |
| `trinity-mini` | Trinity Mini | 26B / 3B | 128K | 8 GB |
### Cloud Models
| Model ID | Provider | Context | Pricing (input/output per 1M tokens) |
|----------|----------|---------|--------------------------------------|
| `gpt-4o` | OpenAI | 128K | $2.50 / $10.00 |
| `gpt-4o-mini` | OpenAI | 128K | $0.15 / $0.60 |
| `gpt-5-mini` | OpenAI | 400K | $0.25 / $2.00 |
| `claude-sonnet-4-20250514` | Anthropic | 200K | $3.00 / $15.00 |
| `claude-opus-4-20250514` | Anthropic | 200K | $15.00 / $75.00 |
| `claude-opus-4-6` | Anthropic | 200K | $5.00 / $25.00 |
| `gemini-2.5-pro` | Google | 1M | $1.25 / $10.00 |
| `gemini-2.5-flash` | Google | 1M | $0.30 / $2.50 |
### Registering Built-in Models
The `register_builtin_models()` function populates the `ModelRegistry` with all built-in models. It skips models that are already registered, making it safe to call multiple times:
```python
from openjarvis.intelligence import register_builtin_models
register_builtin_models()
# All BUILTIN_MODELS are now in ModelRegistry
```
---
## Auto-Discovery: Merging Runtime Models
When engines are discovered at runtime, they report models that may not be in the built-in catalog. The `merge_discovered_models()` function creates minimal `ModelSpec` entries for these:
```python
from openjarvis.intelligence import merge_discovered_models
# Models reported by Ollama that aren't in the catalog
merge_discovered_models("ollama", ["phi3:3.8b", "codellama:7b"])
```
For each model ID not already in the registry, a `ModelSpec` is created with the model ID as both the `model_id` and `name`, with zero-value defaults for unknown fields. This ensures the routing system can still select from all available models, even ones it has no metadata for.
---
## HeuristicRouter
The `HeuristicRouter` is a rule-based model router that selects the best model based on query characteristics. It applies six priority rules in order:
### Routing Rules
| Priority | Rule | Condition | Action |
|----------|------|-----------|--------|
| 1 | Code detection | Query contains code patterns (backticks, `def`, `class`, `import`, `function`, `=>`, etc.) | Prefer model with "code" or "coder" in name; fall back to largest model |
| 2 | Math detection | Query contains math keywords (`solve`, `integral`, `equation`, `calculate`, `compute`, etc.) | Select the largest available model |
| 3 | Short query | Query length < 50 characters, no code/math | Select the smallest available model (faster response) |
| 4 | Long/complex query | Query length > 500 characters OR contains reasoning keywords (`explain`, `analyze`, `compare`, `step-by-step`, etc.) | Select the largest available model |
| 5 | High urgency | `urgency > 0.8` | Override to smallest model (fastest response) |
| 6 | Default fallback | None of the above match | Use `default_model`, then `fallback_model`, then first available |
!!! note "Priority 5 overrides all others"
The urgency check (rule 5) is actually evaluated **first** in the code -- if urgency exceeds 0.8, the router immediately returns the smallest model regardless of query content.
### Usage
```python
from openjarvis.intelligence import HeuristicRouter, build_routing_context
router = HeuristicRouter(
available_models=["qwen3:8b", "llama3.2:3b", "deepseek-coder-v2:16b"],
default_model="qwen3:8b",
fallback_model="llama3.2:3b",
)
ctx = build_routing_context("Write a Python function to sort a list")
model = router.select_model(ctx) # Returns "deepseek-coder-v2:16b" (has "coder")
```
---
## build_routing_context()
The `build_routing_context()` function analyzes a raw query string and produces a `RoutingContext` dataclass:
```python
@dataclass(slots=True)
class RoutingContext:
query: str = ""
query_length: int = 0
has_code: bool = False
has_math: bool = False
language: str = "en"
urgency: float = 0.5 # 0 = low priority, 1 = real-time
metadata: Dict[str, Any] = field(default_factory=dict)
```
**Code detection** uses regex patterns matching:
- Backtick code blocks (`` ``` `` or `` `inline` ``)
- Language keywords (`def`, `class`, `import`, `function`, `const`, `var`, `let`)
- Syntax patterns (`if (`, `->`, `=>`, `{ }`, `for x in`, `#include`, `System.out`)
**Math detection** uses regex patterns matching:
- Mathematical terms (`solve`, `integral`, `equation`, `proof`, `derivative`, `matrix`)
- Computational keywords (`calculate`, `compute`, `sigma`, `sum`, `limit`, `probability`)
```python
from openjarvis.intelligence import build_routing_context
ctx = build_routing_context("Solve the integral of x^2 dx")
# ctx.has_math = True, ctx.has_code = False, ctx.query_length = 32
ctx = build_routing_context("```python\ndef hello():\n pass\n```")
# ctx.has_code = True, ctx.has_math = False
```
---
## Integration with Learning
The `HeuristicRouter` implements the `RouterPolicy` ABC from the Learning pillar, which means it can be swapped out for a `TraceDrivenPolicy` or any other policy via the `RouterPolicyRegistry`. See the [Learning & Traces](learning.md) documentation for details on how trace-driven routing works.
The router is registered as `"heuristic"` in the `RouterPolicyRegistry` and is the default routing policy. Users can switch policies via the `--router` CLI flag or the `learning.default_policy` config setting.
+406
View File
@@ -0,0 +1,406 @@
# Learning & Traces
The Learning system is a **cross-cutting concern** that connects all four pillars through trace-driven feedback. It determines which model handles each query (router policies), records the full interaction as a trace, analyzes outcomes, and updates policies based on what worked.
---
## RouterPolicy ABC
All routing policies implement the `RouterPolicy` abstract base class:
```python
class RouterPolicy(ABC):
@abstractmethod
def select_model(self, context: RoutingContext) -> str:
"""Return the model registry key best suited for *context*."""
```
### RoutingContext
The `RoutingContext` dataclass captures the characteristics of an incoming query:
```python
@dataclass(slots=True)
class RoutingContext:
query: str = "" # The raw query text
query_length: int = 0 # Character count
has_code: bool = False # Whether code patterns were detected
has_math: bool = False # Whether math keywords were detected
language: str = "en" # Detected language
urgency: float = 0.5 # 0 = low priority, 1 = real-time
metadata: Dict[str, Any] = field(default_factory=dict)
```
---
## RouterPolicyRegistry
Router policies are registered in the `RouterPolicyRegistry` and selected at runtime. The system ships with three policies:
| Registry Key | Policy Class | Status | Description |
|-------------|-------------|--------|-------------|
| `heuristic` | `HeuristicRouter` | Active | Rule-based routing with 6 priority rules |
| `learned` | `TraceDrivenPolicy` | Active | Learns from trace outcomes |
| `grpo` | `GRPORouterPolicy` | Stub | Placeholder for future RL training |
Users select a policy via `config.toml` or the `--router` CLI flag:
```toml
[learning]
default_policy = "heuristic"
```
```bash
jarvis ask --router learned "What is the capital of France?"
```
### The `ensure_registered()` Pattern
Learning modules use a lazy registration pattern to survive registry clearing in tests:
```python
def ensure_registered() -> None:
"""Register TraceDrivenPolicy if not already present."""
if not RouterPolicyRegistry.contains("learned"):
RouterPolicyRegistry.register_value("learned", TraceDrivenPolicy)
ensure_registered() # Called at module import time
```
This ensures that policies are available even after `RouterPolicyRegistry.clear()` is called in test teardown, because re-importing the module re-registers them.
---
## HeuristicRouter (Heuristic Policy)
The `HeuristicRouter` is the default routing policy. It uses static rules to select models based on query characteristics. See the [Intelligence Pillar](intelligence.md) documentation for full details on its six priority rules.
The `heuristic_policy.py` module wires the existing `HeuristicRouter` (from the Intelligence pillar) into the `RouterPolicyRegistry`:
```python
# learning/heuristic_policy.py
def ensure_registered() -> None:
if not RouterPolicyRegistry.contains("heuristic"):
RouterPolicyRegistry.register_value("heuristic", HeuristicRouter)
ensure_registered()
```
---
## TraceDrivenPolicy (Learned Policy)
The `TraceDrivenPolicy` learns from historical traces to determine which model performs best for different types of queries. Unlike the heuristic router's static rules, this policy adapts based on actual outcomes.
### Query Classification
Queries are classified into broad categories for grouping:
| Category | Condition |
|----------|-----------|
| `code` | Contains code patterns (backticks, `def`, `class`, `import`, `function`) |
| `math` | Contains math keywords (`solve`, `integral`, `equation`, `calculate`, `compute`) |
| `short` | Query length < 50 characters |
| `long` | Query length > 500 characters |
| `general` | None of the above |
### Model Selection
When `select_model()` is called:
1. Classify the query into a category
2. If the policy map has an entry for this category **and** the confidence (sample count) exceeds `min_samples` (default: 5), use the learned model
3. Otherwise, fall back to: `default_model` -> `fallback_model` -> first available model
### Batch Updates via `update_from_traces()`
The primary update mechanism reads all traces from a `TraceAnalyzer` and recomputes the policy map:
```python
from openjarvis.learning.trace_policy import TraceDrivenPolicy
from openjarvis.traces.analyzer import TraceAnalyzer
from openjarvis.traces.store import TraceStore
store = TraceStore("traces.db")
analyzer = TraceAnalyzer(store)
policy = TraceDrivenPolicy(
analyzer=analyzer,
available_models=["qwen3:8b", "llama3.2:3b", "deepseek-coder-v2:16b"],
default_model="qwen3:8b",
)
# Recompute routing decisions from trace history
result = policy.update_from_traces()
# {"updated": True, "query_classes": 3, "total_traces": 150, "changes": {...}}
```
The update algorithm:
1. Fetches all traces (optionally filtered by time range)
2. Groups traces by query classification
3. For each query class, scores each model using a **composite score**:
- 60% success rate (fraction of traces with `outcome="success"`)
- 40% average feedback score (user quality ratings)
4. Selects the model with the highest composite score for each query class
5. Returns a summary of changes
### Online Updates via `observe()`
For real-time policy updates after every interaction:
```python
policy.observe(
query="Write a Python function",
model="deepseek-coder-v2:16b",
outcome="success",
feedback=0.9,
)
```
The online update uses a conservative strategy: it only switches the preferred model for a query class when the new model shows clearly better outcomes (`feedback > 0.7`) and the existing policy has fewer than `min_samples` observations.
---
## GRPORouterPolicy (Stub)
The `GRPORouterPolicy` is a placeholder for future reinforcement learning-based routing. Currently, calling `select_model()` raises `NotImplementedError`:
```python
class GRPORouterPolicy(RouterPolicy):
def select_model(self, context: RoutingContext) -> str:
raise NotImplementedError(
"GRPORouterPolicy is not yet implemented. "
"GRPO training will be available in Phase 5."
)
```
---
## RewardFunction ABC
The `RewardFunction` ABC defines how to score completed inferences for use in training:
```python
class RewardFunction(ABC):
@abstractmethod
def compute(
self,
context: RoutingContext,
model_key: str,
response: str,
**kwargs: Any,
) -> float:
"""Return a reward in [0, 1]."""
```
### HeuristicRewardFunction
The built-in reward function computes a weighted combination of three factors:
| Factor | Weight (default) | Normalization | Score Range |
|--------|-----------------|---------------|-------------|
| **Latency** | 0.4 | `1 - (latency / max_latency)` | 0 = 30s+, 1 = instant |
| **Cost** | 0.3 | `1 - (cost / max_cost)` | 0 = $0.01+, 1 = free |
| **Efficiency** | 0.3 | `completion_tokens / total_tokens` | 0 = all prompt, 1 = all completion |
```python
from openjarvis.learning.heuristic_reward import HeuristicRewardFunction
reward_fn = HeuristicRewardFunction(
weight_latency=0.4,
weight_cost=0.3,
weight_efficiency=0.3,
max_latency=30.0, # seconds
max_cost=0.01, # USD
)
reward = reward_fn.compute(
context=routing_context,
model_key="qwen3:8b",
response="The answer is 42.",
latency_seconds=1.2,
cost_usd=0.0,
prompt_tokens=50,
completion_tokens=10,
)
# Returns a float in [0, 1]
```
---
## Trace System
The trace system records the full sequence of steps in every agent interaction, providing the raw data that the learning system uses to improve.
### TraceStore
`TraceStore` is an append-only SQLite store for interaction traces:
```python
from openjarvis.traces.store import TraceStore
store = TraceStore("~/.openjarvis/traces.db")
store.save(trace) # Persist a complete trace
trace = store.get("abc123") # Retrieve by trace ID
traces = store.list_traces( # Query with filters
agent="orchestrator",
model="qwen3:8b",
outcome="success",
since=1700000000.0,
limit=100,
)
count = store.count() # Total trace count
```
**Database schema:**
- `traces` table -- one row per interaction (trace_id, query, agent, model, engine, result, outcome, feedback, timing, tokens, metadata)
- `trace_steps` table -- one row per step within a trace (step_type, timestamp, duration, input, output, metadata)
**EventBus integration:** The store can subscribe to `TRACE_COMPLETE` events for automatic persistence:
```python
store.subscribe_to_bus(bus)
# Any TRACE_COMPLETE event will now auto-save the trace
```
### TraceCollector
`TraceCollector` wraps any `BaseAgent` and automatically records a `Trace` for every `run()` call:
```python
from openjarvis.traces.collector import TraceCollector
agent = OrchestratorAgent(engine, model, tools=tools, bus=bus)
collector = TraceCollector(agent, store=trace_store, bus=bus)
result = collector.run("What is 2+2?")
# Trace is automatically saved to trace_store
```
How it works:
1. Subscribes to EventBus events before running the agent:
- `INFERENCE_START` / `INFERENCE_END` -- creates `GENERATE` steps
- `TOOL_CALL_START` / `TOOL_CALL_END` -- creates `TOOL_CALL` steps
- `MEMORY_RETRIEVE` -- creates `RETRIEVE` steps
2. Runs the wrapped agent's `run()` method
3. Unsubscribes from events
4. Adds a final `RESPOND` step
5. Builds a `Trace` object with all collected steps
6. Saves to the `TraceStore` and publishes `TRACE_COMPLETE`
### TraceAnalyzer
`TraceAnalyzer` provides a read-only query layer over stored traces, computing aggregated statistics:
```python
from openjarvis.traces.analyzer import TraceAnalyzer
analyzer = TraceAnalyzer(store)
# Overall summary
summary = analyzer.summary()
# TraceSummary(total_traces=150, avg_latency=2.3, success_rate=0.85, ...)
# Stats grouped by (model, agent) routing decisions
route_stats = analyzer.per_route_stats()
# [RouteStats(model="qwen3:8b", agent="orchestrator", count=45, avg_latency=1.8, ...), ...]
# Stats grouped by tool
tool_stats = analyzer.per_tool_stats()
# [ToolStats(tool_name="calculator", call_count=23, avg_latency=0.01, success_rate=1.0), ...]
# Find traces matching query characteristics
code_traces = analyzer.traces_for_query_type(has_code=True)
# Export traces as plain dicts (for JSON serialization)
exported = analyzer.export_traces(limit=1000)
```
**Computed statistics:**
| Dataclass | Fields |
|-----------|--------|
| `TraceSummary` | total_traces, total_steps, avg_steps_per_trace, avg_latency, avg_tokens, success_rate, step_type_distribution |
| `RouteStats` | model, agent, count, avg_latency, avg_tokens, success_rate, avg_feedback |
| `ToolStats` | tool_name, call_count, avg_latency, success_rate |
---
## The Learning Loop
The trace-driven learning loop connects all the pieces:
```mermaid
graph TB
subgraph "Runtime"
Q["User Query"] --> AGT["Agent executes"]
AGT --> ENG["Engine generates"]
ENG --> RESP["Response returned"]
end
subgraph "Recording"
AGT -.->|"events"| COL["TraceCollector"]
ENG -.->|"events"| COL
COL -->|"save"| STO["TraceStore<br/>(SQLite)"]
end
subgraph "Analysis"
STO -->|"read"| ANA["TraceAnalyzer"]
ANA -->|"summary(),<br/>per_route_stats()"| STATS["Aggregated<br/>Statistics"]
end
subgraph "Learning"
STATS -->|"update_from_traces()"| POL["TraceDrivenPolicy"]
POL -->|"select_model()"| Q
end
style Q fill:#e1f5fe
style RESP fill:#e8f5e9
style POL fill:#fff3e0
```
### Step-by-step cycle:
1. **Query arrives** -- The system needs to select a model
2. **Router policy selects model** -- `TraceDrivenPolicy.select_model()` checks the learned policy map; falls back to heuristic if insufficient data
3. **Agent executes** -- The agent processes the query, calling tools and memory as needed
4. **Events captured** -- The `TraceCollector` captures all events (inference, tool calls, memory retrieval) during execution
5. **Trace saved** -- A complete `Trace` with all `TraceStep` objects is saved to `TraceStore`
6. **Analysis** -- Periodically, `TraceAnalyzer` computes aggregate statistics from stored traces
7. **Policy update** -- `TraceDrivenPolicy.update_from_traces()` recomputes the `query_class -> model` mapping based on success rates and feedback scores
8. **Better routing** -- The next query benefits from the updated routing decisions
### Trace Data Model
Each interaction produces a `Trace` containing multiple `TraceStep` objects:
```
Trace
trace_id: "a1b2c3d4e5f6"
query: "What is 2+2?"
agent: "orchestrator"
model: "qwen3:8b"
engine: "ollama"
steps:
[0] GENERATE -- model inference, 0.8s, 150 tokens
[1] TOOL_CALL -- calculator, 0.01s, success
[2] GENERATE -- model inference, 0.5s, 80 tokens
[3] RESPOND -- final answer
result: "2+2 = 4"
outcome: "success"
feedback: 1.0
total_latency_seconds: 1.31
total_tokens: 230
```
**Step types:**
| StepType | Description | Created By |
|----------|-------------|------------|
| `ROUTE` | Model selection decision | Router policy |
| `RETRIEVE` | Memory search | Memory backend |
| `GENERATE` | LLM inference call | Engine |
| `TOOL_CALL` | Tool execution | ToolExecutor |
| `RESPOND` | Final response | TraceCollector |
+347
View File
@@ -0,0 +1,347 @@
# Memory Pillar
The Memory pillar provides **persistent, searchable storage** for documents and knowledge. It enables context injection -- retrieving relevant information from indexed documents and prepending it to prompts so the LLM can answer questions grounded in specific content.
---
## MemoryBackend ABC
All memory backends implement the `MemoryBackend` abstract base class:
```python
class MemoryBackend(ABC):
backend_id: str
@abstractmethod
def store(
self,
content: str,
*,
source: str = "",
metadata: Optional[Dict[str, Any]] = None,
) -> str:
"""Persist *content* and return a unique document id."""
@abstractmethod
def retrieve(
self,
query: str,
*,
top_k: int = 5,
**kwargs: Any,
) -> List[RetrievalResult]:
"""Search for *query* and return the top-k results."""
@abstractmethod
def delete(self, doc_id: str) -> bool:
"""Delete a document by id. Return True if it existed."""
@abstractmethod
def clear(self) -> None:
"""Remove all stored documents."""
```
### RetrievalResult
Search results are returned as `RetrievalResult` objects:
```python
@dataclass(slots=True)
class RetrievalResult:
content: str # The document text
score: float = 0.0 # Relevance score (higher is better)
source: str = "" # Originating file path or identifier
metadata: Dict[str, Any] = field(default_factory=dict)
```
---
## Backend Comparison
| Backend | Registry Key | Index Type | Extra Dependencies | GPU Required | Quality | Speed | Persistence |
|---------|-------------|-----------|-------------------|-------------|---------|-------|-------------|
| **SQLite/FTS5** | `sqlite` | Full-text (BM25) | None | No | Good | Fast | Disk (SQLite) |
| **FAISS** | `faiss` | Dense vector | `faiss-cpu`, `sentence-transformers` | Optional | Very Good | Fast | In-memory |
| **ColBERTv2** | `colbert` | Late interaction | `colbert-ai`, `torch` | Optional | Excellent | Slower | In-memory |
| **BM25** | `bm25` | Term-frequency | `rank-bm25` | No | Good | Fast | In-memory |
| **Hybrid** | `hybrid` | RRF fusion | Depends on sub-backends | Depends | Best | Moderate | Depends |
### SQLite/FTS5 (Default)
The zero-dependency default backend. Uses SQLite's built-in FTS5 extension for full-text search with BM25 ranking.
- **Storage:** Documents stored in a `documents` table with automatic FTS5 indexing via triggers
- **Search:** FTS5 `MATCH` queries with BM25 ranking (more negative rank = better match, converted to positive scores)
- **Query escaping:** Each word is quoted to avoid FTS5 syntax errors
- **Persistence:** Data persists across restarts in `~/.openjarvis/memory.db`
### FAISS
Dense retrieval using Facebook AI Similarity Search. Documents are embedded into vector space and searched via cosine similarity.
- **Index type:** `IndexFlatIP` (inner-product, equivalent to cosine similarity when vectors are L2-normalized)
- **Embedding model:** `all-MiniLM-L6-v2` by default (384-dim, ~22 MB)
- **Deletion:** Soft-delete (documents are marked as deleted but remain in the index)
- **Persistence:** In-memory only -- data is lost on restart
### ColBERTv2
Late interaction retrieval using token-level embeddings with MaxSim scoring. Provides the highest retrieval quality at the cost of higher latency.
- **Scoring:** For each query token, finds the maximum cosine similarity across all document tokens, then sums across query tokens
- **Checkpoint:** `colbert-ir/colbertv2.0` (lazily loaded on first use)
- **Persistence:** In-memory only
!!! warning "Heavy dependencies"
ColBERTv2 requires `colbert-ai` and `torch`, which are large packages. Install with:
`pip install openjarvis[memory-colbert]`
### BM25
Classic Okapi BM25 probabilistic ranking function using the `rank_bm25` library.
- **Tokenization:** Lowercase whitespace split
- **Index:** Rebuilt on every `store()` and `delete()` operation
- **Filtering:** Results are filtered to require at least one shared token with the query (handles edge cases where BM25 assigns IDF=0)
- **Persistence:** In-memory only
### Hybrid (RRF Fusion)
Combines a sparse retriever and a dense retriever using Reciprocal Rank Fusion:
$$\text{RRF}(d) = \sum_{i} \frac{w_i}{k + \text{rank}_i(d)}$$
- **Sub-backends:** Any two `MemoryBackend` implementations (e.g., SQLite + FAISS)
- **Over-fetch:** Retrieves `top_k * 3` results from each sub-backend for better fusion
- **Configurable:** RRF constant `k` (default 60) and per-backend weights
```python
from openjarvis.memory.sqlite import SQLiteMemory
from openjarvis.memory.faiss_backend import FAISSMemory
from openjarvis.memory.hybrid import HybridMemory
hybrid = HybridMemory(
sparse=SQLiteMemory(db_path="memory.db"),
dense=FAISSMemory(),
sparse_weight=1.0,
dense_weight=1.5, # Weight dense retrieval more heavily
)
```
---
## Chunking Pipeline
Large documents are split into manageable chunks before storage. The chunking pipeline is defined in `memory/chunking.py`.
### ChunkConfig
```python
@dataclass(slots=True)
class ChunkConfig:
chunk_size: int = 512 # Maximum tokens per chunk (whitespace-split)
chunk_overlap: int = 64 # Tokens to overlap between consecutive chunks
min_chunk_size: int = 50 # Minimum tokens for a chunk to be kept
```
### Chunk
```python
@dataclass(slots=True)
class Chunk:
content: str # The chunk text
source: str = "" # Originating file path
offset: int = 0 # Token offset within the original document
index: int = 0 # Chunk index (0, 1, 2, ...)
metadata: Dict[str, Any] = field(default_factory=dict)
```
### Chunking Algorithm
The `chunk_text()` function splits text using paragraph boundaries:
1. Split the document on double newlines (`\n\n`) into paragraphs
2. Accumulate paragraphs into the current chunk until `chunk_size` is exceeded
3. When a chunk is full, flush it and keep the last `chunk_overlap` tokens as overlap for the next chunk
4. If a single paragraph exceeds `chunk_size`, split it into fixed-size windows with overlap
5. Discard chunks smaller than `min_chunk_size`
```python
from openjarvis.memory.chunking import chunk_text, ChunkConfig
config = ChunkConfig(chunk_size=256, chunk_overlap=32)
chunks = chunk_text(document_text, source="docs/guide.md", config=config)
```
---
## Document Ingestion
The `memory/ingest.py` module handles reading files and directories into chunks.
### File Type Detection
| Extension | Detected Type |
|-----------|--------------|
| `.md`, `.markdown`, `.mdx` | `markdown` |
| `.pdf` | `pdf` |
| `.py`, `.js`, `.ts`, `.rs`, `.go`, `.java`, `.c`, `.cpp`, `.yaml`, `.json`, `.html`, `.css`, ... | `code` |
| Everything else | `text` |
### `ingest_path(path, config=None)`
Ingests a file or directory into chunks:
- **Single file:** Reads the file, detects its type, and chunks the content
- **Directory:** Recursively walks the tree, skipping:
- Hidden directories (starting with `.`)
- Common non-content directories (`__pycache__`, `node_modules`, `.git`, `.venv`, etc.)
- Binary files (images, audio, video, archives, compiled files)
- Hidden files (starting with `.`)
```python
from pathlib import Path
from openjarvis.memory.ingest import ingest_path
# Ingest a single file
chunks = ingest_path(Path("docs/guide.md"))
# Ingest an entire directory
chunks = ingest_path(Path("./docs/"))
```
### PDF Support
PDF files are read using `pdfplumber`, extracting text from each page and joining with double newlines. This requires the optional `pdfplumber` dependency:
```bash
pip install openjarvis[memory-pdf]
```
---
## Embeddings
Dense retrieval backends (FAISS, ColBERT) require text embeddings. The `memory/embeddings.py` module provides the `Embedder` ABC and a default implementation.
### Embedder ABC
```python
class Embedder(ABC):
@abstractmethod
def embed(self, texts: list[str]) -> Any:
"""Embed texts and return a numpy array of shape (n, dim)."""
@abstractmethod
def dim(self) -> int:
"""Return the dimensionality of the embedding vectors."""
```
### SentenceTransformerEmbedder
The default embedder wraps the `sentence-transformers` library:
- **Default model:** `all-MiniLM-L6-v2` (384 dimensions, ~22 MB)
- **Output:** NumPy arrays of shape `(n, dim)`
```python
from openjarvis.memory.embeddings import SentenceTransformerEmbedder
embedder = SentenceTransformerEmbedder(model_name="all-MiniLM-L6-v2")
vectors = embedder.embed(["Hello world", "How are you?"])
# Shape: (2, 384)
```
---
## Context Injection
The context injection pipeline retrieves relevant documents and prepends them to the prompt with source attribution. This is defined in `memory/context.py`.
### ContextConfig
```python
@dataclass(slots=True)
class ContextConfig:
enabled: bool = True # Whether context injection is active
top_k: int = 5 # Maximum results to retrieve
min_score: float = 0.1 # Minimum relevance score threshold
max_context_tokens: int = 2048 # Maximum tokens of context to inject
```
### `inject_context()`
The main function for context injection:
```python
def inject_context(
query: str,
messages: List[Message],
backend: MemoryBackend,
*,
config: Optional[ContextConfig] = None,
) -> List[Message]:
```
How it works:
1. Retrieves results from the memory backend using the query
2. Filters results below `min_score`
3. Truncates to `max_context_tokens` (approximate token count via whitespace split)
4. Formats results with source attribution tags: `[Source: docs/guide.md] The content...`
5. Creates a system message with the formatted context
6. Returns a **new** message list with the context message prepended
```python
from openjarvis.memory.context import inject_context, ContextConfig
config = ContextConfig(top_k=3, min_score=0.2)
messages = inject_context("What is the API?", messages, backend, config=config)
```
### Source Attribution
Context is injected as a system message with clear source tags:
```
The following context was retrieved from the knowledge base. Use it to
inform your response, citing sources where applicable:
[Source: docs/api.md] The API exposes a /v1/chat/completions endpoint...
[Source: docs/setup.md] To configure the API server, edit config.toml...
```
---
## Backend Registration
Memory backends are registered via the `@MemoryRegistry.register("name")` decorator:
```python
from openjarvis.core.registry import MemoryRegistry
from openjarvis.memory._stubs import MemoryBackend
@MemoryRegistry.register("my-backend")
class MyMemoryBackend(MemoryBackend):
backend_id = "my-backend"
def store(self, content, *, source="", metadata=None) -> str: ...
def retrieve(self, query, *, top_k=5, **kwargs) -> list: ...
def delete(self, doc_id) -> bool: ...
def clear(self) -> None: ...
```
The default backend is configured in `~/.openjarvis/config.toml`:
```toml
[memory]
default_backend = "sqlite"
db_path = "~/.openjarvis/memory.db"
context_injection = true
context_top_k = 5
context_min_score = 0.1
context_max_tokens = 2048
chunk_size = 512
chunk_overlap = 64
```
+272
View File
@@ -0,0 +1,272 @@
# Architecture Overview
OpenJarvis is a research framework for studying on-device AI systems. Its architecture is organized around **four core abstractions** -- Intelligence, Engine, Agentic Logic, and Memory -- plus a cross-cutting **Learning** system that ties them together through trace-driven feedback.
---
## The Four Pillars + Learning
```mermaid
graph TB
subgraph "Core Pillars"
INT["Intelligence<br/><i>Model management<br/>& query routing</i>"]
ENG["Engine<br/><i>Inference runtime<br/>backends</i>"]
AGT["Agentic Logic<br/><i>Agents, tools,<br/>orchestration</i>"]
MEM["Memory<br/><i>Persistent searchable<br/>storage</i>"]
end
subgraph "Cross-cutting"
LRN["Learning & Traces<br/><i>Router policies,<br/>trace recording,<br/>feedback loop</i>"]
end
subgraph "Infrastructure"
EVT["EventBus<br/><i>Pub/sub telemetry</i>"]
REG["Registries<br/><i>Runtime discovery</i>"]
CFG["Config<br/><i>Hardware detection,<br/>TOML loading</i>"]
end
AGT -->|"selects model via"| INT
AGT -->|"generates via"| ENG
AGT -->|"retrieves context from"| MEM
INT -->|"routes using"| LRN
LRN -->|"learns from traces of"| AGT
LRN -->|"updates routing for"| INT
EVT -.->|"connects all pillars"| INT
EVT -.-> ENG
EVT -.-> AGT
EVT -.-> MEM
EVT -.-> LRN
CFG -->|"configures"| ENG
CFG -->|"configures"| MEM
REG -->|"discovers"| ENG
REG -->|"discovers"| AGT
REG -->|"discovers"| MEM
```
---
## Pillar Descriptions
### Intelligence
The Intelligence pillar handles **model management and query routing**. It maintains a catalog of known models (`BUILTIN_MODELS`) with metadata such as parameter count, context length, VRAM requirements, and supported engines. When a query arrives, the `HeuristicRouter` analyzes it for characteristics like code patterns, math keywords, length, and urgency, then selects the most appropriate model from those available.
Models discovered at runtime from running engines are automatically merged into the `ModelRegistry`, so the system always has an up-to-date view of what is available.
### Engine
The Engine pillar provides the **inference runtime** -- the layer that actually runs language models. All backends implement the `InferenceEngine` ABC with a uniform interface: `generate()`, `stream()`, `list_models()`, and `health()`. Supported backends include Ollama, vLLM, SGLang, llama.cpp, and Cloud (OpenAI, Anthropic, Google).
Engine discovery probes all registered backends for health, returning healthy engines sorted with the user's configured default first. The system automatically falls back to any available engine if the preferred one is unavailable.
### Agentic Logic
The Agentic Logic pillar implements **pluggable agents** that handle queries with varying levels of sophistication. `SimpleAgent` provides single-turn query-to-response without tools. `OrchestratorAgent` implements a multi-turn tool-calling loop where the LLM can invoke tools like `calculator`, `think`, `retrieval`, `llm`, and `file_read`, with results fed back for further processing. `OpenClawAgent` communicates with external OpenClaw servers via HTTP or subprocess transport.
All agents implement the `BaseAgent` ABC with a `run()` method, and are registered via `@AgentRegistry.register("name")`.
### Memory
The Memory pillar provides **persistent, searchable storage** for documents and knowledge. Five backends are available: SQLite/FTS5 (zero-dependency default), FAISS (dense vector retrieval), ColBERTv2 (late interaction), BM25 (classic term-frequency), and Hybrid (Reciprocal Rank Fusion of sparse + dense).
The memory pipeline includes document ingestion, chunking, embedding generation, and context injection. When a user sends a query, relevant documents are retrieved and prepended to the prompt with source attribution.
### Learning & Traces (Cross-cutting)
The Learning system is a cross-cutting concern that connects all pillars through **trace-driven feedback**. Every agent interaction can produce a `Trace` capturing the full sequence of steps -- routing decisions, memory retrieval, inference calls, tool invocations, and final responses. The `TraceAnalyzer` computes statistics from accumulated traces, and the `TraceDrivenPolicy` uses these statistics to learn which model/agent/tool combinations produce the best outcomes for different query types.
---
## The Registry Pattern
All extensible components in OpenJarvis use a **decorator-based registry** for runtime discovery. The pattern is implemented in `RegistryBase[T]`, a generic base class that provides isolated storage per typed subclass.
```python
from openjarvis.core.registry import EngineRegistry
@EngineRegistry.register("ollama")
class OllamaEngine(InferenceEngine):
...
```
Each registry provides:
| Method | Description |
|--------|-------------|
| `register(key)` | Decorator that registers a class under a key |
| `register_value(key, value)` | Imperative registration |
| `get(key)` | Retrieve by key (raises `KeyError` if missing) |
| `create(key, *args, **kwargs)` | Look up and instantiate |
| `items()` | All `(key, entry)` pairs |
| `keys()` | All registered keys |
| `contains(key)` | Check if key exists |
| `clear()` | Remove all entries (for tests) |
**Typed registries** in the system:
| Registry | Type Parameter | Purpose |
|----------|---------------|---------|
| `ModelRegistry` | `Any` (ModelSpec) | Model metadata |
| `EngineRegistry` | `Type[InferenceEngine]` | Inference backends |
| `MemoryRegistry` | `Type[MemoryBackend]` | Memory backends |
| `AgentRegistry` | `Type[BaseAgent]` | Agent implementations |
| `ToolRegistry` | `Any` (BaseTool classes) | Tool implementations |
| `RouterPolicyRegistry` | `Any` (RouterPolicy classes) | Router policies |
| `BenchmarkRegistry` | `Any` (BaseBenchmark classes) | Benchmark implementations |
!!! info "Adding a new component"
To add a new backend, implement the appropriate ABC and decorate it with
the corresponding registry decorator. No factory modifications are needed --
the component becomes automatically discoverable at runtime.
---
## Source Directory Layout
```
src/openjarvis/
core/ Core infrastructure shared by all pillars
registry.py RegistryBase[T] and typed subclass registries
types.py Message, ModelSpec, Trace, TelemetryRecord, etc.
config.py JarvisConfig, hardware detection, TOML loading
events.py EventBus pub/sub system (EventType, Event)
intelligence/ Intelligence pillar -- model management & routing
router.py HeuristicRouter, build_routing_context()
model_catalog.py BUILTIN_MODELS list, merge_discovered_models()
_stubs.py (Shared with learning -- RoutingContext)
engine/ Engine pillar -- inference runtime backends
_stubs.py InferenceEngine ABC
_base.py EngineConnectionError, messages_to_dicts()
_openai_compat.py Shared base for OpenAI-compatible engines
_discovery.py discover_engines(), discover_models(), get_engine()
ollama.py Ollama backend (native HTTP API)
vllm.py vLLM backend (OpenAI-compatible)
sglang.py SGLang backend (OpenAI-compatible)
llamacpp.py llama.cpp backend (OpenAI-compatible)
cloud.py Cloud backend (OpenAI, Anthropic, Google SDKs)
agents/ Agentic Logic pillar -- pluggable agents
_stubs.py BaseAgent ABC, AgentContext, AgentResult
simple.py SimpleAgent (single-turn, no tools)
orchestrator.py OrchestratorAgent (multi-turn tool loop)
openclaw.py OpenClawAgent (HTTP/subprocess transport)
custom.py CustomAgent (template for user-defined agents)
openclaw_protocol.py Wire protocol (MessageType, serialize/deserialize)
openclaw_transport.py Transport ABC, HttpTransport, SubprocessTransport
openclaw_plugin.py ProviderPlugin, MemorySearchManager
memory/ Memory pillar -- persistent searchable storage
_stubs.py MemoryBackend ABC, RetrievalResult
sqlite.py SQLite/FTS5 backend (zero-dependency default)
faiss_backend.py FAISS dense retrieval backend
colbert_backend.py ColBERTv2 late interaction backend
bm25.py BM25 (Okapi) term-frequency backend
hybrid.py Hybrid RRF fusion backend
chunking.py ChunkConfig, Chunk, chunk_text()
ingest.py Document ingestion (file reading, directory walking)
context.py Context injection (inject_context, source attribution)
embeddings.py Embedder ABC, SentenceTransformerEmbedder
learning/ Learning system -- router policies & rewards
_stubs.py RouterPolicy ABC, RewardFunction ABC, RoutingContext
heuristic_policy.py Wires HeuristicRouter into RouterPolicyRegistry
trace_policy.py TraceDrivenPolicy (learns from trace outcomes)
grpo_policy.py GRPORouterPolicy (stub for future RL)
heuristic_reward.py HeuristicRewardFunction (latency/cost/efficiency)
traces/ Trace system -- interaction recording
store.py TraceStore (SQLite persistence)
collector.py TraceCollector (wraps agents, records traces)
analyzer.py TraceAnalyzer (aggregated statistics)
tools/ Tool system -- pluggable tool implementations
_stubs.py BaseTool ABC, ToolSpec, ToolExecutor
calculator.py CalculatorTool (ast-based safe eval)
think.py ThinkTool (reasoning scratchpad)
retrieval.py RetrievalTool (memory search)
llm.py LLMTool (sub-model calls)
file_read.py FileReadTool (safe file reading)
telemetry/ Telemetry -- inference metrics recording
store.py TelemetryStore (SQLite, EventBus subscription)
aggregator.py TelemetryAggregator (per-model/engine stats)
wrapper.py instrumented_generate() wrapper
server/ API server -- OpenAI-compatible HTTP API
app.py FastAPI application factory
routes.py /v1/chat/completions, /v1/models, /health
bench/ Benchmarking framework
_stubs.py BaseBenchmark ABC, BenchmarkSuite
latency.py LatencyBenchmark (per-call latency)
throughput.py ThroughputBenchmark (tokens/second)
cli/ CLI commands (Click-based)
ask.py jarvis ask -- query the assistant
serve.py jarvis serve -- start API server
sdk.py Jarvis class -- high-level Python SDK
mcp/ MCP (Model Context Protocol) layer
```
---
## How the Pillars Interact
### EventBus: The Connective Tissue
All pillars communicate through a **thread-safe pub/sub EventBus** defined in `core/events.py`. The bus uses synchronous dispatch -- subscribers are called in registration order within the publishing thread.
```mermaid
graph LR
subgraph "Event Publishers"
E1["Engine<br/>INFERENCE_START/END"]
A1["Agents<br/>AGENT_TURN_START/END"]
T1["Tools<br/>TOOL_CALL_START/END"]
M1["Memory<br/>MEMORY_STORE/RETRIEVE"]
end
BUS["EventBus"]
subgraph "Event Subscribers"
TEL["TelemetryStore<br/>records metrics"]
TRC["TraceCollector<br/>builds traces"]
TRS["TraceStore<br/>persists traces"]
end
E1 --> BUS
A1 --> BUS
T1 --> BUS
M1 --> BUS
BUS --> TEL
BUS --> TRC
BUS --> TRS
```
**Event types** in the system:
| Event | Publisher | Purpose |
|-------|----------|---------|
| `INFERENCE_START` / `INFERENCE_END` | Engine / Agent | Track inference calls |
| `TOOL_CALL_START` / `TOOL_CALL_END` | ToolExecutor | Track tool usage |
| `MEMORY_STORE` / `MEMORY_RETRIEVE` | Memory backends | Track memory operations |
| `AGENT_TURN_START` / `AGENT_TURN_END` | Agents | Track agent lifecycle |
| `TELEMETRY_RECORD` | TelemetryStore | Publish telemetry records |
| `TRACE_STEP` / `TRACE_COMPLETE` | TraceCollector | Trace lifecycle events |
### Dependency Flow
The pillars form a directed dependency graph:
1. **Agentic Logic** depends on Engine (for inference) and Memory (for context)
2. **Intelligence** provides model selection to agents via Learning policies
3. **Learning** reads from Traces, which are produced by Agentic Logic
4. **Memory** is independent but consumed by agents and tools
5. **Engine** is independent but consumed by agents and the SDK
This creates a feedback loop: agents produce traces, traces inform learning, learning improves routing, and better routing improves agent performance.
+318
View File
@@ -0,0 +1,318 @@
# Query Flow
This page traces the end-to-end journey of a user query through the OpenJarvis system, from the moment it enters the CLI or SDK to the final response and telemetry recording.
---
## Sequence Diagram
```mermaid
sequenceDiagram
actor User
participant CLI as CLI / SDK
participant CFG as Config & Discovery
participant RTR as Router Policy
participant AGT as Agent
participant MEM as Memory Backend
participant CTX as Context Injection
participant ENG as Inference Engine
participant TEL as Telemetry
participant TRC as Trace Collector
User->>CLI: jarvis ask "query" / j.ask("query")
CLI->>CFG: load_config()
CFG-->>CLI: JarvisConfig (hardware, engine defaults)
CLI->>CFG: get_engine(config)
CFG-->>CLI: (engine_key, engine_instance)
CLI->>CFG: discover_engines() + discover_models()
CFG-->>CLI: available models per engine
alt Model not specified
CLI->>RTR: select_model(RoutingContext)
RTR-->>CLI: model_key (e.g., "qwen3:8b")
end
alt Agent mode (--agent flag)
CLI->>AGT: agent.run(query, context)
AGT->>MEM: retrieve(query, top_k=5)
MEM-->>AGT: RetrievalResult[]
AGT->>CTX: inject_context(query, messages, backend)
CTX-->>AGT: messages with context prepended
loop Tool-calling loop (max_turns)
AGT->>ENG: generate(messages, model, tools)
ENG-->>AGT: {content, tool_calls, usage}
opt Tool calls present
AGT->>AGT: ToolExecutor.execute(tool_call)
AGT->>AGT: Append tool results to messages
end
end
AGT-->>CLI: AgentResult(content, tool_results, turns)
else Direct mode (no agent)
CLI->>MEM: retrieve(query)
MEM-->>CLI: RetrievalResult[]
CLI->>CTX: inject_context(query, messages, backend)
CTX-->>CLI: messages with context
CLI->>ENG: instrumented_generate(messages, model)
ENG-->>CLI: {content, usage}
end
CLI->>TEL: TelemetryStore records metrics
CLI->>TRC: TraceCollector saves Trace
CLI-->>User: Response text
```
---
## Direct Mode vs Agent Mode
OpenJarvis supports two query processing paths, selected by the `--agent` CLI flag or the `agent` parameter in the SDK.
### Direct Mode (Default)
In direct mode, the query goes straight to the inference engine with optional memory context. This is the simplest path -- one inference call, no tool loop.
```bash
# CLI
jarvis ask "What is the capital of France?"
# SDK
j = Jarvis()
response = j.ask("What is the capital of France?")
```
### Agent Mode
In agent mode, the query is handled by a named agent that can perform multiple inference rounds and invoke tools. The `OrchestratorAgent` is the most common choice, enabling a multi-turn tool-calling loop.
```bash
# CLI
jarvis ask --agent orchestrator --tools calculator,think "What is 2^10 + 3^5?"
# SDK
response = j.ask("What is 2^10 + 3^5?", agent="orchestrator", tools=["calculator"])
```
---
## Step-by-Step Walkthrough
### Step 1: Configuration Loading
The journey begins with loading the system configuration:
```python
config = load_config() # Reads ~/.openjarvis/config.toml
```
This step:
- Detects system hardware (GPU vendor/model, CPU, RAM)
- Recommends the best inference engine for the detected hardware
- Overlays any user overrides from the TOML file
- Returns a `JarvisConfig` dataclass with all settings
### Step 2: Engine Discovery
Next, the system finds a running inference engine:
```python
resolved = get_engine(config, engine_key)
# Returns (engine_key, engine_instance) or None
```
The discovery process:
1. If a specific engine was requested (`--engine` flag), try that engine
2. Otherwise, try the default engine from config (e.g., `"ollama"`)
3. If the default is unhealthy, probe all registered engines and use the first healthy one
4. If no engine is available, exit with an error message
### Step 3: Model Discovery and Registration
Once an engine is found, the system discovers available models:
```python
register_builtin_models() # Register known models (catalog)
all_engines = discover_engines(config)
all_models = discover_models(all_engines)
for ek, model_ids in all_models.items():
merge_discovered_models(ek, model_ids) # Register runtime-discovered models
```
### Step 4: Model Routing
If no model was explicitly specified, the router policy selects one:
```python
from openjarvis.learning import ensure_registered
ensure_registered() # Ensure learning policies are registered
policy_key = router_policy or config.learning.default_policy
router_cls = RouterPolicyRegistry.get(policy_key)
router = router_cls(
available_models=all_models.get(engine_name, []),
default_model=config.intelligence.default_model,
fallback_model=config.intelligence.fallback_model,
)
ctx = build_routing_context(query_text)
model_name = router.select_model(ctx)
```
The `build_routing_context()` function analyzes the query for code patterns, math keywords, length, and urgency. The router then applies its rules (heuristic or learned) to select the optimal model.
### Step 5: Memory Context Injection
If memory context injection is enabled (default: `true`) and the memory backend has indexed documents:
```python
backend = _get_memory_backend(config)
if backend is not None:
ctx_cfg = ContextConfig(
top_k=config.memory.context_top_k, # Default: 5
min_score=config.memory.context_min_score, # Default: 0.1
max_context_tokens=config.memory.context_max_tokens, # Default: 2048
)
messages = inject_context(query_text, messages, backend, config=ctx_cfg)
```
This retrieves relevant chunks from the memory backend and prepends a system message with the retrieved context and source attribution.
!!! tip "Disabling context injection"
Use `--no-context` on the CLI or `context=False` in the SDK to skip memory context injection.
### Step 6: Inference Generation
**In direct mode**, the query is sent to the engine via the instrumented wrapper:
```python
result = instrumented_generate(
engine, messages,
model=model_name,
bus=bus,
temperature=temperature,
max_tokens=max_tokens,
)
```
The `instrumented_generate()` wrapper:
1. Publishes `INFERENCE_START` on the event bus
2. Records the start time
3. Calls `engine.generate()`
4. Records end time, calculates latency
5. Publishes `INFERENCE_END` with timing and token counts
6. Publishes `TELEMETRY_RECORD` with the full `TelemetryRecord`
**In agent mode**, the agent manages inference calls internally, potentially making multiple rounds with tool calls in between.
### Step 7: Tool Execution (Agent Mode Only)
When the `OrchestratorAgent` receives tool calls in the model's response:
1. Each tool call is dispatched to the `ToolExecutor`
2. The executor publishes `TOOL_CALL_START`, executes the tool, publishes `TOOL_CALL_END`
3. Tool results are appended to the message history as `TOOL` messages
4. The updated messages are sent back to the engine for the next round
5. This loop continues until the model responds without tool calls or `max_turns` is reached
### Step 8: Telemetry Recording
After every inference call, a `TelemetryRecord` is created and persisted:
```python
@dataclass(slots=True)
class TelemetryRecord:
timestamp: float
model_id: str
prompt_tokens: int
completion_tokens: int
total_tokens: int
latency_seconds: float
ttft: float # Time to first token
cost_usd: float
energy_joules: float
power_watts: float
engine: str
agent: str
metadata: Dict[str, Any]
```
The `TelemetryStore` subscribes to `TELEMETRY_RECORD` events on the EventBus and writes records to `~/.openjarvis/telemetry.db`.
### Step 9: Trace Recording
When a `TraceCollector` is wrapping the agent, a complete `Trace` is built from the events captured during execution:
1. All `INFERENCE_START`/`END` events become `GENERATE` steps
2. All `TOOL_CALL_START`/`END` events become `TOOL_CALL` steps
3. All `MEMORY_RETRIEVE` events become `RETRIEVE` steps
4. A final `RESPOND` step captures the output
5. The trace is saved to the `TraceStore` and `TRACE_COMPLETE` is published
### Step 10: Response Delivery
The final response is delivered to the user:
- **CLI:** Printed to stdout (or as JSON with `--json`)
- **SDK:** Returned as a string from `ask()` or as a dict from `ask_full()`
---
## EventBus Activity During a Query
The following events are published during a typical query in agent mode:
```
AGENT_TURN_START {agent: "orchestrator", input: "What is 2+2?"}
INFERENCE_START {model: "qwen3:8b", engine: "ollama", turn: 1}
INFERENCE_END {model: "qwen3:8b", engine: "ollama", turn: 1}
TELEMETRY_RECORD {model_id: "qwen3:8b", latency: 0.8, tokens: 150}
TOOL_CALL_START {tool: "calculator", arguments: {expression: "2+2"}}
TOOL_CALL_END {tool: "calculator", success: true, latency: 0.01}
INFERENCE_START {model: "qwen3:8b", engine: "ollama", turn: 2}
INFERENCE_END {model: "qwen3:8b", engine: "ollama", turn: 2}
TELEMETRY_RECORD {model_id: "qwen3:8b", latency: 0.5, tokens: 80}
AGENT_TURN_END {agent: "orchestrator", turns: 2, content_length: 12}
TRACE_COMPLETE {trace: Trace(...)}
```
---
## SDK Query Flow
The `Jarvis` class in `sdk.py` provides the same query flow through a Python API:
```python
from openjarvis import Jarvis
j = Jarvis(model="qwen3:8b", engine_key="ollama")
# Direct mode
response = j.ask("Hello")
# Agent mode with tools
response = j.ask(
"What is 2^10?",
agent="orchestrator",
tools=["calculator"],
)
# Full result with metadata
result = j.ask_full("Hello")
# {
# "content": "Hello! How can I help you?",
# "usage": {"prompt_tokens": 10, "completion_tokens": 15, "total_tokens": 25},
# "model": "qwen3:8b",
# "engine": "ollama",
# }
j.close()
```
The SDK handles lazy engine initialization, telemetry setup, memory context injection, and resource cleanup internally. The `ask()` method delegates to `ask_full()` and extracts just the content string.
+455
View File
@@ -0,0 +1,455 @@
# API Server
OpenJarvis includes an OpenAI-compatible API server built on FastAPI and uvicorn. It exposes chat completion, model listing, and health check endpoints, making it a drop-in replacement for the OpenAI API when working with local models.
## Starting the Server
The server requires the `[server]` extra (FastAPI + uvicorn):
```bash
pip install 'openjarvis[server]'
```
Start with default settings:
```bash
jarvis serve
```
The server reads defaults from `~/.openjarvis/config.toml` and auto-detects available engines and models. Override any option via CLI flags:
```bash
jarvis serve --host 0.0.0.0 --port 8000 --engine ollama --model qwen3:8b --agent orchestrator
```
### CLI Options
| Option | Description | Default |
|----------------------|------------------------------------------------------------------------------|--------------------|
| `--host` | Network address to bind to | From config (`0.0.0.0`) |
| `--port` | Port number to listen on | From config (`8000`) |
| `-e` / `--engine` | Inference engine backend (`ollama`, `vllm`, `llamacpp`, `sglang`) | Auto-detected |
| `-m` / `--model` | Default model for completions | First available |
| `-a` / `--agent` | Agent for non-streaming requests (`simple`, `orchestrator`, `react`, `openhands`) | From config (`orchestrator`) |
On startup, the server prints a summary:
```
Starting OpenJarvis API server
Engine: ollama
Model: qwen3:8b
Agent: orchestrator
URL: http://0.0.0.0:8000
```
!!! warning "Server dependency check"
If the `[server]` extra is not installed, `jarvis serve` exits with a clear error message explaining how to install the required dependencies.
## Endpoints
### `POST /v1/chat/completions`
The primary endpoint for generating chat completions. Accepts the same request format as the OpenAI Chat Completions API.
#### Request Body
```json
{
"model": "qwen3:8b",
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "What is the capital of France?"}
],
"temperature": 0.7,
"max_tokens": 1024,
"stream": false,
"tools": null
}
```
| Parameter | Type | Default | Description |
|---------------|-------------------|---------|--------------------------------------------------------------|
| `model` | `string` | -- | **Required.** Model identifier to use for generation. |
| `messages` | `array` | -- | **Required.** Array of message objects with `role` and `content`. |
| `temperature` | `float` | `0.7` | Sampling temperature (0.0 to 2.0). |
| `max_tokens` | `integer` | `1024` | Maximum number of tokens to generate. |
| `stream` | `boolean` | `false` | Whether to stream the response via SSE. |
| `tools` | `array` or `null` | `null` | Tool definitions in OpenAI function-calling format. |
Each message object:
| Field | Type | Description |
|----------------|-------------------|-------------------------------------------------------|
| `role` | `string` | One of `system`, `user`, `assistant`, or `tool`. |
| `content` | `string` | The message content. |
| `name` | `string` or `null`| Optional name for the message author. |
| `tool_calls` | `array` or `null` | Tool calls made by the assistant (in assistant messages). |
| `tool_call_id` | `string` or `null`| ID of the tool call this message responds to (in tool messages). |
#### Response (Non-Streaming)
```json
{
"id": "chatcmpl-abc123def456",
"object": "chat.completion",
"created": 1740100800,
"model": "qwen3:8b",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "The capital of France is Paris.",
"tool_calls": null
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 25,
"completion_tokens": 8,
"total_tokens": 33
}
}
```
When an agent is configured on the server, non-streaming requests are routed through the agent, which can perform multi-turn reasoning with tool calls before returning a final response. When no agent is configured, requests go directly to the inference engine.
#### Tool Calls
When `tools` are provided in the request, the engine may return `tool_calls` in the assistant message:
```json
{
"choices": [
{
"message": {
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "calculator",
"arguments": "{\"expression\": \"2 + 2\"}"
}
}
]
},
"finish_reason": "tool_calls"
}
]
}
```
### `GET /v1/models`
Lists all models available on the configured inference engine.
#### Response
```json
{
"object": "list",
"data": [
{
"id": "qwen3:8b",
"object": "model",
"created": 1740100800,
"owned_by": "openjarvis"
},
{
"id": "llama3.1:8b",
"object": "model",
"created": 1740100800,
"owned_by": "openjarvis"
}
]
}
```
### `GET /health`
Health check endpoint that verifies the inference engine is responsive.
#### Response (Healthy)
HTTP 200:
```json
{"status": "ok"}
```
#### Response (Unhealthy)
HTTP 503:
```json
{"detail": "Engine unhealthy"}
```
### `GET /dashboard`
Serves the built-in Savings Dashboard, an HTML page that displays real-time statistics on inference calls served locally and estimated cost savings compared to cloud API providers. The dashboard auto-refreshes every 5 seconds by polling the `/v1/savings` endpoint.
## Streaming via SSE
When `"stream": true` is set in the request, the server returns a `text/event-stream` response using Server-Sent Events (SSE). The response follows the same format as the OpenAI streaming API.
Each event is a `data:` line containing a JSON chunk, followed by a blank line:
```
data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1740100800,"model":"qwen3:8b","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}
data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1740100800,"model":"qwen3:8b","choices":[{"index":0,"delta":{"content":"The"},"finish_reason":null}]}
data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1740100800,"model":"qwen3:8b","choices":[{"index":0,"delta":{"content":" capital"},"finish_reason":null}]}
...
data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1740100800,"model":"qwen3:8b","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: [DONE]
```
The stream follows this sequence:
1. **Role chunk** -- first chunk contains `"delta": {"role": "assistant"}` with no content.
2. **Content chunks** -- subsequent chunks each contain a `"delta": {"content": "..."}` with one or more tokens.
3. **Finish chunk** -- a chunk with an empty `delta` and `"finish_reason": "stop"`.
4. **Done signal** -- the literal string `data: [DONE]` indicates the stream is complete.
Response headers include `Cache-Control: no-cache` and `Connection: keep-alive` for proper SSE behavior.
## Client Examples
=== "curl"
**Non-streaming request:**
```bash
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qwen3:8b",
"messages": [
{"role": "user", "content": "Explain quantum computing in one paragraph."}
],
"temperature": 0.7,
"max_tokens": 256
}'
```
**Streaming request:**
```bash
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-N \
-d '{
"model": "qwen3:8b",
"messages": [
{"role": "user", "content": "Write a haiku about programming."}
],
"stream": true
}'
```
**List models:**
```bash
curl http://localhost:8000/v1/models
```
**Health check:**
```bash
curl http://localhost:8000/health
```
=== "Python (openai)"
The OpenAI Python library works as a drop-in client by pointing `base_url` at the local server:
```python
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8000/v1",
api_key="not-needed", # Required by the library but not validated
)
# Non-streaming
response = client.chat.completions.create(
model="qwen3:8b",
messages=[
{"role": "user", "content": "What is the capital of France?"}
],
temperature=0.7,
max_tokens=256,
)
print(response.choices[0].message.content)
# Streaming
stream = client.chat.completions.create(
model="qwen3:8b",
messages=[
{"role": "user", "content": "Write a short poem about AI."}
],
stream=True,
)
for chunk in stream:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
print()
# List models
models = client.models.list()
for model in models.data:
print(model.id)
```
=== "Python (httpx)"
Using `httpx` for direct HTTP requests:
```python
import httpx
import json
BASE_URL = "http://localhost:8000"
# Non-streaming request
response = httpx.post(
f"{BASE_URL}/v1/chat/completions",
json={
"model": "qwen3:8b",
"messages": [
{"role": "user", "content": "What is the capital of France?"}
],
"temperature": 0.7,
"max_tokens": 256,
},
)
data = response.json()
print(data["choices"][0]["message"]["content"])
# Streaming request
with httpx.stream(
"POST",
f"{BASE_URL}/v1/chat/completions",
json={
"model": "qwen3:8b",
"messages": [
{"role": "user", "content": "Write a haiku about code."}
],
"stream": True,
},
) as response:
for line in response.iter_lines():
if line.startswith("data: ") and line != "data: [DONE]":
chunk = json.loads(line[6:])
content = chunk["choices"][0]["delta"].get("content", "")
if content:
print(content, end="", flush=True)
print()
# List models
response = httpx.get(f"{BASE_URL}/v1/models")
for model in response.json()["data"]:
print(model["id"])
# Health check
response = httpx.get(f"{BASE_URL}/health")
print(response.json())
```
## Configuration via `config.toml`
The `[server]` section of `~/.openjarvis/config.toml` controls default server behavior:
```toml
[server]
host = "0.0.0.0"
port = 8000
agent = "orchestrator"
model = ""
workers = 1
```
| Key | Type | Default | Description |
|-----------|-----------|-----------------|----------------------------------------------------------------------------|
| `host` | `string` | `"0.0.0.0"` | Network address to bind to. Use `"127.0.0.1"` for localhost-only access. |
| `port` | `integer` | `8000` | Port number. |
| `agent` | `string` | `"orchestrator"`| Default agent for non-streaming requests. Set to `""` for direct engine mode. |
| `model` | `string` | `""` | Default model name. When empty, falls back to `[intelligence] default_model` or the first model discovered on the engine. |
| `workers` | `integer` | `1` | Number of uvicorn workers (for future use). |
CLI flags override config file values. For example, `jarvis serve --port 9000` overrides the `port` setting in the config file.
The server also reads from other config sections at startup:
- **`[engine]`** -- determines which inference backend to connect to and its host URL.
- **`[intelligence]`** -- provides the fallback `default_model` when no model is specified.
- **`[agent]`** -- supplies `max_turns` for multi-turn agents like `orchestrator`.
## Running Behind a Reverse Proxy
For production deployments, run OpenJarvis behind a reverse proxy like Nginx or Caddy for TLS termination, rate limiting, and authentication.
### Nginx
```nginx
server {
listen 443 ssl;
server_name jarvis.example.com;
ssl_certificate /etc/ssl/certs/jarvis.pem;
ssl_certificate_key /etc/ssl/private/jarvis.key;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# SSE streaming support
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 300s;
}
}
```
!!! important "Disable buffering for SSE"
The `proxy_buffering off` directive is critical for streaming responses. Without it, Nginx buffers the SSE chunks and delivers them in batches, defeating the purpose of streaming.
### Caddy
```
jarvis.example.com {
reverse_proxy 127.0.0.1:8000 {
flush_interval -1
}
}
```
The `flush_interval -1` setting disables response buffering, which is required for SSE streaming.
### Bind to Localhost
When running behind a reverse proxy, bind the server to `127.0.0.1` so it only accepts connections from the proxy:
```bash
jarvis serve --host 127.0.0.1 --port 8000
```
Or in `config.toml`:
```toml
[server]
host = "127.0.0.1"
port = 8000
```
+337
View File
@@ -0,0 +1,337 @@
# Docker Deployment
OpenJarvis provides Docker images for both CPU-only and GPU-accelerated deployments, along with a Docker Compose configuration that bundles the API server with an Ollama inference backend.
## Quick Start
The fastest way to get OpenJarvis running in Docker is with Docker Compose, which starts both the API server and an Ollama backend:
```bash
docker compose up -d
```
This brings up two services:
| Service | Port | Description |
|----------|-------|------------------------------------|
| `jarvis` | 8000 | OpenJarvis API server |
| `ollama` | 11434 | Ollama inference engine |
Verify the server is running:
```bash
curl http://localhost:8000/health
```
Expected response:
```json
{"status": "ok"}
```
## Docker Images
### CPU-Only Image (`Dockerfile`)
The default `Dockerfile` uses a multi-stage build based on `python:3.12-slim` to produce a minimal image.
**Build stages:**
1. **Builder stage** -- installs `uv` and the `openjarvis[server]` package (which includes FastAPI, uvicorn, and all server dependencies) from the project source.
2. **Runtime stage** -- copies only the installed Python packages and application code from the builder, keeping the final image small.
```dockerfile
FROM python:3.12-slim AS builder
WORKDIR /app
COPY pyproject.toml README.md ./
COPY src/ src/
RUN pip install --no-cache-dir uv && \
uv pip install --system ".[server]"
FROM python:3.12-slim
COPY --from=builder /usr/local /usr/local
COPY --from=builder /app /app
WORKDIR /app
EXPOSE 8000
ENTRYPOINT ["jarvis"]
CMD ["serve", "--host", "0.0.0.0", "--port", "8000"]
```
Build it manually:
```bash
docker build -t openjarvis:latest .
```
Run it standalone:
```bash
docker run -d -p 8000:8000 openjarvis:latest
```
### GPU Image (`Dockerfile.gpu`)
The GPU image is built on `nvidia/cuda:12.4.0-runtime-ubuntu22.04` and includes the CUDA 12.4 runtime libraries, enabling GPU-accelerated inference when paired with a GPU-capable engine like vLLM or SGLang.
```dockerfile
FROM nvidia/cuda:12.4.0-runtime-ubuntu22.04 AS builder
RUN apt-get update && \
apt-get install -y --no-install-recommends python3 python3-pip python3-venv && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY pyproject.toml README.md ./
COPY src/ src/
RUN pip install --no-cache-dir uv && \
uv pip install --system ".[server]"
FROM nvidia/cuda:12.4.0-runtime-ubuntu22.04
RUN apt-get update && \
apt-get install -y --no-install-recommends python3 python3-pip && \
rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/local /usr/local
COPY --from=builder /app /app
WORKDIR /app
EXPOSE 8000
ENTRYPOINT ["jarvis"]
CMD ["serve", "--host", "0.0.0.0", "--port", "8000"]
```
Build the GPU image:
```bash
docker build -f Dockerfile.gpu -t openjarvis:gpu .
```
Run with GPU access (requires the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)):
```bash
docker run -d --gpus all -p 8000:8000 openjarvis:gpu
```
!!! note "NVIDIA Container Toolkit required"
The host machine must have the NVIDIA Container Toolkit installed for `--gpus` to work. See the [NVIDIA installation guide](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) for setup instructions.
## Docker Compose Configuration
The `docker-compose.yml` defines a complete deployment with the OpenJarvis API server and an Ollama backend:
```yaml
version: "3.9"
services:
jarvis:
build:
context: .
dockerfile: Dockerfile
ports:
- "8000:8000"
environment:
- OPENJARVIS_ENGINE_DEFAULT=ollama
- OPENJARVIS_OLLAMA_HOST=http://ollama:11434
depends_on:
- ollama
restart: unless-stopped
ollama:
image: ollama/ollama
ports:
- "11434:11434"
volumes:
- ollama-models:/root/.ollama
restart: unless-stopped
volumes:
ollama-models:
```
### Environment Variables
The `jarvis` service is configured through environment variables:
| Variable | Description | Default |
|-------------------------------|---------------------------------------------------------|----------------------------|
| `OPENJARVIS_ENGINE_DEFAULT` | Inference engine backend to use | `ollama` |
| `OPENJARVIS_OLLAMA_HOST` | URL of the Ollama server (uses Docker service name) | `http://ollama:11434` |
### Volumes
The `ollama-models` named volume persists downloaded models across container restarts, so models do not need to be re-pulled after a `docker compose down` / `docker compose up` cycle.
### Service Dependencies
The `jarvis` service declares `depends_on: ollama`, ensuring the Ollama container starts before the API server. Both services use `restart: unless-stopped` to automatically recover from crashes.
## Custom Configuration
### Mounting a Configuration File
To use a custom `config.toml`, mount it into the container at the expected path (`~/.openjarvis/config.toml`, which is `/root/.openjarvis/config.toml` in the container):
```yaml
services:
jarvis:
build:
context: .
dockerfile: Dockerfile
ports:
- "8000:8000"
volumes:
- ./my-config.toml:/root/.openjarvis/config.toml:ro
environment:
- OPENJARVIS_ENGINE_DEFAULT=ollama
- OPENJARVIS_OLLAMA_HOST=http://ollama:11434
depends_on:
- ollama
restart: unless-stopped
```
### Persisting Data
To persist telemetry data, memory databases, and trace records across container restarts, mount the entire OpenJarvis data directory:
```yaml
services:
jarvis:
# ... other config ...
volumes:
- openjarvis-data:/root/.openjarvis
volumes:
ollama-models:
openjarvis-data:
```
This preserves:
- `telemetry.db` -- inference call telemetry records
- `memory.db` -- the default SQLite memory backend
- `traces.db` -- interaction trace records
- `config.toml` -- user configuration
### Using the GPU Image with Compose
To use the GPU Dockerfile in your Compose setup, change the `dockerfile` field and add GPU resource reservations:
```yaml
services:
jarvis:
build:
context: .
dockerfile: Dockerfile.gpu
ports:
- "8000:8000"
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
environment:
- OPENJARVIS_ENGINE_DEFAULT=ollama
- OPENJARVIS_OLLAMA_HOST=http://ollama:11434
depends_on:
- ollama
restart: unless-stopped
```
## Health Check
The API server exposes a `GET /health` endpoint that checks whether the underlying inference engine is responsive:
```bash
curl http://localhost:8000/health
```
A healthy response returns HTTP 200:
```json
{"status": "ok"}
```
An unhealthy engine returns HTTP 503:
```json
{"detail": "Engine unhealthy"}
```
You can integrate this into your Docker Compose healthcheck:
```yaml
services:
jarvis:
# ... other config ...
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
```
## Building Custom Images
### Adding Extra Dependencies
To include additional engine backends (such as vLLM or ColBERT memory), modify the install command in the Dockerfile:
```dockerfile
RUN pip install --no-cache-dir uv && \
uv pip install --system ".[server,inference-vllm,memory-colbert]"
```
### Overriding the Default Command
The entrypoint is `jarvis` and the default command is `serve --host 0.0.0.0 --port 8000`. Override the command to change server options:
```bash
docker run -d -p 9000:9000 openjarvis:latest \
serve --host 0.0.0.0 --port 9000 --engine ollama --model qwen3:8b
```
Or in Docker Compose:
```yaml
services:
jarvis:
build: .
command: ["serve", "--host", "0.0.0.0", "--port", "9000", "--model", "qwen3:8b"]
ports:
- "9000:9000"
```
### Available CLI Options for `jarvis serve`
| Option | Description |
|----------------------|-----------------------------------------------------|
| `--host` | Bind address (default: from config, typically `0.0.0.0`) |
| `--port` | Port number (default: from config, typically `8000`) |
| `-e` / `--engine` | Engine backend (`ollama`, `vllm`, `llamacpp`, `sglang`) |
| `-m` / `--model` | Default model name |
| `-a` / `--agent` | Agent for non-streaming requests (`simple`, `orchestrator`, `react`, `openhands`) |
## Pulling Models
After starting the Ollama container, you need to pull at least one model before the API server can serve requests:
```bash
docker compose exec ollama ollama pull qwen3:8b
```
Verify models are available through the API:
```bash
curl http://localhost:8000/v1/models
```
+251
View File
@@ -0,0 +1,251 @@
# launchd Service (macOS)
OpenJarvis includes a launchd property list (plist) for running the API server as a background service on macOS. This provides automatic startup at login, automatic restart if the process exits, and log capture.
## Prerequisites
Before installing the service, ensure that OpenJarvis is installed and the `jarvis` command is available at `/usr/local/bin/jarvis`. If you installed via `uv` or `pip` with a different prefix, adjust the path in the plist accordingly.
```bash
pip install 'openjarvis[server]'
which jarvis # Verify the installation path
```
Also ensure that an inference engine (such as Ollama) is running and accessible on the machine.
## Installing the Service
Copy the plist file to `~/Library/LaunchAgents` and load it:
```bash
cp deploy/launchd/com.openjarvis.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.openjarvis.plist
```
The service starts immediately (due to `RunAtLoad`) and will automatically restart at each login.
Verify it is running:
```bash
launchctl list | grep openjarvis
```
You should see a line with the PID and the label `com.openjarvis`. A `0` in the status column indicates the service is running normally.
Confirm the server is responding:
```bash
curl http://localhost:8000/health
```
## Plist Reference
The provided plist file at `deploy/launchd/com.openjarvis.plist`:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.openjarvis</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/jarvis</string>
<string>serve</string>
<string>--host</string>
<string>0.0.0.0</string>
<string>--port</string>
<string>8000</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/openjarvis.stdout.log</string>
<key>StandardErrorPath</key>
<string>/tmp/openjarvis.stderr.log</string>
</dict>
</plist>
```
### Key-by-Key Explanation
| Key | Value | Description |
|----------------------|--------------------------------|------------------------------------------------------------------------------------------------------|
| `Label` | `com.openjarvis` | Unique identifier for the service. Used with `launchctl` commands to manage the service. |
| `ProgramArguments` | `["/usr/local/bin/jarvis", "serve", "--host", "0.0.0.0", "--port", "8000"]` | The command and arguments to execute. Each element of the command line is a separate string in the array. |
| `RunAtLoad` | `true` | Start the service immediately when the plist is loaded (and on each login). |
| `KeepAlive` | `true` | Automatically restart the service if it exits for any reason. launchd monitors the process and relaunches it. |
| `StandardOutPath` | `/tmp/openjarvis.stdout.log` | File where standard output is written. Contains server startup messages and access logs. |
| `StandardErrorPath` | `/tmp/openjarvis.stderr.log` | File where standard error is written. Contains error messages and stack traces. |
## Viewing Logs
Server output is written to the two log files specified in the plist:
```bash
# View standard output (startup messages, access logs)
cat /tmp/openjarvis.stdout.log
# View standard error (errors, warnings)
cat /tmp/openjarvis.stderr.log
# Follow logs in real time
tail -f /tmp/openjarvis.stdout.log /tmp/openjarvis.stderr.log
```
!!! tip "Persistent log location"
Files in `/tmp` may be cleared on reboot. For persistent logs, change the paths in the plist to a permanent location:
```xml
<key>StandardOutPath</key>
<string>/Users/yourname/.openjarvis/openjarvis.stdout.log</string>
<key>StandardErrorPath</key>
<string>/Users/yourname/.openjarvis/openjarvis.stderr.log</string>
```
After changing the plist, unload and reload the service for the changes to take effect.
## Managing the Service
### Loading and Unloading
```bash
# Load the service (starts it due to RunAtLoad)
launchctl load ~/Library/LaunchAgents/com.openjarvis.plist
# Unload the service (stops it and prevents it from starting at login)
launchctl unload ~/Library/LaunchAgents/com.openjarvis.plist
```
### Starting and Stopping
If the service is loaded but you want to manually stop or start it without unloading:
```bash
# Stop the service
launchctl stop com.openjarvis
# Start the service
launchctl start com.openjarvis
```
!!! warning
Because `KeepAlive` is set to `true`, using `launchctl stop` will cause launchd to restart the service almost immediately. To fully stop the service, use `launchctl unload` instead.
### Checking Status
```bash
# List all loaded services matching "openjarvis"
launchctl list | grep openjarvis
```
The output columns are:
| Column | Description |
|--------|----------------------------------------------------------------|
| PID | Process ID (or `-` if not running) |
| Status | Last exit status (`0` = normal) |
| Label | The service label (`com.openjarvis`) |
## Configuration Changes
### Changing the Port or Host
Edit the `ProgramArguments` array in the plist. Each argument must be a separate `<string>` element:
```xml
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/jarvis</string>
<string>serve</string>
<string>--host</string>
<string>127.0.0.1</string>
<string>--port</string>
<string>9000</string>
</array>
```
### Specifying an Engine and Model
Add additional arguments to the array:
```xml
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/jarvis</string>
<string>serve</string>
<string>--host</string>
<string>0.0.0.0</string>
<string>--port</string>
<string>8000</string>
<string>--engine</string>
<string>ollama</string>
<string>--model</string>
<string>qwen3:8b</string>
</array>
```
### Setting Environment Variables
Add an `EnvironmentVariables` dictionary to the plist:
```xml
<key>EnvironmentVariables</key>
<dict>
<key>OPENJARVIS_ENGINE_DEFAULT</key>
<string>ollama</string>
<key>OPENJARVIS_OLLAMA_HOST</key>
<string>http://localhost:11434</string>
</dict>
```
### Using a Different `jarvis` Binary Path
If `jarvis` is installed in a virtual environment or a non-standard location, update the first element of `ProgramArguments`:
```xml
<key>ProgramArguments</key>
<array>
<string>/Users/yourname/.local/bin/jarvis</string>
<string>serve</string>
<string>--host</string>
<string>0.0.0.0</string>
<string>--port</string>
<string>8000</string>
</array>
```
### Applying Changes
After editing the plist file, unload and reload the service:
```bash
launchctl unload ~/Library/LaunchAgents/com.openjarvis.plist
launchctl load ~/Library/LaunchAgents/com.openjarvis.plist
```
## System-Wide Installation
The instructions above install the service as a **user agent** (runs only when you are logged in). To run OpenJarvis as a system-wide daemon that starts at boot regardless of user login:
1. Copy the plist to `/Library/LaunchDaemons/` (requires `sudo`).
2. Set the file ownership to `root:wheel`.
3. Optionally add a `UserName` key to run as a specific user.
```bash
sudo cp deploy/launchd/com.openjarvis.plist /Library/LaunchDaemons/
sudo chown root:wheel /Library/LaunchDaemons/com.openjarvis.plist
sudo launchctl load /Library/LaunchDaemons/com.openjarvis.plist
```
!!! note
System daemons in `/Library/LaunchDaemons/` run as root by default. Add a `UserName` key to run as a less-privileged user:
```xml
<key>UserName</key>
<string>openjarvis</string>
```
+242
View File
@@ -0,0 +1,242 @@
# systemd Service (Linux)
OpenJarvis includes a systemd unit file for running the API server as a managed background service on Linux. This provides automatic startup on boot, crash recovery, and integration with standard Linux service management tools.
## Prerequisites
Before installing the service, ensure that:
1. OpenJarvis is installed in a virtual environment at `/opt/openjarvis/.venv` (or adjust paths accordingly).
2. A dedicated `openjarvis` system user exists (recommended for security).
3. An inference engine (such as Ollama) is running and accessible.
Create the user and installation directory:
```bash
sudo useradd --system --create-home --home-dir /opt/openjarvis openjarvis
sudo -u openjarvis python3 -m venv /opt/openjarvis/.venv
sudo -u openjarvis /opt/openjarvis/.venv/bin/pip install 'openjarvis[server]'
```
## Installing the Service
Copy the unit file to the systemd directory, reload the daemon, and enable the service:
```bash
sudo cp deploy/systemd/openjarvis.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable openjarvis
sudo systemctl start openjarvis
```
Verify it is running:
```bash
sudo systemctl status openjarvis
```
## Service File Reference
The provided unit file at `deploy/systemd/openjarvis.service`:
```ini
[Unit]
Description=OpenJarvis API Server
After=network.target
[Service]
Type=simple
User=openjarvis
WorkingDirectory=/opt/openjarvis
ExecStart=/opt/openjarvis/.venv/bin/jarvis serve --host 0.0.0.0 --port 8000
Restart=on-failure
RestartSec=5
Environment=HOME=/opt/openjarvis
[Install]
WantedBy=multi-user.target
```
### `[Unit]` Section
| Directive | Value | Description |
|---------------|--------------------|-----------------------------------------------------------------------------|
| `Description` | `OpenJarvis API Server` | Human-readable name shown in `systemctl status` and logs. |
| `After` | `network.target` | Delays startup until the network stack is available, since the server binds to a network socket and may need to reach a remote engine. |
### `[Service]` Section
| Directive | Value | Description |
|--------------------|--------------------------------------------------------------------|-------------------------------------------------------------------------------------------------|
| `Type` | `simple` | The process started by `ExecStart` is the main service process. systemd considers the service started immediately. |
| `User` | `openjarvis` | Runs the server as the `openjarvis` user rather than root, limiting the blast radius of any security issue. |
| `WorkingDirectory` | `/opt/openjarvis` | Sets the working directory for the process. This is where OpenJarvis looks for local files and writes data. |
| `ExecStart` | `/opt/openjarvis/.venv/bin/jarvis serve --host 0.0.0.0 --port 8000` | The command to start the server. Uses the full path to the `jarvis` binary inside the virtual environment. |
| `Restart` | `on-failure` | Automatically restarts the service if it exits with a non-zero exit code. Does not restart on clean shutdown (`systemctl stop`). |
| `RestartSec` | `5` | Waits 5 seconds before attempting a restart, preventing rapid restart loops if the service crashes immediately on startup. |
| `Environment` | `HOME=/opt/openjarvis` | Sets the `HOME` environment variable so OpenJarvis finds its configuration at `~/.openjarvis/config.toml` (resolving to `/opt/openjarvis/.openjarvis/config.toml`). |
### `[Install]` Section
| Directive | Value | Description |
|--------------|---------------------|---------------------------------------------------------------------------------------------|
| `WantedBy` | `multi-user.target` | The service starts when the system reaches multi-user mode (standard boot target for servers). `systemctl enable` creates a symlink under this target. |
## Configuration Options
### Changing the Bind Address and Port
Edit the `ExecStart` line to change the host or port:
```ini
ExecStart=/opt/openjarvis/.venv/bin/jarvis serve --host 127.0.0.1 --port 9000
```
!!! tip
Binding to `127.0.0.1` restricts access to localhost only. Use this when running behind a reverse proxy like Nginx or Caddy.
### Setting the Engine and Model
Pass additional flags to `jarvis serve`:
```ini
ExecStart=/opt/openjarvis/.venv/bin/jarvis serve --host 0.0.0.0 --port 8000 --engine ollama --model qwen3:8b
```
### Adding Environment Variables
Add multiple `Environment` directives or use `EnvironmentFile` for complex configurations:
```ini
[Service]
Environment=HOME=/opt/openjarvis
Environment=OPENJARVIS_ENGINE_DEFAULT=vllm
Environment=OPENJARVIS_OLLAMA_HOST=http://localhost:11434
```
Or load from a file:
```ini
[Service]
EnvironmentFile=/opt/openjarvis/.env
```
### Changing the User
If you prefer a different service user, update both the `User` directive and the paths:
```ini
[Service]
User=myuser
WorkingDirectory=/home/myuser/openjarvis
ExecStart=/home/myuser/openjarvis/.venv/bin/jarvis serve --host 0.0.0.0 --port 8000
Environment=HOME=/home/myuser/openjarvis
```
### Using a Configuration File
Ensure the configuration file exists at the path where `HOME` points:
```bash
sudo -u openjarvis mkdir -p /opt/openjarvis/.openjarvis
sudo -u openjarvis cp config.toml /opt/openjarvis/.openjarvis/config.toml
```
The server reads `~/.openjarvis/config.toml` on startup, where `~` resolves from the `HOME` environment variable.
## Viewing Logs
OpenJarvis logs are captured by journald. View them with `journalctl`:
```bash
# View all logs for the service
sudo journalctl -u openjarvis
# Follow logs in real time
sudo journalctl -u openjarvis -f
# View logs since the last boot
sudo journalctl -u openjarvis -b
# View logs from the last hour
sudo journalctl -u openjarvis --since "1 hour ago"
# View only error-level messages
sudo journalctl -u openjarvis -p err
```
## Managing the Service
### Start, Stop, and Restart
```bash
# Start the service
sudo systemctl start openjarvis
# Stop the service
sudo systemctl stop openjarvis
# Restart the service (stop + start)
sudo systemctl restart openjarvis
# Reload configuration without full restart (sends SIGHUP)
sudo systemctl reload-or-restart openjarvis
```
### Check Status
```bash
sudo systemctl status openjarvis
```
Example output:
```
● openjarvis.service - OpenJarvis API Server
Loaded: loaded (/etc/systemd/system/openjarvis.service; enabled; preset: enabled)
Active: active (running) since Fri 2026-02-21 10:00:00 UTC; 2h ago
Main PID: 12345 (jarvis)
Tasks: 4 (limit: 4915)
Memory: 256.0M
CPU: 1min 23s
CGroup: /system.slice/openjarvis.service
└─12345 /opt/openjarvis/.venv/bin/python /opt/openjarvis/.venv/bin/jarvis serve --host 0.0.0.0 --port 8000
```
### Enable and Disable on Boot
```bash
# Enable automatic start on boot
sudo systemctl enable openjarvis
# Disable automatic start on boot
sudo systemctl disable openjarvis
```
### Apply Changes After Editing the Unit File
After modifying `/etc/systemd/system/openjarvis.service`, reload the systemd daemon and restart the service:
```bash
sudo systemctl daemon-reload
sudo systemctl restart openjarvis
```
## Running Alongside Ollama
If Ollama is also managed via systemd, you can add an ordering dependency so the OpenJarvis service waits for Ollama to start:
```ini
[Unit]
Description=OpenJarvis API Server
After=network.target ollama.service
Requires=ollama.service
```
| Directive | Description |
|------------|--------------------------------------------------------------------------|
| `After` | Ensures OpenJarvis starts after Ollama. |
| `Requires` | If Ollama fails to start, OpenJarvis will not start either. |
!!! note
Use `Wants` instead of `Requires` if you want OpenJarvis to start even when Ollama is unavailable (for example, if you plan to start Ollama manually later).
+227
View File
@@ -0,0 +1,227 @@
# Changelog
All notable changes to OpenJarvis are documented in this file.
---
## v1.0.0
*Phase 5 -- SDK, Production Readiness, and Documentation*
### Added
- **Python SDK** -- `Jarvis` class providing a high-level sync API for
programmatic use
- `ask()` / `ask_full()` methods for direct engine and agent mode queries
- `MemoryHandle` proxy for lazy memory backend initialization
- `list_models()` and `list_engines()` for runtime introspection
- Router policy selection via config (`learning.default_policy`)
- Lazy engine initialization with automatic discovery and health probing
- Resource cleanup via `close()`
- **OpenClaw agent infrastructure**
- `OpenClawAgent` with HTTP and subprocess transports
- `ProtocolMessage` dataclass with JSON-line serialization/deserialization
- `MessageType` enum for structured agent communication
- `HttpTransport` for HTTP POST-based communication with OpenClaw servers
- `SubprocessTransport` for Node.js stdin/stdout communication
- `ProviderPlugin` wrapping inference engines for OpenClaw
- `MemorySearchManager` wrapping memory backends for OpenClaw
- **Benchmarking framework**
- `BaseBenchmark` ABC and `BenchmarkSuite` runner
- `LatencyBenchmark` measuring per-call latency (mean, p50, p95, min, max)
- `ThroughputBenchmark` measuring tokens-per-second throughput
- `BenchmarkResult` dataclass with JSONL export
- `jarvis bench run` CLI with options for model, engine, sample count,
benchmark selection, and JSON/JSONL output
- **Docker deployment**
- `Dockerfile` -- Multi-stage Python 3.12-slim build with `[server]` extra
- `Dockerfile.gpu` -- NVIDIA CUDA 12.4 runtime variant
- `docker-compose.yml` -- Services for `jarvis` (port 8000) and `ollama`
(port 11434)
- `deploy/systemd/openjarvis.service` -- systemd unit file for Linux
- `deploy/launchd/com.openjarvis.plist` -- launchd plist for macOS
- **Documentation site** -- MkDocs Material with mkdocstrings, covering
getting started, user guide, architecture, API reference, deployment, and
development
---
## v0.5.0
*Phase 4 -- Learning, Telemetry, and Router Policies*
### Added
- **Learning system**
- `RouterPolicy` ABC and `RoutingContext` dataclass
- `RewardFunction` ABC for scoring inference results
- `HeuristicRewardFunction` scoring on latency, cost, and efficiency
- `RouterPolicyRegistry` for pluggable routing strategies
- `HeuristicRouter` registered as `"heuristic"` policy (6 priority rules:
code detection, math detection, short/long queries, urgency override,
default fallback)
- `TraceDrivenPolicy` registered as `"learned"` policy with batch updates
via `update_from_traces()` and online updates via `observe()`
- `GRPORouterPolicy` stub registered as `"grpo"` for future RL training
- `ensure_registered()` pattern for lazy, test-safe registration
- **Telemetry aggregation**
- `TelemetryAggregator` with `per_model_stats()`, `per_engine_stats()`,
`top_models()`, `summary()`, `export_records()`, and `clear()` methods
- Time-range filtering via `since` / `until` parameters
- `ModelStats` and `EngineStats` dataclasses
- `AggregatedStats` summary dataclass
- **CLI enhancements**
- `--router` flag on `jarvis ask` for explicit policy selection
- `jarvis telemetry stats` -- display aggregated telemetry statistics
- `jarvis telemetry export --format json|csv` -- export telemetry records
- `jarvis telemetry clear --yes` -- delete all telemetry records
---
## v0.4.0
*Phase 3 -- Agents, Tools, and API Server*
### Added
- **Agent system**
- `BaseAgent` ABC with `run()` method returning `AgentResult`
- `AgentContext` dataclass with conversation, tools, and memory results
- `AgentResult` dataclass with content, tool results, turns, and metadata
- `AgentRegistry` for pluggable agent implementations
- `SimpleAgent` -- single-turn query-to-response, no tool calling
- `OrchestratorAgent` -- multi-turn tool-calling loop with `ToolExecutor`,
configurable `max_turns`
- `CustomAgent` -- template for user-defined agent behavior
- `OpenClawAgent` -- transport-based agent with tool-call loop and event
bus integration
- **Tool system**
- `BaseTool` ABC with `spec` property and `execute()` method
- `ToolSpec` dataclass describing tool interface and characteristics
- `ToolExecutor` dispatch engine with JSON argument parsing, latency
tracking, and event bus integration (`TOOL_CALL_START` / `TOOL_CALL_END`)
- `ToolRegistry` for tool discovery
- `to_openai_function()` method for OpenAI function calling format
- Built-in tools:
- `CalculatorTool` -- safe math evaluation via AST parsing
- `ThinkTool` -- reasoning scratchpad for chain-of-thought
- `RetrievalTool` -- memory search integration
- `LLMTool` -- sub-model calls within agent loops
- `FileReadTool` -- safe file reading with path validation
- **OpenAI-compatible API server** (`jarvis serve`)
- FastAPI + Uvicorn with optional `[server]` extra
- `POST /v1/chat/completions` -- non-streaming and SSE streaming
- `GET /v1/models` -- list available models
- `GET /health` -- health check endpoint
- Pydantic request/response models matching OpenAI API format
---
## v0.3.0
*Phase 2 -- Memory System*
### Added
- **Memory backends**
- `MemoryBackend` ABC with `store()`, `retrieve()`, `delete()`, `clear()`
- `RetrievalResult` dataclass with content, score, source, and metadata
- `MemoryRegistry` for backend discovery
- `SQLiteMemory` -- zero-dependency default using SQLite FTS5 with BM25
ranking and FTS5 query escaping
- `FAISSMemory` -- vector search using FAISS with sentence-transformers
embeddings (optional `[memory-faiss]` extra)
- `ColBERTMemory` -- ColBERTv2 neural retrieval backend (optional
`[memory-colbert]` extra)
- `BM25Memory` -- BM25 ranking backend using rank-bm25 (optional
`[memory-bm25]` extra)
- `HybridMemory` -- Reciprocal Rank Fusion combining multiple backends
- **Document processing**
- `ChunkConfig` dataclass for chunk size and overlap settings
- `chunk_text()` for splitting documents into overlapping chunks
- `ingest_path()` for recursively indexing files and directories
- `read_document()` with support for plain text, Markdown, and PDF
(optional `[memory-pdf]` extra)
- **Context injection**
- `ContextConfig` with top-k, minimum score, and max context token settings
- `inject_context()` for prepending memory results as system messages with
source attribution
- `--no-context` flag on `jarvis ask` to disable injection
- **CLI commands**
- `jarvis memory index <path>` -- index documents into memory
- `jarvis memory search <query>` -- search memory for relevant chunks
- `jarvis memory stats` -- show backend statistics
- **Event bus integration** -- `MEMORY_STORE` and `MEMORY_RETRIEVE` events
---
## v0.2.0
*Phase 1 -- Intelligence and Inference*
### Added
- **Intelligence pillar**
- `ModelSpec` dataclass with parameter count, context length, quantization,
VRAM requirements, and supported engines
- `ModelRegistry` for model metadata storage
- `BUILTIN_MODELS` catalog with pre-defined model specifications
- `register_builtin_models()` and `merge_discovered_models()` helpers
- `HeuristicRouter` with rule-based model selection
- `build_routing_context()` for query analysis (code detection, math
detection, length classification)
- **Inference engines**
- `InferenceEngine` ABC with `generate()`, `stream()`, `list_models()`,
and `health()` methods
- `EngineRegistry` for engine discovery
- `OllamaEngine` -- Ollama backend via native HTTP API with tool call
extraction
- `VllmEngine` -- vLLM backend via OpenAI-compatible API
- `LlamaCppEngine` -- llama.cpp server backend
- `EngineConnectionError` for unreachable engines
- `messages_to_dicts()` for Message-to-OpenAI-format conversion
- **Engine discovery**
- `discover_engines()` -- probe all registered engines for health
- `discover_models()` -- aggregate model lists across engines
- `get_engine()` -- get configured default with automatic fallback
- **Hardware detection**
- NVIDIA GPU detection via `nvidia-smi`
- AMD GPU detection via `rocm-smi`
- Apple Silicon detection via `system_profiler`
- CPU brand detection via `/proc/cpuinfo` and `sysctl`
- `recommend_engine()` mapping hardware to best engine
- **Telemetry**
- `TelemetryRecord` dataclass with timing, tokens, energy, and cost
- `TelemetryStore` with SQLite persistence and EventBus subscription
- `instrumented_generate()` wrapper for automatic telemetry recording
- **CLI**
- `jarvis ask <query>` -- query via discovered engine
- `jarvis ask --agent simple <query>` -- route through SimpleAgent
- `jarvis model list` -- list models from running engines
- `jarvis model info <model>` -- show model details
---
## v0.1.0
*Phase 0 -- Project Scaffolding*
### Added
- **Project structure** -- `hatchling` build backend, `uv` package manager,
`pyproject.toml` with extras for optional backends
- **Registry system** -- `RegistryBase[T]` generic base class with
class-specific entry isolation, `register()` decorator, `get()`, `create()`,
`items()`, `keys()`, `contains()`, `clear()` methods
- **Typed registries** -- `ModelRegistry`, `EngineRegistry`, `MemoryRegistry`,
`AgentRegistry`, `ToolRegistry`, `RouterPolicyRegistry`, `BenchmarkRegistry`
- **Core types** -- `Role` enum, `Message`, `Conversation` (with sliding
window), `ModelSpec`, `Quantization` enum, `ToolCall`, `ToolResult`,
`TelemetryRecord`, `StepType` enum, `TraceStep`, `Trace`
- **Configuration** -- `JarvisConfig` dataclass hierarchy, TOML loader with
overlay semantics, hardware auto-detection, `generate_default_toml()` for
`jarvis init`
- **Event bus** -- Synchronous pub/sub `EventBus` with `EventType` enum for
inter-pillar communication
- **CLI skeleton** -- Click-based `jarvis` command group with `--version`,
`--help`, and `init` subcommand
+417
View File
@@ -0,0 +1,417 @@
# Contributing Guide
This guide covers how to set up a development environment, run tests, and
contribute code to OpenJarvis.
---
## Development Setup
### Prerequisites
| Requirement | Version | Notes |
|---|---|---|
| Python | 3.10+ | Required |
| [uv](https://docs.astral.sh/uv/) | Latest | Package manager |
| Node.js | 22+ | Only needed for OpenClaw agent |
### Clone and Install
```bash
git clone https://github.com/jonsaadfalcon/OpenJarvis.git
cd OpenJarvis
uv sync --extra dev
```
This installs the package in editable mode along with all development
dependencies (pytest, ruff, respx, pytest-asyncio, pytest-cov).
!!! tip "Optional extras"
Install additional extras for specific backends you want to work on:
```bash
# Memory backends
uv sync --extra dev --extra memory-faiss --extra memory-colbert --extra memory-bm25
# Cloud inference
uv sync --extra dev --extra inference-cloud --extra inference-google
# API server
uv sync --extra dev --extra server
# Documentation
uv sync --extra dev --extra docs
```
### Verify Installation
```bash
uv run jarvis --version # Should print 1.0.0
uv run jarvis --help # Show all subcommands
```
---
## Running Tests
OpenJarvis uses [pytest](https://docs.pytest.org/) with approximately 1,000+
tests organized by module.
### Full Test Suite
```bash
uv run pytest tests/ -v
```
### Run a Specific Test File
```bash
uv run pytest tests/core/test_registry.py -v
uv run pytest tests/engine/test_ollama.py -v
uv run pytest tests/memory/test_sqlite.py -v
```
### Run a Specific Test
```bash
uv run pytest tests/core/test_registry.py::test_register_and_get -v
```
### Run Tests by Module
```bash
uv run pytest tests/agents/ -v # All agent tests
uv run pytest tests/tools/ -v # All tool tests
uv run pytest tests/learning/ -v # All learning tests
```
### Test Coverage
```bash
uv run pytest tests/ --cov=openjarvis --cov-report=html
```
### Test Markers
Tests that require specific hardware or running services are gated behind
pytest markers. By default, these tests are collected but will skip
gracefully if the requirement is not met.
| Marker | Description | Example |
|---|---|---|
| `live` | Requires a running inference engine (Ollama, vLLM, etc.) | `@pytest.mark.live` |
| `cloud` | Requires cloud API keys (`OPENAI_API_KEY`, etc.) | `@pytest.mark.cloud` |
| `nvidia` | Requires an NVIDIA GPU | `@pytest.mark.nvidia` |
| `amd` | Requires an AMD GPU with ROCm | `@pytest.mark.amd` |
| `apple` | Requires Apple Silicon | `@pytest.mark.apple` |
| `slow` | Long-running test | `@pytest.mark.slow` |
Run only tests matching a specific marker:
```bash
uv run pytest tests/ -m live -v # Only live engine tests
uv run pytest tests/ -m "not slow" -v # Skip slow tests
uv run pytest tests/ -m "not cloud" -v # Skip cloud tests
```
!!! info "Registry isolation in tests"
The test `conftest.py` includes an `autouse` fixture that clears all
registries and resets the event bus before every test. This ensures
complete isolation between tests. Modules that need their registrations
to survive clearing use the `ensure_registered()` pattern described
below.
---
## Linting
OpenJarvis uses [Ruff](https://docs.astral.sh/ruff/) for linting, configured
in `pyproject.toml`:
```bash
uv run ruff check src/ tests/
```
The Ruff configuration targets Python 3.10 and enables the following rule sets:
- **E** -- pycodestyle errors
- **F** -- Pyflakes
- **I** -- isort (import ordering)
- **W** -- pycodestyle warnings
Fix auto-fixable issues:
```bash
uv run ruff check src/ tests/ --fix
```
---
## Building Documentation
The documentation site uses [MkDocs Material](https://squidfunnel.com/mkdocs-material/).
```bash
# Install docs dependencies
uv sync --extra docs
# Serve locally with hot reload
uv run mkdocs serve --dev-addr 127.0.0.1:8001
# Build static site
uv run mkdocs build
```
The site configuration lives in `mkdocs.yml`. API reference pages use
[mkdocstrings](https://mkdocstrings.github.io/) to auto-generate from
docstrings with the NumPy docstring style.
---
## Project Structure
The source code is organized under `src/openjarvis/`:
```
src/openjarvis/
__init__.py # Package root, __version__
sdk.py # Jarvis class — high-level Python SDK
core/ # Shared infrastructure
config.py # JarvisConfig, hardware detection, TOML loader
events.py # EventBus pub/sub system
registry.py # RegistryBase[T] and all typed registries
types.py # Message, ModelSpec, ToolResult, Trace, etc.
intelligence/ # Model management and query routing
model_catalog.py # BUILTIN_MODELS, register/merge helpers
router.py # HeuristicRouter, build_routing_context
engine/ # Inference engine backends
_stubs.py # InferenceEngine ABC
_base.py # EngineConnectionError, messages_to_dicts
_discovery.py # discover_engines, discover_models, get_engine
_openai_compat.py # OpenAI-compatible wrapper
ollama.py # OllamaEngine
vllm.py # VllmEngine
llamacpp.py # LlamaCppEngine
sglang.py # SGLangEngine
cloud.py # CloudEngine (OpenAI/Anthropic/Google)
agents/ # Agent implementations
_stubs.py # BaseAgent ABC, AgentContext, AgentResult
simple.py # SimpleAgent — single-turn, no tools
orchestrator.py # OrchestratorAgent — multi-turn tool calling
custom.py # CustomAgent — user template
react.py # ReActAgent
openhands.py # OpenHands agent
openclaw.py # OpenClawAgent — HTTP/subprocess transport
openclaw_protocol.py # OpenClaw message protocol
openclaw_transport.py # OpenClaw transports (HTTP, subprocess)
openclaw_plugin.py # OpenClaw provider/memory plugins
memory/ # Memory / retrieval backends
_stubs.py # MemoryBackend ABC, RetrievalResult
sqlite.py # SQLiteMemory — FTS5 default backend
faiss_backend.py # FAISS vector backend
colbert_backend.py # ColBERTv2 backend
bm25.py # BM25 backend
hybrid.py # Hybrid (RRF fusion) backend
chunking.py # ChunkConfig, chunk_text
context.py # ContextConfig, inject_context
ingest.py # ingest_path, read_document
tools/ # Tool system
_stubs.py # BaseTool ABC, ToolSpec, ToolExecutor
calculator.py # CalculatorTool — safe AST math
think.py # ThinkTool — reasoning scratchpad
retrieval.py # RetrievalTool — memory search
llm_tool.py # LLMTool — sub-model calls
file_read.py # FileReadTool — safe file reading
web_search.py # WebSearchTool
code_interpreter.py # CodeInterpreterTool
learning/ # Router policies and reward functions
_stubs.py # RouterPolicy ABC, RewardFunction ABC
heuristic_policy.py # Wire HeuristicRouter to registry
trace_policy.py # TraceDrivenPolicy — learns from traces
grpo_policy.py # GRPORouterPolicy — RL training stub
heuristic_reward.py # HeuristicRewardFunction
traces/ # Full interaction recording
store.py # TraceStore — SQLite persistence
collector.py # TraceCollector — wraps agents
analyzer.py # TraceAnalyzer — aggregated queries
telemetry/ # Inference telemetry
store.py # TelemetryStore — SQLite persistence
aggregator.py # TelemetryAggregator — per-model/engine stats
wrapper.py # instrumented_generate() wrapper
bench/ # Benchmarking framework
_stubs.py # BaseBenchmark ABC, BenchmarkSuite
latency.py # LatencyBenchmark
throughput.py # ThroughputBenchmark
server/ # OpenAI-compatible API server
app.py # FastAPI application factory
routes.py # /v1/chat/completions, /v1/models, /health
mcp/ # MCP (Model Context Protocol) layer
cli/ # Click CLI commands
__init__.py # main group
ask.py # jarvis ask
init_cmd.py # jarvis init
model.py # jarvis model list/info
memory_cmd.py # jarvis memory index/search/stats
telemetry_cmd.py # jarvis telemetry stats/export/clear
bench_cmd.py # jarvis bench run
serve.py # jarvis serve
```
---
## Code Conventions
### File Naming
| Pattern | Purpose | Examples |
|---|---|---|
| `_stubs.py` | ABC definitions and dataclasses | `engine/_stubs.py`, `agents/_stubs.py`, `tools/_stubs.py` |
| `_discovery.py` | Auto-detection and probing logic | `engine/_discovery.py` |
| `_base.py` | Shared utilities and re-exports | `engine/_base.py` |
| `*_cmd.py` | CLI command modules | `init_cmd.py`, `memory_cmd.py`, `bench_cmd.py` |
### Registry Pattern
All extensible components use the decorator-based registry pattern. New
implementations are added by decorating a class -- no factory modifications
needed:
```python
from openjarvis.core.registry import EngineRegistry
@EngineRegistry.register("my_engine")
class MyEngine(InferenceEngine):
...
```
Available registries:
| Registry | Stores | Key examples |
|---|---|---|
| `ModelRegistry` | `ModelSpec` objects | `"qwen3:8b"`, `"llama3.1:70b"` |
| `EngineRegistry` | `InferenceEngine` classes | `"ollama"`, `"vllm"`, `"llamacpp"` |
| `MemoryRegistry` | `MemoryBackend` classes | `"sqlite"`, `"faiss"`, `"bm25"` |
| `AgentRegistry` | `BaseAgent` classes | `"simple"`, `"orchestrator"` |
| `ToolRegistry` | `BaseTool` classes | `"calculator"`, `"think"`, `"retrieval"` |
| `RouterPolicyRegistry` | `RouterPolicy` classes | `"heuristic"`, `"learned"` |
| `BenchmarkRegistry` | `BaseBenchmark` classes | `"latency"`, `"throughput"` |
### Optional Dependencies
Backends that depend on optional packages use the `try/except ImportError`
pattern to fail gracefully when deps are not installed:
```python
# In __init__.py — import to trigger registration
try:
import openjarvis.memory.faiss_backend # noqa: F401
except ImportError:
pass
```
This ensures the package always loads, even if `faiss-cpu` or other optional
dependencies are not installed.
### The `ensure_registered()` Pattern
Benchmark and learning modules use lazy registration so that their entries
survive registry clearing in tests:
```python
def ensure_registered() -> None:
"""Register the latency benchmark if not already present."""
if not BenchmarkRegistry.contains("latency"):
BenchmarkRegistry.register_value("latency", LatencyBenchmark)
```
This pattern checks `contains()` before registering, making it safe to call
multiple times without raising a duplicate-key error.
### Dataclass Conventions
- Use `slots=True` on all dataclasses for memory efficiency:
```python
@dataclass(slots=True)
class BenchmarkResult:
benchmark_name: str
model: str
...
```
### Type Hints
- All function signatures must have type annotations
- Use `from __future__ import annotations` at the top of every module
- Use `Optional[X]` for nullable types
- Use `Sequence` for read-only collections, `List` for mutable ones
### Import Style
- Absolute imports only (`from openjarvis.core.registry import ...`)
- Sort imports with `ruff` (isort rules enabled)
- Place `from __future__ import annotations` as the first import
---
## PR Guidelines
### Before Submitting
1. **Run the full test suite** and verify no regressions:
```bash
uv run pytest tests/ -v
```
2. **Run the linter** and fix all issues:
```bash
uv run ruff check src/ tests/
```
3. **Add tests** for new functionality. Place them in the corresponding
`tests/` subdirectory (e.g., new engine tests go in `tests/engine/`).
4. **Follow the registry pattern** for any new extensible component.
### Commit Messages
- Use the imperative mood (e.g., "Add FAISS memory backend")
- Keep the first line under 72 characters
- Reference relevant issues or PRs
### What Makes a Good PR
- **Focused**: One feature, fix, or refactor per PR
- **Tested**: Include unit tests that cover the new code paths
- **Documented**: Update docstrings and documentation pages if adding
public API
- **Backwards compatible**: Avoid breaking existing interfaces without
discussion
### Adding a New Pillar Component
When adding a new engine, memory backend, agent, tool, benchmark, or router
policy:
1. Implement the corresponding ABC
2. Register with the appropriate `@XRegistry.register("key")` decorator
3. Add an import in the module's `__init__.py` (with `try/except ImportError`
if the component has optional deps)
4. Add tests in the matching `tests/` subdirectory
5. Add an entry in `pyproject.toml` under `[project.optional-dependencies]`
if the component requires new packages
See the [Extending OpenJarvis](extending.md) guide for complete examples.
+855
View File
@@ -0,0 +1,855 @@
# Extending OpenJarvis
OpenJarvis is designed to be extended through its registry pattern. Every
major subsystem defines an abstract base class (ABC) and uses a typed registry
for runtime discovery. To add a new component, implement the ABC, decorate
it with the registry, and import it in the module's `__init__.py`.
This guide provides complete, working code examples for each extension point.
---
## Adding a New Inference Engine
Inference engines connect OpenJarvis to an LLM runtime. All engines implement
the `InferenceEngine` ABC defined in `engine/_stubs.py`.
### Step 1: Create the Engine Module
Create `src/openjarvis/engine/my_engine.py`:
```python
"""My custom inference engine backend."""
from __future__ import annotations
from collections.abc import AsyncIterator, Sequence
from typing import Any, Dict, List
import httpx
from openjarvis.core.registry import EngineRegistry
from openjarvis.core.types import Message
from openjarvis.engine._base import (
EngineConnectionError,
InferenceEngine,
messages_to_dicts,
)
@EngineRegistry.register("my_engine") # (1)!
class MyEngine(InferenceEngine):
"""Custom inference engine backend."""
engine_id = "my_engine" # (2)!
def __init__(
self,
host: str = "http://localhost:9000",
*,
timeout: float = 120.0,
) -> None:
self._host = host.rstrip("/")
self._client = httpx.Client(base_url=self._host, timeout=timeout)
def generate(
self,
messages: Sequence[Message],
*,
model: str,
temperature: float = 0.7,
max_tokens: int = 1024,
**kwargs: Any,
) -> Dict[str, Any]:
"""Synchronous completion."""
payload = {
"model": model,
"messages": messages_to_dicts(messages), # (3)!
"temperature": temperature,
"max_tokens": max_tokens,
}
# Pass tools if provided
tools = kwargs.get("tools")
if tools:
payload["tools"] = tools
try:
resp = self._client.post("/v1/chat/completions", json=payload)
resp.raise_for_status()
except (httpx.ConnectError, httpx.TimeoutException) as exc:
raise EngineConnectionError(
f"Engine not reachable at {self._host}"
) from exc
data = resp.json()
choice = data.get("choices", [{}])[0]
message = choice.get("message", {})
usage = data.get("usage", {})
result: Dict[str, Any] = {
"content": message.get("content", ""),
"usage": {
"prompt_tokens": usage.get("prompt_tokens", 0),
"completion_tokens": usage.get("completion_tokens", 0),
"total_tokens": usage.get("total_tokens", 0),
},
"model": data.get("model", model),
"finish_reason": choice.get("finish_reason", "stop"),
}
# Extract tool calls if present
raw_tool_calls = message.get("tool_calls", [])
if raw_tool_calls:
result["tool_calls"] = [
{
"id": tc.get("id", f"call_{i}"),
"name": tc["function"]["name"],
"arguments": tc["function"]["arguments"],
}
for i, tc in enumerate(raw_tool_calls)
]
return result
async def stream(
self,
messages: Sequence[Message],
*,
model: str,
temperature: float = 0.7,
max_tokens: int = 1024,
**kwargs: Any,
) -> AsyncIterator[str]:
"""Yield token strings as they are generated."""
# Implement SSE or WebSocket streaming for your engine
result = self.generate(
messages, model=model, temperature=temperature,
max_tokens=max_tokens, **kwargs,
)
yield result.get("content", "")
def list_models(self) -> List[str]:
"""Return identifiers of models available on this engine."""
try:
resp = self._client.get("/v1/models")
resp.raise_for_status()
data = resp.json()
return [m["id"] for m in data.get("data", [])]
except Exception:
return []
def health(self) -> bool:
"""Return True when the engine is reachable and healthy."""
try:
resp = self._client.get("/health", timeout=2.0)
return resp.status_code == 200
except Exception:
return False
```
1. The `@EngineRegistry.register("my_engine")` decorator makes this engine
discoverable by key at runtime.
2. The `engine_id` class attribute is used in telemetry and benchmark results.
3. `messages_to_dicts()` converts `Message` objects to OpenAI-format dicts.
### Step 2: Register in `__init__.py`
Add your engine import to `src/openjarvis/engine/__init__.py`:
```python
import openjarvis.engine.my_engine # noqa: F401
```
If your engine requires optional dependencies, wrap the import:
```python
try:
import openjarvis.engine.my_engine # noqa: F401
except ImportError:
pass
```
### Step 3: Add Optional Dependencies
If your engine needs extra packages, add them to `pyproject.toml`:
```toml
[project.optional-dependencies]
inference-myengine = [
"my-engine-sdk>=1.0",
]
```
### Required ABC Methods
| Method | Signature | Returns | Description |
|---|---|---|---|
| `generate` | `(messages, *, model, temperature, max_tokens, **kwargs)` | `Dict[str, Any]` | Synchronous completion with `content` and `usage` keys |
| `stream` | `(messages, *, model, temperature, max_tokens, **kwargs)` | `AsyncIterator[str]` | Yields token strings as they are generated |
| `list_models` | `()` | `List[str]` | Model identifiers available on this engine |
| `health` | `()` | `bool` | `True` when the engine is reachable |
The `generate` return dict must include at minimum:
```python
{
"content": "The response text",
"usage": {
"prompt_tokens": 10,
"completion_tokens": 20,
"total_tokens": 30,
},
"model": "model-name",
"finish_reason": "stop", # or "tool_calls"
}
```
!!! tip "Tool call support"
If your engine supports tool/function calling, include a `"tool_calls"`
key in the return dict. Each tool call should have `id`, `name`, and
`arguments` (JSON string) keys.
---
## Adding a New Memory Backend
Memory backends provide persistent, searchable storage. All backends implement
the `MemoryBackend` ABC defined in `memory/_stubs.py`.
### Complete Example
Create `src/openjarvis/memory/my_backend.py`:
```python
"""Custom memory backend example."""
from __future__ import annotations
from typing import Any, Dict, List, Optional
from openjarvis.core.registry import MemoryRegistry
from openjarvis.memory._stubs import MemoryBackend, RetrievalResult
@MemoryRegistry.register("my_backend")
class MyMemoryBackend(MemoryBackend):
"""Custom memory backend implementation."""
backend_id = "my_backend"
def __init__(self, **kwargs: Any) -> None:
# Initialize your storage (database, index, etc.)
self._store: Dict[str, Dict[str, Any]] = {}
def store(
self,
content: str,
*,
source: str = "",
metadata: Optional[Dict[str, Any]] = None,
) -> str:
"""Persist content and return a unique document id."""
import uuid
doc_id = uuid.uuid4().hex
self._store[doc_id] = {
"content": content,
"source": source,
"metadata": metadata or {},
}
return doc_id
def retrieve(
self,
query: str,
*,
top_k: int = 5,
**kwargs: Any,
) -> List[RetrievalResult]:
"""Search for query and return the top-k results."""
results: List[RetrievalResult] = []
for doc_id, doc in self._store.items():
# Implement your search/ranking logic here
if query.lower() in doc["content"].lower():
results.append(RetrievalResult(
content=doc["content"],
score=1.0,
source=doc["source"],
metadata=doc["metadata"],
))
return results[:top_k]
def delete(self, doc_id: str) -> bool:
"""Delete a document by id. Return True if it existed."""
return self._store.pop(doc_id, None) is not None
def clear(self) -> None:
"""Remove all stored documents."""
self._store.clear()
```
### Register in `__init__.py`
Add to `src/openjarvis/memory/__init__.py`:
```python
try:
import openjarvis.memory.my_backend # noqa: F401
except ImportError:
pass
```
### Required ABC Methods
| Method | Signature | Returns | Description |
|---|---|---|---|
| `store` | `(content, *, source, metadata)` | `str` | Persist content, return document ID |
| `retrieve` | `(query, *, top_k, **kwargs)` | `List[RetrievalResult]` | Search and return ranked results |
| `delete` | `(doc_id)` | `bool` | Delete by ID, return whether it existed |
| `clear` | `()` | `None` | Remove all stored documents |
The `RetrievalResult` dataclass has these fields:
```python
@dataclass(slots=True)
class RetrievalResult:
content: str # The retrieved text
score: float = 0.0 # Relevance score
source: str = "" # Source identifier
metadata: Dict[str, Any] = field(default_factory=dict)
```
---
## Adding a New Agent
Agents implement the logic for handling queries, calling tools, and managing
multi-turn interactions. All agents implement the `BaseAgent` ABC from
`agents/_stubs.py`.
### Complete Example
Create `src/openjarvis/agents/my_agent.py`:
```python
"""Custom agent implementation."""
from __future__ import annotations
from typing import Any, Optional
from openjarvis.agents._stubs import AgentContext, AgentResult, BaseAgent
from openjarvis.core.events import EventBus, EventType
from openjarvis.core.registry import AgentRegistry
from openjarvis.core.types import Message, Role
from openjarvis.engine._stubs import InferenceEngine
from openjarvis.telemetry.wrapper import instrumented_generate
@AgentRegistry.register("my_agent")
class MyAgent(BaseAgent):
"""Custom agent with specialized behavior."""
agent_id = "my_agent"
def __init__(
self,
engine: InferenceEngine,
model: str,
*,
bus: Optional[EventBus] = None,
temperature: float = 0.7,
max_tokens: int = 1024,
) -> None:
self._engine = engine
self._model = model
self._bus = bus
self._temperature = temperature
self._max_tokens = max_tokens
def run(
self,
input: str,
context: Optional[AgentContext] = None,
**kwargs: Any,
) -> AgentResult:
"""Execute the agent on input and return an AgentResult."""
# Emit turn start event
if self._bus:
self._bus.publish(EventType.AGENT_TURN_START, {
"agent": self.agent_id,
"input": input,
})
# Build messages from context + user input
messages: list[Message] = []
# Add a system prompt for your agent's personality
messages.append(Message(
role=Role.SYSTEM,
content="You are a helpful assistant with specialized knowledge.",
))
# Include any prior conversation from context
if context and context.conversation.messages:
messages.extend(context.conversation.messages)
messages.append(Message(role=Role.USER, content=input))
# Generate via instrumented path for telemetry
if self._bus:
result = instrumented_generate(
self._engine,
messages,
model=self._model,
bus=self._bus,
temperature=self._temperature,
max_tokens=self._max_tokens,
)
else:
result = self._engine.generate(
messages,
model=self._model,
temperature=self._temperature,
max_tokens=self._max_tokens,
)
content = result.get("content", "")
# Emit turn end event
if self._bus:
self._bus.publish(EventType.AGENT_TURN_END, {
"agent": self.agent_id,
"content_length": len(content),
})
return AgentResult(content=content, turns=1)
```
### Register in `__init__.py`
Add to `src/openjarvis/agents/__init__.py`:
```python
try:
import openjarvis.agents.my_agent # noqa: F401
except ImportError:
pass
```
### Key Types
=== "AgentContext"
```python
@dataclass(slots=True)
class AgentContext:
conversation: Conversation = field(default_factory=Conversation)
tools: List[str] = field(default_factory=list)
memory_results: List[Any] = field(default_factory=list)
metadata: Dict[str, Any] = field(default_factory=dict)
```
=== "AgentResult"
```python
@dataclass(slots=True)
class AgentResult:
content: str
tool_results: List[ToolResult] = field(default_factory=list)
turns: int = 0
metadata: Dict[str, Any] = field(default_factory=dict)
```
---
## Adding a New Tool
Tools are callable capabilities that agents can invoke during multi-turn
reasoning. All tools implement the `BaseTool` ABC from `tools/_stubs.py`.
### Complete Example
Create `src/openjarvis/tools/my_tool.py`:
```python
"""Custom tool implementation."""
from __future__ import annotations
from typing import Any
from openjarvis.core.registry import ToolRegistry
from openjarvis.core.types import ToolResult
from openjarvis.tools._stubs import BaseTool, ToolSpec
@ToolRegistry.register("my_tool")
class MyTool(BaseTool):
"""A custom tool that does something useful."""
tool_id = "my_tool"
@property
def spec(self) -> ToolSpec:
"""Return the tool specification."""
return ToolSpec(
name="my_tool",
description="Does something useful with the provided input.",
parameters={ # (1)!
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The input to process",
},
"max_results": {
"type": "integer",
"description": "Maximum number of results to return",
"default": 5,
},
},
"required": ["query"],
},
category="utility",
cost_estimate=0.001, # Estimated cost in USD per call
latency_estimate=0.5, # Estimated latency in seconds
requires_confirmation=False,
)
def execute(self, **params: Any) -> ToolResult:
"""Execute the tool with the given parameters."""
query = params.get("query", "")
max_results = params.get("max_results", 5)
if not query:
return ToolResult(
tool_name="my_tool",
content="No query provided.",
success=False,
)
try:
# Your tool logic here
result_text = f"Processed '{query}' (max_results={max_results})"
return ToolResult(
tool_name="my_tool",
content=result_text,
success=True,
)
except Exception as exc:
return ToolResult(
tool_name="my_tool",
content=f"Error: {exc}",
success=False,
)
```
1. The `parameters` dict follows the [JSON Schema](https://json-schema.org/)
format used by OpenAI function calling. The `ToolExecutor` will parse
incoming JSON arguments and pass them as keyword arguments to `execute()`.
### Register in `__init__.py`
Add to `src/openjarvis/tools/__init__.py`:
```python
try:
import openjarvis.tools.my_tool # noqa: F401
except ImportError:
pass
```
### How Tools Are Invoked
The `ToolExecutor` handles the dispatch loop:
1. The agent's LLM generates a `tool_calls` response with tool name and
JSON arguments
2. `ToolExecutor.execute()` parses the JSON arguments
3. The matching tool's `execute(**params)` is called
4. The `ToolResult` is returned to the agent for the next turn
```python
from openjarvis.tools._stubs import ToolExecutor
executor = ToolExecutor(
tools=[MyTool()],
bus=event_bus, # Optional — enables TOOL_CALL_START/END events
)
# Dispatch a tool call
from openjarvis.core.types import ToolCall
call = ToolCall(id="call_1", name="my_tool", arguments='{"query": "test"}')
result = executor.execute(call)
```
The `to_openai_function()` method converts a tool's spec to OpenAI function
calling format, which is sent to the LLM alongside the conversation:
```python
tool = MyTool()
openai_format = tool.to_openai_function()
# {
# "type": "function",
# "function": {
# "name": "my_tool",
# "description": "Does something useful...",
# "parameters": { ... }
# }
# }
```
---
## Adding a New Benchmark
Benchmarks measure engine performance. All benchmarks implement the
`BaseBenchmark` ABC from `bench/_stubs.py` and use the `ensure_registered()`
pattern for lazy registration.
### Complete Example
Create `src/openjarvis/bench/my_benchmark.py`:
```python
"""Custom benchmark — measures time to first token."""
from __future__ import annotations
import time
from openjarvis.bench._stubs import BaseBenchmark, BenchmarkResult
from openjarvis.core.registry import BenchmarkRegistry
from openjarvis.core.types import Message, Role
from openjarvis.engine._stubs import InferenceEngine
class TTFTBenchmark(BaseBenchmark):
"""Measures time-to-first-token across multiple samples."""
@property
def name(self) -> str:
return "ttft"
@property
def description(self) -> str:
return "Measures time-to-first-token latency"
def run(
self,
engine: InferenceEngine,
model: str,
*,
num_samples: int = 10,
) -> BenchmarkResult:
ttft_values: list[float] = []
errors = 0
for _ in range(num_samples):
messages = [Message(role=Role.USER, content="Hello")]
t0 = time.time()
try:
engine.generate(messages, model=model)
ttft_values.append(time.time() - t0)
except Exception:
errors += 1
if not ttft_values:
return BenchmarkResult(
benchmark_name=self.name,
model=model,
engine=engine.engine_id,
metrics={},
samples=num_samples,
errors=errors,
)
return BenchmarkResult(
benchmark_name=self.name,
model=model,
engine=engine.engine_id,
metrics={
"mean_ttft": sum(ttft_values) / len(ttft_values),
"min_ttft": min(ttft_values),
"max_ttft": max(ttft_values),
},
samples=num_samples,
errors=errors,
)
def ensure_registered() -> None: # (1)!
"""Register the TTFT benchmark if not already present."""
if not BenchmarkRegistry.contains("ttft"):
BenchmarkRegistry.register_value("ttft", TTFTBenchmark)
```
1. The `ensure_registered()` function uses `contains()` before
`register_value()` so it can be called multiple times safely. This is
required because tests clear all registries between runs.
### Register in `__init__.py`
Update `src/openjarvis/bench/__init__.py` to call `ensure_registered()`:
```python
from openjarvis.bench.my_benchmark import ensure_registered as _reg_ttft
_reg_ttft()
```
### BenchmarkResult Fields
```python
@dataclass(slots=True)
class BenchmarkResult:
benchmark_name: str # e.g. "latency", "throughput"
model: str # Model identifier
engine: str # Engine identifier
metrics: Dict[str, float] = ... # Measured values
metadata: Dict[str, Any] = ... # Extra info
samples: int = 0 # Number of samples run
errors: int = 0 # Number of failed samples
```
---
## Adding a New Router Policy
Router policies determine which model handles a given query. All policies
implement the `RouterPolicy` ABC from `learning/_stubs.py`.
### Complete Example
Create `src/openjarvis/learning/my_policy.py`:
```python
"""Custom router policy — selects model based on query length."""
from __future__ import annotations
from typing import List, Optional
from openjarvis.core.registry import RouterPolicyRegistry
from openjarvis.learning._stubs import RouterPolicy, RoutingContext
class QueryLengthPolicy(RouterPolicy):
"""Routes queries to models based on query length.
Short queries go to a fast, small model. Long or complex queries
go to a larger, more capable model.
"""
def __init__(
self,
available_models: Optional[List[str]] = None,
*,
default_model: str = "",
fallback_model: str = "",
short_threshold: int = 100,
long_threshold: int = 500,
) -> None:
self._available = available_models or []
self._default = default_model
self._fallback = fallback_model
self._short_threshold = short_threshold
self._long_threshold = long_threshold
def select_model(self, context: RoutingContext) -> str:
"""Return the model registry key best suited for this context."""
available = self._available
if not available:
return self._default or self._fallback or ""
if context.query_length < self._short_threshold:
# Prefer the first (presumably smallest) available model
return available[0]
elif context.query_length > self._long_threshold:
# Prefer the last (presumably largest) available model
return available[-1]
# Default to configured model
if self._default and self._default in available:
return self._default
return available[0]
def ensure_registered() -> None:
"""Register QueryLengthPolicy if not already present."""
if not RouterPolicyRegistry.contains("query_length"):
RouterPolicyRegistry.register_value("query_length", QueryLengthPolicy)
ensure_registered()
```
### Register in `__init__.py`
Update `src/openjarvis/learning/__init__.py`:
```python
from openjarvis.learning.my_policy import ensure_registered as _reg_ql
_reg_ql()
```
### Using Your Policy
Once registered, your policy can be selected via the config file or CLI:
=== "Config (TOML)"
```toml
[learning]
default_policy = "query_length"
```
=== "CLI"
```bash
uv run jarvis ask --router query_length "Hello"
```
### The RoutingContext
The `RoutingContext` dataclass provides all the information a router needs:
```python
@dataclass(slots=True)
class RoutingContext:
query: str = ""
query_length: int = 0
has_code: bool = False
has_math: bool = False
language: str = "en"
urgency: float = 0.5 # 0 = low priority, 1 = real-time
metadata: Dict[str, Any] = field(default_factory=dict)
```
The `build_routing_context()` helper in `intelligence/router.py` populates
this from a raw query string, detecting code and math patterns automatically.
---
## Summary
| Component | ABC | Registry | Key location |
|---|---|---|---|
| Inference Engine | `InferenceEngine` | `EngineRegistry` | `engine/_stubs.py` |
| Memory Backend | `MemoryBackend` | `MemoryRegistry` | `memory/_stubs.py` |
| Agent | `BaseAgent` | `AgentRegistry` | `agents/_stubs.py` |
| Tool | `BaseTool` | `ToolRegistry` | `tools/_stubs.py` |
| Benchmark | `BaseBenchmark` | `BenchmarkRegistry` | `bench/_stubs.py` |
| Router Policy | `RouterPolicy` | `RouterPolicyRegistry` | `learning/_stubs.py` |
The general pattern for all extension points:
1. Implement the ABC in a new module
2. Decorate the class with `@XRegistry.register("key")` or use
`ensure_registered()` for lazy registration
3. Import the module in the package's `__init__.py` (with `try/except
ImportError` if optional deps are involved)
4. Add tests in `tests/<module>/`
5. Add optional dependencies to `pyproject.toml` if needed
+85
View File
@@ -0,0 +1,85 @@
# Roadmap
OpenJarvis development follows a phased approach, with each version adding
a major pillar or cross-cutting capability to the framework.
---
## Development Phases
| Version | Phase | Status | Delivers |
|---|---|---|---|
| **v0.1** | Phase 0 -- Scaffolding | :material-check-circle:{ .green } Complete | Project scaffolding, registry system (`RegistryBase[T]`), core types (`Message`, `ModelSpec`, `Conversation`, `ToolResult`), configuration loader with hardware detection, Click CLI skeleton |
| **v0.2** | Phase 1 -- Intelligence + Inference | :material-check-circle:{ .green } Complete | Intelligence pillar (model catalog, heuristic router), inference engines (Ollama, vLLM, llama.cpp), engine discovery and health probing, `jarvis ask` command working end-to-end |
| **v0.3** | Phase 2 -- Memory | :material-check-circle:{ .green } Complete | Memory backends (SQLite/FTS5, FAISS, ColBERTv2, BM25, Hybrid/RRF), document chunking and ingestion pipeline, context injection with source attribution, `jarvis memory` commands |
| **v0.4** | Phase 3 -- Agents + Tools + Server | :material-check-circle:{ .green } Complete | Agent system (SimpleAgent, OrchestratorAgent, OpenClawAgent, CustomAgent), tool system (Calculator, Think, Retrieval, LLM, FileRead), ToolExecutor dispatch engine, OpenAI-compatible API server (`jarvis serve`) |
| **v0.5** | Phase 4 -- Learning + Telemetry | :material-check-circle:{ .green } Complete | Learning system (HeuristicRouter policy, TraceDrivenPolicy, GRPO stub), reward functions, telemetry aggregation (per-model/engine stats, export), `--router` CLI flag, `jarvis telemetry` commands |
| **v1.0** | Phase 5 -- SDK + Production | :material-check-circle:{ .green } Complete | Python SDK (`Jarvis` class, `MemoryHandle`), OpenClaw agent infrastructure (protocol, transports, plugins), benchmarking framework (latency, throughput), Docker deployment (CPU + GPU), MkDocs documentation site |
| **v1.1** | Phase 6 -- Traces + Learning | :material-progress-clock:{ .amber } In Progress | Trace system (`TraceStore`, `TraceCollector`, `TraceAnalyzer`), trace-driven learning, pluggable agent architectures (ReAct, OpenHands), MCP integration layer |
---
## Current Status
OpenJarvis v1.0 is complete. The framework provides:
- **Four core abstractions** -- Intelligence, Engine, Agentic Logic, Memory -- each with an ABC interface and registry-based discovery
- **Five inference engines** -- Ollama, vLLM, llama.cpp, SGLang, Cloud (OpenAI/Anthropic/Google)
- **Five memory backends** -- SQLite/FTS5, FAISS, ColBERTv2, BM25, Hybrid (RRF fusion)
- **Multiple agent types** -- Simple, Orchestrator, Custom, OpenClaw, ReAct, OpenHands
- **Seven built-in tools** -- Calculator, Think, Retrieval, LLM, FileRead, WebSearch, CodeInterpreter
- **Python SDK** -- `Jarvis` class for programmatic use
- **OpenAI-compatible API server** -- `POST /v1/chat/completions`, `GET /v1/models`
- **Benchmarking framework** -- Latency and throughput measurements
- **Telemetry and traces** -- SQLite-backed recording and aggregation
- **Docker deployment** -- CPU and GPU images with docker-compose
Phase 6 is actively in progress, adding the trace system and trace-driven
learning capabilities.
---
## Phase 6 Details
Phase 6 focuses on closing the loop between execution and learning:
### Trace System
- **TraceStore** -- Persists complete `Trace` objects to SQLite, capturing the
full sequence of steps (route, retrieve, generate, tool_call, respond) with
timing, inputs, outputs, and outcomes
- **TraceCollector** -- Wraps any `BaseAgent` to automatically record traces
during execution via EventBus subscription
- **TraceAnalyzer** -- Read-only query layer providing aggregated statistics
(per-route, per-tool, by query type, time-range filtering)
### Trace-Driven Learning
- **TraceDrivenPolicy** -- A router policy that learns from historical trace
outcomes to improve model selection over time
- Query classification groups traces by type (code, math, short, long, general)
- Per-model scoring combines success rate and user feedback
- Online updates via `observe()` for incremental learning
### Pluggable Agents
- **ReActAgent** -- Reasoning + Acting pattern for systematic tool use
- **OpenHands** -- Integration with the OpenHands agent framework
---
## Future Directions
Beyond Phase 6, areas of ongoing exploration include:
- **GRPO training** -- Reinforcement learning from trace data to train the
routing policy, moving beyond heuristics and simple statistics
- **Streaming telemetry** -- Real-time performance dashboards and alerting
- **Multi-model orchestration** -- Coordinating multiple models within a
single query pipeline (e.g., small model for classification, large model
for generation)
- **Federated memory** -- Memory backends that synchronize across devices
- **Plugin ecosystem** -- Community-contributed engines, tools, and agents
distributed as Python packages
- **Energy-aware routing** -- Using power consumption data from telemetry to
optimize for energy efficiency alongside latency and quality
+561
View File
@@ -0,0 +1,561 @@
---
title: Configuration
description: Complete reference for OpenJarvis configuration
---
# Configuration
OpenJarvis uses a TOML configuration file to control engine selection, model routing, memory backends, agent behavior, and more. This page is the complete reference for every configuration option.
## Config File Location
The configuration file lives at:
```
~/.openjarvis/config.toml
```
OpenJarvis creates the `~/.openjarvis/` directory and populates it with a default config when you run `jarvis init`.
## Generating Configuration
### First-Time Setup
```bash
jarvis init
```
This command:
1. Runs hardware auto-detection (GPU vendor/model/VRAM, CPU brand/cores, RAM)
2. Selects the recommended engine based on your hardware
3. Writes `~/.openjarvis/config.toml` with sensible defaults
### Regenerating Configuration
To overwrite an existing config:
```bash
jarvis init --force
```
!!! warning
`--force` overwrites your existing config file. Back up your config first if you have custom settings.
## Configuration Sections
The config file is organized into seven TOML sections. Every field has a default value, so you only need to specify the values you want to change.
---
### `[engine]` -- Inference Engine
Controls which inference engine is used and where each engine is listening.
```toml
[engine]
default = "ollama"
ollama_host = "http://localhost:11434"
vllm_host = "http://localhost:8000"
llamacpp_host = "http://localhost:8080"
llamacpp_path = ""
sglang_host = "http://localhost:30000"
```
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `default` | string | Auto-detected | Default engine backend. One of: `ollama`, `vllm`, `llamacpp`, `sglang`, `cloud`. Set automatically by `jarvis init` based on hardware detection. |
| `ollama_host` | string | `http://localhost:11434` | Base URL for the Ollama API server. |
| `vllm_host` | string | `http://localhost:8000` | Base URL for the vLLM OpenAI-compatible server. |
| `llamacpp_host` | string | `http://localhost:8080` | Base URL for the llama.cpp HTTP server (`llama-server`). |
| `llamacpp_path` | string | `""` | Path to the llama.cpp binary, if not on `$PATH`. |
| `sglang_host` | string | `http://localhost:30000` | Base URL for the SGLang server. |
!!! tip "Engine fallback"
If the configured default engine is unreachable, OpenJarvis automatically probes all registered engines and falls back to any healthy one.
---
### `[intelligence]` -- Model Routing
Controls which model is selected by default and which model to fall back to.
```toml
[intelligence]
default_model = ""
fallback_model = ""
```
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `default_model` | string | `""` (empty) | Preferred model identifier (e.g., `qwen3:8b`). When empty, the router policy selects the model dynamically. |
| `fallback_model` | string | `""` (empty) | Model to use if the default is unavailable. |
When both fields are empty, OpenJarvis uses the configured router policy (see `[learning]`) to select a model from those available on the active engine.
---
### `[learning]` -- Router Policy
Controls how the learning system selects models for incoming queries.
```toml
[learning]
default_policy = "heuristic"
reward_weights = ""
```
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `default_policy` | string | `"heuristic"` | Router policy to use for model selection. Available: `heuristic`, `learned` (trace-driven), `grpo` (reinforcement learning stub). |
| `reward_weights` | string | `""` | Comma-separated key=value pairs for the reward function. Example: `"latency=0.4,cost=0.3,quality=0.3"`. |
**Router policies:**
| Policy | Description |
|--------|-------------|
| `heuristic` | Rule-based selection using 6 priority rules. Considers model availability, parameter count, context length, and query characteristics. Default. |
| `learned` | Trace-driven policy that learns from past interaction outcomes stored in the trace system. |
| `grpo` | Group Relative Policy Optimization stub for future RL-based routing. |
You can also override the router policy per-query via the CLI:
```bash
jarvis ask --router heuristic "Hello"
```
---
### `[memory]` -- Memory Backend
Controls the persistent memory system: which backend to use, how documents are chunked, and how context injection works.
```toml
[memory]
default_backend = "sqlite"
db_path = "~/.openjarvis/memory.db"
context_injection = true
context_top_k = 5
context_min_score = 0.1
context_max_tokens = 2048
chunk_size = 512
chunk_overlap = 64
```
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `default_backend` | string | `"sqlite"` | Memory backend to use. Available: `sqlite` (FTS5), `faiss`, `colbert`, `bm25`, `hybrid`. |
| `db_path` | string | `~/.openjarvis/memory.db` | Path to the SQLite memory database. Used by the `sqlite` backend. |
| `context_injection` | bool | `true` | Whether to automatically inject relevant memory context into queries. |
| `context_top_k` | int | `5` | Number of top memory results to inject as context. |
| `context_min_score` | float | `0.1` | Minimum relevance score for a memory result to be included in context. |
| `context_max_tokens` | int | `2048` | Maximum number of tokens to use for injected context. |
| `chunk_size` | int | `512` | Size of document chunks (in tokens) when indexing documents. |
| `chunk_overlap` | int | `64` | Overlap between adjacent chunks (in tokens) when indexing. |
**Memory backends:**
| Backend | Extra Required | Description |
|---------|---------------|-------------|
| `sqlite` | None | SQLite with FTS5 full-text search. Zero dependencies. Default. |
| `faiss` | `memory-faiss` | Facebook AI Similarity Search with sentence-transformer embeddings. |
| `colbert` | `memory-colbert` | ColBERTv2 late-interaction retrieval. Requires PyTorch. |
| `bm25` | `memory-bm25` | BM25 sparse retrieval via `rank-bm25`. |
| `hybrid` | Depends on sub-backends | Reciprocal Rank Fusion combining multiple backends. |
!!! note "Context injection"
When `context_injection` is enabled and documents have been indexed, every query automatically searches memory for relevant chunks and prepends them as system context. This gives the model access to your indexed knowledge base without any extra steps. Disable with `--no-context` on the CLI or `context=False` in the SDK.
---
### `[agent]` -- Agent Defaults
Controls the default agent behavior for queries.
```toml
[agent]
default_agent = "simple"
max_turns = 10
default_tools = ""
temperature = 0.7
max_tokens = 1024
```
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `default_agent` | string | `"simple"` | Default agent to use. Available: `simple`, `orchestrator`, `custom`, `openclaw`. |
| `max_turns` | int | `10` | Maximum number of tool-calling turns for the orchestrator agent before it must produce a final answer. |
| `default_tools` | string | `""` | Comma-separated list of tools to enable by default (e.g., `"calculator,think"`). |
| `temperature` | float | `0.7` | Default sampling temperature for generation. |
| `max_tokens` | int | `1024` | Default maximum tokens for generation. |
---
### `[server]` -- API Server
Controls the OpenAI-compatible API server started by `jarvis serve`.
```toml
[server]
host = "0.0.0.0"
port = 8000
agent = "orchestrator"
model = ""
workers = 1
```
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `host` | string | `"0.0.0.0"` | Bind address for the server. Use `"127.0.0.1"` to restrict to localhost. |
| `port` | int | `8000` | Port number for the server. |
| `agent` | string | `"orchestrator"` | Agent to use for non-streaming chat completion requests. |
| `model` | string | `""` | Default model for the server. When empty, uses `intelligence.default_model` or the first available model. |
| `workers` | int | `1` | Number of uvicorn worker processes. |
CLI options override config values:
```bash
jarvis serve --host 127.0.0.1 --port 9000 --model qwen3:8b --agent simple
```
---
### `[telemetry]` -- Telemetry Persistence
Controls whether inference telemetry is recorded and where it is stored.
```toml
[telemetry]
enabled = true
db_path = "~/.openjarvis/telemetry.db"
```
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | bool | `true` | Whether to record telemetry for each inference call. Records timing, token counts, model, engine, and cost. |
| `db_path` | string | `~/.openjarvis/telemetry.db` | Path to the SQLite telemetry database. |
!!! info "Telemetry is local-only"
All telemetry data is stored locally in a SQLite database. No data is ever sent to external services.
---
## Hardware Auto-Detection
When you run `jarvis init`, OpenJarvis probes your system to detect available hardware. The detection runs in this order:
### GPU Detection
1. **NVIDIA GPU** -- Checks for `nvidia-smi` on `$PATH`. If found, queries GPU name, VRAM (in MB), and GPU count via:
```
nvidia-smi --query-gpu=name,memory.total,count --format=csv,noheader,nounits
```
2. **AMD GPU** -- Checks for `rocm-smi` on `$PATH`. If found, queries the product name via:
```
rocm-smi --showproductname
```
3. **Apple Silicon** -- On macOS only. Runs `system_profiler SPDisplaysDataType` and looks for "Apple" in the chipset model line.
If none of these detect a GPU, the system is treated as CPU-only.
### CPU and RAM Detection
- **CPU brand**: Reads from `sysctl -n machdep.cpu.brand_string` on macOS, or parses `model name` from `/proc/cpuinfo` on Linux.
- **CPU count**: Uses Python's `os.cpu_count()`.
- **RAM**: Reads from `sysctl -n hw.memsize` on macOS, or parses `MemTotal` from `/proc/meminfo` on Linux.
### Detected Hardware Dataclass
The detection result is stored as a `HardwareInfo` dataclass:
```python
@dataclass
class HardwareInfo:
platform: str # "linux", "darwin", "windows"
cpu_brand: str # e.g., "AMD EPYC 7763"
cpu_count: int # e.g., 128
ram_gb: float # e.g., 512.0
gpu: GpuInfo | None
@dataclass
class GpuInfo:
vendor: str # "nvidia", "amd", "apple"
name: str # e.g., "NVIDIA A100-SXM4-80GB"
vram_gb: float # e.g., 80.0
compute_capability: str # (NVIDIA only)
count: int # e.g., 8
```
---
## Engine Recommendation Logic
Based on the detected hardware, `recommend_engine()` selects the optimal default engine:
```mermaid
graph TD
A[detect_hardware] --> B{GPU detected?}
B -->|No| C[llamacpp]
B -->|Yes| D{GPU vendor?}
D -->|Apple| E[ollama]
D -->|NVIDIA| F{Datacenter GPU?}
D -->|AMD| G[vllm]
F -->|Yes: A100, H100, H200, L40, A10, A30| H[vllm]
F -->|No: consumer GPU| I[ollama]
```
| Hardware | Recommended Engine | Reason |
|----------|--------------------|--------|
| No GPU | `llamacpp` | Efficient CPU inference with GGUF quantized models |
| Apple Silicon | `ollama` | Native Metal acceleration, easy model management |
| NVIDIA consumer GPU (RTX 3090, 4090, etc.) | `ollama` | Simple setup, good performance for single-user |
| NVIDIA datacenter GPU (A100, H100, H200, L40, A10, A30) | `vllm` | High-throughput batched serving, continuous batching |
| AMD GPU | `vllm` | ROCm support via vLLM |
---
## Example Configurations
### Apple Silicon Mac
```toml
# ~/.openjarvis/config.toml
# Apple Silicon MacBook Pro (M3 Max, 128 GB unified memory)
[engine]
default = "ollama"
ollama_host = "http://localhost:11434"
[intelligence]
default_model = "qwen3:8b"
fallback_model = "llama3.2:3b"
[memory]
default_backend = "sqlite"
context_injection = true
context_top_k = 5
[agent]
default_agent = "simple"
max_turns = 10
[server]
host = "127.0.0.1"
port = 8000
agent = "orchestrator"
[learning]
default_policy = "heuristic"
[telemetry]
enabled = true
```
### NVIDIA Datacenter (Multi-GPU)
```toml
# ~/.openjarvis/config.toml
# 8x NVIDIA A100 80GB server
[engine]
default = "vllm"
vllm_host = "http://localhost:8000"
ollama_host = "http://localhost:11434"
[intelligence]
default_model = "Qwen/Qwen2.5-72B-Instruct"
fallback_model = "Qwen/Qwen2.5-7B-Instruct"
[memory]
default_backend = "faiss"
context_injection = true
context_top_k = 10
context_min_score = 0.05
context_max_tokens = 4096
chunk_size = 1024
chunk_overlap = 128
[agent]
default_agent = "orchestrator"
max_turns = 15
default_tools = "calculator,think,retrieval"
temperature = 0.5
max_tokens = 4096
[server]
host = "0.0.0.0"
port = 8000
agent = "orchestrator"
model = "Qwen/Qwen2.5-72B-Instruct"
workers = 1
[learning]
default_policy = "heuristic"
[telemetry]
enabled = true
```
### CPU-Only (No GPU)
```toml
# ~/.openjarvis/config.toml
# CPU-only machine
[engine]
default = "llamacpp"
llamacpp_host = "http://localhost:8080"
[intelligence]
default_model = ""
fallback_model = ""
[memory]
default_backend = "sqlite"
context_injection = true
context_top_k = 3
context_max_tokens = 1024
chunk_size = 256
chunk_overlap = 32
[agent]
default_agent = "simple"
max_turns = 5
temperature = 0.7
max_tokens = 512
[server]
host = "127.0.0.1"
port = 8000
[learning]
default_policy = "heuristic"
[telemetry]
enabled = true
```
### Cloud-Only (No Local Engine)
```toml
# ~/.openjarvis/config.toml
# Using cloud APIs only (OpenAI, Anthropic)
# Set OPENAI_API_KEY and/or ANTHROPIC_API_KEY environment variables
[engine]
default = "cloud"
[intelligence]
default_model = "gpt-4o"
fallback_model = "claude-sonnet-4-20250514"
[memory]
default_backend = "sqlite"
context_injection = true
[agent]
default_agent = "orchestrator"
default_tools = "calculator,think"
[telemetry]
enabled = true
```
### Hybrid (Local + Cloud Fallback)
```toml
# ~/.openjarvis/config.toml
# Local Ollama as primary, cloud as fallback
[engine]
default = "ollama"
ollama_host = "http://localhost:11434"
[intelligence]
default_model = "qwen3:8b"
fallback_model = "gpt-4o-mini"
[memory]
default_backend = "sqlite"
context_injection = true
[agent]
default_agent = "orchestrator"
max_turns = 10
default_tools = "calculator,think"
[learning]
default_policy = "heuristic"
[telemetry]
enabled = true
```
---
## Programmatic Configuration
You can also configure OpenJarvis entirely from Python without a TOML file:
```python
from openjarvis import Jarvis
from openjarvis.core.config import (
AgentConfig,
EngineConfig,
IntelligenceConfig,
JarvisConfig,
MemoryConfig,
)
config = JarvisConfig(
engine=EngineConfig(
default="ollama",
ollama_host="http://my-server:11434",
),
intelligence=IntelligenceConfig(
default_model="qwen3:8b",
),
memory=MemoryConfig(
default_backend="sqlite",
context_injection=True,
context_top_k=10,
),
agent=AgentConfig(
default_agent="orchestrator",
max_turns=15,
),
)
j = Jarvis(config=config)
response = j.ask("Hello")
j.close()
```
Or load from a custom path:
```python
j = Jarvis(config_path="/path/to/my-config.toml")
```
---
## Environment Variables
OpenJarvis respects the following environment variables:
| Variable | Description |
|----------|-------------|
| `OPENAI_API_KEY` | API key for OpenAI cloud inference. Required for the `cloud` engine with OpenAI models. |
| `ANTHROPIC_API_KEY` | API key for Anthropic cloud inference. Required for the `cloud` engine with Claude models. |
| `GOOGLE_API_KEY` | API key for Google Gemini inference. Required for the `google` engine. |
| `TAVILY_API_KEY` | API key for the Tavily web search tool. Required for the `web_search` tool. |
## Next Steps
- [Quick Start](quickstart.md) -- Run your first query
- [CLI Reference](../user-guide/cli.md) -- Full reference for all CLI commands
- [Architecture Overview](../architecture/overview.md) -- Understand how the pieces fit together
+251
View File
@@ -0,0 +1,251 @@
---
title: Installation
description: Install OpenJarvis and set up an inference backend
---
# Installation
This guide covers installing OpenJarvis, its optional extras, and setting up an inference backend.
## Requirements
| Requirement | Version | Notes |
|-------------|---------|-------|
| Python | 3.10+ | Required |
| Inference backend | Any | At least one of Ollama, vLLM, llama.cpp, SGLang, or a cloud API |
| Node.js | 22+ | Only required for OpenClaw agent infrastructure |
## Installing OpenJarvis
=== "uv (recommended)"
```bash
uv pip install openjarvis
```
=== "pip"
```bash
pip install openjarvis
```
=== "From source"
```bash
git clone https://github.com/jonsaadfalcon/OpenJarvis.git
cd OpenJarvis
uv sync
```
For development with all dev tools:
```bash
uv sync --extra dev
```
## Optional Extras
OpenJarvis uses optional extras to keep the base installation lightweight. Install only what you need.
### Inference Backends
| Extra | Install Command | Dependencies | Description |
|-------|----------------|--------------|-------------|
| `inference-ollama` | `pip install 'openjarvis[inference-ollama]'` | None (HTTP-based) | Ollama backend. Communicates via HTTP API. |
| `inference-vllm` | `pip install 'openjarvis[inference-vllm]'` | None (HTTP-based) | vLLM backend. Communicates via OpenAI-compatible API. |
| `inference-llamacpp` | `pip install 'openjarvis[inference-llamacpp]'` | None (HTTP-based) | llama.cpp server backend. |
| `inference-cloud` | `pip install 'openjarvis[inference-cloud]'` | `openai>=1.30`, `anthropic>=0.30` | Cloud inference via OpenAI and Anthropic APIs. |
| `inference-google` | `pip install 'openjarvis[inference-google]'` | `google-genai>=1.0` | Google Gemini API backend. |
!!! note "Ollama, vLLM, and llama.cpp are HTTP-based"
The `inference-ollama`, `inference-vllm`, and `inference-llamacpp` extras have no additional Python dependencies. OpenJarvis communicates with these engines over HTTP using the `httpx` library that is already a core dependency. You still need the actual engine software running on your machine or network.
### Memory Backends
| Extra | Install Command | Dependencies | Description |
|-------|----------------|--------------|-------------|
| `memory-faiss` | `pip install 'openjarvis[memory-faiss]'` | `faiss-cpu>=1.7`, `sentence-transformers>=2.2`, `numpy>=1.24` | FAISS vector store with sentence-transformer embeddings. |
| `memory-colbert` | `pip install 'openjarvis[memory-colbert]'` | `colbert-ai>=0.2`, `torch>=2.0` | ColBERTv2 late-interaction retrieval. |
| `memory-bm25` | `pip install 'openjarvis[memory-bm25]'` | `rank-bm25>=0.2.2` | BM25 sparse retrieval backend. |
| `memory-pdf` | `pip install 'openjarvis[memory-pdf]'` | `pdfplumber>=0.10` | PDF document ingestion support. |
!!! tip "SQLite memory is always available"
The default SQLite/FTS5 memory backend requires no additional dependencies. It is always available and suitable for most use cases.
### Tools
| Extra | Install Command | Dependencies | Description |
|-------|----------------|--------------|-------------|
| `tools-search` | `pip install 'openjarvis[tools-search]'` | `tavily-python>=0.3` | Web search tool via the Tavily API. |
### Server
| Extra | Install Command | Dependencies | Description |
|-------|----------------|--------------|-------------|
| `server` | `pip install 'openjarvis[server]'` | `fastapi>=0.110`, `uvicorn>=0.30`, `pydantic>=2.0` | OpenAI-compatible API server (`jarvis serve`). |
### Other Extras
| Extra | Install Command | Dependencies | Description |
|-------|----------------|--------------|-------------|
| `agents` | `pip install 'openjarvis[agents]'` | None | Agent infrastructure (included in base). |
| `learning` | `pip install 'openjarvis[learning]'` | None | Learning/router policy system (included in base). |
| `openclaw` | `pip install 'openjarvis[openclaw]'` | None | OpenClaw agent transport layer. Requires Node.js 22+ at runtime. |
| `docs` | `pip install 'openjarvis[docs]'` | `mkdocs>=1.6`, `mkdocs-material>=9.5`, `mkdocstrings[python]>=0.25` | Documentation build tools. |
| `dev` | `pip install 'openjarvis[dev]'` | `pytest>=8`, `pytest-asyncio>=0.24`, `pytest-cov>=5`, `respx>=0.22`, `ruff>=0.4` | Development and testing tools. |
### Installing Multiple Extras
Combine extras with commas:
```bash
pip install 'openjarvis[server,memory-faiss,inference-cloud]'
```
Or with `uv`:
```bash
uv pip install 'openjarvis[server,memory-faiss,inference-cloud]'
```
## Verifying Installation
After installation, verify that the CLI is available:
```bash
jarvis --version
```
Expected output:
```
jarvis, version 1.0.0
```
View all available commands:
```bash
jarvis --help
```
Expected output:
```
Usage: jarvis [OPTIONS] COMMAND [ARGS]...
OpenJarvis -- modular AI assistant backend
Options:
--version Show the version and exit.
--help Show this message and exit.
Commands:
ask Ask Jarvis a question.
bench Run inference benchmarks.
init Detect hardware and generate ~/.openjarvis/config.toml.
memory Manage the memory store.
model Manage language models.
serve Start the OpenAI-compatible API server.
telemetry Query and manage inference telemetry data.
```
## Setting Up an Inference Backend
OpenJarvis requires at least one inference backend to generate responses. Choose the backend that best matches your hardware.
### Ollama (Recommended for most users)
Ollama is the easiest way to get started. It handles model downloading and serving automatically.
1. Install Ollama from [ollama.com](https://ollama.com)
2. Start the server:
```bash
ollama serve
```
3. Pull a model:
```bash
ollama pull qwen3:8b
```
Or pull directly via the Jarvis CLI:
```bash
jarvis model pull qwen3:8b
```
4. Verify the engine is detected:
```bash
jarvis model list
```
!!! tip "Best for: Apple Silicon Macs, consumer NVIDIA GPUs, CPU-only systems"
### vLLM (High-throughput serving)
vLLM provides high-throughput serving optimized for datacenter GPUs.
1. Install vLLM following the [official guide](https://docs.vllm.ai)
2. Start the server:
```bash
vllm serve Qwen/Qwen2.5-7B-Instruct
```
3. OpenJarvis will auto-detect it at `http://localhost:8000`
!!! tip "Best for: NVIDIA datacenter GPUs (A100, H100, L40), AMD GPUs"
### llama.cpp (Lightweight, CPU-friendly)
llama.cpp provides efficient CPU and GPU inference with GGUF quantized models.
1. Build llama.cpp from [github.com/ggerganov/llama.cpp](https://github.com/ggerganov/llama.cpp)
2. Start the server:
```bash
llama-server -m /path/to/model.gguf --port 8080
```
3. OpenJarvis will auto-detect it at `http://localhost:8080`
!!! tip "Best for: CPU-only machines, constrained environments, GGUF models"
### SGLang
SGLang provides structured generation and high-performance serving.
1. Install SGLang following the [official guide](https://github.com/sgl-project/sglang)
2. Start the server:
```bash
python -m sglang.launch_server --model Qwen/Qwen2.5-7B-Instruct --port 30000
```
3. OpenJarvis will auto-detect it at `http://localhost:30000`
### Cloud APIs (OpenAI, Anthropic, Google)
For cloud-based inference, install the cloud extras and set your API keys:
```bash
pip install 'openjarvis[inference-cloud,inference-google]'
```
Set environment variables:
```bash
export OPENAI_API_KEY="sk-..."
export ANTHROPIC_API_KEY="sk-ant-..."
export GOOGLE_API_KEY="..."
```
OpenJarvis will automatically detect available cloud providers.
## Next Steps
- [Quick Start](quickstart.md) — Run your first query
- [Configuration](configuration.md) — Customize engine hosts, model routing, memory, and more
+444
View File
@@ -0,0 +1,444 @@
---
title: Quick Start
description: Get up and running with OpenJarvis in minutes
---
# Quick Start
This guide walks through the core workflows of OpenJarvis: asking questions, using agents with tools, managing memory, running benchmarks, and starting the API server.
!!! info "Prerequisites"
Make sure you have [installed OpenJarvis](installation.md) and have at least one inference backend running (e.g., `ollama serve`).
## Initialize Configuration
Start by detecting your hardware and generating a configuration file:
```bash
jarvis init
```
This runs hardware auto-detection (GPU vendor, VRAM, CPU, RAM) and writes a config file to `~/.openjarvis/config.toml` with sensible defaults for your system. It also selects the recommended inference engine.
```
Detecting hardware...
Platform : linux
CPU : AMD EPYC 7763 (128 cores)
RAM : 512.0 GB
GPU : NVIDIA A100 (80.0 GB VRAM, x8)
Config written successfully.
```
To overwrite an existing config:
```bash
jarvis init --force
```
See [Configuration](configuration.md) for the full config reference.
## Your First Question
### Via CLI
The simplest way to interact with OpenJarvis is the `ask` command:
```bash
jarvis ask "What is the capital of France?"
```
OpenJarvis will auto-detect a running engine, select a model using the configured router policy, and return the response.
#### CLI Options
| Option | Description | Example |
|--------|-------------|---------|
| `-m`, `--model` | Override model selection | `jarvis ask -m qwen3:8b "Hello"` |
| `-e`, `--engine` | Force a specific engine | `jarvis ask -e ollama "Hello"` |
| `-t`, `--temperature` | Sampling temperature (default: 0.7) | `jarvis ask -t 0.2 "Hello"` |
| `--max-tokens` | Max tokens to generate (default: 1024) | `jarvis ask --max-tokens 2048 "Hello"` |
| `--json` | Output raw JSON result | `jarvis ask --json "Hello"` |
| `--no-stream` | Disable streaming | `jarvis ask --no-stream "Hello"` |
| `--no-context` | Disable memory context injection | `jarvis ask --no-context "Hello"` |
| `-a`, `--agent` | Use an agent | `jarvis ask -a orchestrator "Hello"` |
| `--tools` | Comma-separated tools | `jarvis ask --tools calculator,think "2+2"` |
| `--router` | Router policy for model selection | `jarvis ask --router heuristic "Hello"` |
### Via Python SDK
The `Jarvis` class provides a high-level Python interface:
```python
from openjarvis import Jarvis
j = Jarvis()
response = j.ask("What is the capital of France?")
print(response)
j.close()
```
For detailed results including token usage and model info:
```python
result = j.ask_full("What is the capital of France?")
print(result["content"]) # The response text
print(result["model"]) # Model that handled the query
print(result["engine"]) # Engine that ran inference
print(result["usage"]) # Token usage statistics
```
#### SDK Constructor Options
```python
# Use default config (auto-detected hardware, ~/.openjarvis/config.toml)
j = Jarvis()
# Override the model
j = Jarvis(model="qwen3:8b")
# Override the engine
j = Jarvis(engine_key="ollama")
# Use a custom config file
j = Jarvis(config_path="/path/to/config.toml")
```
!!! warning "Always call `close()`"
The `Jarvis` instance holds references to telemetry stores and memory backends. Call `j.close()` when you are done to release resources.
## Using Agents with Tools
Agents add multi-turn reasoning and tool-calling capabilities. The `orchestrator` agent runs a tool-calling loop, invoking tools as needed to answer the query.
### Available Agents
| Agent | Description |
|-------|-------------|
| `simple` | Single-turn, no tools. Sends the query directly to the model. |
| `orchestrator` | Multi-turn tool-calling loop. Invokes tools iteratively until it has an answer. |
| `custom` | Template for user-defined agent logic. |
| `openclaw` | HTTP/subprocess transport to external OpenClaw agent processes. |
### Available Built-in Tools
| Tool | Description |
|------|-------------|
| `calculator` | Safe mathematical expression evaluation (ast-based). |
| `think` | Reasoning scratchpad for chain-of-thought. |
| `retrieval` | Search the memory store for relevant context. |
| `llm` | Make sub-queries to another model. |
| `file_read` | Read files with path validation. |
| `web_search` | Web search via the Tavily API (requires `tools-search` extra). |
### CLI Example
```bash
jarvis ask --agent orchestrator --tools calculator,think "What is 137 * 42?"
```
### SDK Example
```python
from openjarvis import Jarvis
j = Jarvis()
result = j.ask_full(
"What is the square root of 144?",
agent="orchestrator",
tools=["calculator", "think"],
)
print(result["content"])
print(result["tool_results"]) # List of tool invocations and results
print(result["turns"]) # Number of agent turns
j.close()
```
## Memory: Indexing and Search
The memory system lets you index documents and inject relevant context into queries automatically.
### Index Documents
Index a file or directory. OpenJarvis chunks the content and stores it in the configured memory backend (SQLite/FTS5 by default).
=== "CLI"
```bash
# Index a directory
jarvis memory index ./docs/
# Index a single file with custom chunk size
jarvis memory index ./paper.txt --chunk-size 256 --chunk-overlap 32
```
=== "Python SDK"
```python
from openjarvis import Jarvis
j = Jarvis()
result = j.memory.index("./docs/", chunk_size=512, chunk_overlap=64)
print(f"Indexed {result['chunks']} chunks")
j.close()
```
### Search Memory
Query the memory store to find relevant chunks:
=== "CLI"
```bash
jarvis memory search "configuration options"
jarvis memory search -k 10 "how to deploy"
```
=== "Python SDK"
```python
results = j.memory.search("configuration options", top_k=5)
for r in results:
print(f"[{r['score']:.4f}] {r['source']}: {r['content'][:100]}")
```
### Check Memory Statistics
=== "CLI"
```bash
jarvis memory stats
```
=== "Python SDK"
```python
stats = j.memory.stats()
print(f"Backend: {stats['backend']}, Documents: {stats.get('count', 'N/A')}")
```
### Automatic Context Injection
When you have indexed documents, OpenJarvis automatically injects relevant context into your queries. The memory system searches for chunks matching your query and prepends them as system context before sending to the model.
To disable this behavior:
=== "CLI"
```bash
jarvis ask --no-context "Hello"
```
=== "Python SDK"
```python
response = j.ask("Hello", context=False)
```
Context injection is controlled by the `[memory]` config section. See [Configuration](configuration.md) for details on `context_injection`, `context_top_k`, `context_min_score`, and `context_max_tokens`.
## Model Management
### List Available Models
See all models available on running engines:
```bash
jarvis model list
```
This produces a table showing each model, its engine, parameter count, context length, and VRAM requirements.
### Get Model Details
```bash
jarvis model info qwen3:8b
```
### Pull a Model (Ollama)
```bash
jarvis model pull qwen3:8b
```
### SDK Model Listing
```python
from openjarvis import Jarvis
j = Jarvis()
models = j.list_models()
engines = j.list_engines()
print(f"Models: {models}")
print(f"Engines: {engines}")
j.close()
```
## Running Benchmarks
The benchmarking framework measures inference latency and throughput against your engine.
=== "All benchmarks"
```bash
jarvis bench run
```
=== "Specific benchmark"
```bash
jarvis bench run -b latency
jarvis bench run -b throughput
```
=== "Custom options"
```bash
# 20 samples, JSON output
jarvis bench run -n 20 --json
# Specific model and engine, write to file
jarvis bench run -m qwen3:8b -e ollama -o results.jsonl
```
Example output:
```
Running 2 benchmark(s) on ollama/qwen3:8b (10 samples)...
latency (10 samples, 0 errors)
mean_ms: 245.3200
p50_ms: 238.1000
p95_ms: 312.4500
min_ms: 201.2000
max_ms: 345.6000
throughput (10 samples, 0 errors)
tokens_per_second: 42.1500
total_tokens: 4215
total_seconds: 100.0000
```
## Starting the API Server
OpenJarvis provides an OpenAI-compatible API server for integration with existing tools and frontends.
!!! note "Requires the `server` extra"
```bash
pip install 'openjarvis[server]'
```
### Start the Server
```bash
jarvis serve --port 8000
```
With custom options:
```bash
jarvis serve --host 0.0.0.0 --port 8000 --engine ollama --model qwen3:8b --agent orchestrator
```
### API Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/v1/chat/completions` | `POST` | Chat completions (streaming and non-streaming) |
| `/v1/models` | `GET` | List available models |
| `/health` | `GET` | Health check |
### Use with Any OpenAI-Compatible Client
Once the server is running, point any OpenAI-compatible client at it:
```python
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed")
response = client.chat.completions.create(
model="qwen3:8b",
messages=[{"role": "user", "content": "Hello!"}],
)
print(response.choices[0].message.content)
```
Or with `curl`:
```bash
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qwen3:8b",
"messages": [{"role": "user", "content": "Hello!"}]
}'
```
## Telemetry
OpenJarvis records telemetry for every inference call (timing, tokens, cost). View aggregated statistics:
```bash
jarvis telemetry stats
```
Export telemetry data:
```bash
jarvis telemetry export --format json
jarvis telemetry export --format csv -o telemetry.csv
```
Clear all telemetry records:
```bash
jarvis telemetry clear --yes
```
## Complete Working Example
Here is a complete end-to-end session combining multiple features:
```python
from openjarvis import Jarvis
# Initialize with defaults (auto-detect hardware and engine)
j = Jarvis()
# 1. Index some documentation
index_result = j.memory.index("./docs/", chunk_size=512)
print(f"Indexed {index_result['chunks']} chunks from {index_result['path']}")
# 2. Search memory
results = j.memory.search("how to configure engines")
for r in results:
print(f" [{r['score']:.3f}] {r['source']}")
# 3. Ask a question (memory context is injected automatically)
answer = j.ask("How do I configure the Ollama engine host?")
print(f"\nAnswer: {answer}")
# 4. Use an agent with tools
calc_result = j.ask_full(
"Calculate the compound interest on $10,000 at 5% for 10 years",
agent="orchestrator",
tools=["calculator", "think"],
)
print(f"\nCalculation: {calc_result['content']}")
print(f"Tools used: {[t['tool_name'] for t in calc_result['tool_results']]}")
print(f"Agent turns: {calc_result['turns']}")
# 5. List available models
models = j.list_models()
print(f"\nAvailable models: {models}")
# 6. Clean up
j.close()
```
## Next Steps
- [Configuration](configuration.md) — Fine-tune engine hosts, model routing, memory settings, and more
- [CLI Reference](../user-guide/cli.md) — Full reference for all CLI commands and options
- [Python SDK](../user-guide/python-sdk.md) — Detailed SDK documentation
- [Architecture Overview](../architecture/overview.md) — Understand the four-pillar design
+13
View File
@@ -0,0 +1,13 @@
*[ABC]: Abstract Base Class
*[FTS5]: Full-Text Search version 5
*[GRPO]: Group Relative Policy Optimization
*[RRF]: Reciprocal Rank Fusion
*[SSE]: Server-Sent Events
*[VRAM]: Video Random Access Memory
*[MoE]: Mixture of Experts
*[GGUF]: GPT-Generated Unified Format
*[MCP]: Model Context Protocol
*[SDK]: Software Development Kit
*[CLI]: Command-Line Interface
*[API]: Application Programming Interface
*[LLM]: Large Language Model
+184
View File
@@ -0,0 +1,184 @@
---
title: OpenJarvis
description: Programming abstractions for on-device AI
---
# OpenJarvis
**Programming abstractions for on-device AI.**
OpenJarvis is a modular framework for building, running, and learning from local AI systems. It provides composable abstractions across four core pillars — Intelligence, Engine, Agentic Logic, and Memory — with a cross-cutting trace-driven learning system that improves routing decisions over time.
Everything runs on your hardware. Cloud APIs are optional.
---
## Key Features
<div class="grid cards" markdown>
- **Four Core Pillars**
---
Intelligence (model routing), Engine (inference runtime), Agentic Logic (tool-calling agents), and Memory (persistent searchable storage) — each with a clear ABC interface and decorator-based registry.
- **5 Engine Backends**
---
Ollama, vLLM, SGLang, llama.cpp, and cloud (OpenAI/Anthropic/Google). All implement the same `InferenceEngine` ABC with `generate()`, `stream()`, `list_models()`, and `health()`.
- **5 Memory Backends**
---
SQLite/FTS5 (default, zero-dependency), FAISS, ColBERTv2, BM25, and Hybrid (reciprocal rank fusion). Document chunking, indexing, and context injection built in.
- **Hardware-Aware**
---
Auto-detects GPU vendor, model, and VRAM via `nvidia-smi`, `rocm-smi`, and `system_profiler`. Recommends the optimal engine for your hardware automatically.
- **Offline-First**
---
All core functionality works without a network connection. Cloud API backends are optional extras for when you need them.
- **OpenAI-Compatible API**
---
`jarvis serve` starts a FastAPI server with `POST /v1/chat/completions`, `GET /v1/models`, and SSE streaming. Drop-in replacement for OpenAI-compatible clients.
- **Trace-Driven Learning**
---
Every agent interaction is recorded as a trace. The learning system uses accumulated traces to improve model routing decisions. Pluggable router policies: heuristic, trace-driven, and GRPO.
- **Python SDK**
---
The `Jarvis` class provides a high-level sync API. Three lines of code to ask a question. Full access to agents, tools, memory, and model routing.
- **CLI-First**
---
`jarvis ask`, `jarvis serve`, `jarvis memory`, `jarvis bench`, `jarvis telemetry` — every capability is accessible from the command line with rich terminal output.
</div>
---
## Quick Start
### Python SDK
```python
from openjarvis import Jarvis
j = Jarvis()
response = j.ask("Explain quicksort in two sentences.")
print(response)
j.close()
```
For more control, use `ask_full()` to get usage stats, model info, and tool results:
```python
result = j.ask_full(
"What is 2 + 2?",
agent="orchestrator",
tools=["calculator"],
)
print(result["content"]) # "4"
print(result["tool_results"]) # [{tool_name: "calculator", ...}]
```
### CLI
```bash
# Ask a question
jarvis ask "What is the capital of France?"
# Use an agent with tools
jarvis ask --agent orchestrator --tools calculator,think "What is 137 * 42?"
# Start the API server
jarvis serve --port 8000
# Index documents and search memory
jarvis memory index ./docs/
jarvis memory search "configuration options"
# Run inference benchmarks
jarvis bench run --json
```
---
## Project Status
OpenJarvis v1.0 is complete. The framework includes the full four-pillar architecture, Python SDK, CLI, OpenAI-compatible API server, OpenClaw agent infrastructure, benchmarking framework, and Docker deployment. The test suite contains over 1,000 tests. Phase 6 (trace system and trace-driven learning) is in active development.
| Component | Status |
|-----------|--------|
| Intelligence (model routing) | Stable |
| Engine (5 backends) | Stable |
| Agentic Logic (agents + tools) | Stable |
| Memory (5 backends) | Stable |
| Python SDK | Stable |
| CLI | Stable |
| API Server | Stable |
| Trace System | Active Development |
| Trace-Driven Learning | Active Development |
| Docker Deployment | Stable |
---
## Documentation
<div class="grid cards" markdown>
- **[Getting Started](getting-started/installation.md)**
---
Install OpenJarvis, configure your first engine, and run your first query in minutes.
- **[User Guide](user-guide/cli.md)**
---
Comprehensive guides for the CLI, Python SDK, agents, memory, tools, telemetry, and benchmarks.
- **[Architecture](architecture/overview.md)**
---
Deep dive into the four-pillar design, registry pattern, query flow, and cross-cutting learning system.
- **[API Reference](api/index.md)**
---
Auto-generated reference for every module: SDK, core, engine, agents, memory, tools, intelligence, learning, traces, telemetry, and server.
- **[Deployment](deployment/docker.md)**
---
Deploy OpenJarvis with Docker, systemd, or launchd. Includes GPU-accelerated container images.
- **[Development](development/contributing.md)**
---
Contributing guide, extension patterns, roadmap, and changelog.
</div>
+269
View File
@@ -0,0 +1,269 @@
# Agents
Agents are the agentic logic layer of OpenJarvis. They determine how a query is processed -- whether it goes directly to a model, through a tool-calling loop, or via an external agent runtime. All agents implement the `BaseAgent` ABC and are registered via the `AgentRegistry`.
## Overview
| Agent | Registry Key | Tools | Multi-turn | Description |
|------------------|-----------------|-------|------------|----------------------------------------------|
| `SimpleAgent` | `simple` | No | No | Single-turn query-to-response |
| `OrchestratorAgent` | `orchestrator` | Yes | Yes | Multi-turn tool-calling loop |
| `OpenClawAgent` | `openclaw` | Yes | Yes | External agent via HTTP or subprocess |
| `CustomAgent` | `custom` | -- | -- | Template for user-defined agents |
---
## BaseAgent ABC
All agents extend the abstract `BaseAgent` class.
```python
from abc import ABC, abstractmethod
from openjarvis.agents._stubs import AgentContext, AgentResult
class BaseAgent(ABC):
agent_id: str
@abstractmethod
def run(
self,
input: str,
context: AgentContext | None = None,
**kwargs,
) -> AgentResult:
"""Execute the agent on the given input."""
```
### AgentContext
The runtime context handed to an agent on each invocation.
| Field | Type | Description |
|------------------|--------------------|------------------------------------------------|
| `conversation` | `Conversation` | Message history (pre-filled with context if memory injection is active) |
| `tools` | `list[str]` | Tool names available to the agent |
| `memory_results` | `list[Any]` | Pre-fetched memory retrieval results |
| `metadata` | `dict[str, Any]` | Arbitrary metadata for the run |
### AgentResult
The result returned after an agent completes a run.
| Field | Type | Description |
|----------------|--------------------|------------------------------------------------|
| `content` | `str` | The final response text |
| `tool_results` | `list[ToolResult]` | Results from tool executions during the run |
| `turns` | `int` | Number of turns (inference calls) taken |
| `metadata` | `dict[str, Any]` | Arbitrary metadata about the run |
---
## SimpleAgent
The `SimpleAgent` is a single-turn agent that sends the query directly to the inference engine and returns the response. It does not support tool calling.
**How it works:**
1. Builds a message list from the conversation context (if provided) plus the user query.
2. Calls the inference engine via `instrumented_generate()` for telemetry tracking.
3. Returns the response as an `AgentResult` with `turns=1`.
**Constructor parameters:**
| Parameter | Type | Default | Description |
|---------------|-------------------|---------|------------------------------------|
| `engine` | `InferenceEngine` | -- | The inference engine to use |
| `model` | `str` | -- | Model identifier |
| `bus` | `EventBus` | `None` | Event bus for telemetry |
| `temperature` | `float` | `0.7` | Sampling temperature |
| `max_tokens` | `int` | `1024` | Maximum tokens to generate |
**When to use:** For straightforward question-answering without tool calling or multi-turn reasoning.
---
## OrchestratorAgent
The `OrchestratorAgent` is a multi-turn agent that implements a tool-calling loop. It is the primary agent for queries that require computation, knowledge retrieval, or structured reasoning.
**How it works:**
1. Builds the initial message list from context and the user query.
2. Sends messages with tool definitions (OpenAI function-calling format) to the engine.
3. If the engine responds with `tool_calls`, the `ToolExecutor` dispatches each call.
4. Tool results are appended as `TOOL` messages and the loop continues.
5. If no `tool_calls` are returned, the response is treated as the final answer.
6. The loop stops after `max_turns` iterations (default: 10), returning whatever content is available along with a `max_turns_exceeded` metadata flag.
**Constructor parameters:**
| Parameter | Type | Default | Description |
|---------------|-------------------|---------|------------------------------------|
| `engine` | `InferenceEngine` | -- | The inference engine to use |
| `model` | `str` | -- | Model identifier |
| `tools` | `list[BaseTool]` | `[]` | Tool instances to make available |
| `bus` | `EventBus` | `None` | Event bus for telemetry |
| `max_turns` | `int` | `10` | Maximum number of tool-calling turns |
| `temperature` | `float` | `0.7` | Sampling temperature |
| `max_tokens` | `int` | `1024` | Maximum tokens to generate |
**When to use:** For queries that need calculation, memory search, sub-model calls, file reading, or multi-step reasoning.
!!! info "Tool-Calling Loop"
The orchestrator follows the OpenAI function-calling convention. The engine must support returning `tool_calls` in its response for the loop to engage. If tools are provided but the engine does not return any tool calls, the agent behaves like a single-turn agent.
---
## OpenClawAgent
The `OpenClawAgent` wraps the OpenClaw Pi agent runtime, communicating via either HTTP or subprocess transport. It supports tool calling through the OpenClaw protocol.
**How it works:**
1. Checks transport health.
2. Sends a `QUERY` protocol message through the transport.
3. If the response is a `TOOL_CALL`, dispatches the tool locally via `ToolExecutor`.
4. Sends the tool result back as a `TOOL_RESULT` message.
5. Continues the tool-call loop until the response is a final answer or error (up to 10 turns).
**Constructor parameters:**
| Parameter | Type | Default | Description |
|--------------|----------------------|-----------|-------------------------------------------|
| `engine` | `Any` | `None` | Inference engine (fallback/provider) |
| `model` | `str` | `""` | Model identifier |
| `transport` | `OpenClawTransport` | `None` | Pre-configured transport (overrides mode) |
| `mode` | `str` | `"http"` | Transport mode: `"http"` or `"subprocess"` |
| `bus` | `EventBus` | `None` | Event bus for telemetry |
**Transport modes:**
- **HTTP** (`HttpTransport`): Sends HTTP POST requests to an OpenClaw server.
- **Subprocess** (`SubprocessTransport`): Spawns a Node.js process and communicates via stdin/stdout using JSON-line protocol.
!!! warning "Node.js Requirement"
The subprocess transport mode requires Node.js 22+ to be installed on the system.
---
## CustomAgent
The `CustomAgent` is a template for building user-defined agents. It raises `NotImplementedError` by default -- subclass it and override `run()` to implement your logic.
```python
from openjarvis.agents._stubs import AgentContext, AgentResult, BaseAgent
from openjarvis.core.registry import AgentRegistry
@AgentRegistry.register("my-agent")
class MyAgent(BaseAgent):
agent_id = "my-agent"
def __init__(self, engine, model, **kwargs):
self._engine = engine
self._model = model
def run(self, input: str, context: AgentContext | None = None, **kwargs) -> AgentResult:
# Your custom logic here
result = self._engine.generate(
[{"role": "user", "content": input}],
model=self._model,
)
return AgentResult(
content=result.get("content", ""),
turns=1,
)
```
After registration, you can use your custom agent via the CLI or SDK:
```bash
jarvis ask --agent my-agent "Hello"
```
```python
response = j.ask("Hello", agent="my-agent")
```
---
## Using Agents
### Via CLI
```bash
# Simple agent
jarvis ask --agent simple "What is the capital of France?"
# Orchestrator with tools
jarvis ask --agent orchestrator --tools calculator,think "What is sqrt(256)?"
# OpenClaw agent
jarvis ask --agent openclaw "Tell me a story"
```
### Via Python SDK
```python
from openjarvis import Jarvis
j = Jarvis()
# Simple agent
response = j.ask("Hello", agent="simple")
# Orchestrator with tools
response = j.ask(
"Calculate 15% of 340",
agent="orchestrator",
tools=["calculator"],
)
# Full result with tool details
result = j.ask_full(
"What is the square root of 144?",
agent="orchestrator",
tools=["calculator", "think"],
)
print(result["content"])
print(result["turns"])
print(result["tool_results"])
j.close()
```
---
## Agent Registration
Agents are registered via the `@AgentRegistry.register()` decorator. This makes them discoverable by name at runtime:
```python
from openjarvis.core.registry import AgentRegistry
# Check if an agent is registered
AgentRegistry.contains("orchestrator") # True
# Get the agent class
agent_cls = AgentRegistry.get("orchestrator")
# List all registered agent keys
AgentRegistry.keys() # ["simple", "orchestrator", "openclaw", "custom"]
```
---
## Event Bus Integration
All agents publish events on the `EventBus` when a bus is provided:
| Event | When |
|-------------------------|---------------------------------------------|
| `AGENT_TURN_START` | At the beginning of a run |
| `AGENT_TURN_END` | At the end of a run (includes turn count) |
| `INFERENCE_START` | Before each engine call (orchestrator) |
| `INFERENCE_END` | After each engine call (orchestrator) |
| `TOOL_CALL_START` | Before each tool execution (openclaw) |
| `TOOL_CALL_END` | After each tool execution (openclaw) |
These events enable the telemetry and trace systems to record detailed interaction data automatically.
+337
View File
@@ -0,0 +1,337 @@
# Benchmarks
The benchmarking framework measures inference engine performance with reproducible, standardized tests. It includes built-in benchmarks for latency and throughput, a suite runner for batch execution, and support for custom benchmarks.
## Overview
OpenJarvis ships with two benchmarks:
| Benchmark | Registry Key | Measures |
|---------------|----------------|-----------------------------------------------|
| **Latency** | `latency` | Per-call inference latency (mean, p50, p95, min, max) |
| **Throughput**| `throughput` | Tokens per second throughput |
---
## BaseBenchmark ABC
All benchmarks implement the `BaseBenchmark` abstract base class.
```python
from abc import ABC, abstractmethod
from openjarvis.bench._stubs import BenchmarkResult
from openjarvis.engine._stubs import InferenceEngine
class BaseBenchmark(ABC):
@property
@abstractmethod
def name(self) -> str:
"""Short identifier for this benchmark."""
@property
@abstractmethod
def description(self) -> str:
"""Human-readable description of what this benchmark measures."""
@abstractmethod
def run(
self,
engine: InferenceEngine,
model: str,
*,
num_samples: int = 10,
) -> BenchmarkResult:
"""Execute the benchmark and return results."""
```
### BenchmarkResult
Each benchmark run produces a `BenchmarkResult`:
| Field | Type | Description |
|------------------|------------------|------------------------------------------|
| `benchmark_name` | `str` | Name of the benchmark |
| `model` | `str` | Model used |
| `engine` | `str` | Engine backend used |
| `metrics` | `dict[str, float]` | Key-value pairs of measured metrics |
| `metadata` | `dict[str, Any]` | Additional metadata |
| `samples` | `int` | Number of samples run |
| `errors` | `int` | Number of errors encountered |
---
## Built-in Benchmarks
### Latency Benchmark
Measures per-call inference latency using short, fixed prompts. Each sample sends a simple prompt to the engine and measures wall-clock time.
**Prompts used:** The benchmark rotates through a set of short canned prompts ("Hello", "What is 2+2?", "Explain gravity in one sentence") to keep input variation consistent across runs.
**Metrics produced:**
| Metric | Description |
|-----------------|-----------------------------------------------------|
| `mean_latency` | Average latency across all successful samples |
| `p50_latency` | Median latency (50th percentile) |
| `p95_latency` | 95th percentile latency (tail performance) |
| `min_latency` | Fastest single call |
| `max_latency` | Slowest single call |
**Example output:**
```
latency (10 samples, 0 errors)
mean_latency: 0.2345
p50_latency: 0.2100
p95_latency: 0.3800
min_latency: 0.1500
max_latency: 0.4200
```
### Throughput Benchmark
Measures inference throughput in tokens per second. Each sample sends a longer prompt ("Write a short paragraph about artificial intelligence") and measures both the time taken and the number of completion tokens generated.
**Metrics produced:**
| Metric | Description |
|-----------------------|------------------------------------------------|
| `tokens_per_second` | Total completion tokens / total time |
| `total_tokens` | Total completion tokens across all samples |
| `total_time_seconds` | Total wall-clock time across all samples |
**Example output:**
```
throughput (10 samples, 0 errors)
tokens_per_second: 45.6789
total_tokens: 1250.0000
total_time_seconds: 27.3600
```
---
## Interpreting Results
### Latency Metrics
- **mean_latency:** The average response time. Use this for general performance comparison.
- **p50_latency (median):** The typical response time. Less affected by outliers than the mean.
- **p95_latency:** The worst-case response time for 95% of requests. Critical for user experience -- if this is too high, some users will experience noticeable delays.
- **min/max_latency:** The best and worst individual calls. A large gap between min and max indicates inconsistent performance.
!!! tip "What to look for"
A healthy setup has `p95 / p50 < 2`. If the p95 is much higher than the median, investigate whether the engine is experiencing contention, thermal throttling, or memory pressure.
### Throughput Metrics
- **tokens_per_second:** The main throughput indicator. Higher is better. Typical ranges:
- CPU-only: 5-20 tokens/second
- Consumer GPU (RTX 3060-4090): 30-100 tokens/second
- Data-center GPU (A100, H100): 100-500+ tokens/second
- **total_tokens / total_time:** The raw data behind the throughput calculation. Useful for verifying that the engine is generating meaningful output (not returning empty responses).
---
## BenchmarkSuite
The `BenchmarkSuite` class runs a collection of benchmarks and provides aggregation and serialization utilities.
```python
from openjarvis.bench._stubs import BenchmarkSuite
from openjarvis.bench.latency import LatencyBenchmark
from openjarvis.bench.throughput import ThroughputBenchmark
suite = BenchmarkSuite([LatencyBenchmark(), ThroughputBenchmark()])
# Run all benchmarks
results = suite.run_all(engine, model, num_samples=20)
# Serialize to JSONL (one JSON object per line)
jsonl = suite.to_jsonl(results)
# Get a summary dict
summary = suite.summary(results)
```
### Methods
| Method | Returns | Description |
|-------------------------|--------------------|--------------------------------------------|
| `run_all(engine, model, num_samples=10)` | `list[BenchmarkResult]` | Run all benchmarks sequentially |
| `to_jsonl(results)` | `str` | Serialize results to JSONL format |
| `summary(results)` | `dict[str, Any]` | Create a summary dictionary |
### JSONL Format
Each line in the JSONL output is a JSON object:
```json
{"benchmark_name": "latency", "model": "qwen3:8b", "engine": "ollama", "metrics": {"mean_latency": 0.234, "p50_latency": 0.21, "p95_latency": 0.38, "min_latency": 0.15, "max_latency": 0.42}, "metadata": {}, "samples": 10, "errors": 0}
{"benchmark_name": "throughput", "model": "qwen3:8b", "engine": "ollama", "metrics": {"tokens_per_second": 45.67, "total_tokens": 1250.0, "total_time_seconds": 27.36}, "metadata": {}, "samples": 10, "errors": 0}
```
### Summary Format
```json
{
"benchmark_count": 2,
"benchmarks": [
{
"name": "latency",
"model": "qwen3:8b",
"engine": "ollama",
"metrics": {"mean_latency": 0.234, ...},
"samples": 10,
"errors": 0
},
{
"name": "throughput",
"model": "qwen3:8b",
"engine": "ollama",
"metrics": {"tokens_per_second": 45.67, ...},
"samples": 10,
"errors": 0
}
]
}
```
---
## CLI Usage
```bash
# Run all benchmarks with default settings (10 samples)
jarvis bench run
# Run with more samples for better statistical accuracy
jarvis bench run -n 50
# Run only the latency benchmark
jarvis bench run -b latency
# Run only the throughput benchmark with 20 samples
jarvis bench run -b throughput -n 20
# Specify model and engine
jarvis bench run -m qwen3:8b -e ollama
# Output JSON summary to stdout
jarvis bench run --json
# Write JSONL results to a file
jarvis bench run -o results.jsonl
# Combine options
jarvis bench run -b latency -n 100 -m qwen3:8b --json -o latency.jsonl
```
| Option | Type | Default | Description |
|----------------------------|--------|---------|------------------------------------------|
| `-m`, `--model MODEL` | string | auto | Model to benchmark |
| `-e`, `--engine ENGINE` | string | auto | Engine backend |
| `-n`, `--samples N` | int | `10` | Number of samples per benchmark |
| `-b`, `--benchmark NAME` | string | all | Specific benchmark to run (`latency` or `throughput`) |
| `-o`, `--output PATH` | path | none | Write JSONL results to file |
| `--json` | flag | off | Output JSON summary to stdout |
---
## Adding Custom Benchmarks
Create a custom benchmark by subclassing `BaseBenchmark` and registering it with the `BenchmarkRegistry`.
### Step 1: Implement the Benchmark
```python
import time
from openjarvis.bench._stubs import BaseBenchmark, BenchmarkResult
from openjarvis.core.registry import BenchmarkRegistry
from openjarvis.core.types import Message, Role
from openjarvis.engine._stubs import InferenceEngine
class ContextLengthBenchmark(BaseBenchmark):
"""Measures how latency scales with input length."""
@property
def name(self) -> str:
return "context_length"
@property
def description(self) -> str:
return "Measures latency scaling with increasing input length"
def run(
self,
engine: InferenceEngine,
model: str,
*,
num_samples: int = 10,
) -> BenchmarkResult:
latencies = {}
errors = 0
for length in [100, 500, 1000, 2000]:
prompt = "x " * length
messages = [Message(role=Role.USER, content=prompt)]
t0 = time.time()
try:
engine.generate(messages, model=model)
latencies[f"latency_{length}_tokens"] = time.time() - t0
except Exception:
errors += 1
return BenchmarkResult(
benchmark_name=self.name,
model=model,
engine=engine.engine_id,
metrics=latencies,
samples=len(latencies),
errors=errors,
)
```
### Step 2: Register the Benchmark
Use the `ensure_registered()` pattern to survive registry clearing in tests:
```python
def ensure_registered() -> None:
"""Register the benchmark if not already present."""
if not BenchmarkRegistry.contains("context_length"):
BenchmarkRegistry.register_value("context_length", ContextLengthBenchmark)
```
Alternatively, use the decorator at class definition time:
```python
@BenchmarkRegistry.register("context_length")
class ContextLengthBenchmark(BaseBenchmark):
...
```
!!! info "The `ensure_registered()` Pattern"
The `ensure_registered()` function is preferred over the decorator for benchmark modules because it survives registry clearing during testing. The built-in `latency` and `throughput` benchmarks both use this pattern. The benchmark CLI command calls `ensure_registered()` before looking up benchmarks.
### Step 3: Use Your Benchmark
Once registered, your benchmark is available through the CLI:
```bash
jarvis bench run -b context_length
```
And through the `BenchmarkSuite`:
```python
from openjarvis.core.registry import BenchmarkRegistry
bench_cls = BenchmarkRegistry.get("context_length")
bench = bench_cls()
result = bench.run(engine, model, num_samples=5)
```
+395
View File
@@ -0,0 +1,395 @@
# CLI Reference
OpenJarvis provides a command-line interface through the `jarvis` command. Built on [Click](https://click.palletsprojects.com/), it offers subcommands for querying models, managing memory, running benchmarks, and serving an OpenAI-compatible API.
## Global Options
```bash
jarvis --version # Print the OpenJarvis version
jarvis --help # Show top-level help with all subcommands
```
## `jarvis init`
Detect local hardware (CPU, GPU, RAM) and generate a configuration file at `~/.openjarvis/config.toml`.
```bash
jarvis init # Interactive — refuses to overwrite existing config
jarvis init --force # Overwrite existing config without prompting
```
| Option | Description |
|-----------|-----------------------------------------------|
| `--force` | Overwrite existing configuration without prompting |
The `init` command auto-detects:
- **Platform** (Linux, macOS, Windows)
- **CPU** brand and core count
- **RAM** in GB
- **GPU** vendor, model, VRAM, and count (via `nvidia-smi`, `rocm-smi`, or `system_profiler`)
Based on the detected hardware, it recommends an appropriate inference engine and writes a pre-configured TOML file.
**Example output:**
```
Detecting hardware...
Platform : linux
CPU : AMD Ryzen 9 7950X (32 cores)
RAM : 64 GB
GPU : NVIDIA RTX 4090 (24.0 GB VRAM, x1)
Config written successfully.
```
---
## `jarvis ask`
Send a query to the inference engine (directly or through an agent) and print the response.
```bash
jarvis ask "What is the capital of France?"
```
### Options
| Option | Type | Default | Description |
|-------------------------------|---------|------------|-------------------------------------------------------|
| `-m`, `--model MODEL` | string | auto | Model to use for inference |
| `-e`, `--engine ENGINE` | string | auto | Engine backend (ollama, vllm, llamacpp, etc.) |
| `-t`, `--temperature TEMP` | float | `0.7` | Sampling temperature |
| `--max-tokens N` | int | `1024` | Maximum tokens to generate |
| `--json` | flag | off | Output raw JSON result instead of plain text |
| `--no-stream` | flag | off | Disable streaming (synchronous mode) |
| `--no-context` | flag | off | Disable memory context injection |
| `-a`, `--agent AGENT` | string | none | Agent to use (`simple`, `orchestrator`) |
| `--tools TOOLS` | string | none | Comma-separated tool names to enable |
| `--router POLICY` | string | config | Router policy for model selection (`heuristic`, `grpo`)|
### Direct Mode vs Agent Mode
**Direct mode** (default) sends the query straight to the inference engine:
```bash
jarvis ask "Explain quantum computing"
```
**Agent mode** routes the query through an agent that can use tools and manage multi-turn interactions:
```bash
jarvis ask --agent orchestrator "What is 2+2?"
jarvis ask --agent orchestrator --tools calculator,think "Calculate sqrt(144) + 3^2"
jarvis ask --agent simple "Hello"
```
### Usage Examples
```bash
# Basic query
jarvis ask "What is machine learning?"
# Specify a model
jarvis ask -m qwen3:8b "Summarize this concept"
# Use the orchestrator agent with tools
jarvis ask --agent orchestrator --tools calculator "What is 15% of 340?"
# Get JSON output
jarvis ask --json "Hello"
# Disable memory context injection
jarvis ask --no-context "Tell me about Python"
# Specify router policy and temperature
jarvis ask --router heuristic -t 0.3 "Write a haiku"
# Set maximum token generation
jarvis ask --max-tokens 2048 "Write a detailed essay about AI"
```
### JSON Output Format
When using `--json` in **direct mode**, the output includes:
```json
{
"content": "The response text...",
"usage": {
"prompt_tokens": 12,
"completion_tokens": 85,
"total_tokens": 97
}
}
```
When using `--json` in **agent mode**, the output includes:
```json
{
"content": "The response text...",
"turns": 3,
"tool_results": [
{
"tool_name": "calculator",
"content": "51.0",
"success": true
}
]
}
```
---
## `jarvis model`
Manage and inspect language models available on running engines.
### `jarvis model list`
List all models available from running inference engines, displayed as a Rich table with model parameters, context length, and VRAM requirements.
```bash
jarvis model list
```
**Example output:**
```
Available Models
┌─────────┬────────────────┬────────┬─────────┬──────┐
│ Engine │ Model │ Params │ Context │ VRAM │
├─────────┼────────────────┼────────┼─────────┼──────┤
│ ollama │ qwen3:8b │ 8B │ 32,768 │ 6GB │
│ ollama │ llama3.2:3b │ 3B │ 8,192 │ 3GB │
└─────────┴────────────────┴────────┴─────────┴──────┘
```
### `jarvis model info <model>`
Show detailed information about a specific model.
```bash
jarvis model info qwen3:8b
```
**Example output:**
```
┌─ Qwen 3 8B ──────────────────────────────┐
│ Model ID: qwen3:8b │
│ Name: Qwen 3 8B │
│ Parameters: 8B │
│ Context: 32,768 │
│ Quantization: none │
│ Min VRAM: 6GB │
│ Engines: ollama, vllm │
│ Provider: Alibaba │
│ API Key: not required │
└───────────────────────────────────────────┘
```
### `jarvis model pull <model>`
Download a model via Ollama. Shows a progress bar during download.
```bash
jarvis model pull qwen3:8b
```
!!! note
The `pull` command requires a running Ollama instance. It connects to the Ollama API at the host configured in your `config.toml`.
---
## `jarvis memory`
Manage the document memory store for retrieval-augmented generation.
### `jarvis memory index <path>`
Index documents from a file or directory into the memory store.
```bash
jarvis memory index ./docs/
jarvis memory index ./notes.md
jarvis memory index ./data/ --chunk-size 256 --chunk-overlap 32
jarvis memory index ./docs/ --backend sqlite
```
| Option | Type | Default | Description |
|-----------------------------|--------|---------|--------------------------------------|
| `--backend`, `-b` | string | config | Override the default memory backend |
| `--chunk-size` | int | `512` | Chunk size in tokens |
| `--chunk-overlap` | int | `64` | Overlap between chunks in tokens |
The ingestion pipeline supports text, markdown, code files, and PDF (with `pdfplumber` installed). Binary files and hidden directories are automatically skipped.
### `jarvis memory search <query>`
Search the memory store for relevant document chunks.
```bash
jarvis memory search "machine learning basics"
jarvis memory search -k 10 "neural networks"
jarvis memory search --backend faiss "embeddings"
```
| Option | Type | Default | Description |
|--------------------|--------|---------|--------------------------------------|
| `--top-k`, `-k` | int | `5` | Number of results to return |
| `--backend`, `-b` | string | config | Override the default memory backend |
Results are displayed in a table with rank, score, source file, and a content preview.
### `jarvis memory stats`
Show memory store statistics including document count and database size.
```bash
jarvis memory stats
jarvis memory stats --backend sqlite
```
| Option | Type | Default | Description |
|--------------------|--------|---------|--------------------------------------|
| `--backend`, `-b` | string | config | Override the default memory backend |
---
## `jarvis telemetry`
Query and manage inference telemetry data stored in SQLite.
### `jarvis telemetry stats`
Show aggregated telemetry statistics including total calls, tokens, cost, and latency, broken down by model and engine.
```bash
jarvis telemetry stats
jarvis telemetry stats -n 5 # Show top 5 models
```
| Option | Type | Default | Description |
|-----------------|------|---------|-------------------------------|
| `-n`, `--top` | int | `10` | Number of top models to show |
### `jarvis telemetry export`
Export raw telemetry records in JSON or CSV format.
```bash
jarvis telemetry export # JSON to stdout
jarvis telemetry export --format csv # CSV to stdout
jarvis telemetry export --format json -o data.json # JSON to file
jarvis telemetry export -f csv -o metrics.csv # CSV to file
```
| Option | Type | Default | Description |
|-----------------------|--------|----------|---------------------------------|
| `-f`, `--format` | choice | `json` | Output format: `json` or `csv` |
| `-o`, `--output` | path | stdout | Output file path |
### `jarvis telemetry clear`
Delete all telemetry records from the database.
```bash
jarvis telemetry clear # Interactive confirmation
jarvis telemetry clear --yes # Skip confirmation
```
| Option | Type | Default | Description |
|----------------|------|---------|-------------------------------|
| `-y`, `--yes` | flag | off | Skip confirmation prompt |
!!! warning
This permanently deletes all stored telemetry data. Use `--yes` to skip the confirmation prompt in automated scripts.
---
## `jarvis bench`
Run inference benchmarks against a running engine.
### `jarvis bench run`
Execute benchmarks and report results.
```bash
jarvis bench run # Run all benchmarks, 10 samples
jarvis bench run -n 20 # 20 samples per benchmark
jarvis bench run -b latency # Only the latency benchmark
jarvis bench run -b throughput -n 50 --json # Throughput, 50 samples, JSON output
jarvis bench run -o results.jsonl # Write JSONL results to file
jarvis bench run -m qwen3:8b -e ollama # Specific model and engine
```
| Option | Type | Default | Description |
|----------------------------|--------|---------|------------------------------------------|
| `-m`, `--model MODEL` | string | auto | Model to benchmark |
| `-e`, `--engine ENGINE` | string | auto | Engine backend |
| `-n`, `--samples N` | int | `10` | Number of samples per benchmark |
| `-b`, `--benchmark NAME` | string | all | Specific benchmark to run |
| `-o`, `--output PATH` | path | none | Write JSONL results to file |
| `--json` | flag | off | Output JSON summary to stdout |
Available benchmarks:
- **latency** -- Measures per-call inference latency (mean, p50, p95, min, max)
- **throughput** -- Measures tokens-per-second throughput
---
## `jarvis serve`
Start an OpenAI-compatible API server.
```bash
jarvis serve # Default host/port from config
jarvis serve --port 8000 # Custom port
jarvis serve --host 0.0.0.0 --port 9000 # Bind to all interfaces
jarvis serve --model qwen3:8b # Specify default model
jarvis serve --agent orchestrator # Route requests through an agent
```
| Option | Type | Default | Description |
|--------------------------|--------|---------|------------------------------------------|
| `--host HOST` | string | config | Bind address |
| `--port PORT` | int | config | Port number |
| `-e`, `--engine ENGINE` | string | auto | Engine backend |
| `-m`, `--model MODEL` | string | config | Default model for inference |
| `-a`, `--agent AGENT` | string | none | Agent for non-streaming requests |
!!! note "Server Dependencies"
The `serve` command requires the server extra:
```bash
pip install 'openjarvis[server]'
```
This installs FastAPI, uvicorn, and related dependencies.
### API Endpoints
The server exposes the following OpenAI-compatible endpoints:
| Method | Path | Description |
|--------|--------------------------|--------------------------------|
| POST | `/v1/chat/completions` | Chat completions (streaming & non-streaming) |
| GET | `/v1/models` | List available models |
| GET | `/health` | Health check |
**Example with curl:**
```bash
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qwen3:8b",
"messages": [{"role": "user", "content": "Hello!"}]
}'
```
When an agent is configured (e.g., `--agent orchestrator`), non-streaming requests are routed through the agent with access to all registered tools. For tool-capable agents (`orchestrator`, `react`, `openhands`), all registered tools are automatically loaded and made available.
+365
View File
@@ -0,0 +1,365 @@
# Memory
The memory system provides persistent, searchable document storage for retrieval-augmented generation (RAG). It supports multiple retrieval backends, a configurable chunking pipeline, document ingestion from files and directories, and automatic context injection into prompts.
## Architecture
```
Documents --> Chunking Pipeline --> Memory Backend --> Context Injection --> Prompt
(files) (split + overlap) (store + index) (retrieve + format) (to LLM)
```
---
## MemoryBackend ABC
All memory backends implement the `MemoryBackend` abstract base class.
```python
class MemoryBackend(ABC):
backend_id: str
def store(self, content: str, *, source: str = "", metadata: dict | None = None) -> str:
"""Persist content and return a unique document ID."""
def retrieve(self, query: str, *, top_k: int = 5, **kwargs) -> list[RetrievalResult]:
"""Search for query and return the top-k results."""
def delete(self, doc_id: str) -> bool:
"""Delete a document by ID. Return True if it existed."""
def clear(self) -> None:
"""Remove all stored documents."""
```
### RetrievalResult
Each retrieval returns a list of `RetrievalResult` objects:
| Field | Type | Description |
|------------|------------------|--------------------------------------------|
| `content` | `str` | The retrieved text chunk |
| `score` | `float` | Relevance score (higher is better) |
| `source` | `str` | Originating file path or identifier |
| `metadata` | `dict[str, Any]` | Additional metadata (chunk index, etc.) |
---
## Backends
### SQLite / FTS5 (Default)
**Registry key:** `sqlite`
The default backend using SQLite's built-in FTS5 full-text search extension. Zero external dependencies -- uses Python's standard `sqlite3` module.
- **Scoring:** BM25 ranking via FTS5 MATCH queries
- **Persistence:** SQLite database file (default: `~/.openjarvis/memory.db`)
- **Dependencies:** None (built into Python)
```python
from openjarvis.core.registry import MemoryRegistry
backend = MemoryRegistry.create("sqlite", db_path="./memory.db")
doc_id = backend.store("Hello world", source="test.txt")
results = backend.retrieve("hello")
backend.close()
```
!!! tip "When to use SQLite/FTS5"
Use this backend when you want zero-configuration setup, keyword-based search is sufficient, and you need persistent storage across restarts. It works well for small to medium document collections.
### FAISS
**Registry key:** `faiss`
Dense neural retrieval using Facebook AI Similarity Search. Embeds documents and queries into dense vectors and retrieves by cosine similarity.
- **Scoring:** Cosine similarity via inner-product search on L2-normalized vectors
- **Persistence:** In-memory only (data is lost on restart)
- **Dependencies:** `faiss-cpu` (or `faiss-gpu`), `sentence-transformers`
```bash
pip install faiss-cpu sentence-transformers
```
```python
backend = MemoryRegistry.create("faiss")
doc_id = backend.store("Neural networks are computational models")
results = backend.retrieve("deep learning architectures")
```
!!! tip "When to use FAISS"
Use this backend when you need semantic search (finding conceptually similar content even without exact keyword matches). Best for use cases where you can re-index on each run since data is not persisted.
### ColBERTv2
**Registry key:** `colbert`
Late-interaction retrieval using ColBERT's token-level embeddings with MaxSim scoring. Provides the highest retrieval quality among the available backends.
- **Scoring:** MaxSim -- for each query token, take the maximum cosine similarity across all document tokens, then sum
- **Persistence:** In-memory only
- **Dependencies:** `colbert-ai`, `torch`
```bash
pip install colbert-ai torch
```
```python
backend = MemoryRegistry.create(
"colbert",
checkpoint="colbert-ir/colbertv2.0",
device="cpu",
)
```
| Parameter | Default | Description |
|--------------|----------------------------|-------------------------------------|
| `checkpoint` | `"colbert-ir/colbertv2.0"` | ColBERT model checkpoint |
| `device` | `"cpu"` | Computation device (`cpu` or `cuda`) |
!!! tip "When to use ColBERTv2"
Use this backend when retrieval quality is the top priority and you have the compute resources for it. The checkpoint is lazily loaded on first use to avoid slow imports. Best for research and evaluation workloads.
### BM25
**Registry key:** `bm25`
Classic probabilistic ranking using the BM25 Okapi algorithm. In-memory implementation using the `rank_bm25` library.
- **Scoring:** BM25 Okapi term-frequency scoring
- **Persistence:** In-memory only
- **Dependencies:** `rank-bm25`
```bash
pip install rank-bm25
```
```python
backend = MemoryRegistry.create("bm25")
backend.store("Python is a programming language", source="intro.txt")
results = backend.retrieve("programming language")
```
!!! tip "When to use BM25"
Use this backend when you want classic keyword-based retrieval without database dependencies. Useful as the sparse component in a hybrid retrieval setup.
### Hybrid (RRF Fusion)
**Registry key:** `hybrid`
Combines a sparse retriever and a dense retriever using Reciprocal Rank Fusion (RRF). Documents are stored in both sub-backends, and retrieval results are merged.
- **Scoring:** `RRF_score(d) = sum(weight_i / (k + rank_i(d)))` across both ranked lists
- **Persistence:** Depends on sub-backends
- **Dependencies:** Depends on sub-backends
```python
from openjarvis.memory.bm25 import BM25Memory
from openjarvis.memory.faiss_backend import FAISSMemory
sparse = BM25Memory()
dense = FAISSMemory()
backend = MemoryRegistry.create(
"hybrid",
sparse=sparse,
dense=dense,
k=60,
sparse_weight=1.0,
dense_weight=1.0,
)
```
| Parameter | Default | Description |
|-----------------|---------|------------------------------------------|
| `sparse` | -- | Sparse retrieval backend (e.g., BM25) |
| `dense` | -- | Dense retrieval backend (e.g., FAISS) |
| `k` | `60` | RRF constant |
| `sparse_weight` | `1.0` | Weight for sparse retriever results |
| `dense_weight` | `1.0` | Weight for dense retriever results |
The hybrid backend over-fetches (3x `top_k`) from each sub-backend before applying fusion to improve result quality.
!!! tip "When to use Hybrid"
Use this backend when you want the best of both keyword matching and semantic similarity. The RRF fusion approach is robust and does not require tuning score distributions across different retrieval methods.
---
## Backend Comparison
| Backend | Search Type | Persistence | Dependencies | Quality | Speed |
|-------------|-------------------|-------------|----------------------|----------|----------|
| SQLite/FTS5 | Keyword (BM25) | Yes | None | Good | Fast |
| FAISS | Dense (cosine) | No | faiss, transformers | Better | Fast |
| ColBERTv2 | Late interaction | No | colbert-ai, torch | Best | Slower |
| BM25 | Keyword (Okapi) | No | rank-bm25 | Good | Fast |
| Hybrid | Fusion (RRF) | Mixed | Sub-backend deps | Better | Medium |
---
## Chunking Pipeline
Documents are split into chunks before storage using a configurable pipeline. The chunker respects paragraph boundaries when possible.
### ChunkConfig
| Field | Type | Default | Description |
|-----------------|-------|---------|------------------------------------------|
| `chunk_size` | `int` | `512` | Target chunk size in whitespace tokens |
| `chunk_overlap` | `int` | `64` | Overlap between consecutive chunks |
| `min_chunk_size`| `int` | `50` | Minimum chunk size (smaller chunks are discarded) |
### How Chunking Works
1. The document is split into paragraphs (separated by double newlines).
2. Paragraphs are accumulated until the token count exceeds `chunk_size`.
3. The accumulated content is emitted as a chunk.
4. The last `chunk_overlap` tokens are retained as context for the next chunk.
5. Paragraphs exceeding `chunk_size` are split into fixed-size windows with overlap.
### Chunk Output
Each chunk is a `Chunk` object with:
| Field | Type | Description |
|------------|------------------|------------------------------------------|
| `content` | `str` | The chunk text |
| `source` | `str` | Originating file path |
| `offset` | `int` | Token offset within the document |
| `index` | `int` | Sequential chunk index |
| `metadata` | `dict[str, Any]` | Additional metadata |
---
## Document Ingestion
The `ingest_path()` function reads files or recursively walks directories, producing chunks ready for storage.
### Supported File Types
| Type | Extensions |
|----------|-------------------------------------------------------------|
| Text | `.txt` and other plain text files |
| Markdown | `.md`, `.markdown`, `.mdx` |
| Code | `.py`, `.js`, `.ts`, `.rs`, `.go`, `.java`, `.c`, `.cpp`, `.rb`, `.sh`, `.yaml`, `.json`, `.html`, `.css`, and more |
| PDF | `.pdf` (requires `pdfplumber`: `pip install openjarvis[memory-pdf]`) |
### Automatic Skipping
The ingestion pipeline automatically skips:
- Hidden files and directories (starting with `.`)
- Common non-content directories: `__pycache__`, `node_modules`, `.venv`, `.git`, etc.
- Binary files: images, audio, video, archives, compiled files
- Files that cannot be read (permission errors, encoding issues)
### Usage
```python
from pathlib import Path
from openjarvis.memory.chunking import ChunkConfig
from openjarvis.memory.ingest import ingest_path
# Default chunking
chunks = ingest_path(Path("./docs/"))
# Custom chunking
config = ChunkConfig(chunk_size=256, chunk_overlap=32)
chunks = ingest_path(Path("./notes.md"), config=config)
print(f"Produced {len(chunks)} chunks")
for chunk in chunks[:3]:
print(f" [{chunk.index}] {chunk.source}: {chunk.content[:60]}...")
```
---
## Context Injection
When memory context injection is enabled (the default), queries are automatically augmented with relevant retrieved documents before being sent to the model. Each retrieved passage includes source attribution.
### ContextConfig
| Field | Type | Default | Description |
|---------------------|---------|---------|--------------------------------------------------|
| `enabled` | `bool` | `True` | Whether context injection is active |
| `top_k` | `int` | `5` | Number of results to retrieve |
| `min_score` | `float` | `0.1` | Minimum relevance score threshold |
| `max_context_tokens`| `int` | `2048` | Maximum total tokens in injected context |
### How It Works
1. The user's query is searched against the memory backend.
2. Results below `min_score` are filtered out.
3. Results are truncated to fit within `max_context_tokens`.
4. A system message is prepended to the conversation with the formatted context:
```
The following context was retrieved from the knowledge base.
Use it to inform your response, citing sources where applicable:
[Source: docs/intro.md] OpenJarvis is a modular AI framework...
[Source: docs/config.md] Configuration is stored in TOML format...
```
### Disabling Context Injection
=== "CLI"
```bash
jarvis ask --no-context "Tell me about Python"
```
=== "Python SDK"
```python
response = j.ask("Tell me about Python", context=False)
```
---
## CLI Usage
```bash
# Index a directory
jarvis memory index ./docs/
# Index with custom chunking
jarvis memory index ./notes/ --chunk-size 256 --chunk-overlap 32
# Search the memory store
jarvis memory search "machine learning"
# Search with more results
jarvis memory search -k 10 "neural networks"
# Show memory statistics
jarvis memory stats
```
## SDK Usage
```python
from openjarvis import Jarvis
j = Jarvis()
# Index documents
result = j.memory.index("./docs/", chunk_size=512, chunk_overlap=64)
print(f"Indexed {result['chunks']} chunks")
# Search
results = j.memory.search("configuration", top_k=3)
for r in results:
print(f" [{r['score']:.4f}] {r['source']}: {r['content'][:80]}...")
# Statistics
stats = j.memory.stats()
print(f"Backend: {stats['backend']}, Documents: {stats.get('count', 'N/A')}")
# Clean up
j.close()
```
+376
View File
@@ -0,0 +1,376 @@
# Python SDK
The OpenJarvis Python SDK provides a high-level interface for interacting with local inference engines, managing memory, and running agent workflows. The primary entry point is the `Jarvis` class.
## Installation
```bash
pip install openjarvis
```
## Quick Start
```python
from openjarvis import Jarvis
j = Jarvis()
response = j.ask("What is the capital of France?")
print(response)
j.close()
```
---
## Jarvis Class
### Constructor
```python
Jarvis(
*,
config: JarvisConfig | None = None,
config_path: str | None = None,
engine_key: str | None = None,
model: str | None = None,
)
```
| Parameter | Type | Default | Description |
|---------------|------------------|---------|----------------------------------------------------------------|
| `config` | `JarvisConfig` | `None` | Provide a pre-built configuration object |
| `config_path` | `str` | `None` | Path to a TOML configuration file |
| `engine_key` | `str` | `None` | Override the engine backend (`"ollama"`, `"vllm"`, etc.) |
| `model` | `str` | `None` | Override the default model (e.g., `"qwen3:8b"`) |
If no `config` or `config_path` is provided, the SDK loads configuration from the default location (`~/.openjarvis/config.toml`), falling back to built-in defaults.
**Examples:**
```python
# Default configuration — auto-detects engine
j = Jarvis()
# Override the model
j = Jarvis(model="qwen3:8b")
# Override the engine
j = Jarvis(engine_key="ollama")
# Load from a specific config file
j = Jarvis(config_path="/path/to/config.toml")
```
### Properties
| Property | Type | Description |
|-----------|----------------|-----------------------------------|
| `config` | `JarvisConfig` | The active configuration object |
| `version` | `str` | The OpenJarvis version string |
| `memory` | `MemoryHandle` | Proxy for memory operations |
---
## `ask()` Method
Send a query and receive a plain-text response.
```python
ask(
query: str,
*,
model: str | None = None,
agent: str | None = None,
tools: list[str] | None = None,
temperature: float = 0.7,
max_tokens: int = 1024,
context: bool = True,
) -> str
```
| Parameter | Type | Default | Description |
|---------------|--------------|---------|------------------------------------------------------|
| `query` | `str` | -- | The question or prompt to send |
| `model` | `str` | `None` | Override the model for this call |
| `agent` | `str` | `None` | Route through an agent (`"simple"`, `"orchestrator"`) |
| `tools` | `list[str]` | `None` | Tool names to enable (requires agent mode) |
| `temperature` | `float` | `0.7` | Sampling temperature |
| `max_tokens` | `int` | `1024` | Maximum tokens to generate |
| `context` | `bool` | `True` | Whether to inject memory context |
**Returns:** A `str` containing the model's response text.
**Examples:**
```python
# Simple query
response = j.ask("What is machine learning?")
# Override model for this call
response = j.ask("Hello", model="llama3.2:3b")
# Disable memory context injection
response = j.ask("Tell me about Python", context=False)
# Adjust generation parameters
response = j.ask("Write a haiku", temperature=0.3, max_tokens=50)
```
---
## `ask_full()` Method
Send a query and receive a detailed result dictionary with metadata.
```python
ask_full(
query: str,
*,
model: str | None = None,
agent: str | None = None,
tools: list[str] | None = None,
temperature: float = 0.7,
max_tokens: int = 1024,
context: bool = True,
) -> dict[str, Any]
```
The parameters are identical to `ask()`.
**Returns:** A dictionary with the following keys:
=== "Direct Mode"
| Key | Type | Description |
|-----------|--------|------------------------------------------|
| `content` | `str` | The response text |
| `usage` | `dict` | Token usage (`prompt_tokens`, `completion_tokens`, `total_tokens`) |
| `model` | `str` | The model used |
| `engine` | `str` | The engine backend used |
=== "Agent Mode"
| Key | Type | Description |
|----------------|--------------|------------------------------------------|
| `content` | `str` | The response text |
| `usage` | `dict` | Token usage (may be empty in agent mode) |
| `tool_results` | `list[dict]` | Tool execution results |
| `turns` | `int` | Number of agent turns taken |
| `model` | `str` | The model used |
| `engine` | `str` | The engine backend used |
**Example:**
```python
result = j.ask_full("What is 2+2?")
print(result["content"]) # "4"
print(result["model"]) # "qwen3:8b"
print(result["engine"]) # "ollama"
print(result["usage"]) # {"prompt_tokens": 10, ...}
```
---
## Agent Mode
Pass the `agent` parameter to route queries through an agent. Agents can manage multi-turn conversations and use tools.
```python
# Simple agent — single turn, no tools
response = j.ask("Hello", agent="simple")
# Orchestrator agent — multi-turn with tool calling
response = j.ask(
"What is sqrt(144) + 3^2?",
agent="orchestrator",
tools=["calculator", "think"],
)
```
When using agent mode with `ask_full()`, the result includes `tool_results` showing each tool invocation:
```python
result = j.ask_full(
"Calculate 15% of 340",
agent="orchestrator",
tools=["calculator"],
)
print(result["content"]) # "15% of 340 is 51.0"
print(result["turns"]) # 2
print(result["tool_results"])
# [{"tool_name": "calculator", "content": "51.0", "success": True}]
```
Available agents: `simple`, `orchestrator`, `openclaw`, `custom`
Available tools: `calculator`, `think`, `retrieval`, `llm`, `file_read`
---
## MemoryHandle
The `Jarvis.memory` attribute provides a `MemoryHandle` for document indexing, search, and statistics. The memory backend is lazily initialized on first use.
### `index()`
Index a file or directory into the memory store.
```python
index(
path: str,
*,
chunk_size: int = 512,
chunk_overlap: int = 64,
) -> dict[str, Any]
```
| Parameter | Type | Default | Description |
|-----------------|-------|---------|---------------------------------------|
| `path` | `str` | -- | Path to a file or directory to index |
| `chunk_size` | `int` | `512` | Chunk size in tokens |
| `chunk_overlap` | `int` | `64` | Overlap between chunks in tokens |
**Returns:** A dictionary with `chunks` (count), `doc_ids` (list), and `path`.
```python
result = j.memory.index("./docs/")
print(f"Indexed {result['chunks']} chunks")
# Indexed 42 chunks
# Custom chunking parameters
result = j.memory.index("./notes/", chunk_size=256, chunk_overlap=32)
```
### `search()`
Search the memory store for relevant chunks.
```python
search(
query: str,
*,
top_k: int = 5,
) -> list[dict[str, Any]]
```
| Parameter | Type | Default | Description |
|-----------|-------|---------|--------------------------------|
| `query` | `str` | -- | The search query |
| `top_k` | `int` | `5` | Number of results to return |
**Returns:** A list of dictionaries, each containing `content`, `score`, `source`, and `metadata`.
```python
results = j.memory.search("neural networks")
for r in results:
print(f"[{r['score']:.4f}] {r['source']}: {r['content'][:80]}...")
```
### `stats()`
Return memory backend statistics.
```python
stats() -> dict[str, Any]
```
**Returns:** A dictionary with `backend` (name) and `count` (document count, if available).
```python
info = j.memory.stats()
print(f"Backend: {info['backend']}, Documents: {info.get('count', 'N/A')}")
```
### `close()`
Release the memory backend and its resources.
```python
j.memory.close()
```
---
## Model and Engine Discovery
### `list_models()`
Return a list of model identifiers available on the active engine.
```python
models = j.list_models()
print(models) # ["qwen3:8b", "llama3.2:3b", ...]
```
### `list_engines()`
Return a list of registered engine keys.
```python
engines = j.list_engines()
print(engines) # ["ollama", "vllm", "llamacpp", ...]
```
---
## Resource Management
### `close()`
Release all resources held by the `Jarvis` instance, including the memory backend, telemetry store, and engine connection.
```python
j.close()
```
!!! tip "Context Manager Pattern"
While `Jarvis` does not implement `__enter__`/`__exit__` directly, you should always call `close()` when done to free database connections and other resources:
```python
j = Jarvis()
try:
response = j.ask("Hello")
print(response)
finally:
j.close()
```
---
## Complete Example
```python
from openjarvis import Jarvis
# Initialize with auto-detected engine
j = Jarvis(model="qwen3:8b")
# Index documents for context-augmented responses
result = j.memory.index("./docs/")
print(f"Indexed {result['chunks']} chunks from {result['path']}")
# Simple query with memory context
response = j.ask("What are the main features?")
print(response)
# Detailed query with agent and tools
full_result = j.ask_full(
"Calculate the square root of 256 and add 10",
agent="orchestrator",
tools=["calculator"],
)
print(f"Answer: {full_result['content']}")
print(f"Turns: {full_result['turns']}")
print(f"Tools used: {[t['tool_name'] for t in full_result['tool_results']]}")
# Search memory directly
results = j.memory.search("configuration")
for r in results:
print(f" [{r['score']:.3f}] {r['source']}")
# List available models
print("Models:", j.list_models())
# Clean up
j.close()
```
+423
View File
@@ -0,0 +1,423 @@
# Telemetry & Traces
OpenJarvis has two complementary observability systems: **telemetry** for per-inference metrics and **traces** for full interaction-level recording. Together, they provide comprehensive insight into system behavior and power the learning system's routing policy updates.
---
## Telemetry
The telemetry system records metrics for every inference call -- latency, token counts, cost, and energy consumption. Data is stored in SQLite and can be queried, exported, and aggregated.
### TelemetryRecord
Each inference call produces a `TelemetryRecord` with the following fields:
| Field | Type | Description |
|----------------------|------------------|------------------------------------------|
| `timestamp` | `float` | Unix timestamp of the call |
| `model_id` | `str` | Model identifier |
| `engine` | `str` | Engine backend used |
| `agent` | `str` | Agent used (if any) |
| `prompt_tokens` | `int` | Input tokens consumed |
| `completion_tokens` | `int` | Output tokens generated |
| `total_tokens` | `int` | Total tokens (prompt + completion) |
| `latency_seconds` | `float` | Wall-clock inference time |
| `ttft` | `float` | Time to first token |
| `cost_usd` | `float` | Estimated cost in USD |
| `energy_joules` | `float` | Estimated energy consumption |
| `power_watts` | `float` | Power draw during inference |
| `metadata` | `dict[str, Any]` | Additional metadata |
### TelemetryStore
The `TelemetryStore` is an append-only SQLite database that persists telemetry records. It integrates with the event bus to capture records automatically.
```python
from openjarvis.telemetry.store import TelemetryStore
from openjarvis.core.events import EventBus
bus = EventBus()
store = TelemetryStore(db_path="~/.openjarvis/telemetry.db")
store.subscribe_to_bus(bus)
# Records are now captured automatically when TELEMETRY_RECORD events fire.
# No manual recording needed -- instrumented_generate() handles this.
store.close()
```
The store subscribes to `TELEMETRY_RECORD` events on the event bus. When the `instrumented_generate()` wrapper is used (which happens automatically in both CLI and SDK), telemetry records are published and stored without any manual intervention.
### `instrumented_generate()`
This wrapper function calls `engine.generate()` and automatically publishes telemetry events:
1. Publishes `INFERENCE_START` with model and engine info.
2. Calls the engine and measures wall-clock latency.
3. Extracts token usage from the engine response.
4. Creates a `TelemetryRecord` from the measurements.
5. Publishes `INFERENCE_END` and `TELEMETRY_RECORD` events.
All CLI commands and SDK methods use this wrapper, so telemetry is recorded transparently.
### TelemetryAggregator
The `TelemetryAggregator` provides read-only query and aggregation methods over stored telemetry data.
```python
from openjarvis.telemetry.aggregator import TelemetryAggregator
agg = TelemetryAggregator(db_path="~/.openjarvis/telemetry.db")
# Overall summary
summary = agg.summary()
print(f"Total calls: {summary.total_calls}")
print(f"Total tokens: {summary.total_tokens}")
print(f"Total cost: ${summary.total_cost:.6f}")
# Per-model breakdown
for ms in agg.per_model_stats():
print(f" {ms.model_id}: {ms.call_count} calls, {ms.avg_latency:.3f}s avg")
# Per-engine breakdown
for es in agg.per_engine_stats():
print(f" {es.engine}: {es.call_count} calls, {es.total_tokens} tokens")
# Top models by usage
top = agg.top_models(n=5)
# Export raw records
records = agg.export_records()
# Time-range filtering (Unix timestamps)
recent = agg.summary(since=1700000000.0)
# Clear all records
count = agg.clear()
print(f"Deleted {count} records")
agg.close()
```
#### Aggregation Methods
| Method | Returns | Description |
|---------------------|--------------------|--------------------------------------------|
| `summary()` | `AggregatedStats` | Total calls, tokens, cost, latency + per-model and per-engine breakdowns |
| `per_model_stats()` | `list[ModelStats]` | Call count, tokens, latency, cost grouped by model |
| `per_engine_stats()`| `list[EngineStats]` | Call count, tokens, latency, cost grouped by engine |
| `top_models(n)` | `list[ModelStats]` | Top N models by call count |
| `export_records()` | `list[dict]` | All records as plain dictionaries |
| `record_count()` | `int` | Total number of stored records |
| `clear()` | `int` | Delete all records, return count |
All query methods accept optional `since` and `until` parameters (Unix timestamps) for time-range filtering.
#### Data Classes
**ModelStats:**
| Field | Type | Description |
|--------------------|---------|--------------------------------|
| `model_id` | `str` | Model identifier |
| `call_count` | `int` | Total inference calls |
| `total_tokens` | `int` | Total tokens processed |
| `prompt_tokens` | `int` | Total input tokens |
| `completion_tokens`| `int` | Total output tokens |
| `total_latency` | `float` | Sum of all latencies |
| `avg_latency` | `float` | Average latency per call |
| `total_cost` | `float` | Total cost in USD |
**EngineStats:**
| Field | Type | Description |
|-----------------|---------|--------------------------------|
| `engine` | `str` | Engine identifier |
| `call_count` | `int` | Total inference calls |
| `total_tokens` | `int` | Total tokens processed |
| `total_latency` | `float` | Sum of all latencies |
| `avg_latency` | `float` | Average latency per call |
| `total_cost` | `float` | Total cost in USD |
**AggregatedStats:**
| Field | Type | Description |
|-----------------|--------------------|--------------------------------|
| `total_calls` | `int` | Total inference calls |
| `total_tokens` | `int` | Total tokens across all models |
| `total_cost` | `float` | Total cost in USD |
| `total_latency` | `float` | Total latency in seconds |
| `per_model` | `list[ModelStats]` | Breakdown by model |
| `per_engine` | `list[EngineStats]` | Breakdown by engine |
### CLI Commands
```bash
# Show aggregated statistics
jarvis telemetry stats
jarvis telemetry stats -n 5 # Top 5 models only
# Export records
jarvis telemetry export # JSON to stdout
jarvis telemetry export -f csv # CSV to stdout
jarvis telemetry export -o data.json # JSON to file
jarvis telemetry export -f csv -o metrics.csv
# Clear all records
jarvis telemetry clear # With confirmation prompt
jarvis telemetry clear --yes # Without confirmation
```
---
## Traces
While telemetry captures per-inference metrics, the trace system records **complete interaction sequences** -- the full chain of steps an agent takes to handle a query. Traces are the primary input to the learning system.
### What is a Trace?
A `Trace` captures the entire lifecycle of handling a user query:
| Field | Type | Description |
|--------------------------|--------------------|-----------------------------------------------|
| `trace_id` | `str` | Unique identifier (auto-generated) |
| `query` | `str` | The original user query |
| `agent` | `str` | Agent that handled the query |
| `model` | `str` | Model used for inference |
| `engine` | `str` | Engine backend used |
| `steps` | `list[TraceStep]` | Ordered list of processing steps |
| `result` | `str` | Final response content |
| `outcome` | `str` or `None` | `"success"`, `"failure"`, or `None` (unknown) |
| `feedback` | `float` or `None` | User quality score [0, 1] |
| `started_at` | `float` | Unix timestamp when processing began |
| `ended_at` | `float` | Unix timestamp when processing ended |
| `total_tokens` | `int` | Total tokens across all steps |
| `total_latency_seconds` | `float` | Total latency across all steps |
| `metadata` | `dict[str, Any]` | Additional metadata |
### Trace vs Telemetry
| Aspect | Telemetry | Traces |
|----------------|----------------------------------------|----------------------------------------------|
| **Scope** | Single inference call | Full interaction (multiple steps) |
| **Granularity**| Per-call metrics | Step-by-step sequence |
| **Purpose** | Performance monitoring, cost tracking | Learning, routing optimization, debugging |
| **Data** | Latency, tokens, cost, energy | Route, retrieve, generate, tool_call, respond |
| **Storage** | Flat table of records | Traces table + steps table |
### TraceStep
Each step in a trace records a single action the agent took.
| Field | Type | Description |
|--------------------|------------------|------------------------------------------|
| `step_type` | `StepType` | Type of step (see below) |
| `timestamp` | `float` | When the step occurred |
| `duration_seconds` | `float` | How long the step took |
| `input` | `dict[str, Any]` | Input data for the step |
| `output` | `dict[str, Any]` | Output data from the step |
| `metadata` | `dict[str, Any]` | Additional metadata |
### StepType
| Type | Description | Example Input | Example Output |
|--------------|--------------------------------------------------|------------------------------|------------------------------------|
| `route` | Model/agent selection decision | `{"query_type": "math"}` | `{"model": "qwen3:8b"}` |
| `retrieve` | Memory search for context | `{"query": "topic"}` | `{"num_results": 3}` |
| `generate` | LLM inference call | `{"model": "qwen3:8b"}` | `{"tokens": 128}` |
| `tool_call` | Tool execution | `{"tool": "calculator"}` | `{"success": true}` |
| `respond` | Final response to the user | `{}` | `{"content": "...", "turns": 2}` |
### TraceCollector
The `TraceCollector` wraps any `BaseAgent` to automatically record a `Trace` for every `run()` call. It subscribes to event bus events during execution and converts them into `TraceStep` objects.
```python
from openjarvis.agents.orchestrator import OrchestratorAgent
from openjarvis.traces.collector import TraceCollector
from openjarvis.traces.store import TraceStore
from openjarvis.core.events import EventBus
bus = EventBus()
store = TraceStore(db_path="./traces.db")
agent = OrchestratorAgent(engine, model, tools=tools, bus=bus)
collector = TraceCollector(agent, store=store, bus=bus)
# The trace is recorded automatically
result = collector.run("What is 2+2?")
print(result.content)
# Trace is now saved to the store and published on the bus
```
**How the collector works:**
1. Subscribes to `INFERENCE_START`, `INFERENCE_END`, `TOOL_CALL_START`, `TOOL_CALL_END`, and `MEMORY_RETRIEVE` events.
2. Executes the wrapped agent's `run()` method.
3. Converts captured events into `TraceStep` objects with timing data.
4. Appends a final `RESPOND` step with the result.
5. Builds a complete `Trace` object and saves it to the `TraceStore`.
6. Publishes a `TRACE_COMPLETE` event on the bus.
7. Unsubscribes from events after the run completes.
### TraceStore
The `TraceStore` is an SQLite-backed database for persisting complete traces with their steps.
```python
from openjarvis.traces.store import TraceStore
store = TraceStore(db_path="./traces.db")
# Save a trace
store.save(trace)
# Get a specific trace
trace = store.get("abc123def456")
# List traces with filters
traces = store.list_traces(
agent="orchestrator",
model="qwen3:8b",
outcome="success",
since=1700000000.0,
limit=50,
)
# Count total traces
count = store.count()
# Subscribe to event bus for automatic saving
store.subscribe_to_bus(bus)
store.close()
```
#### Filtering Options
| Parameter | Type | Description |
|-----------|---------|------------------------------------------|
| `agent` | `str` | Filter by agent ID |
| `model` | `str` | Filter by model ID |
| `outcome` | `str` | Filter by outcome (`"success"`, `"failure"`) |
| `since` | `float` | Start of time range (Unix timestamp) |
| `until` | `float` | End of time range (Unix timestamp) |
| `limit` | `int` | Maximum number of traces to return (default: 100) |
### TraceAnalyzer
The `TraceAnalyzer` provides read-only aggregated statistics over stored traces. These statistics are used by the learning system to update routing policies.
```python
from openjarvis.traces.analyzer import TraceAnalyzer
analyzer = TraceAnalyzer(store=trace_store)
# Overall summary
summary = analyzer.summary()
print(f"Total traces: {summary.total_traces}")
print(f"Total steps: {summary.total_steps}")
print(f"Avg steps/trace: {summary.avg_steps_per_trace:.1f}")
print(f"Avg latency: {summary.avg_latency:.3f}s")
print(f"Success rate: {summary.success_rate:.1%}")
print(f"Step distribution: {summary.step_type_distribution}")
# Per-route statistics (model + agent combinations)
for rs in analyzer.per_route_stats():
print(f" {rs.model}/{rs.agent}: {rs.count} traces, "
f"{rs.avg_latency:.3f}s avg, {rs.success_rate:.1%} success")
# Per-tool statistics
for ts in analyzer.per_tool_stats():
print(f" {ts.tool_name}: {ts.call_count} calls, "
f"{ts.avg_latency:.3f}s avg, {ts.success_rate:.1%} success")
# Find traces matching query characteristics
code_traces = analyzer.traces_for_query_type(has_code=True)
short_traces = analyzer.traces_for_query_type(max_length=100)
# Export traces as plain dicts
exported = analyzer.export_traces(limit=500)
```
#### Analysis Methods
| Method | Returns | Description |
|---------------------------|--------------------|------------------------------------------------------|
| `summary()` | `TraceSummary` | Overall statistics: counts, averages, distributions |
| `per_route_stats()` | `list[RouteStats]` | Stats grouped by (model, agent) combinations |
| `per_tool_stats()` | `list[ToolStats]` | Stats grouped by tool name |
| `traces_for_query_type()` | `list[Trace]` | Filter traces by query characteristics |
| `export_traces()` | `list[dict]` | Export traces as serializable dictionaries |
All analysis methods accept optional `since` and `until` parameters for time-range filtering.
#### Data Classes
**TraceSummary:**
| Field | Type | Description |
|--------------------------|-----------------|------------------------------------------|
| `total_traces` | `int` | Total number of traces |
| `total_steps` | `int` | Total steps across all traces |
| `avg_steps_per_trace` | `float` | Average number of steps per trace |
| `avg_latency` | `float` | Average total latency per trace |
| `avg_tokens` | `float` | Average tokens per trace |
| `success_rate` | `float` | Fraction of evaluated traces that succeeded |
| `step_type_distribution` | `dict[str, int]`| Count of each step type |
**RouteStats:**
| Field | Type | Description |
|----------------|----------------|------------------------------------------|
| `model` | `str` | Model identifier |
| `agent` | `str` | Agent identifier |
| `count` | `int` | Number of traces for this route |
| `avg_latency` | `float` | Average latency for this route |
| `avg_tokens` | `float` | Average tokens for this route |
| `success_rate` | `float` | Success rate for this route |
| `avg_feedback` | `float` or `None` | Average user feedback (if available) |
**ToolStats:**
| Field | Type | Description |
|----------------|---------|------------------------------------------|
| `tool_name` | `str` | Tool identifier |
| `call_count` | `int` | Number of times the tool was called |
| `avg_latency` | `float` | Average execution latency |
| `success_rate` | `float` | Fraction of successful executions |
---
## Data Flow
The following diagram shows how telemetry and trace data flows through the system:
```
User Query
|
v
Agent.run() --> EventBus --> TraceCollector (captures steps)
| |
v v
Engine.generate() TelemetryStore (captures per-call metrics)
|
v
instrumented_generate()
|
+---> INFERENCE_START event
+---> INFERENCE_END event
+---> TELEMETRY_RECORD event
|
v
TraceCollector
|
+---> Builds Trace with TraceSteps
+---> Saves to TraceStore
+---> Publishes TRACE_COMPLETE event
|
v
TraceAnalyzer / TelemetryAggregator --> Learning System
```
Both systems operate transparently -- no manual instrumentation is needed when using the CLI or SDK, as they automatically set up the event bus and telemetry store.
+391
View File
@@ -0,0 +1,391 @@
# Tools
The tool system enables agents to perform actions beyond text generation -- calculations, memory lookups, file reading, and sub-model calls. Tools follow a spec-driven design with a central dispatch engine and OpenAI function-calling format support.
## Architecture
```
Agent --> Engine (with tool defs) --> tool_calls response --> ToolExecutor --> Tool.execute()
^ |
| v
+------------------------------- ToolResult <--------------------------------------------+
```
---
## BaseTool ABC
All tools implement the `BaseTool` abstract base class.
```python
from abc import ABC, abstractmethod
from openjarvis.tools._stubs import ToolSpec
from openjarvis.core.types import ToolResult
class BaseTool(ABC):
tool_id: str
@property
@abstractmethod
def spec(self) -> ToolSpec:
"""Return the tool specification."""
@abstractmethod
def execute(self, **params) -> ToolResult:
"""Execute the tool with the given parameters."""
def to_openai_function(self) -> dict:
"""Convert to OpenAI function-calling format."""
```
The `to_openai_function()` method is provided by the base class and converts the tool's spec into the format expected by OpenAI-compatible APIs:
```json
{
"type": "function",
"function": {
"name": "calculator",
"description": "Evaluate a mathematical expression safely.",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "Math expression to evaluate"
}
},
"required": ["expression"]
}
}
}
```
---
## ToolSpec
The `ToolSpec` dataclass describes a tool's interface and characteristics.
| Field | Type | Default | Description |
|------------------------|------------------|---------|----------------------------------------------------|
| `name` | `str` | -- | Unique tool identifier |
| `description` | `str` | -- | Human-readable description (sent to the model) |
| `parameters` | `dict[str, Any]` | `{}` | JSON Schema for the tool's parameters |
| `category` | `str` | `""` | Tool category (e.g., `math`, `memory`, `reasoning`) |
| `cost_estimate` | `float` | `0.0` | Estimated cost per invocation |
| `latency_estimate` | `float` | `0.0` | Estimated latency per invocation |
| `requires_confirmation`| `bool` | `False` | Whether the tool requires user confirmation |
| `metadata` | `dict[str, Any]` | `{}` | Additional metadata |
---
## ToolResult
The `ToolResult` dataclass holds the result of a tool execution.
| Field | Type | Default | Description |
|-------------------|------------------|---------|------------------------------------------|
| `tool_name` | `str` | -- | Name of the tool that was called |
| `content` | `str` | -- | The tool's output (text) |
| `success` | `bool` | `True` | Whether the execution succeeded |
| `usage` | `dict[str, Any]` | `{}` | Token usage (for LLM tool) |
| `cost_usd` | `float` | `0.0` | Actual cost of the invocation |
| `latency_seconds` | `float` | `0.0` | Measured execution latency |
| `metadata` | `dict[str, Any]` | `{}` | Additional metadata |
---
## ToolExecutor
The `ToolExecutor` is the central dispatch engine for tool calls. It manages a set of tool instances, parses JSON arguments, measures execution latency, and publishes events on the event bus.
```python
from openjarvis.tools._stubs import ToolExecutor
executor = ToolExecutor(tools=[calculator, think_tool], bus=event_bus)
# Get OpenAI-format tool definitions
openai_tools = executor.get_openai_tools()
# Execute a tool call
from openjarvis.core.types import ToolCall
tc = ToolCall(id="call_1", name="calculator", arguments='{"expression": "2+2"}')
result = executor.execute(tc)
print(result.content) # "4"
```
### Execution Flow
1. **Parse arguments:** The `arguments` JSON string from the `ToolCall` is deserialized.
2. **Publish start event:** `TOOL_CALL_START` is emitted on the event bus with tool name and arguments.
3. **Execute:** The tool's `execute()` method is called with the parsed parameters.
4. **Measure latency:** Execution time is recorded in `result.latency_seconds`.
5. **Publish end event:** `TOOL_CALL_END` is emitted with success status and latency.
6. **Return result:** The `ToolResult` is returned to the caller.
If the tool name is unknown, a `ToolResult` with `success=False` is returned. If JSON parsing fails or the tool raises an exception, the error is captured and returned as a failed `ToolResult`.
### Methods
| Method | Returns | Description |
|---------------------|------------------------|--------------------------------------------|
| `execute(tool_call)`| `ToolResult` | Parse args, dispatch, measure, emit events |
| `available_tools()` | `list[ToolSpec]` | Return specs for all registered tools |
| `get_openai_tools()`| `list[dict]` | Return tools in OpenAI function format |
---
## Built-in Tools
### Calculator
**Registry key:** `calculator` | **Category:** `math`
Evaluates mathematical expressions safely using Python's `ast` module. No arbitrary code execution -- only whitelisted operations are allowed.
**Parameters:**
| Parameter | Type | Required | Description |
|--------------|--------|----------|------------------------------------------|
| `expression` | string | Yes | Math expression (e.g., `"2+3*4"`, `"sqrt(16)"`) |
**Supported operations:**
| Category | Operations |
|--------------|---------------------------------------------------------------|
| Arithmetic | `+`, `-`, `*`, `/`, `//` (floor div), `%` (mod), `**` (power) |
| Functions | `abs`, `round`, `min`, `max`, `sqrt`, `log`, `log10`, `log2` |
| Trigonometry | `sin`, `cos`, `tan` |
| Rounding | `ceil`, `floor` |
| Constants | `pi`, `e` |
**Example:**
```python
from openjarvis.tools.calculator import CalculatorTool
calc = CalculatorTool()
result = calc.execute(expression="sqrt(144) + 3**2")
print(result.content) # "21.0"
print(result.success) # True
```
### Think
**Registry key:** `think` | **Category:** `reasoning`
A zero-cost reasoning scratchpad. The input is echoed back as the output, allowing the model to "think out loud" during a tool-calling loop. This enables chain-of-thought reasoning within the agent workflow.
**Parameters:**
| Parameter | Type | Required | Description |
|-----------|--------|----------|------------------------------------------|
| `thought` | string | Yes | The reasoning or thought process |
**Example:**
```python
from openjarvis.tools.think import ThinkTool
think = ThinkTool()
result = think.execute(thought="Let me break this problem into steps...")
print(result.content) # "Let me break this problem into steps..."
print(result.success) # True
```
!!! info "Cost and Latency"
The Think tool has zero cost and near-zero latency, making it ideal for structured reasoning without consuming additional resources.
### Retrieval
**Registry key:** `retrieval` | **Category:** `memory`
Searches the memory backend for relevant context and returns formatted results with source attribution.
**Parameters:**
| Parameter | Type | Required | Description |
|-----------|---------|----------|------------------------------------------|
| `query` | string | Yes | Search query |
| `top_k` | integer | No | Number of results (default: 5) |
**Constructor parameters:**
| Parameter | Type | Default | Description |
|-----------|-----------------|---------|--------------------------------|
| `backend` | `MemoryBackend` | `None` | Memory backend to search |
| `top_k` | `int` | `5` | Default number of results |
**Example:**
```python
from openjarvis.tools.retrieval import RetrievalTool
from openjarvis.memory.sqlite import SQLiteMemory
backend = SQLiteMemory(db_path="./memory.db")
retrieval = RetrievalTool(backend=backend)
result = retrieval.execute(query="machine learning")
print(result.content) # Formatted context with source tags
```
### LLM
**Registry key:** `llm` | **Category:** `inference`
Delegates a sub-query to an inference engine. Useful for summarization, sub-questions, or generating structured output within an agent workflow.
**Parameters:**
| Parameter | Type | Required | Description |
|-----------|--------|----------|------------------------------------------|
| `prompt` | string | Yes | The prompt to send to the model |
| `system` | string | No | Optional system message for context |
**Constructor parameters:**
| Parameter | Type | Default | Description |
|-----------|-------------------|---------|--------------------------------|
| `engine` | `InferenceEngine` | `None` | Inference engine to use |
| `model` | `str` | `""` | Model identifier |
**Example:**
```python
from openjarvis.tools.llm_tool import LLMTool
llm = LLMTool(engine=my_engine, model="qwen3:8b")
result = llm.execute(
prompt="Summarize: AI is transforming industries...",
system="You are a concise summarizer.",
)
print(result.content)
```
### FileRead
**Registry key:** `file_read` | **Category:** `filesystem`
Reads file contents with safety validations. Supports optional directory restrictions, file size limits (1 MB max), and line count limiting.
**Parameters:**
| Parameter | Type | Required | Description |
|-------------|---------|----------|------------------------------------------|
| `path` | string | Yes | Path to the file to read |
| `max_lines` | integer | No | Maximum lines to return (default: all) |
**Constructor parameters:**
| Parameter | Type | Default | Description |
|----------------|--------------|---------|-----------------------------------------------|
| `allowed_dirs` | `list[str]` | `None` | Restrict file access to these directories |
**Safety features:**
- Path validation against allowed directories (when configured)
- Maximum file size: 1 MB
- UTF-8 encoding required (rejects binary files)
- Existence and file-type checks
**Example:**
```python
from openjarvis.tools.file_read import FileReadTool
reader = FileReadTool(allowed_dirs=["/home/user/projects"])
result = reader.execute(path="/home/user/projects/README.md", max_lines=50)
print(result.content)
print(result.metadata) # {"path": "/home/user/projects/README.md", "size_bytes": 1234}
```
---
## Tool Registration
Tools are registered via the `@ToolRegistry.register()` decorator, making them discoverable by name at runtime.
```python
from openjarvis.core.registry import ToolRegistry
from openjarvis.tools._stubs import BaseTool, ToolSpec
from openjarvis.core.types import ToolResult
@ToolRegistry.register("my_tool")
class MyTool(BaseTool):
tool_id = "my_tool"
@property
def spec(self) -> ToolSpec:
return ToolSpec(
name="my_tool",
description="A custom tool that does something useful.",
parameters={
"type": "object",
"properties": {
"input": {
"type": "string",
"description": "The input to process.",
},
},
"required": ["input"],
},
category="custom",
)
def execute(self, **params) -> ToolResult:
value = params.get("input", "")
return ToolResult(
tool_name="my_tool",
content=f"Processed: {value}",
success=True,
)
```
After registration, use the tool with an agent:
```bash
jarvis ask --agent orchestrator --tools my_tool "Process this data"
```
---
## Using Tools with Agents
### Via CLI
Tools are specified as a comma-separated list with the `--tools` flag. An agent (typically `orchestrator`) must be selected:
```bash
# Single tool
jarvis ask --agent orchestrator --tools calculator "What is 15% of 340?"
# Multiple tools
jarvis ask --agent orchestrator --tools calculator,think "Solve: 2x + 5 = 13"
# All available tools (list them)
jarvis ask --agent orchestrator --tools calculator,think,retrieval,file_read "..."
```
### Via Python SDK
Tools are passed as a list of name strings:
```python
from openjarvis import Jarvis
j = Jarvis()
# Use calculator and think tools
result = j.ask_full(
"What is the area of a circle with radius 7?",
agent="orchestrator",
tools=["calculator", "think"],
)
for tr in result["tool_results"]:
print(f" {tr['tool_name']}: {tr['content']} (success={tr['success']})")
j.close()
```
The SDK automatically instantiates tool objects with appropriate dependencies. For example, the `retrieval` tool receives the configured memory backend, and the `llm` tool receives the active engine and model.
+165
View File
@@ -0,0 +1,165 @@
site_name: OpenJarvis
site_url: https://jonsaadfalcon.github.io/OpenJarvis/
site_description: Programming abstractions for on-device AI
site_author: OpenJarvis Contributors
repo_url: https://github.com/jonsaadfalcon/OpenJarvis
repo_name: jonsaadfalcon/OpenJarvis
edit_uri: edit/main/docs/
copyright: Copyright &copy; 2026 OpenJarvis Contributors
theme:
name: material
language: en
features:
- navigation.tabs
- navigation.tabs.sticky
- navigation.sections
- navigation.top
- navigation.indexes
- navigation.footer
- search.suggest
- search.highlight
- content.code.copy
- content.code.annotate
- content.tabs.link
- toc.follow
palette:
- media: "(prefers-color-scheme: light)"
scheme: default
primary: indigo
accent: amber
toggle:
icon: material/brightness-7
name: Switch to dark mode
- media: "(prefers-color-scheme: dark)"
scheme: slate
primary: indigo
accent: amber
toggle:
icon: material/brightness-4
name: Switch to light mode
icon:
repo: fontawesome/brands/github
plugins:
- search
- mkdocstrings:
default_handler: python
handlers:
python:
paths: [src]
options:
show_source: true
show_root_heading: true
show_root_full_path: false
show_category_heading: true
show_symbol_type_heading: true
show_symbol_type_toc: true
members_order: source
docstring_style: numpy
docstring_section_style: spacy
merge_init_into_class: true
show_signature_annotations: true
separate_signature: true
signature_crossrefs: true
show_if_no_docstring: false
inherited_members: false
filters:
- "!^_"
- "^__init__$"
- "^__all__$"
markdown_extensions:
- abbr
- admonition
- attr_list
- def_list
- footnotes
- md_in_html
- tables
- toc:
permalink: true
toc_depth: 3
- pymdownx.betterem:
smart_enable: all
- pymdownx.caret
- pymdownx.details
- pymdownx.emoji:
emoji_index: !!python/name:material.extensions.emoji.twemoji
emoji_generator: !!python/name:material.extensions.emoji.to_svg
- pymdownx.highlight:
anchor_linenums: true
line_spans: __span
pygments_lang_class: true
- pymdownx.inlinehilite
- pymdownx.keys
- pymdownx.mark
- pymdownx.smartsymbols
- pymdownx.snippets:
auto_append:
- docs/includes/abbreviations.md
check_paths: false
- pymdownx.superfences:
custom_fences:
- name: mermaid
class: mermaid
format: !!python/name:pymdownx.superfences.fence_code_format
- pymdownx.tabbed:
alternate_style: true
- pymdownx.tasklist:
custom_checkbox: true
- pymdownx.tilde
extra:
social:
- icon: fontawesome/brands/github
link: https://github.com/jonsaadfalcon/OpenJarvis
nav:
- Home: index.md
- Getting Started:
- Installation: getting-started/installation.md
- Quick Start: getting-started/quickstart.md
- Configuration: getting-started/configuration.md
- User Guide:
- CLI Reference: user-guide/cli.md
- Python SDK: user-guide/python-sdk.md
- Agents: user-guide/agents.md
- Memory: user-guide/memory.md
- Tools: user-guide/tools.md
- Telemetry & Traces: user-guide/telemetry.md
- Benchmarks: user-guide/benchmarks.md
- Architecture:
- Overview: architecture/overview.md
- Intelligence: architecture/intelligence.md
- Inference Engine: architecture/engine.md
- Agentic Logic: architecture/agents.md
- Memory & Storage: architecture/memory.md
- Learning & Traces: architecture/learning.md
- Query Flow: architecture/query-flow.md
- Design Principles: architecture/design-principles.md
- API Reference:
- api/index.md
- SDK (Jarvis): api/sdk.md
- Core: api/core.md
- Engine: api/engine.md
- Agents: api/agents.md
- Memory: api/memory.md
- Tools: api/tools.md
- Intelligence: api/intelligence.md
- Learning: api/learning.md
- Traces: api/traces.md
- Telemetry: api/telemetry.md
- Benchmarks: api/bench.md
- Server: api/server.md
- Deployment:
- Docker: deployment/docker.md
- systemd (Linux): deployment/systemd.md
- launchd (macOS): deployment/launchd.md
- API Server: deployment/api-server.md
- Development:
- Contributing: development/contributing.md
- Extending OpenJarvis: development/extending.md
- Roadmap: development/roadmap.md
- Changelog: development/changelog.md
+5
View File
@@ -55,6 +55,11 @@ server = [
agents = []
learning = []
openclaw = []
docs = [
"mkdocs>=1.6",
"mkdocs-material>=9.5",
"mkdocstrings[python]>=0.25",
]
[project.scripts]
jarvis = "openjarvis.cli:main"