mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-30 02:42:16 +00:00
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:
co-authored by
Claude Opus 4.6
parent
990d7d8a79
commit
f75afefcfb
@@ -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
|
||||
@@ -36,3 +36,11 @@ Thumbs.db
|
||||
# Project
|
||||
*.sqlite
|
||||
*.db
|
||||
|
||||
# MkDocs build output
|
||||
site/
|
||||
|
||||
# Frontend
|
||||
frontend/node_modules/
|
||||
frontend/dist/
|
||||
src/openjarvis/server/static/
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 |
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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}")
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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 |
|
||||
@@ -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
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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>
|
||||
```
|
||||
@@ -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).
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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>
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
```
|
||||
@@ -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()
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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
@@ -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 © 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
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user