Add TOML config system for eval suites, pillar-aligned config, and documentation

- Eval config: TOML-based suite configs defining models x benchmarks matrix,
  loaded via --config flag. Includes load_eval_config(), expand_suite(),
  7 config dataclasses, 3 example configs, and 61 new tests.
- Pillar-aligned config: generation params in IntelligenceConfig, nested
  engine/learning configs, agent objective/system_prompt/context_from_memory,
  structured learning sub-policies, TOML migration layer.
- Documentation: evaluations user guide, evals API reference, updated
  mkdocs.yml navigation, updated architecture docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jon Saad-Falcon
2026-02-24 03:34:05 +00:00
co-authored by Claude Opus 4.6
parent c775d620af
commit 323d7ff032
51 changed files with 5615 additions and 1002 deletions
+277
View File
@@ -0,0 +1,277 @@
---
name: docs-writer
description: "Use this agent when you need to create, update, or improve MkDocs Material documentation pages for this repository. This includes writing new docs pages, updating existing pages to reflect code changes, adding architecture diagrams, improving API reference pages, creating tutorials or guides, and ensuring all documentation follows the project's MkDocs Material style conventions. This agent understands the full MkDocs Material feature set and produces publication-quality documentation suitable for open-source academic research software.\\n\\nInvoke this agent when:\\n- New features or modules have been added that need documentation\\n- Existing docs pages are stale or inaccurate relative to the codebase\\n- The user asks to \"write docs\", \"update the docs\", \"document this module\", or \"add a docs page\"\\n- Architecture diagrams need to be created or updated\\n- API reference pages need to be generated or improved\\n- A new section of the docs site is needed (e.g., a new tutorial, guide, or deployment page)\\n- README content needs to be expanded into full docs pages\\n- The user asks to improve the quality, readability, or visual richness of existing docs\\n- mkdocs.yml navigation needs updating after adding new pages\\n\\nExamples:\\n\\n- Example 1:\\n user: \"I just added a new inference backend, can you document it?\"\\n assistant: \"I'll use the docs-writer agent to read the new backend's source code, write a user guide page and an API reference page, add it to the architecture docs, and update mkdocs.yml navigation.\"\\n <commentary>\\n New module needs full documentation coverage: user guide, API reference, architecture mention, and nav update. Use the Task tool to launch the docs-writer agent.\\n </commentary>\\n\\n- Example 2:\\n user: \"The memory docs are outdated, can you update them?\"\\n assistant: \"I'll use the docs-writer agent to cross-reference the memory docs against the current source code and update them to reflect the actual API, configuration options, and behavior.\"\\n <commentary>\\n Stale docs need to be refreshed by reading the current source and making targeted updates. Use the Task tool to launch the docs-writer agent.\\n </commentary>\\n\\n- Example 3:\\n user: \"Can you add a Mermaid diagram showing the query flow?\"\\n assistant: \"I'll use the docs-writer agent to create a detailed Mermaid flowchart or sequence diagram illustrating the end-to-end query processing pipeline.\"\\n <commentary>\\n Architecture diagram request — the docs-writer agent knows how to write Mermaid diagrams that render correctly in MkDocs Material. Use the Task tool to launch it.\\n </commentary>\\n\\n- Example 4:\\n user: \"Write a getting started tutorial for new users\"\\n assistant: \"I'll use the docs-writer agent to create a step-by-step quickstart guide with installation instructions, first query examples, and progressively more advanced usage — using content tabs, admonitions, and annotated code blocks.\"\\n <commentary>\\n Tutorial writing with full MkDocs Material feature usage for a polished, professional result. Use the Task tool to launch the docs-writer agent.\\n </commentary>\\n\\n- Example 5:\\n user: \"Document the CLI commands\"\\n assistant: \"I'll use the docs-writer agent to read the CLI source code, extract all commands and options, and write a comprehensive CLI reference page with usage examples and annotated code blocks.\"\\n <commentary>\\n CLI documentation generated directly from source code inspection. Use the Task tool to launch the docs-writer agent.\\n </commentary>\\n\\n- Example 6:\\n user: \"Make the docs look more professional — add diagrams, better examples, etc.\"\\n assistant: \"I'll use the docs-writer agent to audit the existing docs and enhance them with Mermaid diagrams, admonitions, content tabs, annotated code blocks, and card grids where appropriate.\"\\n <commentary>\\n Quality improvement pass — upgrading plain markdown to rich MkDocs Material features. Use the Task tool to launch the docs-writer agent.\\n </commentary>"
model: sonnet
color: purple
---
You are an expert technical documentation writer specializing in MkDocs Material documentation sites for open-source academic research software. You produce publication-quality documentation that is clear enough for researchers to reproduce results, rich enough to be visually engaging, and accurate enough to serve as a trusted reference.
You have deep expertise in the full MkDocs Material feature set and write documentation that leverages these features to maximum effect. Your docs read like the best open-source project documentation (FastAPI, Pydantic, Typer) — clear, beautiful, and genuinely helpful.
You are working on the OpenJarvis project — a research framework for studying on-device AI systems. The project uses Python 3.10+, uv as package manager, hatchling build backend, and Click-based CLI. The core abstractions are Intelligence, Engine, Agentic Logic, Memory, with trace-driven learning as a cross-cutting concern.
### Your Core Responsibilities
#### 1. Read Source Code First, Then Write
- **Always** read the relevant source files before writing or updating any documentation
- Extract information from: module docstrings, class/function signatures, type hints, default values, Click decorators (for CLI), registry decorators, ABC interfaces
- Cross-reference multiple source files to understand how components interact
- Never guess or fabricate API details — if you can't find something in the source, say so
- For API reference pages using mkdocstrings, verify that the module paths are correct by checking actual file locations
#### 2. MkDocs Material Feature Mastery
Use the full MkDocs Material feature set appropriately. Here is your reference for each feature:
**Admonitions** — Use for warnings, tips, notes, and important callouts:
```markdown
!!! note "Title here"
Content indented by 4 spaces.
!!! warning "Breaking Change"
This API changed in v0.5.
!!! tip "Performance Tip"
Use batch mode for >100 queries.
!!! example "Example"
Here's how to use this feature.
??? info "Click to expand"
Collapsible admonition using ??? instead of !!!
???+ note "Expanded by default"
Use ???+ to start expanded.
```
Available types: `note`, `abstract`, `info`, `tip`, `success`, `question`, `warning`, `failure`, `danger`, `bug`, `example`, `quote`
**Content Tabs** — Use for alternative approaches, OS-specific instructions, or language variants:
```markdown
=== "pip"
```bash
pip install openjarvis
```
=== "uv"
```bash
uv add openjarvis
```
=== "From Source"
```bash
git clone https://github.com/jonsaadfalcon/OpenJarvis.git
cd OpenJarvis
uv sync --extra dev
```
```
**Code Blocks** — Always use language tags, titles, line highlighting, and annotations:
````markdown
```python title="basic_query.py" hl_lines="3 4"
from openjarvis import Jarvis
jarvis = Jarvis() # (1)!
response = jarvis.ask("What is quantum computing?") # (2)!
print(response)
```
1. :material-cog: Initializes with auto-detected hardware and default config
2. :material-lightning-bolt: Routes to the optimal model based on query complexity
````
Key code block features:
- `title="filename.py"` — adds a filename header
- `hl_lines="3 4"` — highlights specific lines
- `linenums="1"` — adds line numbers
- `# (1)!` — code annotation marker (the `!` strips the comment from display)
- Inline highlighting with `` `#!python some_code` `` for inline code with syntax colors
**Mermaid Diagrams** — Use for architecture, flow, sequence, class, and state diagrams:
````markdown
```mermaid
graph LR
A[User Query] --> B{Router}
B -->|Simple| C[Local Model]
B -->|Complex| D[Cloud API]
C --> E[Response]
D --> E
```
```mermaid
sequenceDiagram
participant U as User
participant J as Jarvis
participant R as Router
participant E as Engine
U->>J: query("explain transformers")
J->>R: classify(query)
R-->>J: model_selection
J->>E: generate(query, model)
E-->>J: response
J-->>U: Response object
```
```mermaid
classDiagram
class InferenceEngine {
<<abstract>>
+generate(prompt, model) Response
+list_models() list
+health_check() bool
}
InferenceEngine <|-- OllamaEngine
InferenceEngine <|-- LlamaCppEngine
InferenceEngine <|-- VLLMEngine
```
````
Supported diagram types for Material theme styling: flowchart, sequence, class, state, and entity-relationship. Others (pie, gantt, git) work but don't get theme-matched colors.
**Grids and Cards** — Use for feature overviews, landing pages, and navigation:
```markdown
<div class="grid cards" markdown>
- :material-lightning-bolt:{ .lg .middle } **Fast Inference**
---
Run models locally with optimized backends for your hardware
[:octicons-arrow-right-24: Learn more](user-guide/engines.md)
- :material-brain:{ .lg .middle } **Smart Routing**
---
Automatically route queries to the best model based on complexity
[:octicons-arrow-right-24: Learn more](architecture/intelligence.md)
</div>
```
**Other Features to Use**:
- **Keys extension**: ++ctrl+c++ for keyboard shortcuts
- **Critic markup**: {--deleted--} {++inserted++} {~~old~>new~~} for showing changes
- **Abbreviations**: Define in `docs/includes/abbreviations.md`, auto-tooltips throughout site
- **Data tables**: Standard markdown tables with sortable columns
- **Footnotes**: `[^1]` for academic-style references
- **Icons/Emojis**: `:material-icon-name:` from Material Design Icons, `:fontawesome-brands-python:` from Font Awesome
#### 3. Documentation Structure & Content Standards
**Page Structure** — Every docs page should follow this pattern:
1. **Title** (`# Page Title`) — clear, descriptive
2. **Intro paragraph** — 2-3 sentences explaining what this page covers and why it matters
3. **Prerequisites/Requirements** (if applicable) — as an admonition
4. **Main content** — organized with `##` and `###` headers
5. **Examples** — real, runnable code examples (not pseudocode)
6. **See Also / Next Steps** — links to related pages
**Writing Style**:
- Write in second person ("you can configure...") for guides/tutorials
- Write in third person ("the router selects...") for architecture/reference docs
- Use active voice
- Keep paragraphs short (3-5 sentences max)
- Lead with the most common use case, then cover edge cases
- Every code example should be complete enough to actually run
- Include expected output where helpful
**API Reference Pages** — Use mkdocstrings directives:
```markdown
## Jarvis
::: openjarvis.sdk.Jarvis
options:
show_source: true
members_order: source
show_root_heading: true
heading_level: 3
```
For API pages, add brief prose introductions before each mkdocstrings block explaining what the class/module does and when you'd use it. Don't just dump auto-generated API docs — contextualize them.
#### 4. Diagram Guidelines for Research Software
For academic research projects, diagrams are especially important:
- **Architecture overviews**: Use flowcharts showing component relationships
- **Data flow**: Use sequence diagrams for request/response flows
- **Class hierarchies**: Use class diagrams for ABC inheritance trees
- **State machines**: Use state diagrams for lifecycle management (agents, connections)
- **Decision logic**: Use flowcharts for routing/selection algorithms
Keep diagrams:
- Focused (one concept per diagram, not everything at once)
- Labeled clearly (no single-letter node names except in simple examples)
- Consistent in style across the docs site
- Accompanied by prose explanation (diagram alone is not documentation)
#### 5. Cross-Referencing and Navigation
- Always update `mkdocs.yml` nav when adding new pages
- Use relative links between docs pages: `[memory backends](../architecture/memory.md)`
- Link to API reference from user guides: "See the [`Jarvis`](../api/sdk.md#openjarvis.sdk.Jarvis) class reference"
- Link to user guides from API reference: "For usage examples, see the [Python SDK guide](../user-guide/python-sdk.md)"
- Add "See Also" sections at the bottom of pages pointing to related content
#### 6. Verification
After writing or updating docs:
- Verify all mkdocstrings module paths exist (e.g., `openjarvis.sdk.Jarvis` is a real importable path)
- Verify all internal links point to actual files
- Verify code examples are syntactically correct and use actual APIs from the source code
- Check that `mkdocs.yml` nav section includes all new pages
- Suggest running `uv run mkdocs build --strict` to catch broken references
### Execution Protocol
When invoked to write or update documentation:
1. **Understand the scope**: What pages need to be created/updated? What source files are relevant?
2. **Read source code**: Read all relevant source files to understand the actual APIs, behavior, and architecture. Read existing docs pages that might need cross-referencing.
3. **Read mkdocs.yml**: Understand the current site structure, enabled extensions, and navigation.
4. **Read existing docs**: If updating, read the current page to understand what needs to change vs. what's fine.
5. **Write content**: Create or update docs pages using the full MkDocs Material feature set.
6. **Update navigation**: Add new pages to `mkdocs.yml` nav if needed.
7. **Cross-reference**: Add links to/from related pages.
8. **Report**: Summarize what was created/updated, and suggest running `uv run mkdocs build --strict` to verify.
### Important Guidelines
- **Source of truth is the code**: Never document features that don't exist. If a docstring says one thing and the code does another, document the actual behavior and flag the docstring discrepancy.
- **Don't over-document**: Not every internal helper function needs a docs page. Focus on public APIs, user-facing features, and architectural concepts.
- **Be conservative with updates**: When updating existing pages, make targeted edits. Don't rewrite pages that are mostly correct.
- **Use features purposefully**: Admonitions, tabs, and diagrams should clarify — not decorate. If a plain paragraph communicates just as well, use a plain paragraph.
- **Research-grade quality**: This is documentation for academic open-source software. It should be precise enough that another researcher can reproduce results and extend the work. Include parameter types, default values, and behavioral edge cases.
- **Internal files are reference only**: Files like CLAUDE.md, VISION.md, ROADMAP.md, and NOTES.md are internal project files. Use them as context while writing, but never mention, cite, or link them in published documentation.
- **Keep abbreviations updated**: If you introduce new acronyms, add them to `docs/includes/abbreviations.md`.
- **Mermaid compatibility**: Use only flowchart, sequence, class, state, and ER diagrams for full Material theme integration. Other diagram types work but won't get theme-matched colors.
- **Code annotation syntax**: Use `# (1)!` (with the `!`) to create annotations that strip the comment marker from the rendered output. The numbered list below the code block provides the annotation content.
- **Test your links**: Use relative paths from the current file's location. A page in `docs/user-guide/` linking to `docs/api/` should use `../api/sdk.md`.
### OpenJarvis-Specific Context
Key source directories to read for documentation:
- `src/openjarvis/sdk.py` — Python SDK (`Jarvis` class)
- `src/openjarvis/engine/` — Inference engine backends
- `src/openjarvis/memory/` — Memory backends
- `src/openjarvis/agents/` — Agent implementations
- `src/openjarvis/tools/` — Tool system
- `src/openjarvis/learning/` — Router policies
- `src/openjarvis/traces/` — Trace system
- `src/openjarvis/telemetry/` — Telemetry system
- `src/openjarvis/bench/` — Benchmarking framework
- `src/openjarvis/server/` — API server
- `src/openjarvis/core/` — Core types, config, registry, events
- `src/openjarvis/cli/` — CLI commands (Click-based)
Key CLI commands: `jarvis init`, `jarvis ask`, `jarvis serve`, `jarvis model`, `jarvis memory`, `jarvis telemetry`, `jarvis bench`
Package extras: `openjarvis[server]`, `openjarvis[inference-vllm]`, `openjarvis[memory-colbert]`, `openjarvis[openclaw]`
+185
View File
@@ -0,0 +1,185 @@
---
name: repo-guardian
description: "Use this agent when you need to perform any combination of: verifying repository health and consistency, reviewing code quality, generating or updating documentation, creating or auditing tests, or checking dependency and CI pipeline health for the repository. This is the go-to agent for maintaining the overall quality, correctness, and professionalism of the codebase.\\n\\nInvoke this agent when:\\n- Significant code changes have been made (new features, refactors, architecture changes)\\n- A pull request needs review or is being prepared\\n- The user asks to \"check repo health\", \"clean up\", \"verify tests\", \"review code\", \"update docs\", or \"check CI\"\\n- End of a development session to ensure everything is in good shape\\n- New files or modules have been added that may need tests, docs, or CI coverage\\n- Merging branches or preparing a release\\n- The user asks for a code review, documentation audit, test coverage check, or dependency update\\n- Periodically during long development sessions as a proactive quality gate\\n\\nExamples:\\n\\n- Example 1:\\n user: \"I just finished implementing the new trace export feature\"\\n assistant: \"Great, the trace export feature looks good. Let me run the repo-guardian agent to review the code quality, verify tests are passing and cover the new code, ensure docs are updated, and confirm the repo is in good shape.\"\\n <uses Task tool to launch repo-guardian agent>\\n Since a significant feature was completed, launch repo-guardian to do a full sweep: code review of the new feature, test verification and generation for uncovered paths, documentation updates, and general repo health.\\n\\n- Example 2:\\n user: \"Can you review this PR before I merge?\"\\n assistant: \"I'll use the repo-guardian agent to do a thorough review — code quality, test coverage, documentation accuracy, dependency health, and overall repo cleanliness.\"\\n <uses Task tool to launch repo-guardian agent>\\n PR review is a natural trigger for the full agent. Code review is primary but all other dimensions matter before merge.\\n\\n- Example 3:\\n user: \"I've been working on a bunch of stuff today, let's wrap up\"\\n assistant: \"Before we wrap up, let me run the repo-guardian agent to make sure everything is orderly — code quality, tests passing with coverage for new code, docs current, dependencies healthy, no stray files.\"\\n <uses Task tool to launch repo-guardian agent>\\n End-of-session checkpoint. Full sweep to leave the repo in clean state.\\n\\n- Example 4:\\n user: \"We changed the architecture for the learning module, can you update the vision doc?\"\\n assistant: \"I'll use the repo-guardian agent to review the architectural changes, update vision/execution docs, ensure READMEs and CLAUDE.md reflect the new architecture, and verify tests still pass.\"\\n <uses Task tool to launch repo-guardian agent>\\n Architecture change triggers doc alignment plus verification that nothing broke.\\n\\n- Example 5:\\n user: \"I added a new utility module but haven't written tests yet\"\\n assistant: \"I'll use the repo-guardian agent to review the new module's code quality, generate comprehensive tests for it, update documentation, and verify everything integrates cleanly.\"\\n <uses Task tool to launch repo-guardian agent>\\n New code without tests is a clear trigger for test generation plus code review.\\n\\n- Example 6:\\n user: \"Are our dependencies up to date? Anything we should bump?\"\\n assistant: \"I'll use the repo-guardian agent to audit all dependencies, check for outdated packages, security advisories, and verify CI pipelines are correctly configured.\"\\n <uses Task tool to launch repo-guardian agent>\\n Explicit dependency question triggers the dependency/CI audit dimension."
model: sonnet
color: green
---
You are an elite repository quality engineer and guardian for this open-source academic research project. You combine deep expertise in Python project maintenance, code review, test engineering, documentation standards, dependency management, CI/CD pipelines, and repository hygiene. Your mission is to keep this repository in exemplary condition — the kind of quality expected of top-tier open-source research software published alongside papers at venues like ICML, NeurIPS, and ICLR.
### Your Core Responsibilities
#### 1. Code Review
- Review changed or newly added files for:
- **Correctness**: Logic errors, off-by-one bugs, race conditions, unhandled edge cases, incorrect API usage
- **Design quality**: Adherence to project patterns (registry pattern, ABC interfaces, Click CLI conventions), proper separation of concerns, appropriate abstraction levels
- **Readability**: Clear naming, appropriate comments (not excessive, not absent), logical code organization
- **Performance**: Unnecessary copies, O(n²) where O(n) suffices, missing caching opportunities, inefficient I/O patterns
- **Security**: Hardcoded secrets, unsafe deserialization, path traversal, SQL injection (if applicable), unsafe eval/exec
- **Type safety**: Proper type hints, consistent use of Optional vs None unions, generic types where appropriate
- Flag issues by severity: 🔴 must-fix, 🟡 should-fix, 🔵 nit/suggestion
- When reviewing, consider the broader context: does this change integrate well with the existing architecture?
- Suggest concrete improvements with code examples, not just problem descriptions
#### 2. Unit Test Health & Test Generation
- **Audit existing tests**:
- Run the full test suite with `uv run pytest tests/ -v` and analyze results
- Verify all tests pass (note: skipped tests for optional deps are expected and acceptable)
- Check for test files that import modules that no longer exist or have been renamed
- Verify test naming conventions follow the project pattern: `test_*.py` files in `tests/` with descriptive test function names
- Flag any tests that are silently skipped without proper `@pytest.mark.skipif` decorators and documented reasons
- If tests fail, diagnose whether it's a code issue, a missing dependency, or an environment issue
- **Generate new tests**:
- Identify source files and functions lacking test coverage
- Write comprehensive tests that cover: happy paths, edge cases, error conditions, boundary values, and type variations
- Follow the project's existing test patterns and conventions (fixtures, parametrize usage, assertion style)
- Include docstrings on test functions explaining what behavior is being verified
- Ensure tests are deterministic — no flaky tests depending on timing, network, or random state
- For complex modules, create both unit tests (isolated with mocks) and integration tests (testing component interaction)
- Aim for meaningful coverage, not just line coverage — test the interesting logic paths
#### 3. Repository Cleanliness — Stray Files
- Scan the repository root and key directories for files that don't belong:
- Log files (`*.log`, `.out`), temporary files (`.tmp`, `*.bak`, `*.swp`, `*~`)
- Python artifacts not in `.gitignore` (`__pycache__`, `*.pyc`, `*.pyo`, `.eggs/`, `*.egg-info/`)
- Database files that shouldn't be committed (`*.db`, `*.sqlite` unless they're test fixtures)
- OS-specific files (`.DS_Store`, `Thumbs.db`, `desktop.ini`)
- IDE/editor artifacts (`.idea/`, `.vscode/` settings that are user-specific, `*.code-workspace`)
- Build artifacts (`dist/`, `build/`, `*.whl`)
- Coverage/profiling output (`.coverage`, `htmlcov/`, `*.prof`)
- Jupyter checkpoints (`.ipynb_checkpoints/`)
- Report any orphaned or misplaced files with recommended actions (delete, move, or add to `.gitignore`)
#### 4. `.gitignore` Maintenance
- Review `.gitignore` for completeness against common Python project patterns
- Ensure it covers: Python bytecode, virtual environments (`venv/`, `.venv/`, `env/`), build artifacts, IDE files, OS files, test/coverage output, database files, log files, `uv` cache
- Check if any tracked files should actually be gitignored
- Check if any gitignored patterns are overly broad and might exclude files that should be tracked
- Suggest additions if new tool configurations or build artifacts have been introduced
#### 5. Documentation — CLAUDE.md, READMEs, Docstrings & API Docs
- **CLAUDE.md and Session Notes**:
- Verify CLAUDE.md accurately reflects the current project state: status, phase, CLI commands, architecture, registries, ABCs, key classes, development phases, SDK examples, build/dev commands
- Check for session notes files and verify they are being maintained if present
- Flag discrepancies between CLAUDE.md documentation and actual codebase state
- When updating, make precise edits — don't rewrite sections unnecessarily
- **README accuracy**:
- Verify installation instructions actually work
- Feature lists match implemented functionality
- Example code would actually run
- Badge/status indicators are current
- Links aren't broken
- Version numbers match `pyproject.toml`
- **Docstrings and API documentation**:
- Check that all public modules, classes, and functions have docstrings
- Verify docstrings follow a consistent format (Google style, NumPy style, or whatever the project uses)
- Ensure parameter descriptions match actual function signatures
- Flag functions with complex logic but no docstring
- For research code: verify that docstrings reference relevant papers, equations, or algorithms where appropriate
- Generate or update docstrings for undocumented code
- **Auto-generated docs**: If the project uses Sphinx, MkDocs, or similar, verify the docs build cleanly and reflect the current API
#### 6. Vision/Execution Document Alignment
- Review any vision documents, roadmaps, or execution plans in the repository
- Cross-reference claimed features/milestones against actual implementation
- Identify features listed as "done" that aren't actually implemented
- Identify implemented features not yet documented in vision/execution docs
- When updating these docs, make surgical edits that maintain the document's voice and structure
- Preserve aspirational/future items but clearly distinguish them from completed work
#### 7. Dependency & CI Pipeline Health
- **Dependency audit**:
- Review `pyproject.toml` (or `requirements.txt`, `setup.py`) for:
- Outdated packages that have newer stable releases
- Pinned versions that are unnecessarily restrictive
- Unpinned versions that could cause reproducibility issues
- Unused dependencies still listed
- Missing dependencies that are imported but not declared
- Dev dependencies properly separated from runtime dependencies
- Check for known security vulnerabilities in dependencies (using `pip-audit` or similar if available)
- Verify lock files (if used) are in sync with dependency specifications
- **CI pipeline health**:
- Review GitHub Actions workflows (or equivalent CI config) for:
- All jobs passing on the default branch
- Test matrix covering appropriate Python versions
- Linting/formatting checks included (ruff, mypy, etc.)
- Build/publish steps configured correctly
- Caching configured for dependencies to speed up CI
- Secrets properly managed (not hardcoded)
- Check that CI runs the same checks a developer would run locally
- Verify CI catches the same issues that local linting and testing would catch
- Suggest missing CI steps: type checking, security scanning, doc building, release automation
- **Lint check**: Run `uv run ruff check src/ tests/` and report any issues
### Execution Protocol
When invoked, perform these steps in order:
1. **Orientation**: Quickly read `CLAUDE.md`, `pyproject.toml`, and scan the directory structure to understand current project state.
2. **Test Suite Check**: Run `uv run pytest tests/ -v` and capture results. Summarize pass/fail/skip counts. If failures exist, provide clear diagnosis.
3. **Lint Check**: Run `uv run ruff check src/ tests/` and report any issues.
4. **Code Review** (if new/changed files are in scope): Review for correctness, design, readability, performance, security, and type safety.
5. **Test Coverage Audit**: Identify source files lacking test coverage. If gaps exist, generate tests or flag for generation.
6. **File Scan**: Walk the repository tree looking for stray/misplaced files using `find` commands or directory listings.
7. **Gitignore Audit**: Read `.gitignore` and compare against best practices and actual repo contents.
8. **Documentation Review**: Read CLAUDE.md, README.md, and any vision/execution docs. Cross-reference key claims against the actual codebase. Check docstring coverage on public APIs.
9. **Dependency & CI Audit**: Review `pyproject.toml` for dependency health. Review `.github/workflows/` for CI pipeline completeness and correctness.
10. **Report**: Produce a structured report covering all dimensions.
### Reporting Format
Structure your report as:
```
## Repository Guardian Report
### Test Suite
[Status, pass/fail/skip counts, any failures with diagnosis]
### Lint
[Status and any issues found]
### Code Review
[Issues found by severity: 🔴 must-fix, 🟡 should-fix, 🔵 nit]
### Test Coverage & Generation
[Coverage gaps identified, tests generated or recommended]
### Repository Cleanliness
[Stray files found, recommended actions]
### .gitignore
[Status, any additions needed]
### Documentation (CLAUDE.md, READMEs, Docstrings)
[Accuracy check results, updates needed]
### Vision/Execution Docs
[Alignment status, discrepancies found]
### Dependencies & CI
[Outdated deps, security issues, CI pipeline status, recommended improvements]
### Summary
[Overall health score and prioritized action items]
```
### Important Guidelines
- **Be precise**: Don't say "some tests might be failing" — run them and report exactly what happened.
- **Be actionable**: Every issue you flag should come with a specific recommended fix.
- **Be conservative with changes**: When updating docs, make minimal targeted edits. Don't rewrite what's working.
- **Respect the project's patterns**: This project uses `uv` as package manager, `hatchling` build backend, Click-based CLI, registry pattern with decorators, ABC interfaces. Recommendations should align with these patterns.
- **Know what's expected**: Skipped tests for optional dependencies are normal. Don't flag these as issues.
- **Prioritize**: 🔴 Critical test failures and security issues > 🟡 Code quality and stale documentation > 🔵 Minor cleanliness and style issues. Report in priority order.
- **Offer to fix**: After reporting, ask if the user wants you to fix any of the identified issues, and if so, make the changes directly.
- **Generate, don't just flag**: When test coverage is lacking, write the tests. When docstrings are missing, write them. When CI is incomplete, draft the workflow. Be a doer, not just an auditor.
- **Track cumulative state**: If you notice the same issue recurring across sessions, flag it prominently as a recurring problem.
- **Research-grade quality**: This is academic open-source software. Documentation should be clear enough for other researchers to reproduce results. Tests should validate scientific correctness, not just software correctness. Code should be publication-ready.
+132
View File
@@ -0,0 +1,132 @@
---
name: repo-health-guardian
description: "Use this agent when you need to verify the overall health, orderliness, and consistency of the OpenJarvis repository. This includes checking that unit tests are properly specified and passing, that no stray log files or temporary artifacts are cluttering the repo, that .gitignore is comprehensive, that CLAUDE.md and session notes reflect the current state of the project, that READMEs are accurate and up to date, and that vision/execution documents align with the actual codebase. This agent should be invoked proactively after significant code changes, periodically during development sessions, when merging branches, or when the user asks to \"check repo health\", \"clean up\", \"verify tests\", or \"update docs\".\\n\\nExamples:\\n\\n- Example 1:\\n user: \"I just finished implementing the new trace export feature\"\\n assistant: \"Great, the trace export feature looks good. Let me now use the repo-health-guardian agent to verify that tests are passing, docs are updated, and the repo is in good shape after these changes.\"\\n <commentary>\\n Since a significant piece of functionality was just completed, use the Task tool to launch the repo-health-guardian agent to run a comprehensive health check — verify tests pass, check for stray files, ensure CLAUDE.md and READMEs reflect the new feature.\\n </commentary>\\n\\n- Example 2:\\n user: \"Can you make sure everything is clean and up to date?\"\\n assistant: \"I'll use the repo-health-guardian agent to perform a full repository health audit.\"\\n <commentary>\\n The user is explicitly asking for a repo cleanliness and currency check. Use the Task tool to launch the repo-health-guardian agent.\\n </commentary>\\n\\n- Example 3:\\n user: \"I've been working on a bunch of stuff today, let's wrap up\"\\n assistant: \"Before we wrap up, let me run the repo-health-guardian agent to make sure everything is orderly — tests passing, no stray files, docs current.\"\\n <commentary>\\n End-of-session is a natural checkpoint. Use the Task tool to launch the repo-health-guardian agent to ensure the repo is in a clean state before the session ends.\\n </commentary>\\n\\n- Example 4:\\n user: \"We changed the architecture for the learning module, can you update the vision doc?\"\\n assistant: \"I'll use the repo-health-guardian agent to review the vision/execution docs against the current codebase and update them to reflect the new learning module architecture.\"\\n <commentary>\\n The user is asking to update vision/execution docs. Use the Task tool to launch the repo-health-guardian agent which handles doc alignment.\\n </commentary>\\n\\n- Example 5:\\n Context: The assistant just added several new source files and a new CLI subcommand.\\n assistant: \"Now that I've added the new files and subcommand, let me run the repo-health-guardian agent to make sure tests cover the new code, .gitignore doesn't need updates, and CLAUDE.md reflects the new subcommand.\"\\n <commentary>\\n Proactively launching the repo-health-guardian agent after structural changes to catch any gaps in test coverage, documentation, or .gitignore.\\n </commentary>"
model: sonnet
color: red
---
You are a meticulous repository health engineer and quality assurance specialist for the OpenJarvis project. You have deep expertise in Python project maintenance, test infrastructure, documentation standards, and repository hygiene. Your mission is to keep this repository in exemplary condition across six critical dimensions.
## Your Core Responsibilities
### 1. Unit Test Health
- Run the full test suite with `uv run pytest tests/ -v` and analyze results
- Verify that all ~576+ tests pass (note: 8 skipped tests for optional deps are expected and acceptable)
- Check for newly added source files that lack corresponding test coverage
- Look for test files that import modules that no longer exist or have been renamed
- Verify test naming conventions follow the project pattern: `test_*.py` files in `tests/` with descriptive test function names
- Flag any tests that are silently skipped without proper `@pytest.mark.skipif` decorators and documented reasons
- If tests fail, diagnose whether it's a code issue, a missing dependency, or an environment issue, and report clearly
### 2. Repository Cleanliness — Stray Files
- Scan the repository root and key directories for files that don't belong:
- Log files (*.log, *.out), temporary files (*.tmp, *.bak, *.swp, *~)
- Python artifacts not in .gitignore (__pycache__, *.pyc, *.pyo, .eggs/, *.egg-info/)
- Database files that shouldn't be committed (*.db, *.sqlite unless they're test fixtures)
- OS-specific files (.DS_Store, Thumbs.db, desktop.ini)
- IDE/editor artifacts (.idea/, .vscode/ settings that are user-specific, *.code-workspace)
- Build artifacts (dist/, build/, *.whl)
- Coverage/profiling output (.coverage, htmlcov/, *.prof)
- Jupyter checkpoints (.ipynb_checkpoints/)
- Report any orphaned or misplaced files with recommended actions (delete, move, or add to .gitignore)
### 3. .gitignore Maintenance
- Review `.gitignore` for completeness against common Python project patterns
- Ensure it covers: Python bytecode, virtual environments (venv/, .venv/, env/), build artifacts, IDE files, OS files, test/coverage output, database files, log files, uv cache
- Check if any tracked files should actually be gitignored
- Check if any gitignored patterns are overly broad and might exclude files that should be tracked
- Suggest additions if new tool configurations or build artifacts have been introduced
### 4. CLAUDE.md and Session Notes
- Verify CLAUDE.md accurately reflects the current state of the project:
- Project status and current phase (Phase 6 in progress)
- All CLI commands listed actually work
- Architecture section matches actual directory structure and module organization
- All registries, ABCs, and key classes mentioned actually exist in the codebase
- Development phases table is current
- Python SDK examples are accurate
- Build/dev commands are correct (especially `uv sync --extra dev`, `uv run pytest`, etc.)
- Check for session notes files and verify they are being maintained if present
- Flag any discrepancies between CLAUDE.md documentation and actual codebase state
- If asked to update, make precise edits — don't rewrite sections unnecessarily
### 5. README Accuracy
- Check that README.md (and any sub-package READMEs) accurately describes:
- Installation instructions that actually work
- Feature lists that match implemented functionality
- Example code that would actually run
- Badge/status indicators that are current
- Links that aren't broken
- Version numbers that match pyproject.toml
- Flag outdated sections and propose specific updates
### 6. Vision/Execution Document Alignment
- Review any vision documents, roadmaps, or execution plans in the repository
- Cross-reference claimed features/milestones against actual implementation
- Identify features listed as "done" that aren't actually implemented
- Identify implemented features not yet documented in vision/execution docs
- When asked to update these docs, make surgical edits that maintain the document's voice and structure
- Preserve aspirational/future items but clearly distinguish them from completed work
## Execution Protocol
When invoked, perform these steps in order:
1. **Test Suite Check**: Run `uv run pytest tests/ -v` and capture results. Summarize pass/fail/skip counts. If failures exist, provide clear diagnosis.
2. **Lint Check**: Run `uv run ruff check src/ tests/` and report any issues.
3. **File Scan**: Walk the repository tree looking for stray/misplaced files. Use `find` commands or directory listings to be thorough.
4. **Gitignore Audit**: Read `.gitignore` and compare against best practices and actual repo contents.
5. **Documentation Review**: Read CLAUDE.md, README.md, and any vision/execution docs. Cross-reference key claims against the actual codebase structure.
6. **Report**: Produce a structured report with:
- ✅ Items that are in good shape
- ⚠️ Items that need attention (with specific recommended actions)
- ❌ Items that are broken or critically out of date (with specific fixes)
## Reporting Format
Structure your report as:
```
## Repository Health Report
### Test Suite
[Status and details]
### Lint
[Status and details]
### Repository Cleanliness
[Status and details]
### .gitignore
[Status and details]
### CLAUDE.md & Session Notes
[Status and details]
### READMEs
[Status and details]
### Vision/Execution Docs
[Status and details]
### Summary
[Overall health score and priority actions]
```
## Important Guidelines
- **Be precise**: Don't say "some tests might be failing" — run them and report exactly what happened.
- **Be actionable**: Every issue you flag should come with a specific recommended fix.
- **Be conservative with changes**: When updating docs, make minimal targeted edits. Don't rewrite what's working.
- **Respect the project's patterns**: This project uses `uv` as package manager, `hatchling` build backend, Click-based CLI, registry pattern with decorators, ABC interfaces. Recommendations should align with these patterns.
- **Know what's expected**: 8 skipped tests for optional dependencies is normal. Don't flag these as issues.
- **Prioritize**: Critical test failures > stale documentation > minor cleanliness issues. Report in priority order.
- **Offer to fix**: After reporting, ask if the user wants you to fix any of the identified issues, and if so, make the changes directly.
- **When updating CLAUDE.md**: Ensure the project status, phase, test count, and architecture sections match reality. Update command examples if CLI has changed.
- **Track cumulative state**: If you notice the same issue recurring across sessions, flag it prominently as a recurring problem.
+9 -7
View File
@@ -4,13 +4,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Status
OpenJarvis is a research framework for studying on-device AI systems. Phase 7 (5-pillar restructuring) complete. Five composable pillars: Intelligence, Engine, Agents, Tools (with storage + MCP), and Learning — with trace-driven learning as a cross-cutting concern. Python SDK (`Jarvis` class), composition layer (`SystemBuilder`/`JarvisSystem`), OpenClaw agent infrastructure, benchmarking framework, Docker deployment all ready. ~1391 tests pass (32 skipped for optional deps).
OpenJarvis is a research framework for studying on-device AI systems. Phase 9 (Pillar-aligned config) complete. Five composable pillars: Intelligence, Engine, Agents, Tools (with storage + MCP), and Learning — with trace-driven learning as a cross-cutting concern. Python SDK (`Jarvis` class), composition layer (`SystemBuilder`/`JarvisSystem`), OpenClaw agent infrastructure, benchmarking framework, Docker deployment all ready. ~1780 tests pass (37 skipped for optional deps).
## Build & Development Commands
```bash
uv sync --extra dev # Install deps + dev tools
uv run pytest tests/ -v # Run ~1391 tests (32 skipped if optional deps missing)
uv run pytest tests/ -v # Run ~1780 tests (37 skipped if optional deps missing)
uv run ruff check src/ tests/ # Lint
uv run jarvis --version # 1.0.0
uv run jarvis ask "Hello" # Query via discovered engine (direct mode)
@@ -73,7 +73,7 @@ OpenJarvis is a research framework for on-device AI organized around **five comp
### Five Pillars
1. **Intelligence** (`src/openjarvis/intelligence/`) — The local LM. Model management and query routing. `ModelRegistry` maps model keys to `ModelSpec`. `RouterPolicy` ABC and `QueryAnalyzer` ABC defined in `intelligence/_stubs.py`. `HeuristicRouter` selects model based on query characteristics. `RoutingContext` lives in `core/types.py`.
1. **Intelligence** (`src/openjarvis/intelligence/`) — The model definition, catalog, and generation defaults. `ModelRegistry` maps model keys to `ModelSpec`. `IntelligenceConfig` holds model identity (default/fallback model, model_path, checkpoint_path, quantization, preferred_engine, provider) and generation defaults (temperature, max_tokens, top_p, top_k, repetition_penalty, stop_sequences). Model catalog (`model_catalog.py`) maintains `BUILTIN_MODELS` list with auto-discovery via `merge_discovered_models()`. Backward-compat shims in `intelligence/_stubs.py` and `intelligence/router.py` re-export from `learning/` for old import paths.
2. **Engine** (`src/openjarvis/engine/`) — The inference runtime. Backends: vLLM, SGLang, Ollama, llama.cpp, MLX. All implement `InferenceEngine` ABC with `generate()`, `stream()`, `list_models()`, `health()`. Engines extract and pass through `tool_calls` in OpenAI format.
3. **Agents** (`src/openjarvis/agents/`) — Pluggable logic for handling queries, making tool/API calls, managing memory. `SimpleAgent` (single-turn, no tools), `OrchestratorAgent` (multi-turn tool-calling loop with `ToolExecutor`), `ReActAgent` (Thought-Action-Observation loop), `OpenHandsAgent` (CodeAct-style), `CustomAgent` (template for user-defined agents), `OpenClawAgent` (HTTP/subprocess transport). All implement `BaseAgent` ABC with `run()`. Agents call `engine.generate()` directly — telemetry is handled by the `InstrumentedEngine` wrapper when enabled.
4. **Tools** (`src/openjarvis/tools/`) — All tools managed via MCP (Model Context Protocol).
@@ -83,7 +83,7 @@ OpenJarvis is a research framework for on-device AI organized around **five comp
- **Storage backends** (`tools/storage/`): SQLite/FTS5 (default), FAISS, ColBERTv2, BM25, Hybrid (RRF fusion). All implement `MemoryBackend` ABC with `store()`, `retrieve()`, `delete()`, `clear()`. Canonical import: `from openjarvis.tools.storage.sqlite import SQLiteMemory`. Backward-compat shims in `memory/` still work.
- **MCP adapter** (`tools/mcp_adapter.py`): `MCPToolAdapter` wraps external MCP server tools as native `BaseTool` instances. `MCPToolProvider` discovers tools from an MCP server.
- **MCP server** (`mcp/server.py`): Exposes all built-in tools via JSON-RPC `tools/list` + `tools/call` (MCP spec 2025-11-25). Any MCP client (Claude, GPT, etc.) can discover and use OpenJarvis tools.
5. **Learning** (`src/openjarvis/learning/`) — Structured learning system with per-pillar policies. `LearningPolicy` ABC taxonomy: `IntelligenceLearningPolicy` (updates model routing), `AgentLearningPolicy` (updates agent logic). Implementations: `SFTRouterPolicy` (learns query→model mapping from traces; backward-compat alias `SFTPolicy`), `AgentAdvisorPolicy` (LM-guided agent restructuring), `ICLUpdaterPolicy` (in-context example + skill discovery, registered as `AgentLearningPolicy`). Router policies: `HeuristicRouter` (registered as "heuristic"), `TraceDrivenPolicy` (registered as "learned"), `GRPORouterPolicy` (stub, registered as "grpo"). `HeuristicRewardFunction` scores inference results on latency/cost/efficiency. Orchestrator training subpackage (`learning/orchestrator/`) provides SFT and GRPO pipelines for structured-mode OrchestratorAgent training.
5. **Learning** (`src/openjarvis/learning/`) — Structured learning system with nested per-pillar sub-policies. `LearningConfig` has nested sections: `routing` (policy selection: heuristic/learned/grpo), `intelligence` (model updates: none/sft), `agent` (agent logic: none/agent_advisor/icl_updater), `metrics` (reward weights: accuracy/latency/cost/efficiency). `RouterPolicy` ABC and `QueryAnalyzer` ABC defined in `learning/_stubs.py`. `HeuristicRouter` and `build_routing_context()` in `learning/router.py`. `LearningPolicy` ABC taxonomy: `IntelligenceLearningPolicy` (updates model routing), `AgentLearningPolicy` (updates agent logic). Implementations: `SFTRouterPolicy` (learns query→model mapping from traces; backward-compat alias `SFTPolicy`), `AgentAdvisorPolicy` (LM-guided agent restructuring), `ICLUpdaterPolicy` (in-context example + skill discovery, registered as `AgentLearningPolicy`). Router policies: `HeuristicRouter` (registered as "heuristic"), `TraceDrivenPolicy` (registered as "learned"), `GRPORouterPolicy` (stub, registered as "grpo"). `HeuristicRewardFunction` scores inference results on latency/cost/efficiency. Orchestrator training subpackage (`learning/orchestrator/`) provides SFT and GRPO pipelines for structured-mode OrchestratorAgent training.
### Cross-cutting: Traces
@@ -168,7 +168,7 @@ OpenJarvis is a research framework for on-device AI organized around **five comp
- `registry.py``RegistryBase[T]` generic base class adapted from IPW. Typed subclasses: `ModelRegistry`, `EngineRegistry`, `MemoryRegistry`, `AgentRegistry`, `ToolRegistry`, `RouterPolicyRegistry`, `BenchmarkRegistry`, `ChannelRegistry`, `LearningRegistry`.
- `types.py``Message`, `Conversation`, `ModelSpec`, `ToolResult`, `TelemetryRecord`, `StepType`, `TraceStep`, `Trace`, `RoutingContext`.
- `config.py``JarvisConfig` dataclass hierarchy with TOML loader. Config classes: `EngineConfig`, `IntelligenceConfig`, `AgentConfig`, `ToolsConfig` (nests `StorageConfig` + `MCPConfig`), `LearningConfig` (default_policy, intelligence_policy, agent_policy, tools_policy, update_interval, reward_weights), `TracesConfig` (enabled, db_path), `TelemetryConfig`, `ServerConfig`, `ChannelConfig`, `SecurityConfig`. `JarvisConfig.memory` property provides backward-compat access to `tools.storage`. User config lives at `~/.openjarvis/config.toml`. TOML sections: `[engine]`, `[intelligence]`, `[learning]`, `[tools.storage]`, `[tools.mcp]`, `[memory]` (backward-compat), `[agent]`, `[server]`, `[telemetry]`, `[traces]`, `[channel]`, `[security]`.
- `config.py``JarvisConfig` dataclass hierarchy with TOML loader. Config classes: `EngineConfig` (nested `OllamaEngineConfig`, `VLLMEngineConfig`, `SGLangEngineConfig`, `LlamaCppEngineConfig`), `IntelligenceConfig` (model identity + generation defaults: temperature, max_tokens, top_p, top_k, repetition_penalty, stop_sequences), `AgentConfig` (default_agent, tools, objective, system_prompt, system_prompt_path, context_from_memory), `ToolsConfig` (nests `StorageConfig` + `MCPConfig`), `LearningConfig` (nested `RoutingLearningConfig`, `IntelligenceLearningConfig`, `AgentLearningConfig`, `MetricsConfig`), `TracesConfig`, `TelemetryConfig`, `ServerConfig`, `ChannelConfig`, `SecurityConfig`. Backward-compat properties: `engine.ollama_host``engine.ollama.host`, `agent.default_tools``agent.tools`, `learning.default_policy``learning.routing.policy`. TOML migration layer handles cross-section moves (`agent.temperature``intelligence.temperature`, `memory.context_injection``agent.context_from_memory`). User config lives at `~/.openjarvis/config.toml`. TOML sections: `[engine]`, `[engine.ollama]`, `[engine.vllm]`, `[engine.sglang]`, `[engine.llamacpp]`, `[intelligence]`, `[agent]`, `[tools.storage]`, `[tools.mcp]`, `[learning]`, `[learning.routing]`, `[learning.intelligence]`, `[learning.agent]`, `[learning.metrics]`, `[memory]` (backward-compat), `[server]`, `[telemetry]`, `[traces]`, `[channel]`, `[security]`.
- `events.py` — Pub/sub event bus for inter-pillar telemetry (synchronous dispatch). EventType values: INFERENCE_START/END, TOOL_CALL_START/END, MEMORY_STORE/RETRIEVE, AGENT_TURN_START/END, TELEMETRY_RECORD, TRACE_STEP/COMPLETE, CHANNEL_MESSAGE_RECEIVED/SENT, SECURITY_SCAN/ALERT/BLOCK.
### Docker & Deployment
@@ -181,7 +181,7 @@ OpenJarvis is a research framework for on-device AI organized around **five comp
### Query Flow
User query &rarr; Security scanning (input) &rarr; Agentic Logic (determine tools/memory needs) &rarr; Memory retrieval &rarr; Context injection with source attribution &rarr; Learning/Router selects model (via RouterPolicyRegistry, heuristic or trace-driven) &rarr; Inference Engine generates response &rarr; Security scanning (output) &rarr; Trace recorded to SQLite (full interaction sequence) &rarr; Telemetry recorded &rarr; Learning policies update from accumulated traces.
User query &rarr; Security scanning (input) &rarr; Intelligence resolves model (default_model → preferred_engine → fallback chain) &rarr; Agentic Logic (determine tools/memory needs) &rarr; Memory retrieval &rarr; Context injection with source attribution &rarr; Inference Engine generates response &rarr; Security scanning (output) &rarr; Trace recorded to SQLite (full interaction sequence) &rarr; Telemetry recorded &rarr; Learning policies update from accumulated traces.
### API Surface
@@ -194,7 +194,7 @@ OpenAI-compatible server via `jarvis serve`: `POST /v1/chat/completions`, `GET /
- **Offline-first:** Cloud APIs are optional. All core functionality works without network.
- **Hardware-aware:** Auto-detect GPU vendor/model/VRAM via `nvidia-smi`, `rocm-smi`, `system_profiler`, `/proc/cpuinfo`. Recommend engine accordingly.
- **Telemetry opt-in:** When enabled, `InstrumentedEngine` wraps the inference engine to transparently record timing, tokens, energy, cost to SQLite via event bus. Agents call `engine.generate()` without awareness of telemetry. `TelemetryAggregator` provides read-only query/aggregation over stored records.
- **Backward-compat shims:** `memory/` re-exports from `tools/storage/`, `learning/_stubs.py` re-exports `RouterPolicy` from `intelligence/_stubs.py` and `RoutingContext` from `core/types.py`. Old import paths continue to work.
- **Backward-compat shims:** `memory/` re-exports from `tools/storage/`, `intelligence/_stubs.py` re-exports `RouterPolicy`/`QueryAnalyzer` from `learning/_stubs.py`, `intelligence/router.py` re-exports `HeuristicRouter`/`build_routing_context`/`DefaultQueryAnalyzer` from `learning/router.py`. Old import paths continue to work. Config backward-compat: `engine.ollama_host``engine.ollama.host`, `agent.default_tools``agent.tools`, `learning.default_policy``learning.routing.policy`, etc. TOML migration: `agent.temperature``intelligence.temperature`, `memory.context_injection``agent.context_from_memory`.
- **`ensure_registered()` pattern:** Benchmark and learning modules use lazy registration via `ensure_registered()` to survive registry clearing in tests.
## Development Phases
@@ -209,3 +209,5 @@ OpenAI-compatible server via `jarvis serve`: `POST /v1/chat/completions`, `GET /
| v1.0 | Phase 5 | SDK, OpenClaw infrastructure, benchmarks, Docker, documentation |
| v1.1 | Phase 6 | Trace system, trace-driven learning, pluggable agent architectures |
| v1.2 | Phase 7 | 5-pillar restructuring: Intelligence ABCs, memory→tools/storage, MCP tool management, composition layer (SystemBuilder/JarvisSystem), InstrumentedEngine, structured learning (SFT/AgentAdvisor/ICL), config schema update |
| v1.3 | Phase 8 | Intelligence = "The Model": routing moved to Learning, enriched IntelligenceConfig (model_path, checkpoint_path, quantization, preferred_engine, provider), intelligence-driven engine selection, backward-compat shims |
| v1.4 | Phase 9 | Pillar-aligned config: generation params in Intelligence, nested engine/learning configs, agent objective/system_prompt/context_from_memory, structured learning sub-policies (routing/intelligence/agent/metrics), TOML migration layer |
+1153
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -22,4 +22,5 @@ classes, concrete implementations, and utility functions.
| [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 |
| [Evals](evals.md) | Evaluation framework: backends, dataset providers, scorers, and suite runner |
| [Server](server.md) | FastAPI application, OpenAI-compatible routes, and Pydantic models |
+26 -14
View File
@@ -234,27 +234,39 @@ Key behaviors:
## Configuration
Engine hosts and defaults are configured in `~/.openjarvis/config.toml`:
Engine hosts and defaults are configured in `~/.openjarvis/config.toml` using **nested per-engine sub-sections**:
```toml
[engine]
default = "ollama"
ollama_host = "http://localhost:11434"
vllm_host = "http://localhost:8000"
llamacpp_host = "http://localhost:8080"
sglang_host = "http://localhost:30000"
[engine.ollama]
host = "http://localhost:11434"
[engine.vllm]
host = "http://localhost:8000"
[engine.sglang]
host = "http://localhost:30000"
# [engine.llamacpp]
# host = "http://localhost:8080"
# binary_path = ""
```
The `EngineConfig` dataclass maps these settings:
The `EngineConfig` dataclass and its per-engine sub-dataclasses map 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) |
| Config Class | Field | Default | Description |
|---|---|---|---|
| `EngineConfig` | `default` | `"ollama"` (hardware-dependent) | Preferred engine backend |
| `OllamaEngineConfig` | `host` | `http://localhost:11434` | Ollama server URL |
| `VLLMEngineConfig` | `host` | `http://localhost:8000` | vLLM server URL |
| `SGLangEngineConfig` | `host` | `http://localhost:30000` | SGLang server URL |
| `LlamaCppEngineConfig` | `host` | `http://localhost:8080` | llama.cpp server URL |
| `LlamaCppEngineConfig` | `binary_path` | `""` | Path to llama.cpp binary (for managed mode) |
!!! note "Backward compatibility"
The old flat field names `ollama_host`, `vllm_host`, `llamacpp_host`, `llamacpp_path`, and `sglang_host` under `[engine]` are still accepted as backward-compatible properties on `EngineConfig`. New configurations should use the nested sub-section format.
---
+125 -88
View File
@@ -1,18 +1,21 @@
# 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.
The Intelligence pillar represents **the model** — its identity, weights, quantization format, fallback chain, and the catalog of well-known models with detailed metadata. It no longer contains routing logic; query analysis and model selection have moved to the [Learning pillar](learning.md).
---
## 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 Intelligence pillar answers a single question: *what is the model?* It maintains a catalog of known models with metadata (parameter count, context length, VRAM requirements, supported engines) and provides helpers for registering built-in models and merging models discovered from running engines at runtime.
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
2. **Auto-discovery** -- merging models discovered from running engines into the catalog
3. **Model configuration** -- `IntelligenceConfig` captures the local model's identity, weight paths, quantization, and preferred engine
!!! info "Routing has moved"
Query analysis (`build_routing_context`) and model selection (`HeuristicRouter`, `RouterPolicy` ABC) now live in the [Learning pillar](learning.md). Backward-compatible re-exports remain in `intelligence/_stubs.py` and `intelligence/router.py` so existing code continues to work.
---
@@ -118,104 +121,138 @@ For each model ID not already in the registry, a `ModelSpec` is created with the
---
## HeuristicRouter
## IntelligenceConfig
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
The `IntelligenceConfig` dataclass (in `core/config.py`) captures the full identity of the model the system is configured to use, as well as the default sampling parameters for generation:
```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")
```
---
## RouterPolicy and QueryAnalyzer ABCs
The Intelligence pillar now defines the `RouterPolicy` and `QueryAnalyzer` ABCs in `intelligence/_stubs.py`:
```python
# intelligence/_stubs.py
class RouterPolicy(ABC):
@abstractmethod
def select_model(self, context: RoutingContext) -> str:
"""Return the model registry key best suited for *context*."""
class QueryAnalyzer(ABC):
@abstractmethod
def analyze(self, query: str) -> RoutingContext:
"""Analyze a raw query string and return a RoutingContext."""
```
The `HeuristicRouter` implements both of these ABCs.
---
## build_routing_context()
The `build_routing_context()` function analyzes a raw query string and produces a `RoutingContext` dataclass (now defined in `core/types.py`):
```python
# core/types.py
@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)
class IntelligenceConfig:
"""The model — identity, paths, quantization, fallback chain, and generation defaults."""
default_model: str = "" # Primary model key (e.g., "qwen3:8b")
fallback_model: str = "" # Fallback when default is unavailable
model_path: str = "" # Local weights (HF repo, GGUF file, etc.)
checkpoint_path: str = "" # Checkpoint/adapter path (e.g., LoRA)
quantization: str = "none" # none, fp8, int8, int4, gguf_q4, gguf_q8
preferred_engine: str = "" # Override engine for this model (e.g., "vllm")
provider: str = "" # local, openai, anthropic, google
# Generation defaults (overridable per-call)
temperature: float = 0.7
max_tokens: int = 1024
top_p: float = 0.9
top_k: int = 40
repetition_penalty: float = 1.0
stop_sequences: str = "" # Comma-separated stop strings
```
**Code detection** uses regex patterns matching:
### Model Identity Fields
- Backtick code blocks (`` ``` `` or `` `inline` ``)
- Language keywords (`def`, `class`, `import`, `function`, `const`, `var`, `let`)
- Syntax patterns (`if (`, `->`, `=>`, `{ }`, `for x in`, `#include`, `System.out`)
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `default_model` | `str` | `""` | Primary model registry key. Resolved at startup; overrides any engine default. |
| `fallback_model` | `str` | `""` | Used when the default model is not available on any running engine. |
| `model_path` | `str` | `""` | Path or HuggingFace repo ID for local weights (e.g., `"./models/qwen3-8b.gguf"` or `"Qwen/Qwen3-8B"`). |
| `checkpoint_path` | `str` | `""` | Path to a fine-tuned checkpoint or LoRA adapter directory. |
| `quantization` | `str` | `"none"` | Quantization format. Accepted values: `none`, `fp8`, `int8`, `int4`, `gguf_q4`, `gguf_q8`. |
| `preferred_engine` | `str` | `""` | When set, `SystemBuilder`, `sdk.py`, and `cli/ask.py` use this engine key instead of `config.engine.default`. |
| `provider` | `str` | `""` | Model provider hint: `local`, `openai`, `anthropic`, `google`. Used by the Cloud engine backend to route API calls. |
**Math detection** uses regex patterns matching:
### Generation Default Fields
- Mathematical terms (`solve`, `integral`, `equation`, `proof`, `derivative`, `matrix`)
- Computational keywords (`calculate`, `compute`, `sigma`, `sum`, `limit`, `probability`)
These fields set the default sampling parameters for every inference call. Individual calls can override them by passing keyword arguments to `engine.generate()`.
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `temperature` | `float` | `0.7` | Sampling temperature. Lower values produce more deterministic output; higher values increase diversity. |
| `max_tokens` | `int` | `1024` | Maximum number of tokens to generate per call. |
| `top_p` | `float` | `0.9` | Nucleus sampling probability mass. At each step, only tokens comprising the top-p probability mass are considered. |
| `top_k` | `int` | `40` | Top-k sampling: only consider the top-k most likely tokens at each step. |
| `repetition_penalty` | `float` | `1.0` | Penalize repeated token sequences. Values greater than 1.0 reduce repetition. |
| `stop_sequences` | `str` | `""` | Comma-separated stop strings. Generation halts when any stop string appears in the output. |
!!! note "Moved from Agent"
Generation parameters (`temperature`, `max_tokens`) previously lived under `[agent]` in the config file. They now live under `[intelligence]`. Old configs with these fields under `[agent]` are automatically migrated at load time. See the [configuration migration guide](../getting-started/configuration.md#migration-guide) for details.
### TOML Configuration
```toml
[intelligence]
default_model = "qwen3:8b"
fallback_model = "llama3.2:3b"
temperature = 0.7
max_tokens = 1024
# top_p = 0.9
# top_k = 40
# repetition_penalty = 1.0
# stop_sequences = ""
# Local weight overrides (optional)
# model_path = "./models/qwen3-8b-instruct.gguf"
# checkpoint_path = "./checkpoints/my-lora"
# quantization = "gguf_q4"
# Engine selection for this model (takes priority over [engine].default)
# preferred_engine = "vllm"
# Provider for cloud models
# provider = "openai"
```
### Engine Selection Priority
When resolving which engine to use, `SystemBuilder`, `sdk.py`, and `cli/ask.py` check `config.intelligence.preferred_engine` before `config.engine.default`:
```
1. Explicit --engine CLI flag or engine_key= SDK parameter
2. config.intelligence.preferred_engine ← new field
3. config.engine.default
4. First healthy engine discovered at runtime
```
This lets you pin a specific model to a specific engine without changing the global engine default. For example, a GGUF quantized model can be pinned to `llamacpp` while the global default remains `ollama`:
```toml
[engine]
default = "ollama"
[intelligence]
default_model = "llama3.2:3b"
model_path = "./models/llama-3.2-3b.Q4_K_M.gguf"
quantization = "gguf_q4"
preferred_engine = "llamacpp"
```
---
## Public API
`intelligence/__init__.py` exports exactly three names:
```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
from openjarvis.intelligence import (
BUILTIN_MODELS, # List[ModelSpec] — the full built-in catalog
merge_discovered_models, # (engine_key, model_ids) -> None
register_builtin_models, # () -> None
)
```
### Backward-Compatibility Shims
The following names are still importable from `openjarvis.intelligence` via shim modules, but their canonical locations have moved:
| Name | Old location | Canonical location |
|------|-------------|-------------------|
| `RouterPolicy` | `intelligence/_stubs.py` | `learning/_stubs.py` |
| `QueryAnalyzer` | `intelligence/_stubs.py` | `learning/_stubs.py` |
| `HeuristicRouter` | `intelligence/router.py` | `learning/router.py` |
| `build_routing_context` | `intelligence/router.py` | `learning/router.py` |
| `DefaultQueryAnalyzer` | `intelligence/router.py` | `learning/router.py` |
New code should import from the canonical `learning.*` locations. The shims in `intelligence/_stubs.py` and `intelligence/router.py` are retained for backward compatibility only.
---
## Integration with Learning
The `HeuristicRouter` implements the `RouterPolicy` ABC (now defined in `intelligence/_stubs.py`), which means it can be swapped out for a `TraceDrivenPolicy`, `SFTRouterPolicy`, or any other policy via the `RouterPolicyRegistry`. See the [Learning & Traces](learning.md) documentation for details on how trace-driven routing and the broader `LearningPolicy` taxonomy work.
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.
The Learning pillar consumes the model catalog to make routing decisions. The `HeuristicRouter` and `TraceDrivenPolicy` both read `ModelRegistry` to compare model sizes when selecting between candidates. See the [Learning & Traces](learning.md) documentation for full details on routing policies, the `RouterPolicy` ABC, and the trace-driven feedback loop.
+63 -7
View File
@@ -17,10 +17,10 @@ All learning policies are registered in the `LearningRegistry` (in `core/registr
## RouterPolicy ABC
The `RouterPolicy` ABC and the `QueryAnalyzer` ABC are defined in `intelligence/_stubs.py`:
The `RouterPolicy` ABC and the `QueryAnalyzer` ABC are defined in `learning/_stubs.py`:
```python
# intelligence/_stubs.py
# learning/_stubs.py
class RouterPolicy(ABC):
@abstractmethod
def select_model(self, context: RoutingContext) -> str:
@@ -33,7 +33,7 @@ class QueryAnalyzer(ABC):
```
!!! note "Backward compatibility"
The `RouterPolicy` and `RoutingContext` names are still importable from `openjarvis.learning._stubs` via backward-compatibility re-exports, but the canonical locations are now `openjarvis.intelligence._stubs` (for `RouterPolicy` and `QueryAnalyzer`) and `openjarvis.core.types` (for `RoutingContext`).
The canonical locations are now `openjarvis.learning._stubs` (for `RouterPolicy` and `QueryAnalyzer`) and `openjarvis.core.types` (for `RoutingContext`). The old `openjarvis.intelligence._stubs` import path still works via a backward-compatibility shim, but new code should import from `openjarvis.learning._stubs`.
### RoutingContext
@@ -77,8 +77,8 @@ And these additional learning policies (registered in `LearningRegistry`):
Users select a policy via `config.toml` or the `--router` CLI flag:
```toml
[learning]
default_policy = "heuristic"
[learning.routing]
policy = "heuristic"
```
```bash
@@ -104,9 +104,65 @@ This ensures that policies are available even after `RouterPolicyRegistry.clear(
## 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 `HeuristicRouter` is the default routing policy. It is defined in `learning/router.py` and applies six static priority rules to select the best model based on query characteristics.
The `heuristic_policy.py` module wires the existing `HeuristicRouter` (from the Intelligence pillar) into the `RouterPolicyRegistry`:
### 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 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.learning.router 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 (in `learning/router.py`) analyzes a raw query string and produces a `RoutingContext` dataclass:
```python
from openjarvis.learning.router 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
```
**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`)
### Registration
The `heuristic_policy.py` module wires `HeuristicRouter` into the `RouterPolicyRegistry`:
```python
# learning/heuristic_policy.py
+8 -3
View File
@@ -335,16 +335,21 @@ class MyMemoryBackend(MemoryBackend):
def clear(self) -> None: ...
```
The default backend is configured in `~/.openjarvis/config.toml`:
The default backend is configured in `~/.openjarvis/config.toml`. Storage settings live under `[tools.storage]`, and context injection is controlled by `agent.context_from_memory`:
```toml
[memory]
[agent]
context_from_memory = true
[tools.storage]
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
```
!!! note "Backward compatibility"
The `[memory]` TOML section is still accepted as a backward-compatible alias for `[tools.storage]`. The old `context_injection` field is automatically migrated to `agent.context_from_memory` at load time.
+16 -14
View File
@@ -9,7 +9,7 @@ OpenJarvis is a research framework for studying on-device AI systems. Its archit
```mermaid
graph TB
subgraph "Core Pillars"
INT["Intelligence<br/><i>Model management<br/>& query routing</i>"]
INT["Intelligence<br/><i>Model definition<br/>& catalog</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>"]
@@ -28,7 +28,6 @@ graph TB
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
@@ -51,31 +50,33 @@ graph TB
### 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.
The Intelligence pillar handles **model definition and catalog**. It maintains a catalog of known models (`BUILTIN_MODELS`) with metadata such as parameter count, context length, VRAM requirements, and supported engines. The `IntelligenceConfig` captures the full identity of the configured model — its weight path, quantization format, preferred engine, fallback chain, and generation defaults (`temperature`, `max_tokens`, `top_p`, `top_k`, `repetition_penalty`, `stop_sequences`).
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.
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. Query routing has moved to the Learning pillar — see the [Learning & Traces](learning.md) documentation.
### 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).
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.
Each engine is configured via its own sub-section in `config.toml` (e.g., `[engine.ollama]`, `[engine.vllm]`, `[engine.llamacpp]`). 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")`.
Agent behavior is configured through `[agent]` in `config.toml`, including the default agent, turn limits, tool list, optional system prompt, and the `context_from_memory` flag (previously `context_injection`) that controls automatic memory context injection. 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 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). Storage backends are configured under `[tools.storage]` in `config.toml` (the `[memory]` section is still accepted as a backward-compatible alias).
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.
The memory pipeline includes document ingestion, chunking, embedding generation, and context injection. When a user sends a query and `agent.context_from_memory` is enabled, 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 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 learning system is configured through nested sub-sections in `config.toml`: `[learning.routing]` controls the router policy (heuristic, learned, sft, grpo), `[learning.intelligence]` controls the model-level learning policy, `[learning.agent]` controls agent advisor and ICL updater policies, and `[learning.metrics]` sets the composite reward function weights.
---
@@ -134,10 +135,10 @@ src/openjarvis/
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()
intelligence/ Intelligence pillar -- model definition & catalog
model_catalog.py BUILTIN_MODELS list, merge_discovered_models()
_stubs.py (Shared with learning -- RoutingContext)
_stubs.py (backward-compat shim -- re-exports from learning._stubs)
router.py (backward-compat shim -- re-exports from learning.router)
engine/ Engine pillar -- inference runtime backends
_stubs.py InferenceEngine ABC
@@ -173,7 +174,8 @@ src/openjarvis/
embeddings.py Embedder ABC, SentenceTransformerEmbedder
learning/ Learning system -- router policies & rewards
_stubs.py RouterPolicy ABC, RewardFunction ABC, RoutingContext
_stubs.py RouterPolicy ABC, QueryAnalyzer ABC, RewardFunction ABC, RoutingContext
router.py HeuristicRouter, DefaultQueryAnalyzer, build_routing_context()
heuristic_policy.py Wires HeuristicRouter into RouterPolicyRegistry
trace_policy.py TraceDrivenPolicy (learns from trace outcomes)
grpo_policy.py GRPORouterPolicy (stub for future RL)
+6 -5
View File
@@ -11,7 +11,7 @@ sequenceDiagram
actor User
participant CLI as CLI / SDK
participant CFG as Config & Discovery
participant RTR as Router Policy
participant LRN as Learning (Router)
participant AGT as Agent
participant MEM as Memory Backend
participant CTX as Context Injection
@@ -30,8 +30,8 @@ sequenceDiagram
CFG-->>CLI: available models per engine
alt Model not specified
CLI->>RTR: select_model(RoutingContext)
RTR-->>CLI: model_key (e.g., "qwen3:8b")
CLI->>LRN: select_model(RoutingContext)
LRN-->>CLI: model_key (e.g., "qwen3:8b")
end
alt Agent mode (--agent flag)
@@ -150,9 +150,10 @@ If no model was explicitly specified, the router policy selects one:
```python
from openjarvis.learning import ensure_registered
from openjarvis.learning.router import build_routing_context
ensure_registered() # Ensure learning policies are registered
policy_key = router_policy or config.learning.default_policy
policy_key = router_policy or config.learning.routing.policy
router_cls = RouterPolicyRegistry.get(policy_key)
router = router_cls(
available_models=all_models.get(engine_name, []),
@@ -164,7 +165,7 @@ 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.
The `build_routing_context()` function (in `learning/router.py`) 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
+6 -6
View File
@@ -711,7 +711,7 @@ class BenchmarkResult:
## Adding a New Router Policy
Router policies determine which model handles a given query. All policies
implement the `RouterPolicy` ABC from `intelligence/_stubs.py`. The
implement the `RouterPolicy` ABC from `learning/_stubs.py`. The
`RoutingContext` dataclass is defined in `core/types.py`.
### Complete Example
@@ -727,7 +727,7 @@ from typing import List, Optional
from openjarvis.core.registry import RouterPolicyRegistry
from openjarvis.core.types import RoutingContext
from openjarvis.intelligence._stubs import RouterPolicy
from openjarvis.learning._stubs import RouterPolicy
class QueryLengthPolicy(RouterPolicy):
@@ -797,8 +797,8 @@ Once registered, your policy can be selected via the config file or CLI:
=== "Config (TOML)"
```toml
[learning]
default_policy = "query_length"
[learning.routing]
policy = "query_length"
```
=== "CLI"
@@ -823,7 +823,7 @@ class RoutingContext:
metadata: Dict[str, Any] = field(default_factory=dict)
```
The `build_routing_context()` helper in `intelligence/router.py` populates
The `build_routing_context()` helper in `learning/router.py` populates
this from a raw query string, detecting code and math patterns automatically.
---
@@ -837,7 +837,7 @@ this from a raw query string, detecting code and math patterns automatically.
| Agent | `BaseAgent` | `AgentRegistry` | `agents/_stubs.py` |
| Tool | `BaseTool` | `ToolRegistry` | `tools/_stubs.py` |
| Benchmark | `BaseBenchmark` | `BenchmarkRegistry` | `bench/_stubs.py` |
| Router Policy | `RouterPolicy` | `RouterPolicyRegistry` | `intelligence/_stubs.py` |
| Router Policy | `RouterPolicy` | `RouterPolicyRegistry` | `learning/_stubs.py` |
| Learning Policy | `LearningPolicy` | `LearningRegistry` | `learning/_stubs.py` |
The general pattern for all extension points:
+484 -200
View File
@@ -5,7 +5,7 @@ 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.
OpenJarvis uses a TOML configuration file to control engine selection, model identity, memory backends, agent behavior, and more. This page is the complete reference for every configuration option, organized by pillar.
## Config File Location
@@ -44,81 +44,245 @@ jarvis init --force
## Configuration Sections
The config file is organized into TOML sections. Every field has a default value, so you only need to specify the values you want to change.
The config file is organized into TOML sections corresponding to the five pillars. Every field has a default value, so you only need to specify values you want to change.
---
### `[engine]` -- Inference Engine
### `[engine]` Inference Engine
Controls which inference engine is used and where each engine is listening.
Controls which inference engine is used and how each engine is reached. Engine settings are now **nested** under per-engine sub-sections instead of flat fields.
```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"
[engine.ollama]
host = "http://localhost:11434"
[engine.vllm]
host = "http://localhost:8000"
[engine.sglang]
host = "http://localhost:30000"
# [engine.llamacpp]
# host = "http://localhost:8080"
# binary_path = ""
```
**`[engine]` top-level:**
| 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. |
**`[engine.ollama]`:**
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `host` | string | `http://localhost:11434` | Base URL for the Ollama API server. |
**`[engine.vllm]`:**
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `host` | string | `http://localhost:8000` | Base URL for the vLLM OpenAI-compatible server. |
**`[engine.sglang]`:**
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `host` | string | `http://localhost:30000` | Base URL for the SGLang server. |
**`[engine.llamacpp]`:**
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `host` | string | `http://localhost:8080` | Base URL for the llama.cpp HTTP server (`llama-server`). |
| `binary_path` | string | `""` | Path to the llama.cpp binary, if not on `$PATH`. |
!!! tip "Engine fallback"
If the configured default engine is unreachable, OpenJarvis automatically probes all registered engines and falls back to any healthy one.
!!! note "Backward compatibility"
The old flat field names (`ollama_host`, `vllm_host`, `llamacpp_host`, `llamacpp_path`, `sglang_host`) are still accepted as backward-compatible properties. New configurations should use the nested sub-section format.
---
### `[intelligence]` -- Model Routing
### `[intelligence]` Model Identity and Generation Defaults
Controls which model is selected by default and which model to fall back to.
Controls which model is used, its weight paths, quantization, and the default sampling parameters for generation. Generation parameters such as `temperature` and `max_tokens` now live here rather than under `[agent]`.
```toml
[intelligence]
default_model = ""
fallback_model = ""
# model_path = ""
# checkpoint_path = ""
# quantization = "none"
# preferred_engine = ""
# provider = ""
temperature = 0.7
max_tokens = 1024
# top_p = 0.9
# top_k = 40
# repetition_penalty = 1.0
# stop_sequences = ""
```
**Model identity fields:**
| 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. |
| `default_model` | string | `""` | Preferred model identifier (e.g., `qwen3:8b`). When empty, the router policy selects the model dynamically. |
| `fallback_model` | string | `""` | Model to use if the default is unavailable. |
| `model_path` | string | `""` | Path or HuggingFace repo ID for local weights (e.g., `"./models/qwen3-8b.gguf"` or `"Qwen/Qwen3-8B"`). |
| `checkpoint_path` | string | `""` | Path to a fine-tuned checkpoint or LoRA adapter directory. |
| `quantization` | string | `"none"` | Quantization format. Accepted values: `none`, `fp8`, `int8`, `int4`, `gguf_q4`, `gguf_q8`. |
| `preferred_engine` | string | `""` | Override engine for this model (e.g., `"vllm"`). Takes priority over `engine.default`. |
| `provider` | string | `""` | Model provider hint: `local`, `openai`, `anthropic`, `google`. Used by the Cloud engine to route API calls. |
When both fields are empty, OpenJarvis uses the configured router policy (see `[learning]`) to select a model from those available on the active engine.
**Generation default fields** (overridable per-call):
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `temperature` | float | `0.7` | Sampling temperature. Lower values produce more deterministic output. |
| `max_tokens` | int | `1024` | Maximum number of tokens to generate per call. |
| `top_p` | float | `0.9` | Nucleus sampling probability mass. |
| `top_k` | int | `40` | Top-k sampling: only consider the top-k tokens at each step. |
| `repetition_penalty` | float | `1.0` | Penalize repeated tokens. Values > 1 reduce repetition. |
| `stop_sequences` | string | `""` | Comma-separated stop strings. Generation halts when any stop string is produced. |
When both `default_model` and `fallback_model` are empty, OpenJarvis uses the configured router policy (see `[learning]`) to select a model from those available on the active engine.
### Engine Selection Priority
When resolving which engine to use for a model, `SystemBuilder`, `sdk.py`, and `cli/ask.py` check fields in this order:
```
1. Explicit --engine CLI flag or engine_key= SDK parameter
2. config.intelligence.preferred_engine
3. config.engine.default
4. First healthy engine discovered at runtime
```
This lets you pin a specific model to a specific engine without changing the global engine default:
```toml
[engine]
default = "ollama"
[intelligence]
default_model = "llama3.2:3b"
model_path = "./models/llama-3.2-3b.Q4_K_M.gguf"
quantization = "gguf_q4"
preferred_engine = "llamacpp"
```
---
### `[learning]` -- Learning Policies
### `[agent]` — Agent Behavior
Controls how the learning system selects models, advises agents, and tunes tool selection.
Controls the default agent, turn limits, tool selection, system prompt, and memory context injection.
```toml
[learning]
default_policy = "heuristic"
reward_weights = ""
intelligence_policy = "heuristic"
agent_policy = ""
tools_policy = ""
update_interval = 100
[agent]
default_agent = "simple"
max_turns = 10
# tools = ""
# objective = ""
# system_prompt = ""
# system_prompt_path = ""
context_from_memory = true
```
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `default_policy` | string | `"heuristic"` | Router policy to use for model selection. Available: `heuristic`, `learned` (trace-driven), `sft` (supervised fine-tuning), `grpo` (RL stub). |
| `reward_weights` | string | `""` | Comma-separated key=value pairs for the reward function. Example: `"latency=0.4,cost=0.3,quality=0.3"`. |
| `intelligence_policy` | string | `"heuristic"` | Intelligence learning policy (model routing). One of: `heuristic`, `learned`, `sft`, `grpo`. |
| `agent_policy` | string | `""` | Agent learning policy (agent behavior advice). Available: `agent_advisor`. Empty means no agent learning. |
| `tools_policy` | string | `""` | Tool learning policy (tool selection). Available: `icl_updater`. Empty means no tool learning. |
| `update_interval` | int | `100` | Number of traces between automatic policy updates. |
| `default_agent` | string | `"simple"` | Default agent to use. Available: `simple`, `orchestrator`, `react`, `custom`, `openclaw`. |
| `max_turns` | int | `10` | Maximum number of tool-calling turns for the orchestrator agent before it must produce a final answer. |
| `tools` | string | `""` | Comma-separated list of tools to enable by default (e.g., `"calculator,think"`). |
| `objective` | string | `""` | Concise purpose string for routing, learning, and documentation. |
| `system_prompt` | string | `""` | Inline system prompt. Takes precedence over `system_prompt_path` when set. |
| `system_prompt_path` | string | `""` | Path to a system prompt file (`.txt` or `.md`). |
| `context_from_memory` | bool | `true` | Whether to automatically inject relevant memory context into queries. |
**Router / Intelligence policies:**
!!! note "Generation parameters moved"
`temperature` and `max_tokens` have moved from `[agent]` to `[intelligence]`. Old configs with these fields under `[agent]` are automatically migrated to `[intelligence]` at load time.
!!! note "Backward compatibility"
The old field name `default_tools` is still accepted as a backward-compatible property for `tools`. New configurations should use `tools`.
!!! info "Context injection"
When `context_from_memory = true` 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.
---
### `[learning]` — Learning Policies
Controls whether the learning system is enabled and configures per-pillar policies through nested sub-sections.
```toml
[learning]
enabled = false
update_interval = 100
# auto_update = false
[learning.routing]
policy = "heuristic"
# min_samples = 5
# [learning.intelligence]
# policy = "none"
# [learning.agent]
# policy = "none"
# [learning.metrics]
# accuracy_weight = 0.6
# latency_weight = 0.2
# cost_weight = 0.1
# efficiency_weight = 0.1
```
**`[learning]` top-level:**
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | bool | `false` | Whether the learning system is active. |
| `update_interval` | int | `100` | Number of traces between automatic policy updates. |
| `auto_update` | bool | `false` | Whether to trigger policy updates automatically when the interval is reached. |
**`[learning.routing]` — Router policy:**
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `policy` | string | `"heuristic"` | Router policy for model selection. Available: `heuristic`, `learned` (trace-driven), `sft` (supervised fine-tuning), `grpo` (RL stub). |
| `min_samples` | int | `5` | Minimum number of traces required before trusting a learned routing decision. |
**`[learning.intelligence]` — Intelligence learning policy:**
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `policy` | string | `"none"` | Intelligence learning policy. Available: `none`, `sft`. Use `sft` to learn model routing from accumulated traces. |
**`[learning.agent]` — Agent learning policy:**
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `policy` | string | `"none"` | Agent learning policy. Available: `none`, `agent_advisor`, `icl_updater`. |
| `max_icl_examples` | int | `20` | Maximum number of in-context examples to maintain in the ICL example library. |
| `advisor_confidence_threshold` | float | `0.7` | Minimum confidence score for the advisor to recommend a strategy change. |
**`[learning.metrics]` — Reward / optimization metric weights:**
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `accuracy_weight` | float | `0.6` | Weight for outcome accuracy in the composite reward score. |
| `latency_weight` | float | `0.2` | Weight for inference latency in the composite reward score. |
| `cost_weight` | float | `0.1` | Weight for per-call cost in the composite reward score. |
| `efficiency_weight` | float | `0.1` | Weight for token efficiency in the composite reward score. |
**Router policies:**
| Policy | Description |
|--------|-------------|
@@ -132,12 +296,7 @@ update_interval = 100
| Policy | Description |
|--------|-------------|
| `agent_advisor` | Advises on agent strategy (tool sets, turn limits) based on trace patterns. |
**Tool policies:**
| Policy | Description |
|--------|-------------|
| `icl_updater` | In-context learning updater for tool selection and configuration. |
| `icl_updater` | In-context learning updater — discovers reusable ICL examples and multi-tool skill sequences from traces. |
You can also override the router policy per-query via the CLI:
@@ -145,17 +304,19 @@ You can also override the router policy per-query via the CLI:
jarvis ask --router heuristic "Hello"
```
!!! note "Backward compatibility"
The old flat field names `default_policy`, `intelligence_policy`, `agent_policy`, and the comma-separated `reward_weights` string are still accepted as backward-compatible properties. New configurations should use the nested sub-section format. The `tools_policy` field has been removed; use `learning.agent.policy = "icl_updater"` instead.
---
### `[memory]` -- Memory Backend
### `[tools.storage]` — Storage Backend
Controls the persistent memory system: which backend to use, how documents are chunked, and how context injection works.
Controls the storage backend used for document memory and context injection. The `context_injection` field has moved to `agent.context_from_memory`.
```toml
[memory]
[tools.storage]
default_backend = "sqlite"
db_path = "~/.openjarvis/memory.db"
context_injection = true
context_top_k = 5
context_min_score = 0.1
context_max_tokens = 2048
@@ -165,9 +326,8 @@ chunk_overlap = 64
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `default_backend` | string | `"sqlite"` | Memory backend to use. Available: `sqlite` (FTS5), `faiss`, `colbert`, `bm25`, `hybrid`. |
| `default_backend` | string | `"sqlite"` | Storage backend. 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. |
@@ -184,35 +344,29 @@ chunk_overlap = 64
| `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.
!!! note "Backward compatibility"
The `[memory]` TOML section is still supported and maps to `[tools.storage]`. New configurations should use `[tools.storage]`. The `context_injection` field under `[memory]` or `[tools.storage]` is automatically migrated to `agent.context_from_memory` at load time.
---
### `[agent]` -- Agent Defaults
### `[tools.mcp]` — MCP (Model Context Protocol)
Controls the default agent behavior for queries.
Controls the MCP server and external MCP tool provider integration. The MCP adapter supports protocol version 2025-11-25.
```toml
[agent]
default_agent = "simple"
max_turns = 10
default_tools = ""
temperature = 0.7
max_tokens = 1024
[tools.mcp]
enabled = true
# servers = "" # JSON list of external MCP server configs
```
| 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. |
| `enabled` | bool | `true` | Whether to enable the MCP adapter for exposing and consuming tools via MCP. |
| `servers` | string | `""` | JSON-encoded list of external MCP server configuration objects. |
---
### `[server]` -- API Server
### `[server]` API Server
Controls the OpenAI-compatible API server started by `jarvis serve`.
@@ -229,7 +383,7 @@ workers = 1
|-------|------|---------|-------------|
| `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. |
| `agent` | string | `"orchestrator"` | Agent to use for 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. |
@@ -241,7 +395,7 @@ jarvis serve --host 127.0.0.1 --port 9000 --model qwen3:8b --agent simple
---
### `[telemetry]` -- Telemetry Persistence
### `[telemetry]` Telemetry Persistence
Controls whether inference telemetry is recorded and where it is stored.
@@ -261,87 +415,58 @@ db_path = "~/.openjarvis/telemetry.db"
---
### `[traces]` -- Trace Recording
### `[traces]` Trace Recording
Controls the trace system that records full interaction sequences for the learning system.
```toml
[traces]
enabled = true
enabled = false
db_path = "~/.openjarvis/traces.db"
```
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | bool | `true` | Whether to record traces for each agent interaction. |
| `enabled` | bool | `false` | Whether to record traces for each agent interaction. |
| `db_path` | string | `~/.openjarvis/traces.db` | Path to the SQLite trace database. |
---
### `[tools.storage]` -- Storage Backend Configuration
### `[channel]` — Channel Messaging
Controls the storage backend used by memory/storage tools. This section configures the same backends previously managed under `[memory]`, but with the canonical tools-based naming.
```toml
[tools.storage]
default_backend = "sqlite"
db_path = "~/.openjarvis/memory.db"
```
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `default_backend` | string | `"sqlite"` | Storage backend to use. Available: `sqlite`, `faiss`, `colbert`, `bm25`, `hybrid`. |
| `db_path` | string | `~/.openjarvis/memory.db` | Path to the SQLite storage database. |
!!! note "Backward compatibility"
The `[memory]` TOML section is still supported and takes effect if `[tools.storage]` is not present. New configurations should prefer `[tools.storage]`.
---
### `[tools.mcp]` -- MCP (Model Context Protocol)
Controls the MCP server and external MCP tool provider integration. The MCP adapter supports protocol version 2025-11-25.
```toml
[tools.mcp]
enabled = false
protocol_version = "2025-11-25"
providers = []
```
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | bool | `false` | Whether to enable the MCP adapter for exposing and consuming tools via MCP. |
| `protocol_version` | string | `"2025-11-25"` | MCP protocol version to use. |
| `providers` | list | `[]` | List of external MCP tool provider URLs to connect to. |
---
### `[channel]` -- Channel Messaging
Controls the channel messaging bridge for multi-platform communication.
Controls the channel messaging bridge for multi-platform communication. Each supported platform has its own nested sub-section.
```toml
[channel]
enabled = false
gateway_url = "ws://127.0.0.1:18789/ws"
default_channel = ""
default_agent = "simple"
reconnect_interval = 5.0
# [channel.telegram]
# bot_token = ""
# [channel.discord]
# bot_token = ""
# [channel.slack]
# bot_token = ""
# app_token = ""
# [channel.webhook]
# url = ""
# secret = ""
# method = "POST"
```
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | bool | `false` | Whether to enable channel messaging support. |
| `gateway_url` | string | `ws://127.0.0.1:18789/ws` | WebSocket URL of the OpenClaw gateway. |
| `default_channel` | string | `""` | Default channel to use when not specified. |
| `default_agent` | string | `"simple"` | Default agent for handling channel messages. |
| `reconnect_interval` | float | `5.0` | Seconds to wait before reconnecting after a disconnect. |
!!! note "Requires OpenClaw gateway"
Channel messaging requires a running OpenClaw gateway. The bridge connects via WebSocket with HTTP fallback.
---
### `[security]` -- Security Guardrails
### `[security]` Security Guardrails
Controls the security scanning pipeline for input/output content.
@@ -379,19 +504,19 @@ When you run `jarvis init`, OpenJarvis probes your system to detect available ha
### GPU Detection
1. **NVIDIA GPU** -- Checks for `nvidia-smi` on `$PATH`. If found, queries GPU name, VRAM (in MB), and GPU count via:
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:
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.
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.
@@ -461,20 +586,23 @@ graph TD
[engine]
default = "ollama"
ollama_host = "http://localhost:11434"
[engine.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
temperature = 0.7
max_tokens = 1024
[agent]
default_agent = "simple"
max_turns = 10
context_from_memory = true
[tools.storage]
default_backend = "sqlite"
[server]
host = "127.0.0.1"
@@ -482,7 +610,10 @@ port = 8000
agent = "orchestrator"
[learning]
default_policy = "heuristic"
enabled = false
[learning.routing]
policy = "heuristic"
[telemetry]
enabled = true
@@ -496,29 +627,33 @@ enabled = true
[engine]
default = "vllm"
vllm_host = "http://localhost:8000"
ollama_host = "http://localhost:11434"
[engine.vllm]
host = "http://localhost:8000"
[engine.ollama]
host = "http://localhost:11434"
[intelligence]
default_model = "Qwen/Qwen2.5-72B-Instruct"
fallback_model = "Qwen/Qwen2.5-7B-Instruct"
temperature = 0.5
max_tokens = 4096
[memory]
[agent]
default_agent = "orchestrator"
max_turns = 15
tools = "calculator,think,retrieval"
context_from_memory = true
[tools.storage]
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
@@ -527,7 +662,10 @@ model = "Qwen/Qwen2.5-72B-Instruct"
workers = 1
[learning]
default_policy = "heuristic"
enabled = false
[learning.routing]
policy = "heuristic"
[telemetry]
enabled = true
@@ -541,88 +679,91 @@ enabled = true
[engine]
default = "llamacpp"
llamacpp_host = "http://localhost:8080"
[engine.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
temperature = 0.7
max_tokens = 512
[agent]
default_agent = "simple"
max_turns = 5
temperature = 0.7
max_tokens = 512
context_from_memory = true
[tools.storage]
default_backend = "sqlite"
context_top_k = 3
context_max_tokens = 1024
chunk_size = 256
chunk_overlap = 32
[server]
host = "127.0.0.1"
port = 8000
[learning]
default_policy = "heuristic"
enabled = false
[learning.routing]
policy = "heuristic"
[telemetry]
enabled = true
```
### Cloud-Only (No Local Engine)
### Trace-Driven Learning Enabled
```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
# Research setup with trace-driven learning active
[engine]
default = "ollama"
ollama_host = "http://localhost:11434"
[engine.ollama]
host = "http://localhost:11434"
[intelligence]
default_model = "qwen3:8b"
fallback_model = "gpt-4o-mini"
[memory]
default_backend = "sqlite"
context_injection = true
temperature = 0.7
max_tokens = 1024
[agent]
default_agent = "orchestrator"
max_turns = 10
default_tools = "calculator,think"
context_from_memory = true
[tools.storage]
default_backend = "sqlite"
[learning]
default_policy = "heuristic"
enabled = true
update_interval = 50
auto_update = true
[learning.routing]
policy = "learned"
min_samples = 10
[learning.intelligence]
policy = "sft"
[learning.agent]
policy = "agent_advisor"
advisor_confidence_threshold = 0.8
[learning.metrics]
accuracy_weight = 0.6
latency_weight = 0.2
cost_weight = 0.1
efficiency_weight = 0.1
[traces]
enabled = true
[telemetry]
enabled = true
@@ -630,9 +771,148 @@ enabled = true
---
## Migration Guide
If you have an existing `~/.openjarvis/config.toml` from a previous version, here is what changed and how to update it.
### Engine: Nested Sub-Sections
=== "Old Format"
```toml
[engine]
default = "ollama"
ollama_host = "http://localhost:11434"
vllm_host = "http://localhost:8000"
llamacpp_path = "/usr/local/bin/llama-server"
```
=== "New Format"
```toml
[engine]
default = "ollama"
[engine.ollama]
host = "http://localhost:11434"
[engine.vllm]
host = "http://localhost:8000"
[engine.llamacpp]
binary_path = "/usr/local/bin/llama-server"
```
!!! note
The old flat names still work as backward-compatible properties. You only need to update your config if you want to use the new fields (e.g., `binary_path`).
### Intelligence: Generation Parameters
=== "Old Format"
```toml
[agent]
temperature = 0.7
max_tokens = 1024
```
=== "New Format"
```toml
[intelligence]
temperature = 0.7
max_tokens = 1024
```
!!! note
Old configs with `temperature` or `max_tokens` under `[agent]` are automatically migrated to `[intelligence]` at load time. No manual update is required, but updating is recommended for clarity.
### Agent: Renamed and Added Fields
=== "Old Format"
```toml
[agent]
default_tools = "calculator,think"
```
=== "New Format"
```toml
[agent]
tools = "calculator,think"
```
The `default_tools` name still works via a backward-compatible property.
### Memory: Context Injection Moved
=== "Old Format"
```toml
[memory]
context_injection = true
default_backend = "sqlite"
```
=== "New Format"
```toml
[agent]
context_from_memory = true
[tools.storage]
default_backend = "sqlite"
```
!!! note
`context_injection` under `[memory]` or `[tools.storage]` is automatically migrated to `agent.context_from_memory` at load time.
### Learning: Nested Sub-Sections
=== "Old Format"
```toml
[learning]
default_policy = "heuristic"
intelligence_policy = "sft"
agent_policy = "agent_advisor"
tools_policy = "icl_updater"
reward_weights = "accuracy=0.6,latency=0.2,cost=0.1,efficiency=0.1"
update_interval = 100
```
=== "New Format"
```toml
[learning]
enabled = true
update_interval = 100
[learning.routing]
policy = "heuristic"
[learning.intelligence]
policy = "sft"
[learning.agent]
policy = "agent_advisor"
[learning.metrics]
accuracy_weight = 0.6
latency_weight = 0.2
cost_weight = 0.1
efficiency_weight = 0.1
```
!!! note
The flat field names `default_policy`, `intelligence_policy`, `agent_policy`, and `reward_weights` are still accepted as backward-compatible properties. The `tools_policy` field has been removed; use `learning.agent.policy = "icl_updater"` instead.
---
## Programmatic Configuration
You can also configure OpenJarvis entirely from Python without a TOML file:
You can configure OpenJarvis entirely from Python without a TOML file:
```python
from openjarvis import Jarvis
@@ -642,29 +922,31 @@ from openjarvis.core.config import (
IntelligenceConfig,
JarvisConfig,
LearningConfig,
MemoryConfig,
OllamaEngineConfig,
StorageConfig,
MCPConfig,
ToolsConfig,
TracesConfig,
)
config = JarvisConfig(
engine=EngineConfig(
default="ollama",
ollama_host="http://my-server:11434",
ollama=OllamaEngineConfig(host="http://my-server:11434"),
),
intelligence=IntelligenceConfig(
default_model="qwen3:8b",
),
memory=MemoryConfig(
default_backend="sqlite",
context_injection=True,
context_top_k=10,
temperature=0.7,
max_tokens=2048,
),
agent=AgentConfig(
default_agent="orchestrator",
max_turns=15,
context_from_memory=True,
),
tools=ToolsConfig(
storage=StorageConfig(
default_backend="sqlite",
context_top_k=10,
),
),
)
@@ -694,6 +976,8 @@ OpenJarvis respects the following environment variables:
## 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
- [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
- [Intelligence Pillar](../architecture/intelligence.md) — Model identity and generation defaults
- [Learning & Traces](../architecture/learning.md) — Router policies and the trace-driven feedback loop
+1 -1
View File
@@ -235,7 +235,7 @@ To disable this behavior:
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`.
Context injection is controlled by `agent.context_from_memory` in `config.toml`. The retrieval parameters (`context_top_k`, `context_min_score`, `context_max_tokens`) live under `[tools.storage]`. See [Configuration](configuration.md) for details.
## Model Management
+543
View File
@@ -0,0 +1,543 @@
# Evaluations
The OpenJarvis evaluation framework (`openjarvis-evals`) measures model **correctness and accuracy** on academic datasets. It is a separate package from the main OpenJarvis library and is designed specifically for research workflows where you need reproducible, dataset-driven quality assessments.
!!! info "Evals vs. Benchmarks"
OpenJarvis has two distinct measurement systems that complement each other:
| System | Package | Measures | Entry Point |
|--------|---------|----------|-------------|
| **Evaluations** | `openjarvis-evals` | Correctness on academic datasets (accuracy, pass rate) | `openjarvis-eval` |
| **Benchmarks** | `openjarvis` | Engine performance (latency, throughput) | `jarvis bench` |
Use evaluations to answer "does this model get the right answer?" and benchmarks to answer "how fast does this model respond?". See the [Benchmarks guide](benchmarks.md) for the performance measurement system.
---
## Installation
The evaluation framework is a standalone package in the `evals/` directory. Install it alongside OpenJarvis:
```bash
pip install -e evals/
```
This installs the `openjarvis-eval` CLI entry point and all required dependencies (`datasets`, `huggingface-hub`, `tqdm`, `rich`).
!!! note "Python version requirement"
Python 3.10 requires the `tomli` package for TOML config parsing. The `evals/pyproject.toml` includes this as a conditional dependency, so it is installed automatically.
---
## Datasets
The framework ships with four datasets covering different capability dimensions:
| Dataset | Key | Category | HuggingFace Path | Default Split |
|---------|-----|----------|------------------|---------------|
| **SuperGPQA** | `supergpqa` | reasoning | `m-a-p/SuperGPQA` | `train` |
| **GAIA** | `gaia` | agentic | `gaia-benchmark/GAIA` | `validation` |
| **FRAMES** | `frames` | rag | `google/frames-benchmark` | `test` |
| **WildChat** | `wildchat` | chat | `allenai/WildChat-1M` | `train` |
**SuperGPQA** is a large-scale multiple-choice benchmark spanning graduate-level questions across scientific disciplines. Each sample has a question, a set of lettered options, and a reference answer letter.
**GAIA** is an agentic benchmark requiring models to complete multi-step tasks that may involve file reading, calculations, and web lookup. Questions are drawn from the 2023 GAIA challenge set.
**FRAMES** tests multi-hop factual retrieval. Each question requires synthesizing information across multiple Wikipedia articles, making it a strong probe of retrieval-augmented generation capability.
**WildChat** uses real user conversations filtered to English single-turn exchanges. The reference answer is the original assistant response from the dataset; the model under evaluation is compared against it by an LLM judge.
!!! tip "GAIA dataset access"
The GAIA dataset requires a HuggingFace account and acceptance of the dataset's terms of use. The loader downloads the full dataset snapshot on first use and caches it at `~/.cache/gaia_benchmark/`. Subsequent runs use the local cache.
---
## Inference Backends
Every evaluation run routes model calls through one of two backends:
| Backend | Key | Description |
|---------|-----|-------------|
| **jarvis-direct** | `jarvis-direct` | Engine-level inference via `SystemBuilder`. Works for local (Ollama, vLLM, llama.cpp) and cloud models. |
| **jarvis-agent** | `jarvis-agent` | Agent-level inference with tool calling. Uses `JarvisSystem.ask()` with the specified agent and tools. |
Use `jarvis-direct` for most evaluations. Use `jarvis-agent` when the benchmark requires tool use — for example, GAIA tasks that reference files that must be read with `file_read`, or arithmetic tasks that benefit from `calculator`.
---
## CLI Usage
### List available benchmarks and backends
```bash
openjarvis-eval list
```
Output:
```
Benchmarks:
supergpqa [reasoning ] SuperGPQA multiple-choice
gaia [agentic ] GAIA agentic benchmark
frames [rag ] FRAMES multi-hop RAG
wildchat [chat ] WildChat conversation quality
Backends:
jarvis-direct Engine-level inference (local or cloud)
jarvis-agent Agent-level inference with tool calling
```
### Run a single benchmark
```bash
# Evaluate qwen3:8b on SuperGPQA (engine-level, 10 samples default)
openjarvis-eval run -b supergpqa -m qwen3:8b
# Evaluate GPT-4o on GAIA using the agent backend with tools
openjarvis-eval run -b gaia -m gpt-4o --backend jarvis-agent \
--agent orchestrator --tools calculator,file_read -n 50
# Run FRAMES with vLLM engine, write output to a file
openjarvis-eval run -b frames -m llama3:70b -e vllm \
-o results/frames_llama70b.jsonl
# Run WildChat with a higher temperature for chat quality
openjarvis-eval run -b wildchat -m qwen3:8b --temperature 0.7 -n 100
```
#### Full option reference
| Option | Short | Type | Default | Description |
|--------|-------|------|---------|-------------|
| `--config` | `-c` | path | — | TOML config file; when provided, `-b` and `-m` are not required |
| `--benchmark` | `-b` | choice | required* | `supergpqa`, `gaia`, `frames`, or `wildchat` |
| `--backend` | | choice | `jarvis-direct` | `jarvis-direct` or `jarvis-agent` |
| `--model` | `-m` | str | required* | Model identifier (e.g., `qwen3:8b`, `gpt-4o`) |
| `--engine` | `-e` | str | auto | Engine key (`ollama`, `vllm`, `cloud`, ...) |
| `--agent` | | str | `orchestrator` | Agent name for `jarvis-agent` backend |
| `--tools` | | str | `""` | Comma-separated tool names (e.g., `calculator,file_read`) |
| `--max-samples` | `-n` | int | all | Limit the number of samples evaluated |
| `--max-workers` | `-w` | int | `4` | Parallel evaluation workers |
| `--judge-model` | | str | `gpt-4o` | LLM used for judge-based scoring |
| `--output` | `-o` | path | auto-generated | Output JSONL file path |
| `--seed` | | int | `42` | Random seed for dataset shuffling |
| `--split` | | str | dataset default | Override the dataset split |
| `--temperature` | | float | `0.0` | Generation temperature |
| `--max-tokens` | | int | `2048` | Maximum output tokens |
| `--verbose` | `-v` | flag | off | Enable debug logging |
*Required when `--config` is not provided.
### Run all benchmarks at once
The `run-all` command evaluates a single model against all four benchmarks sequentially and writes results to an output directory:
```bash
openjarvis-eval run-all -m qwen3:8b
# With options
openjarvis-eval run-all -m gpt-4o -n 100 --output-dir results/gpt4o/
```
Output files are written as `{output_dir}/{benchmark}_{model-slug}.jsonl`. The model slug replaces `/` and `:` with `-`, so `qwen3:8b` becomes `qwen3-8b`.
### Summarize results
After a run, inspect a JSONL results file:
```bash
openjarvis-eval summarize results/supergpqa_qwen3-8b.jsonl
```
Output:
```
File: results/supergpqa_qwen3-8b.jsonl
Benchmark: supergpqa
Model: qwen3:8b
Total: 200
Scored: 198
Correct: 143
Accuracy: 0.7222
Errors: 2
```
---
## TOML Config System
For research workflows that compare multiple models across multiple benchmarks, use a TOML config file to define the evaluation as a **models x benchmarks matrix**. This is the recommended approach for systematic evaluations.
### Running from a config
```bash
openjarvis-eval run --config evals/configs/full-suite.toml
```
When `--config` is provided, the `-b`/`--benchmark` and `-m`/`--model` options are not required. All settings come from the config file. The CLI expands the matrix, prints a progress table, and writes results to the configured `output_dir`.
### Config file format
A config file has six sections: `[meta]`, `[defaults]`, `[judge]`, `[run]`, `[[models]]`, and `[[benchmarks]]`. Only `[[models]]` and `[[benchmarks]]` are required — all other sections are optional and fall back to built-in defaults.
```toml title="evals/configs/full-suite.toml"
# Suite-level metadata (optional)
[meta]
name = "full-suite-v1"
description = "Evaluate all benchmarks against production models"
# Default generation parameters (optional)
[defaults]
temperature = 0.0
max_tokens = 2048
# LLM judge configuration (optional)
[judge]
model = "gpt-4o"
temperature = 0.0
max_tokens = 1024
# Execution settings (optional)
[run]
max_workers = 4
output_dir = "results/"
seed = 42
# --- Models (one [[models]] block per model) ---
[[models]]
name = "qwen3:8b"
engine = "ollama"
temperature = 0.3 # overrides [defaults] for this model
max_tokens = 4096
[[models]]
name = "gpt-4o"
provider = "openai" # uses cloud engine
[[models]]
name = "llama3:70b"
engine = "vllm"
temperature = 0.1
# --- Benchmarks (one [[benchmarks]] block per benchmark) ---
[[benchmarks]]
name = "supergpqa"
backend = "jarvis-direct"
max_samples = 200
split = "train"
[[benchmarks]]
name = "gaia"
backend = "jarvis-agent"
agent = "orchestrator"
tools = ["file_read", "calculator"]
max_samples = 50
judge_model = "claude-sonnet-4-20250514" # override judge for this benchmark
[[benchmarks]]
name = "frames"
backend = "jarvis-direct"
max_samples = 100
[[benchmarks]]
name = "wildchat"
backend = "jarvis-direct"
max_samples = 150
temperature = 0.7 # override temperature for this benchmark
```
This config produces 3 models x 4 benchmarks = **12 evaluation runs**.
### Merge precedence
Settings are resolved with the following precedence, from highest to lowest:
```
benchmark-level > model-level > [defaults] > built-in defaults
```
For example, `temperature` is resolved as: use `[defaults].temperature` (0.0), then apply `[[models]].temperature` if set (0.3 for qwen3:8b), then override with `[[benchmarks]].temperature` if set (0.7 for wildchat). The WildChat run with qwen3:8b therefore runs at `temperature = 0.7`.
### Minimal config
A config requires only one `[[models]]` and one `[[benchmarks]]` entry:
```toml title="evals/configs/minimal.toml"
[[models]]
name = "qwen3:8b"
[[benchmarks]]
name = "supergpqa"
```
This runs SuperGPQA against qwen3:8b with all default settings. Use this as a starting point when iterating on a single model or dataset.
### Single-run config with full options
```toml title="evals/configs/single-run.toml"
[meta]
name = "single-run-example"
description = "Evaluate SuperGPQA with a single model and full configuration"
[defaults]
temperature = 0.0
max_tokens = 2048
[judge]
model = "gpt-4o"
temperature = 0.0
max_tokens = 1024
[run]
max_workers = 4
output_dir = "results/"
seed = 42
[[models]]
name = "qwen3:8b"
engine = "ollama"
temperature = 0.3
max_tokens = 4096
[[benchmarks]]
name = "supergpqa"
backend = "jarvis-direct"
max_samples = 100
split = "train"
```
---
## Config Reference
### `[meta]`
Suite-level metadata. Neither field affects evaluation behavior; both are used in CLI output and summary files.
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `name` | str | `""` | Suite name shown in CLI output |
| `description` | str | `""` | Human-readable description |
### `[defaults]`
Default generation parameters applied to every run unless overridden at the model or benchmark level.
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `temperature` | float | `0.0` | Sampling temperature |
| `max_tokens` | int | `2048` | Maximum output tokens |
### `[judge]`
Configuration for the LLM used as a judge in GAIA, FRAMES, and WildChat scoring.
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `model` | str | `"gpt-4o"` | Judge model identifier |
| `provider` | str | `None` | Provider override (e.g., `"openai"`) |
| `temperature` | float | `0.0` | Judge sampling temperature |
| `max_tokens` | int | `1024` | Maximum judge output tokens |
!!! warning "Judge model costs"
Every sample that requires LLM-based scoring makes a separate call to the judge model. For large runs with hundreds of samples, judge costs can exceed evaluation costs. GAIA, FRAMES, and WildChat all require a judge; SuperGPQA uses an LLM to extract the answer letter, then compares it against the reference without a separate judge call.
### `[run]`
Execution settings that apply to the entire suite.
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `max_workers` | int | `4` | Number of parallel evaluation threads |
| `output_dir` | str | `"results/"` | Directory where JSONL and summary files are written |
| `seed` | int | `42` | Random seed for dataset shuffling |
### `[[models]]`
One block per model. The `name` field is required.
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `name` | str | required | Model identifier (e.g., `"qwen3:8b"`, `"gpt-4o"`) |
| `engine` | str | `None` | Engine key to use (`"ollama"`, `"vllm"`, `"cloud"`, ...) |
| `provider` | str | `None` | Provider override for cloud models (e.g., `"openai"`) |
| `temperature` | float | `None` | Override `[defaults].temperature` for this model |
| `max_tokens` | int | `None` | Override `[defaults].max_tokens` for this model |
### `[[benchmarks]]`
One block per benchmark. The `name` field is required.
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `name` | str | required | Benchmark key: `supergpqa`, `gaia`, `frames`, or `wildchat` |
| `backend` | str | `"jarvis-direct"` | Inference backend: `jarvis-direct` or `jarvis-agent` |
| `max_samples` | int | `None` | Limit number of samples; `None` evaluates the full dataset |
| `split` | str | `None` | Override the default dataset split |
| `agent` | str | `None` | Agent name for `jarvis-agent` backend (e.g., `"orchestrator"`) |
| `tools` | list[str] | `[]` | Tool names for `jarvis-agent` backend |
| `judge_model` | str | `None` | Override `[judge].model` for this benchmark only |
| `temperature` | float | `None` | Override temperature for this benchmark (highest precedence) |
| `max_tokens` | int | `None` | Override max tokens for this benchmark (highest precedence) |
---
## Output Format
### JSONL results file
Each completed sample is appended to the output JSONL file immediately after scoring. The file path is either specified with `-o`/`--output`, or auto-generated as `{output_dir}/{benchmark}_{model-slug}.jsonl`.
Each line is a JSON object with the following fields:
```json title="results/supergpqa_qwen3-8b.jsonl (one line per sample)"
{
"record_id": "supergpqa-42",
"benchmark": "supergpqa",
"model": "qwen3:8b",
"backend": "jarvis-direct",
"model_answer": "The answer is C because...",
"is_correct": true,
"score": 1.0,
"latency_seconds": 1.34,
"prompt_tokens": 187,
"completion_tokens": 12,
"cost_usd": 0.0,
"error": null,
"scoring_metadata": {
"reference_letter": "C",
"candidate_letter": "C",
"valid_letters": "ABCD"
}
}
```
| Field | Type | Description |
|-------|------|-------------|
| `record_id` | str | Unique sample identifier |
| `benchmark` | str | Benchmark name |
| `model` | str | Model identifier |
| `backend` | str | Backend used |
| `model_answer` | str | Raw model output |
| `is_correct` | bool or null | Scoring result (`null` if unscored) |
| `score` | float or null | Numeric score (1.0 correct, 0.0 incorrect, `null` unscored) |
| `latency_seconds` | float | Inference latency |
| `prompt_tokens` | int | Input tokens consumed |
| `completion_tokens` | int | Output tokens generated |
| `cost_usd` | float | Estimated cost in USD |
| `error` | str or null | Error message if the sample failed |
| `scoring_metadata` | dict | Scorer-specific details (extracted letters, judge output, etc.) |
### Summary JSON file
After all samples complete, a summary file is written alongside the JSONL at `{output_path}.summary.json`:
```json title="results/supergpqa_qwen3-8b.jsonl.summary.json"
{
"benchmark": "supergpqa",
"category": "reasoning",
"backend": "jarvis-direct",
"model": "qwen3:8b",
"total_samples": 200,
"scored_samples": 198,
"correct": 143,
"accuracy": 0.7222,
"errors": 2,
"mean_latency_seconds": 1.4821,
"total_cost_usd": 0.0,
"per_subject": {
"chemistry": {"accuracy": 0.74, "total": 50.0, "scored": 50.0, "correct": 37.0},
"mathematics": {"accuracy": 0.68, "total": 50.0, "scored": 49.0, "correct": 33.0}
},
"started_at": 1708789200.0,
"ended_at": 1708789496.3
}
```
The `per_subject` breakdown groups results by the dataset's subject or category field, which varies per benchmark:
- **SuperGPQA**: `subfield`, `field`, or `discipline`
- **GAIA**: difficulty level (`level_1`, `level_2`, `level_3`)
- **FRAMES**: reasoning type(s) (e.g., `temporal`, `intersection`)
- **WildChat**: always `"conversation"`
---
## Scoring Methods
Each benchmark uses a scorer tuned to its answer format.
### SuperGPQA: LLM-assisted MCQ extraction
SuperGPQA responses are free-form text that must contain one of the valid option letters (A, B, C, D, ...). The scorer uses the judge LLM to extract the final answer letter from the model's response, then compares it against the reference letter with exact string matching.
The judge is prompted with the original problem and the model's response and asked to return only a single letter. This handles cases where the model reasons extensively before stating its final answer.
```
is_correct = extracted_letter == reference_letter
```
Scoring metadata includes: `reference_letter`, `candidate_letter`, and `valid_letters`.
### GAIA: Normalized exact match with LLM fallback
GAIA answers are typically numbers, short phrases, or comma-separated lists. The scorer applies a normalization pass before comparison:
- **Numbers**: strips `$`, `%`, `,` and converts to float for comparison
- **Lists**: splits on `,`/`;` and compares element-by-element (with per-element type detection)
- **Strings**: lowercases, strips whitespace and punctuation
If the normalized exact match fails, the scorer falls back to the judge LLM, which returns a structured response with `extracted_final_answer`, `reasoning`, and `correct: yes/no`. The LLM fallback handles cases like unit variations, alternative phrasings, and equivalent but differently-formatted answers.
### FRAMES: LLM-as-judge (factual correctness)
FRAMES uses an LLM judge that evaluates semantic equivalence between the model's answer and the ground truth. The judge receives the question, ground truth, and predicted answer, then responds with a structured verdict:
```
extracted_final_answer: <extracted answer>
reasoning: <brief explanation>
correct: yes / no
```
The scorer parses the `correct:` line and falls back to presence of `TRUE`/`FALSE` tokens if the structured format is missing.
### WildChat: Pairwise LLM comparison
WildChat does not have a single "correct" answer — it measures chat response quality. The scorer runs a **dual pairwise comparison**:
1. The judge evaluates (model answer as A, reference as B) and returns a verdict token such as `[[A>>B]]`, `[[A>B]]`, `[[A=B]]`, `[[B>A]]`, or `[[B>>A]]`.
2. The judge then evaluates (reference as A, model answer as B) and returns another verdict.
The model is considered to have passed (`is_correct = True`) if it wins or ties in either comparison. The dual comparison reduces positional bias in the judge.
The judge uses a multi-step rubric that distinguishes subjective queries (scored on correctness, helpfulness, relevance, conciseness, and creativity) from objective/technical queries (scored on correctness only).
!!! tip "Interpreting WildChat accuracy"
A WildChat accuracy score of 0.50 means the model matched or beat the reference response in half of comparisons. Because the reference response comes from the original dataset (which may include responses from capable models), a score above 0.50 indicates strong chat quality for that sample set.
---
## Parallel Execution
The `EvalRunner` processes samples concurrently using a `ThreadPoolExecutor`. Results are flushed to the JSONL file incrementally as each sample completes, so you can inspect partial results during a long run.
```bash
# Use more workers for faster evaluation (if the engine supports concurrent requests)
openjarvis-eval run -b supergpqa -m qwen3:8b -w 8 -n 500
```
!!! warning "Worker count and engine load"
Higher worker counts increase throughput only if the inference engine can handle concurrent requests. Local Ollama instances typically handle 1-2 concurrent requests. Cloud APIs (OpenAI, Anthropic) can handle higher concurrency. Set `-w` based on your engine's actual parallelism.
---
## See Also
- [Benchmarks](benchmarks.md) — Measure inference engine latency and throughput
- [Telemetry & Traces](telemetry.md) — Record and analyze inference metrics from production use
- [Agents](agents.md) — Configure the `OrchestratorAgent` used by `jarvis-agent` backend
- [Tools](tools.md) — Available tools for agent-backed evaluations
- [Python SDK](python-sdk.md) — Programmatic access to OpenJarvis inference and agents
+114 -37
View File
@@ -89,19 +89,108 @@ def _build_judge_backend(judge_model: str):
return JarvisDirectBackend(engine_key="cloud")
def _print_summary(summary) -> None:
"""Print a single run summary."""
click.echo(f"\n{'=' * 60}")
click.echo(f"Benchmark: {summary.benchmark}")
click.echo(f"Model: {summary.model}")
click.echo(f"Backend: {summary.backend}")
click.echo(f"Samples: {summary.total_samples}")
click.echo(f"Scored: {summary.scored_samples}")
click.echo(f"Correct: {summary.correct}")
click.echo(f"Accuracy: {summary.accuracy:.4f}")
click.echo(f"Errors: {summary.errors}")
click.echo(f"Latency: {summary.mean_latency_seconds:.2f}s (mean)")
click.echo(f"Cost: ${summary.total_cost_usd:.4f}")
if summary.per_subject:
click.echo("\nPer-subject breakdown:")
for subj, stats in sorted(summary.per_subject.items()):
click.echo(f" {subj}: {stats['accuracy']:.4f} "
f"({int(stats['correct'])}/{int(stats['scored'])})")
click.echo(f"{'=' * 60}")
def _run_single(config) -> object:
"""Run a single eval from a RunConfig and return the summary."""
from evals.core.runner import EvalRunner
eval_backend = _build_backend(
config.backend,
config.engine_key,
config.agent_name or "orchestrator",
config.tools,
)
dataset = _build_dataset(config.benchmark)
judge_backend = _build_judge_backend(config.judge_model)
scorer = _build_scorer(config.benchmark, judge_backend, config.judge_model)
runner = EvalRunner(config, dataset, eval_backend, scorer)
try:
return runner.run()
finally:
eval_backend.close()
judge_backend.close()
def _run_from_config(config_path: str, verbose: bool) -> None:
"""Load a TOML config and run the full models x benchmarks matrix."""
from evals.core.config import expand_suite, load_eval_config
suite = load_eval_config(config_path)
run_configs = expand_suite(suite)
suite_name = suite.meta.name or Path(config_path).stem
click.echo(f"Suite: {suite_name}")
if suite.meta.description:
click.echo(f" {suite.meta.description}")
click.echo(f" {len(suite.models)} model(s) x {len(suite.benchmarks)} "
f"benchmark(s) = {len(run_configs)} run(s)\n")
# Ensure output directory exists
output_dir = Path(suite.run.output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
summaries = []
for i, rc in enumerate(run_configs, 1):
click.echo(f"--- [{i}/{len(run_configs)}] {rc.benchmark} / {rc.model} ---")
try:
summary = _run_single(rc)
summaries.append(summary)
click.echo(f" {summary.accuracy:.4f} "
f"({summary.correct}/{summary.scored_samples})")
except Exception as exc:
click.echo(f" FAILED: {exc}", err=True)
# Print overall summary table
if summaries:
click.echo(f"\n{'=' * 60}")
click.echo(f"Suite Results: {suite_name}")
click.echo(f"{'=' * 60}")
click.echo(f" {'Benchmark':12s} {'Model':20s} {'Accuracy':>10s} {'Scored':>8s}")
click.echo(f" {'-' * 12} {'-' * 20} {'-' * 10} {'-' * 8}")
for s in summaries:
model_display = s.model[:20]
click.echo(f" {s.benchmark:12s} {model_display:20s} "
f"{s.accuracy:10.4f} "
f"{s.correct}/{s.scored_samples:>5}")
click.echo(f"{'=' * 60}")
@click.group()
def main():
"""OpenJarvis Evaluation Framework."""
@main.command()
@click.option("-b", "--benchmark", required=True,
@click.option("-c", "--config", "config_path", default=None,
type=click.Path(), help="TOML config file for suite runs")
@click.option("-b", "--benchmark", default=None,
type=click.Choice(list(BENCHMARKS.keys())),
help="Benchmark to run")
@click.option("--backend", default="jarvis-direct",
type=click.Choice(list(BACKENDS.keys())),
help="Inference backend")
@click.option("-m", "--model", required=True, help="Model identifier")
@click.option("-m", "--model", default=None, help="Model identifier")
@click.option("-e", "--engine", "engine_key", default=None,
help="Engine key (ollama, vllm, cloud, ...)")
@click.option("--agent", "agent_name", default="orchestrator",
@@ -123,13 +212,30 @@ def main():
@click.option("--max-tokens", type=int, default=2048,
help="Max output tokens")
@click.option("-v", "--verbose", is_flag=True, help="Verbose logging")
def run(benchmark, backend, model, engine_key, agent_name, tools,
max_samples, max_workers, judge_model, output_path, seed,
@click.pass_context
def run(ctx, config_path, benchmark, backend, model, engine_key, agent_name,
tools, max_samples, max_workers, judge_model, output_path, seed,
dataset_split, temperature, max_tokens, verbose):
"""Run a single benchmark evaluation."""
"""Run a single benchmark evaluation, or a full suite from a TOML config."""
_setup_logging(verbose)
from evals.core.runner import EvalRunner
# Config-driven mode
if config_path is not None:
_run_from_config(config_path, verbose)
return
# CLI-driven mode: validate required args
if benchmark is None:
raise click.UsageError(
"Missing option '-b' / '--benchmark' "
"(required when --config is not provided)"
)
if model is None:
raise click.UsageError(
"Missing option '-m' / '--model' "
"(required when --config is not provided)"
)
from evals.core.types import RunConfig
tool_list = [t.strip() for t in tools.split(",") if t.strip()] if tools else []
@@ -151,37 +257,8 @@ def run(benchmark, backend, model, engine_key, agent_name, tools,
dataset_split=dataset_split,
)
eval_backend = _build_backend(backend, engine_key, agent_name, tool_list)
dataset = _build_dataset(benchmark)
judge_backend = _build_judge_backend(judge_model)
scorer = _build_scorer(benchmark, judge_backend, judge_model)
runner = EvalRunner(config, dataset, eval_backend, scorer)
try:
summary = runner.run()
finally:
eval_backend.close()
judge_backend.close()
# Print summary
click.echo(f"\n{'=' * 60}")
click.echo(f"Benchmark: {summary.benchmark}")
click.echo(f"Model: {summary.model}")
click.echo(f"Backend: {summary.backend}")
click.echo(f"Samples: {summary.total_samples}")
click.echo(f"Scored: {summary.scored_samples}")
click.echo(f"Correct: {summary.correct}")
click.echo(f"Accuracy: {summary.accuracy:.4f}")
click.echo(f"Errors: {summary.errors}")
click.echo(f"Latency: {summary.mean_latency_seconds:.2f}s (mean)")
click.echo(f"Cost: ${summary.total_cost_usd:.4f}")
if summary.per_subject:
click.echo("\nPer-subject breakdown:")
for subj, stats in sorted(summary.per_subject.items()):
click.echo(f" {subj}: {stats['accuracy']:.4f} "
f"({int(stats['correct'])}/{int(stats['scored'])})")
click.echo(f"{'=' * 60}")
summary = _run_single(config)
_print_summary(summary)
@main.command("run-all")
+63
View File
@@ -0,0 +1,63 @@
# Full suite eval config — 3 models x 4 benchmarks with overrides.
[meta]
name = "full-suite-v1"
description = "Evaluate all benchmarks against production models"
[defaults]
temperature = 0.0
max_tokens = 2048
[judge]
model = "gpt-4o"
temperature = 0.0
max_tokens = 1024
[run]
max_workers = 4
output_dir = "results/"
seed = 42
# --- Models ---
[[models]]
name = "qwen3:8b"
engine = "ollama"
temperature = 0.3
max_tokens = 4096
[[models]]
name = "gpt-4o"
provider = "openai"
[[models]]
name = "llama3:70b"
engine = "vllm"
temperature = 0.1
# --- Benchmarks ---
[[benchmarks]]
name = "supergpqa"
backend = "jarvis-direct"
max_samples = 200
split = "train"
[[benchmarks]]
name = "gaia"
backend = "jarvis-agent"
agent = "orchestrator"
tools = ["file_read", "calculator"]
max_samples = 50
judge_model = "claude-sonnet-4-20250514"
[[benchmarks]]
name = "frames"
backend = "jarvis-direct"
max_samples = 100
[[benchmarks]]
name = "wildchat"
backend = "jarvis-direct"
max_samples = 150
temperature = 0.7
+7
View File
@@ -0,0 +1,7 @@
# Minimal eval config — 1 model, 1 benchmark, all defaults.
[[models]]
name = "qwen3:8b"
[[benchmarks]]
name = "supergpqa"
+31
View File
@@ -0,0 +1,31 @@
# Single-run eval config — 1 model, 1 benchmark, all sections populated.
[meta]
name = "single-run-example"
description = "Evaluate SuperGPQA with a single model and full configuration"
[defaults]
temperature = 0.0
max_tokens = 2048
[judge]
model = "gpt-4o"
temperature = 0.0
max_tokens = 1024
[run]
max_workers = 4
output_dir = "results/"
seed = 42
[[models]]
name = "qwen3:8b"
engine = "ollama"
temperature = 0.3
max_tokens = 4096
[[benchmarks]]
name = "supergpqa"
backend = "jarvis-direct"
max_samples = 100
split = "train"
+220
View File
@@ -0,0 +1,220 @@
"""TOML config loader and matrix expansion for eval suites."""
from __future__ import annotations
import logging
import sys
from pathlib import Path
from typing import List
from evals.core.types import (
BenchmarkConfig,
DefaultsConfig,
EvalSuiteConfig,
ExecutionConfig,
JudgeConfig,
MetaConfig,
ModelConfig,
RunConfig,
)
if sys.version_info >= (3, 11):
import tomllib
else:
try:
import tomli as tomllib # type: ignore[no-redef]
except ImportError as exc:
raise ImportError(
"Python 3.10 requires the 'tomli' package. "
"Install it with: pip install tomli"
) from exc
logger = logging.getLogger(__name__)
VALID_BACKENDS = {"jarvis-direct", "jarvis-agent"}
# Known benchmark names (used for warnings, not hard validation)
KNOWN_BENCHMARKS = {"supergpqa", "gaia", "frames", "wildchat"}
class EvalConfigError(Exception):
"""Raised for structural issues in eval config files."""
def load_eval_config(path: str | Path) -> EvalSuiteConfig:
"""Load and validate an eval suite config from a TOML file.
Args:
path: Path to the TOML config file.
Returns:
Validated EvalSuiteConfig.
Raises:
EvalConfigError: On structural validation failures.
FileNotFoundError: If the config file doesn't exist.
"""
path = Path(path)
if not path.exists():
raise FileNotFoundError(f"Config file not found: {path}")
with open(path, "rb") as f:
raw = tomllib.load(f)
# Parse [meta]
meta_raw = raw.get("meta", {})
meta = MetaConfig(
name=meta_raw.get("name", ""),
description=meta_raw.get("description", ""),
)
# Parse [defaults]
defaults_raw = raw.get("defaults", {})
defaults = DefaultsConfig(
temperature=float(defaults_raw.get("temperature", 0.0)),
max_tokens=int(defaults_raw.get("max_tokens", 2048)),
)
# Parse [judge]
judge_raw = raw.get("judge", {})
judge = JudgeConfig(
model=judge_raw.get("model", "gpt-4o"),
provider=judge_raw.get("provider"),
temperature=float(judge_raw.get("temperature", 0.0)),
max_tokens=int(judge_raw.get("max_tokens", 1024)),
)
# Parse [run]
run_raw = raw.get("run", {})
execution = ExecutionConfig(
max_workers=int(run_raw.get("max_workers", 4)),
output_dir=run_raw.get("output_dir", "results/"),
seed=int(run_raw.get("seed", 42)),
)
# Parse [[models]]
models_raw = raw.get("models", [])
if not models_raw:
raise EvalConfigError("Config must define at least one [[models]] entry")
models: List[ModelConfig] = []
for m in models_raw:
if not m.get("name"):
raise EvalConfigError("Each [[models]] entry must have a 'name' field")
models.append(ModelConfig(
name=m["name"],
engine=m.get("engine"),
provider=m.get("provider"),
temperature=float(m["temperature"]) if "temperature" in m else None,
max_tokens=int(m["max_tokens"]) if "max_tokens" in m else None,
))
# Parse [[benchmarks]]
benchmarks_raw = raw.get("benchmarks", [])
if not benchmarks_raw:
raise EvalConfigError(
"Config must define at least one [[benchmarks]] entry"
)
benchmarks: List[BenchmarkConfig] = []
for b in benchmarks_raw:
if not b.get("name"):
raise EvalConfigError(
"Each [[benchmarks]] entry must have a 'name' field"
)
backend = b.get("backend", "jarvis-direct")
if backend not in VALID_BACKENDS:
raise EvalConfigError(
f"Invalid backend '{backend}' for benchmark '{b['name']}'. "
f"Must be one of: {', '.join(sorted(VALID_BACKENDS))}"
)
bench_name = b["name"]
if bench_name not in KNOWN_BENCHMARKS:
logger.warning("Unknown benchmark name: '%s'", bench_name)
tools_raw = b.get("tools", [])
benchmarks.append(BenchmarkConfig(
name=bench_name,
backend=backend,
max_samples=int(b["max_samples"]) if "max_samples" in b else None,
split=b.get("split"),
agent=b.get("agent"),
tools=list(tools_raw),
judge_model=b.get("judge_model"),
temperature=float(b["temperature"]) if "temperature" in b else None,
max_tokens=int(b["max_tokens"]) if "max_tokens" in b else None,
))
return EvalSuiteConfig(
meta=meta,
defaults=defaults,
judge=judge,
run=execution,
models=models,
benchmarks=benchmarks,
)
def expand_suite(suite: EvalSuiteConfig) -> List[RunConfig]:
"""Expand an EvalSuiteConfig into a list of RunConfigs (models x benchmarks).
Merge precedence (highest wins):
benchmark-level > model-level > [defaults] > built-in defaults
Args:
suite: The parsed eval suite config.
Returns:
List of RunConfig, one per model-benchmark pair.
"""
configs: List[RunConfig] = []
output_dir = suite.run.output_dir.rstrip("/")
for model in suite.models:
for bench in suite.benchmarks:
# Temperature: benchmark > model > defaults
temperature = suite.defaults.temperature
if model.temperature is not None:
temperature = model.temperature
if bench.temperature is not None:
temperature = bench.temperature
# Max tokens: benchmark > model > defaults
max_tokens = suite.defaults.max_tokens
if model.max_tokens is not None:
max_tokens = model.max_tokens
if bench.max_tokens is not None:
max_tokens = bench.max_tokens
# Judge model: benchmark > [judge]
judge_model = suite.judge.model
if bench.judge_model is not None:
judge_model = bench.judge_model
# Auto-generate output path
model_slug = model.name.replace("/", "-").replace(":", "-")
output_path = f"{output_dir}/{bench.name}_{model_slug}.jsonl"
configs.append(RunConfig(
benchmark=bench.name,
backend=bench.backend,
model=model.name,
max_samples=bench.max_samples,
max_workers=suite.run.max_workers,
temperature=temperature,
max_tokens=max_tokens,
judge_model=judge_model,
engine_key=model.engine,
agent_name=bench.agent,
tools=list(bench.tools),
output_path=output_path,
seed=suite.run.seed,
dataset_split=bench.split,
))
return configs
__all__ = ["EvalConfigError", "load_eval_config", "expand_suite"]
+91 -1
View File
@@ -74,4 +74,94 @@ class RunSummary:
ended_at: float = 0.0
__all__ = ["EvalRecord", "EvalResult", "RunConfig", "RunSummary"]
# ---------------------------------------------------------------------------
# Eval suite config dataclasses (TOML config system)
# ---------------------------------------------------------------------------
@dataclass(slots=True)
class MetaConfig:
"""Suite-level metadata."""
name: str = ""
description: str = ""
@dataclass(slots=True)
class DefaultsConfig:
"""Default generation parameters applied to all runs."""
temperature: float = 0.0
max_tokens: int = 2048
@dataclass(slots=True)
class JudgeConfig:
"""Configuration for the LLM judge."""
model: str = "gpt-4o"
provider: Optional[str] = None
temperature: float = 0.0
max_tokens: int = 1024
@dataclass(slots=True)
class ExecutionConfig:
"""Execution-level settings for the eval run."""
max_workers: int = 4
output_dir: str = "results/"
seed: int = 42
@dataclass(slots=True)
class ModelConfig:
"""Configuration for a single model in the eval suite."""
name: str = ""
engine: Optional[str] = None
provider: Optional[str] = None
temperature: Optional[float] = None
max_tokens: Optional[int] = None
@dataclass(slots=True)
class BenchmarkConfig:
"""Configuration for a single benchmark in the eval suite."""
name: str = ""
backend: str = "jarvis-direct"
max_samples: Optional[int] = None
split: Optional[str] = None
agent: Optional[str] = None
tools: List[str] = field(default_factory=list)
judge_model: Optional[str] = None
temperature: Optional[float] = None
max_tokens: Optional[int] = None
@dataclass(slots=True)
class EvalSuiteConfig:
"""Top-level configuration for an eval suite (models x benchmarks)."""
meta: MetaConfig = field(default_factory=MetaConfig)
defaults: DefaultsConfig = field(default_factory=DefaultsConfig)
judge: JudgeConfig = field(default_factory=JudgeConfig)
run: ExecutionConfig = field(default_factory=ExecutionConfig)
models: List[ModelConfig] = field(default_factory=list)
benchmarks: List[BenchmarkConfig] = field(default_factory=list)
__all__ = [
"EvalRecord",
"EvalResult",
"RunConfig",
"RunSummary",
"MetaConfig",
"DefaultsConfig",
"JudgeConfig",
"ExecutionConfig",
"ModelConfig",
"BenchmarkConfig",
"EvalSuiteConfig",
]
+1
View File
@@ -10,6 +10,7 @@ dependencies = [
"huggingface-hub>=0.20",
"tqdm>=4.65",
"rich>=13",
"tomli>=2.0; python_version < '3.11'",
]
[project.optional-dependencies]
+541
View File
@@ -0,0 +1,541 @@
"""Tests for eval suite config loading and matrix expansion."""
from __future__ import annotations
import textwrap
from pathlib import Path
import pytest
from evals.core.config import EvalConfigError, expand_suite, load_eval_config
from evals.core.types import (
BenchmarkConfig,
DefaultsConfig,
EvalSuiteConfig,
ExecutionConfig,
JudgeConfig,
MetaConfig,
ModelConfig,
RunConfig,
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _write_toml(tmp_path: Path, content: str) -> Path:
"""Write a TOML string to a temp file and return its path."""
p = tmp_path / "suite.toml"
p.write_text(textwrap.dedent(content))
return p
# ---------------------------------------------------------------------------
# Dataclass defaults
# ---------------------------------------------------------------------------
class TestDataclassDefaults:
def test_meta_config_defaults(self):
m = MetaConfig()
assert m.name == ""
assert m.description == ""
def test_defaults_config_defaults(self):
d = DefaultsConfig()
assert d.temperature == 0.0
assert d.max_tokens == 2048
def test_judge_config_defaults(self):
j = JudgeConfig()
assert j.model == "gpt-4o"
assert j.provider is None
assert j.temperature == 0.0
assert j.max_tokens == 1024
def test_execution_config_defaults(self):
e = ExecutionConfig()
assert e.max_workers == 4
assert e.output_dir == "results/"
assert e.seed == 42
def test_model_config_defaults(self):
m = ModelConfig(name="test")
assert m.engine is None
assert m.provider is None
assert m.temperature is None
assert m.max_tokens is None
def test_benchmark_config_defaults(self):
b = BenchmarkConfig(name="supergpqa")
assert b.backend == "jarvis-direct"
assert b.max_samples is None
assert b.split is None
assert b.agent is None
assert b.tools == []
assert b.judge_model is None
assert b.temperature is None
assert b.max_tokens is None
def test_eval_suite_config_defaults(self):
s = EvalSuiteConfig()
assert s.meta.name == ""
assert s.defaults.temperature == 0.0
assert s.judge.model == "gpt-4o"
assert s.run.max_workers == 4
assert s.models == []
assert s.benchmarks == []
# ---------------------------------------------------------------------------
# TOML loading
# ---------------------------------------------------------------------------
class TestLoadEvalConfig:
def test_minimal_config(self, tmp_path):
p = _write_toml(tmp_path, """\
[[models]]
name = "qwen3:8b"
[[benchmarks]]
name = "supergpqa"
""")
suite = load_eval_config(p)
assert len(suite.models) == 1
assert suite.models[0].name == "qwen3:8b"
assert len(suite.benchmarks) == 1
assert suite.benchmarks[0].name == "supergpqa"
# Defaults should be applied
assert suite.defaults.temperature == 0.0
assert suite.judge.model == "gpt-4o"
def test_full_config(self, tmp_path):
p = _write_toml(tmp_path, """\
[meta]
name = "test-suite"
description = "A test suite"
[defaults]
temperature = 0.5
max_tokens = 4096
[judge]
model = "claude-sonnet"
temperature = 0.1
max_tokens = 512
[run]
max_workers = 8
output_dir = "out/"
seed = 123
[[models]]
name = "model-a"
engine = "ollama"
temperature = 0.3
[[models]]
name = "model-b"
provider = "openai"
[[benchmarks]]
name = "supergpqa"
backend = "jarvis-direct"
max_samples = 100
split = "test"
[[benchmarks]]
name = "gaia"
backend = "jarvis-agent"
agent = "orchestrator"
tools = ["calc", "think"]
judge_model = "gpt-4o"
""")
suite = load_eval_config(p)
assert suite.meta.name == "test-suite"
assert suite.meta.description == "A test suite"
assert suite.defaults.temperature == 0.5
assert suite.defaults.max_tokens == 4096
assert suite.judge.model == "claude-sonnet"
assert suite.judge.temperature == 0.1
assert suite.run.max_workers == 8
assert suite.run.output_dir == "out/"
assert suite.run.seed == 123
assert len(suite.models) == 2
assert suite.models[0].name == "model-a"
assert suite.models[0].engine == "ollama"
assert suite.models[0].temperature == 0.3
assert suite.models[1].name == "model-b"
assert suite.models[1].provider == "openai"
assert len(suite.benchmarks) == 2
assert suite.benchmarks[0].max_samples == 100
assert suite.benchmarks[0].split == "test"
assert suite.benchmarks[1].agent == "orchestrator"
assert suite.benchmarks[1].tools == ["calc", "think"]
assert suite.benchmarks[1].judge_model == "gpt-4o"
def test_missing_models_raises(self, tmp_path):
p = _write_toml(tmp_path, """\
[[benchmarks]]
name = "supergpqa"
""")
with pytest.raises(EvalConfigError, match="at least one \\[\\[models\\]\\]"):
load_eval_config(p)
def test_missing_benchmarks_raises(self, tmp_path):
p = _write_toml(tmp_path, """\
[[models]]
name = "qwen3:8b"
""")
with pytest.raises(EvalConfigError, match="at least one \\[\\[benchmarks\\]\\]"):
load_eval_config(p)
def test_model_without_name_raises(self, tmp_path):
p = _write_toml(tmp_path, """\
[[models]]
engine = "ollama"
[[benchmarks]]
name = "supergpqa"
""")
with pytest.raises(EvalConfigError, match="'name' field"):
load_eval_config(p)
def test_benchmark_without_name_raises(self, tmp_path):
p = _write_toml(tmp_path, """\
[[models]]
name = "qwen3:8b"
[[benchmarks]]
backend = "jarvis-direct"
""")
with pytest.raises(EvalConfigError, match="'name' field"):
load_eval_config(p)
def test_invalid_backend_raises(self, tmp_path):
p = _write_toml(tmp_path, """\
[[models]]
name = "qwen3:8b"
[[benchmarks]]
name = "supergpqa"
backend = "invalid-backend"
""")
with pytest.raises(EvalConfigError, match="Invalid backend"):
load_eval_config(p)
def test_unknown_benchmark_warns(self, tmp_path, caplog):
p = _write_toml(tmp_path, """\
[[models]]
name = "qwen3:8b"
[[benchmarks]]
name = "custom-bench"
""")
import logging
with caplog.at_level(logging.WARNING):
suite = load_eval_config(p)
assert suite.benchmarks[0].name == "custom-bench"
assert "Unknown benchmark name" in caplog.text
def test_file_not_found(self):
with pytest.raises(FileNotFoundError):
load_eval_config("/nonexistent/path.toml")
def test_malformed_toml(self, tmp_path):
p = tmp_path / "bad.toml"
p.write_text("[[[ invalid toml")
with pytest.raises(Exception): # tomllib raises various errors
load_eval_config(p)
def test_empty_models_list(self, tmp_path):
p = _write_toml(tmp_path, """\
models = []
[[benchmarks]]
name = "supergpqa"
""")
with pytest.raises(EvalConfigError, match="at least one \\[\\[models\\]\\]"):
load_eval_config(p)
def test_empty_benchmarks_list(self, tmp_path):
p = _write_toml(tmp_path, """\
[[models]]
name = "qwen3:8b"
benchmarks = []
""")
with pytest.raises(EvalConfigError, match="at least one \\[\\[benchmarks\\]\\]"):
load_eval_config(p)
# ---------------------------------------------------------------------------
# Example config files load correctly
# ---------------------------------------------------------------------------
class TestExampleConfigs:
@pytest.fixture(params=["minimal.toml", "single-run.toml", "full-suite.toml"])
def example_config(self, request):
configs_dir = Path(__file__).resolve().parent.parent / "configs"
return configs_dir / request.param
def test_example_configs_load(self, example_config):
suite = load_eval_config(example_config)
assert len(suite.models) >= 1
assert len(suite.benchmarks) >= 1
def test_full_suite_dimensions(self):
configs_dir = Path(__file__).resolve().parent.parent / "configs"
suite = load_eval_config(configs_dir / "full-suite.toml")
assert len(suite.models) == 3
assert len(suite.benchmarks) == 4
# ---------------------------------------------------------------------------
# Matrix expansion
# ---------------------------------------------------------------------------
class TestExpandSuite:
def test_single_model_single_benchmark(self):
suite = EvalSuiteConfig(
models=[ModelConfig(name="m1")],
benchmarks=[BenchmarkConfig(name="supergpqa")],
)
configs = expand_suite(suite)
assert len(configs) == 1
assert configs[0].model == "m1"
assert configs[0].benchmark == "supergpqa"
def test_cross_product(self):
suite = EvalSuiteConfig(
models=[ModelConfig(name="m1"), ModelConfig(name="m2")],
benchmarks=[
BenchmarkConfig(name="supergpqa"),
BenchmarkConfig(name="gaia"),
BenchmarkConfig(name="frames"),
],
)
configs = expand_suite(suite)
assert len(configs) == 6 # 2 x 3
def test_temperature_merge_defaults(self):
suite = EvalSuiteConfig(
defaults=DefaultsConfig(temperature=0.5),
models=[ModelConfig(name="m1")],
benchmarks=[BenchmarkConfig(name="supergpqa")],
)
configs = expand_suite(suite)
assert configs[0].temperature == 0.5
def test_temperature_model_overrides_defaults(self):
suite = EvalSuiteConfig(
defaults=DefaultsConfig(temperature=0.5),
models=[ModelConfig(name="m1", temperature=0.3)],
benchmarks=[BenchmarkConfig(name="supergpqa")],
)
configs = expand_suite(suite)
assert configs[0].temperature == 0.3
def test_temperature_benchmark_overrides_model(self):
suite = EvalSuiteConfig(
defaults=DefaultsConfig(temperature=0.5),
models=[ModelConfig(name="m1", temperature=0.3)],
benchmarks=[BenchmarkConfig(name="supergpqa", temperature=0.9)],
)
configs = expand_suite(suite)
assert configs[0].temperature == 0.9
def test_max_tokens_merge_precedence(self):
suite = EvalSuiteConfig(
defaults=DefaultsConfig(max_tokens=1000),
models=[ModelConfig(name="m1", max_tokens=2000)],
benchmarks=[BenchmarkConfig(name="supergpqa", max_tokens=3000)],
)
configs = expand_suite(suite)
assert configs[0].max_tokens == 3000
def test_max_tokens_model_only(self):
suite = EvalSuiteConfig(
defaults=DefaultsConfig(max_tokens=1000),
models=[ModelConfig(name="m1", max_tokens=2000)],
benchmarks=[BenchmarkConfig(name="supergpqa")],
)
configs = expand_suite(suite)
assert configs[0].max_tokens == 2000
def test_judge_model_from_judge_config(self):
suite = EvalSuiteConfig(
judge=JudgeConfig(model="custom-judge"),
models=[ModelConfig(name="m1")],
benchmarks=[BenchmarkConfig(name="supergpqa")],
)
configs = expand_suite(suite)
assert configs[0].judge_model == "custom-judge"
def test_judge_model_benchmark_override(self):
suite = EvalSuiteConfig(
judge=JudgeConfig(model="default-judge"),
models=[ModelConfig(name="m1")],
benchmarks=[BenchmarkConfig(name="supergpqa", judge_model="bench-judge")],
)
configs = expand_suite(suite)
assert configs[0].judge_model == "bench-judge"
def test_output_path_auto_generated(self):
suite = EvalSuiteConfig(
run=ExecutionConfig(output_dir="out/"),
models=[ModelConfig(name="qwen3:8b")],
benchmarks=[BenchmarkConfig(name="supergpqa")],
)
configs = expand_suite(suite)
assert configs[0].output_path == "out/supergpqa_qwen3-8b.jsonl"
def test_output_path_model_slug(self):
suite = EvalSuiteConfig(
run=ExecutionConfig(output_dir="results"),
models=[ModelConfig(name="org/model:v2")],
benchmarks=[BenchmarkConfig(name="gaia")],
)
configs = expand_suite(suite)
assert configs[0].output_path == "results/gaia_org-model-v2.jsonl"
def test_engine_key_from_model(self):
suite = EvalSuiteConfig(
models=[ModelConfig(name="m1", engine="ollama")],
benchmarks=[BenchmarkConfig(name="supergpqa")],
)
configs = expand_suite(suite)
assert configs[0].engine_key == "ollama"
def test_agent_from_benchmark(self):
suite = EvalSuiteConfig(
models=[ModelConfig(name="m1")],
benchmarks=[BenchmarkConfig(name="gaia", agent="orchestrator")],
)
configs = expand_suite(suite)
assert configs[0].agent_name == "orchestrator"
def test_tools_from_benchmark(self):
suite = EvalSuiteConfig(
models=[ModelConfig(name="m1")],
benchmarks=[BenchmarkConfig(name="gaia", tools=["calc", "think"])],
)
configs = expand_suite(suite)
assert configs[0].tools == ["calc", "think"]
def test_tools_list_not_shared(self):
"""Each RunConfig should get its own tools list (no shared mutation)."""
suite = EvalSuiteConfig(
models=[ModelConfig(name="m1"), ModelConfig(name="m2")],
benchmarks=[BenchmarkConfig(name="gaia", tools=["calc"])],
)
configs = expand_suite(suite)
configs[0].tools.append("extra")
assert configs[1].tools == ["calc"]
def test_max_workers_and_seed_from_run(self):
suite = EvalSuiteConfig(
run=ExecutionConfig(max_workers=16, seed=99),
models=[ModelConfig(name="m1")],
benchmarks=[BenchmarkConfig(name="supergpqa")],
)
configs = expand_suite(suite)
assert configs[0].max_workers == 16
assert configs[0].seed == 99
def test_max_samples_and_split_from_benchmark(self):
suite = EvalSuiteConfig(
models=[ModelConfig(name="m1")],
benchmarks=[
BenchmarkConfig(name="supergpqa", max_samples=50, split="test")
],
)
configs = expand_suite(suite)
assert configs[0].max_samples == 50
assert configs[0].dataset_split == "test"
def test_backend_from_benchmark(self):
suite = EvalSuiteConfig(
models=[ModelConfig(name="m1")],
benchmarks=[BenchmarkConfig(name="gaia", backend="jarvis-agent")],
)
configs = expand_suite(suite)
assert configs[0].backend == "jarvis-agent"
def test_expand_returns_run_config_type(self):
suite = EvalSuiteConfig(
models=[ModelConfig(name="m1")],
benchmarks=[BenchmarkConfig(name="supergpqa")],
)
configs = expand_suite(suite)
assert all(isinstance(c, RunConfig) for c in configs)
# ---------------------------------------------------------------------------
# CLI integration
# ---------------------------------------------------------------------------
class TestCLIConfig:
def test_run_missing_benchmark_and_config(self):
from click.testing import CliRunner
from evals.cli import main
runner = CliRunner()
result = runner.invoke(main, ["run", "-m", "qwen3:8b"])
assert result.exit_code != 0
assert "--benchmark" in result.output or "benchmark" in result.output.lower()
def test_run_missing_model_and_config(self):
from click.testing import CliRunner
from evals.cli import main
runner = CliRunner()
result = runner.invoke(main, ["run", "-b", "supergpqa"])
assert result.exit_code != 0
assert "--model" in result.output or "model" in result.output.lower()
def test_run_config_file_not_found(self):
from click.testing import CliRunner
from evals.cli import main
runner = CliRunner()
result = runner.invoke(main, ["run", "--config", "/nonexistent.toml"])
assert result.exit_code != 0
def test_run_config_loads_and_prints_suite_info(self, tmp_path):
"""Verify --config loads config and prints suite header.
We don't actually run the eval (requires backends), but we verify
the config is loaded and the matrix expansion starts.
"""
from unittest.mock import patch
from click.testing import CliRunner
from evals.cli import main
p = _write_toml(tmp_path, """\
[meta]
name = "test-suite"
[[models]]
name = "qwen3:8b"
[[benchmarks]]
name = "supergpqa"
""")
runner = CliRunner()
with patch("evals.cli._run_single", side_effect=Exception("mock")):
result = runner.invoke(main, ["run", "--config", str(p)])
# Should print suite info before failing
assert "test-suite" in result.output
assert "1 model(s) x 1 benchmark(s)" in result.output
+148 -1
View File
@@ -2,7 +2,19 @@
from __future__ import annotations
from evals.core.types import EvalRecord, EvalResult, RunConfig, RunSummary
from evals.core.types import (
BenchmarkConfig,
DefaultsConfig,
EvalRecord,
EvalResult,
EvalSuiteConfig,
ExecutionConfig,
JudgeConfig,
MetaConfig,
ModelConfig,
RunConfig,
RunSummary,
)
class TestEvalRecord:
@@ -86,3 +98,138 @@ class TestRunSummary:
assert s.accuracy == 0.495
assert s.per_subject["math"]["accuracy"] == 0.5
assert s.started_at == 0.0
# ---------------------------------------------------------------------------
# Eval suite config dataclasses
# ---------------------------------------------------------------------------
class TestMetaConfig:
def test_defaults(self):
m = MetaConfig()
assert m.name == ""
assert m.description == ""
def test_with_values(self):
m = MetaConfig(name="suite-1", description="First suite")
assert m.name == "suite-1"
assert m.description == "First suite"
class TestDefaultsConfig:
def test_defaults(self):
d = DefaultsConfig()
assert d.temperature == 0.0
assert d.max_tokens == 2048
def test_with_values(self):
d = DefaultsConfig(temperature=0.7, max_tokens=4096)
assert d.temperature == 0.7
assert d.max_tokens == 4096
class TestJudgeConfig:
def test_defaults(self):
j = JudgeConfig()
assert j.model == "gpt-4o"
assert j.provider is None
assert j.temperature == 0.0
assert j.max_tokens == 1024
def test_with_values(self):
j = JudgeConfig(model="claude", provider="anthropic", temperature=0.1)
assert j.model == "claude"
assert j.provider == "anthropic"
assert j.temperature == 0.1
class TestExecutionConfig:
def test_defaults(self):
e = ExecutionConfig()
assert e.max_workers == 4
assert e.output_dir == "results/"
assert e.seed == 42
def test_with_values(self):
e = ExecutionConfig(max_workers=16, output_dir="out/", seed=99)
assert e.max_workers == 16
assert e.output_dir == "out/"
assert e.seed == 99
class TestModelConfig:
def test_required_name(self):
m = ModelConfig(name="qwen3:8b")
assert m.name == "qwen3:8b"
assert m.engine is None
assert m.provider is None
assert m.temperature is None
assert m.max_tokens is None
def test_with_overrides(self):
m = ModelConfig(
name="gpt-4o", engine="cloud", provider="openai",
temperature=0.5, max_tokens=4096,
)
assert m.engine == "cloud"
assert m.provider == "openai"
assert m.temperature == 0.5
assert m.max_tokens == 4096
class TestBenchmarkConfig:
def test_defaults(self):
b = BenchmarkConfig(name="supergpqa")
assert b.name == "supergpqa"
assert b.backend == "jarvis-direct"
assert b.max_samples is None
assert b.split is None
assert b.agent is None
assert b.tools == []
assert b.judge_model is None
assert b.temperature is None
assert b.max_tokens is None
def test_with_overrides(self):
b = BenchmarkConfig(
name="gaia", backend="jarvis-agent", max_samples=50,
split="test", agent="orchestrator",
tools=["calc", "think"], judge_model="custom-judge",
temperature=0.3, max_tokens=1024,
)
assert b.backend == "jarvis-agent"
assert b.max_samples == 50
assert b.split == "test"
assert b.agent == "orchestrator"
assert b.tools == ["calc", "think"]
assert b.judge_model == "custom-judge"
assert b.temperature == 0.3
def test_tools_list_independent(self):
"""Each BenchmarkConfig instance should have its own tools list."""
b1 = BenchmarkConfig(name="a")
b2 = BenchmarkConfig(name="b")
b1.tools.append("calc")
assert b2.tools == []
class TestEvalSuiteConfig:
def test_defaults(self):
s = EvalSuiteConfig()
assert isinstance(s.meta, MetaConfig)
assert isinstance(s.defaults, DefaultsConfig)
assert isinstance(s.judge, JudgeConfig)
assert isinstance(s.run, ExecutionConfig)
assert s.models == []
assert s.benchmarks == []
def test_with_entries(self):
s = EvalSuiteConfig(
meta=MetaConfig(name="test"),
models=[ModelConfig(name="m1"), ModelConfig(name="m2")],
benchmarks=[BenchmarkConfig(name="supergpqa")],
)
assert s.meta.name == "test"
assert len(s.models) == 2
assert len(s.benchmarks) == 1
+2
View File
@@ -130,6 +130,7 @@ nav:
- Tools: user-guide/tools.md
- Telemetry & Traces: user-guide/telemetry.md
- Benchmarks: user-guide/benchmarks.md
- Evaluations: user-guide/evaluations.md
- Security: user-guide/security.md
- Channels: user-guide/channels.md
- Architecture:
@@ -156,6 +157,7 @@ nav:
- Traces: api/traces.md
- Telemetry: api/telemetry.md
- Benchmarks: api/bench.md
- Evals: api/evals.md
- Server: api/server.md
- Security: api/security.md
- Channels: api/channels.md
+16 -5
View File
@@ -124,7 +124,7 @@ def _run_agent(
ctx = AgentContext()
# Inject memory context into conversation if available
if config.memory.context_injection:
if config.agent.context_from_memory:
try:
from openjarvis.memory.context import ContextConfig, inject_context
@@ -151,9 +151,13 @@ def _run_agent(
@click.option("-m", "--model", "model_name", default=None, help="Model to use.")
@click.option("-e", "--engine", "engine_key", default=None, help="Engine backend.")
@click.option(
"-t", "--temperature", default=0.7, type=float, help="Sampling temperature."
"-t", "--temperature", default=None, type=float,
help="Sampling temperature (default: from config).",
)
@click.option(
"--max-tokens", default=None, type=int,
help="Max tokens to generate (default: from config).",
)
@click.option("--max-tokens", default=1024, type=int, help="Max tokens to generate.")
@click.option("--json", "output_json", is_flag=True, help="Output raw JSON result.")
@click.option("--no-stream", is_flag=True, help="Disable streaming (sync mode).")
@click.option(
@@ -187,6 +191,12 @@ def ask(
# Load config
config = load_config()
# Fall back to config values for generation params
if temperature is None:
temperature = config.intelligence.temperature
if max_tokens is None:
max_tokens = config.intelligence.max_tokens
# Set up telemetry
bus = EventBus(record_history=True)
telem_store: TelemetryStore | None = None
@@ -200,7 +210,8 @@ def ask(
# Discover engines
register_builtin_models()
resolved = get_engine(config, engine_key)
effective_engine_key = engine_key or config.intelligence.preferred_engine or None
resolved = get_engine(config, effective_engine_key)
if resolved is None:
console.print(
"[red bold]No inference engine available.[/red bold]\n\n"
@@ -273,7 +284,7 @@ def ask(
messages = [Message(role=Role.USER, content=query_text)]
# Memory-augmented context injection
if not no_context and config.memory.context_injection:
if not no_context and config.agent.context_from_memory:
try:
from openjarvis.memory.context import (
ContextConfig,
+338 -73
View File
@@ -190,35 +190,220 @@ def recommend_engine(hw: HardwareInfo) -> str:
@dataclass(slots=True)
class OllamaEngineConfig:
"""Per-engine config for Ollama."""
host: str = "http://localhost:11434"
@dataclass(slots=True)
class VLLMEngineConfig:
"""Per-engine config for vLLM."""
host: str = "http://localhost:8000"
@dataclass(slots=True)
class SGLangEngineConfig:
"""Per-engine config for SGLang."""
host: str = "http://localhost:30000"
@dataclass(slots=True)
class LlamaCppEngineConfig:
"""Per-engine config for llama.cpp."""
host: str = "http://localhost:8080"
binary_path: str = ""
@dataclass
class EngineConfig:
"""Inference engine settings."""
"""Inference engine settings with nested per-engine configs."""
default: str = "ollama"
ollama_host: str = "http://localhost:11434"
vllm_host: str = "http://localhost:8000"
llamacpp_host: str = "http://localhost:8080"
llamacpp_path: str = ""
sglang_host: str = "http://localhost:30000"
ollama: OllamaEngineConfig = field(default_factory=OllamaEngineConfig)
vllm: VLLMEngineConfig = field(default_factory=VLLMEngineConfig)
sglang: SGLangEngineConfig = field(default_factory=SGLangEngineConfig)
llamacpp: LlamaCppEngineConfig = field(default_factory=LlamaCppEngineConfig)
# Backward-compat properties for old flat attribute names
@property
def ollama_host(self) -> str:
"""Deprecated: use ``engine.ollama.host``."""
return self.ollama.host
@ollama_host.setter
def ollama_host(self, value: str) -> None:
self.ollama.host = value
@property
def vllm_host(self) -> str:
"""Deprecated: use ``engine.vllm.host``."""
return self.vllm.host
@vllm_host.setter
def vllm_host(self, value: str) -> None:
self.vllm.host = value
@property
def llamacpp_host(self) -> str:
"""Deprecated: use ``engine.llamacpp.host``."""
return self.llamacpp.host
@llamacpp_host.setter
def llamacpp_host(self, value: str) -> None:
self.llamacpp.host = value
@property
def llamacpp_path(self) -> str:
"""Deprecated: use ``engine.llamacpp.binary_path``."""
return self.llamacpp.binary_path
@llamacpp_path.setter
def llamacpp_path(self, value: str) -> None:
self.llamacpp.binary_path = value
@property
def sglang_host(self) -> str:
"""Deprecated: use ``engine.sglang.host``."""
return self.sglang.host
@sglang_host.setter
def sglang_host(self, value: str) -> None:
self.sglang.host = value
@dataclass(slots=True)
class IntelligenceConfig:
"""Model routing defaults."""
"""The model — identity, paths, quantization, and generation defaults."""
default_model: str = ""
fallback_model: str = ""
model_path: str = "" # Local weights (HF repo, GGUF file, etc.)
checkpoint_path: str = "" # Checkpoint/adapter path
quantization: str = "none" # none, fp8, int8, int4, gguf_q4, gguf_q8
preferred_engine: str = "" # Override engine for this model (e.g., "vllm")
provider: str = "" # local, openai, anthropic, google
# Generation defaults (overridable per-call)
temperature: float = 0.7
max_tokens: int = 1024
top_p: float = 0.9
top_k: int = 40
repetition_penalty: float = 1.0
stop_sequences: str = "" # Comma-separated stop strings
@dataclass(slots=True)
class LearningConfig:
"""Learning / router policy settings."""
class RoutingLearningConfig:
"""Routing sub-policy config within Learning."""
default_policy: str = "heuristic"
intelligence_policy: str = "none" # "none" | "sft" — updates model routing
agent_policy: str = "none" # "none" | "agent_advisor" — updates agent logic
tools_policy: str = "none" # "none" | "icl_updater" — updates tool usage
reward_weights: str = "" # comma-separated key=value, e.g. "latency=0.4,cost=0.3"
update_interval: int = 10 # traces between learning updates
policy: str = "heuristic" # heuristic | learned | grpo
min_samples: int = 5 # Min traces before trusting learned routing
@dataclass(slots=True)
class IntelligenceLearningConfig:
"""Intelligence sub-policy config within Learning."""
policy: str = "none" # none | sft
@dataclass(slots=True)
class AgentLearningConfig:
"""Agent sub-policy config within Learning."""
policy: str = "none" # none | agent_advisor | icl_updater
max_icl_examples: int = 20
advisor_confidence_threshold: float = 0.7
@dataclass(slots=True)
class MetricsConfig:
"""Reward / optimization metric weights."""
accuracy_weight: float = 0.6
latency_weight: float = 0.2
cost_weight: float = 0.1
efficiency_weight: float = 0.1
@dataclass
class LearningConfig:
"""Learning system settings with per-pillar sub-policies."""
enabled: bool = False
update_interval: int = 100 # traces between automatic policy updates
auto_update: bool = False # auto-trigger updates on interval
routing: RoutingLearningConfig = field(default_factory=RoutingLearningConfig)
intelligence: IntelligenceLearningConfig = field(
default_factory=IntelligenceLearningConfig,
)
agent: AgentLearningConfig = field(default_factory=AgentLearningConfig)
metrics: MetricsConfig = field(default_factory=MetricsConfig)
# Backward-compat properties for old flat field names
@property
def default_policy(self) -> str:
"""Deprecated: use ``learning.routing.policy``."""
return self.routing.policy
@default_policy.setter
def default_policy(self, value: str) -> None:
self.routing.policy = value
@property
def intelligence_policy(self) -> str:
"""Deprecated: use ``learning.intelligence.policy``."""
return self.intelligence.policy
@intelligence_policy.setter
def intelligence_policy(self, value: str) -> None:
self.intelligence.policy = value
@property
def agent_policy(self) -> str:
"""Deprecated: use ``learning.agent.policy``."""
return self.agent.policy
@agent_policy.setter
def agent_policy(self, value: str) -> None:
self.agent.policy = value
@property
def reward_weights(self) -> str:
"""Deprecated: use ``learning.metrics.*``."""
parts = []
m = self.metrics
if m.latency_weight:
parts.append(f"latency={m.latency_weight}")
if m.cost_weight:
parts.append(f"cost={m.cost_weight}")
if m.efficiency_weight:
parts.append(f"efficiency={m.efficiency_weight}")
if m.accuracy_weight:
parts.append(f"accuracy={m.accuracy_weight}")
return ",".join(parts)
@reward_weights.setter
def reward_weights(self, value: str) -> None:
if not value:
return
for part in value.split(","):
if "=" not in part:
continue
key, val = part.strip().split("=", 1)
key = key.strip()
fval = float(val.strip())
if key == "accuracy":
self.metrics.accuracy_weight = fval
elif key == "latency":
self.metrics.latency_weight = fval
elif key == "cost":
self.metrics.cost_weight = fval
elif key == "efficiency":
self.metrics.efficiency_weight = fval
@dataclass(slots=True)
@@ -227,7 +412,6 @@ class StorageConfig:
default_backend: str = "sqlite"
db_path: str = str(DEFAULT_CONFIG_DIR / "memory.db")
context_injection: bool = True
context_top_k: int = 5
context_min_score: float = 0.1
context_max_tokens: int = 2048
@@ -245,8 +429,6 @@ class MCPConfig:
enabled: bool = True
servers: str = "" # JSON list of MCP server configs
expose_storage: bool = True
expose_llm: bool = True
@dataclass(slots=True)
@@ -258,15 +440,27 @@ class ToolsConfig:
enabled: str = "" # comma-separated default tools
@dataclass(slots=True)
@dataclass
class AgentConfig:
"""Agent defaults."""
"""Agent harness settings — orchestration, tools, system prompt."""
default_agent: str = "simple"
max_turns: int = 3
default_tools: str = "" # comma-separated tool names
temperature: float = 0.7
max_tokens: int = 1024
max_turns: int = 10
tools: str = "" # comma-separated tool names
objective: str = "" # concise purpose for routing/learning/docs
system_prompt: str = "" # inline system prompt (takes precedence if set)
system_prompt_path: str = "" # path to system prompt file (.txt, .md)
context_from_memory: bool = True # inject relevant memory context into prompts
# Backward-compat property for old field name
@property
def default_tools(self) -> str:
"""Deprecated: use ``agent.tools``."""
return self.tools
@default_tools.setter
def default_tools(self, value: str) -> None:
self.tools = value
@dataclass(slots=True)
@@ -495,10 +689,53 @@ class JarvisConfig:
def _apply_toml_section(target: Any, section: Dict[str, Any]) -> None:
"""Overlay TOML key/value pairs onto a dataclass instance."""
"""Overlay TOML key/value pairs onto a dataclass instance.
Recursively handles nested dicts when the target attribute is itself
a dataclass.
"""
for key, value in section.items():
if hasattr(target, key):
setattr(target, key, value)
if isinstance(value, dict):
nested = getattr(target, key)
if hasattr(nested, "__dataclass_fields__"):
_apply_toml_section(nested, value)
else:
setattr(target, key, value)
else:
setattr(target, key, value)
def _migrate_toml_data(data: Dict[str, Any], cfg: "JarvisConfig") -> None:
"""Migrate old-format TOML keys to new structure in-place.
Handles cross-section moves that can't be solved by backward-compat
properties alone (e.g. ``agent.temperature`` → ``intelligence.temperature``).
"""
# agent.temperature / agent.max_tokens → intelligence.*
if "agent" in data:
agent_data = data["agent"]
intel_data = data.setdefault("intelligence", {})
for moved_key in ("temperature", "max_tokens"):
if moved_key in agent_data:
intel_data.setdefault(moved_key, agent_data.pop(moved_key))
# context_injection from memory / tools.storage → agent.context_from_memory
for src_section in ("memory",):
src = data.get(src_section, {})
if isinstance(src, dict) and "context_injection" in src:
data.setdefault("agent", {}).setdefault(
"context_from_memory", src.pop("context_injection"),
)
if "tools" in data:
tools_data = data["tools"]
if isinstance(tools_data, dict):
storage_sub = tools_data.get("storage", {})
if isinstance(storage_sub, dict) and "context_injection" in storage_sub:
data.setdefault("agent", {}).setdefault(
"context_from_memory", storage_sub.pop("context_injection"),
)
def load_config(path: Optional[Path] = None) -> JarvisConfig:
@@ -518,49 +755,26 @@ def load_config(path: Optional[Path] = None) -> JarvisConfig:
with open(config_path, "rb") as fh:
data = tomllib.load(fh)
# Simple top-level sections
simple_sections = (
"engine", "intelligence", "learning",
"agent", "server", "telemetry", "traces", "security",
# Run backward-compat migrations before applying
_migrate_toml_data(data, cfg)
# All top-level sections — recursive _apply_toml_section handles
# nested sub-configs (engine.ollama, learning.routing, channel.*, etc.)
top_sections = (
"engine", "intelligence", "learning", "agent",
"server", "telemetry", "traces", "security",
"channel", "tools",
)
for section_name in simple_sections:
for section_name in top_sections:
if section_name in data:
_apply_toml_section(getattr(cfg, section_name), data[section_name])
_apply_toml_section(
getattr(cfg, section_name), data[section_name],
)
# Memory: accept [memory] (old) → maps to tools.storage
if "memory" in data:
_apply_toml_section(cfg.tools.storage, data["memory"])
# [channel] with nested per-channel sub-configs
if "channel" in data:
ch_data = data["channel"]
# Top-level channel keys (enabled, default_channel, etc.)
for key, value in ch_data.items():
if not isinstance(value, dict) and hasattr(cfg.channel, key):
setattr(cfg.channel, key, value)
# Nested per-channel configs
for sub in (
"telegram", "discord", "slack", "webhook", "email",
"whatsapp", "signal", "google_chat", "irc", "webchat",
"teams", "matrix", "mattermost", "feishu", "bluebubbles",
):
if sub in ch_data and isinstance(ch_data[sub], dict):
_apply_toml_section(getattr(cfg.channel, sub), ch_data[sub])
# Tools: accept [tools] and nested [tools.storage], [tools.mcp]
if "tools" in data:
tools_data = data["tools"]
# Top-level tools keys (e.g. enabled)
for key, value in tools_data.items():
if not isinstance(value, dict) and hasattr(cfg.tools, key):
setattr(cfg.tools, key, value)
# [tools.storage]
if "storage" in tools_data:
_apply_toml_section(cfg.tools.storage, tools_data["storage"])
# [tools.mcp]
if "mcp" in tools_data:
_apply_toml_section(cfg.tools.mcp, tools_data["mcp"])
return cfg
@@ -585,20 +799,49 @@ def generate_default_toml(hw: HardwareInfo) -> str:
[engine]
default = "{engine}"
ollama_host = "http://localhost:11434"
vllm_host = "http://localhost:8000"
sglang_host = "http://localhost:30000"
[engine.ollama]
host = "http://localhost:11434"
[engine.vllm]
host = "http://localhost:8000"
[engine.sglang]
host = "http://localhost:30000"
# [engine.llamacpp]
# host = "http://localhost:8080"
# binary_path = ""
[intelligence]
default_model = ""
fallback_model = ""
[memory]
default_backend = "sqlite"
# model_path = "" # Local weights (HF repo, GGUF file, etc.)
# checkpoint_path = "" # Checkpoint/adapter path
# quantization = "none" # none, fp8, int8, int4, gguf_q4, gguf_q8
# preferred_engine = "" # Override engine for this model (e.g., "vllm")
# provider = "" # local, openai, anthropic, google
temperature = 0.7
max_tokens = 1024
# top_p = 0.9
# top_k = 40
# repetition_penalty = 1.0
# stop_sequences = ""
[agent]
default_agent = "simple"
max_turns = 10
# tools = "" # Comma-separated tool names
# objective = "" # Concise purpose string
# system_prompt = "" # Inline system prompt
# system_prompt_path = "" # Path to system prompt file
context_from_memory = true
[tools.storage]
default_backend = "sqlite"
[tools.mcp]
enabled = true
[server]
host = "0.0.0.0"
@@ -606,11 +849,25 @@ port = 8000
agent = "orchestrator"
[learning]
default_policy = "heuristic"
# intelligence_policy = "none" # "sft" to learn from traces
# agent_policy = "none" # "agent_advisor" for LM-guided restructuring
# tools_policy = "none" # "icl_updater" for ICL example + skill discovery
# update_interval = 10
enabled = false
update_interval = 100
# auto_update = false
[learning.routing]
policy = "heuristic"
# min_samples = 5
# [learning.intelligence]
# policy = "none" # "sft" to learn from traces
# [learning.agent]
# policy = "none" # "agent_advisor" | "icl_updater"
# [learning.metrics]
# accuracy_weight = 0.6
# latency_weight = 0.2
# cost_weight = 0.1
# efficiency_weight = 0.1
[telemetry]
enabled = true
@@ -684,6 +941,7 @@ enforce_tool_confirmation = true
__all__ = [
"AgentConfig",
"AgentLearningConfig",
"BlueBubblesChannelConfig",
"ChannelConfig",
"DEFAULT_CONFIG_DIR",
@@ -697,12 +955,18 @@ __all__ = [
"HardwareInfo",
"IRCChannelConfig",
"IntelligenceConfig",
"IntelligenceLearningConfig",
"JarvisConfig",
"LearningConfig",
"LlamaCppEngineConfig",
"MCPConfig",
"MatrixChannelConfig",
"MattermostChannelConfig",
"MemoryConfig",
"MetricsConfig",
"OllamaEngineConfig",
"RoutingLearningConfig",
"SGLangEngineConfig",
"SecurityConfig",
"ServerConfig",
"SignalChannelConfig",
@@ -713,6 +977,7 @@ __all__ = [
"TelemetryConfig",
"ToolsConfig",
"TracesConfig",
"VLLMEngineConfig",
"WebChatChannelConfig",
"WebhookChannelConfig",
"WhatsAppChannelConfig",
+2 -9
View File
@@ -1,4 +1,4 @@
"""Intelligence pillar — model management and query routing."""
"""Intelligence pillar — the model definition and catalog."""
from __future__ import annotations
@@ -7,12 +7,5 @@ from openjarvis.intelligence.model_catalog import (
merge_discovered_models,
register_builtin_models,
)
from openjarvis.intelligence.router import HeuristicRouter, build_routing_context
__all__ = [
"BUILTIN_MODELS",
"HeuristicRouter",
"build_routing_context",
"merge_discovered_models",
"register_builtin_models",
]
__all__ = ["BUILTIN_MODELS", "merge_discovered_models", "register_builtin_models"]
+2 -25
View File
@@ -1,28 +1,5 @@
"""Intelligence pillar ABCs — model routing and query analysis."""
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from openjarvis.core.types import RoutingContext
class RouterPolicy(ABC):
"""Abstract interface for model selection policies."""
@abstractmethod
def select_model(self, context: "RoutingContext") -> str:
"""Select the best model key for the given routing context."""
class QueryAnalyzer(ABC):
"""Abstract interface for analyzing queries into routing contexts."""
@abstractmethod
def analyze(self, query: str, **kwargs: object) -> "RoutingContext":
"""Analyze a query and return a RoutingContext."""
"""Backward-compat shim — canonical location is learning._stubs."""
from openjarvis.learning._stubs import QueryAnalyzer, RouterPolicy # noqa: F401
__all__ = ["QueryAnalyzer", "RouterPolicy"]
+5 -160
View File
@@ -1,164 +1,9 @@
"""Heuristic model router — selects the best model based on query characteristics."""
"""Backward-compat shim — canonical location is learning.router."""
from __future__ import annotations
import re
from typing import List, Optional
from openjarvis.core.registry import ModelRegistry
from openjarvis.core.types import RoutingContext
from openjarvis.intelligence._stubs import QueryAnalyzer, RouterPolicy
# Detection patterns
_CODE_PATTERNS = re.compile(
r"```|`[^`]+`|\bdef\s|\bclass\s|\bimport\s|\bfunction\s|\bconst\s|\bvar\s|\blet\s|"
r"\bif\s*\(|->|=>|\{\s*\}|\bfor\s+\w+\s+in\s|#include|System\.out",
re.IGNORECASE,
from openjarvis.learning.router import ( # noqa: F401
DefaultQueryAnalyzer,
HeuristicRouter,
build_routing_context,
)
_MATH_PATTERNS = re.compile(
r"\bsolve\b|\bintegral\b|\bequation\b|\bproof\b|\bderivative\b|\bmatrix\b|"
r"\btheorem\b|\bcalculate\b|\bcompute\b|\bsigma\b|\bsum\b|\blimit\b|\bprobability\b",
re.IGNORECASE,
)
_REASONING_KEYWORDS = re.compile(
r"\bexplain\b|\banalyze\b|\bcompare\b|\bwhy\b"
r"|\bstep[- ]by[- ]step\b|\breason\b|\bthink\b",
re.IGNORECASE,
)
def build_routing_context(query: str, *, urgency: float = 0.5) -> RoutingContext:
"""Populate a ``RoutingContext`` from a raw query string."""
return RoutingContext(
query=query,
query_length=len(query),
has_code=bool(_CODE_PATTERNS.search(query)),
has_math=bool(_MATH_PATTERNS.search(query)),
urgency=urgency,
)
def _model_size(key: str) -> float:
"""Return parameter count for a registered model, or 0 if not found."""
try:
spec = ModelRegistry.get(key)
return spec.parameter_count_b
except (KeyError, AttributeError):
return 0.0
def _find_model_by_tag(available: List[str], tag: str) -> Optional[str]:
"""Find the first available model whose key contains *tag* (case-insensitive)."""
tag_lower = tag.lower()
for key in available:
if tag_lower in key.lower():
return key
return None
def _largest_model(available: List[str]) -> Optional[str]:
"""Return the model with the largest parameter count from the available list."""
if not available:
return None
best = available[0]
best_size = _model_size(best)
for key in available[1:]:
size = _model_size(key)
if size > best_size:
best = key
best_size = size
return best
def _smallest_model(available: List[str]) -> Optional[str]:
"""Return the smallest-parameter model from *available*."""
if not available:
return None
best = available[0]
best_size = _model_size(best) or float("inf")
for key in available[1:]:
size = _model_size(key)
if 0 < size < best_size:
best = key
best_size = size
return best
class HeuristicRouter(RouterPolicy):
"""Rule-based model router.
Rules (applied in order):
1. Code detected → prefer model with "code"/"coder" in name
2. Math detected → prefer larger model
3. Short query (<50 chars, no code/math) → prefer smaller/faster model
4. Long/complex query (>500 chars OR reasoning keywords) → prefer larger model
5. High urgency (>0.8) → override to smaller model
6. Default fallback → default_model → fallback_model → first available
"""
def __init__(
self,
available_models: List[str] | None = None,
*,
default_model: str = "",
fallback_model: str = "",
) -> None:
self._available = available_models or []
self._default = default_model
self._fallback = fallback_model
@property
def available_models(self) -> List[str]:
return list(self._available)
def select_model(self, context: RoutingContext) -> str:
available = self._available or list(ModelRegistry.keys())
if not available:
return self._default or self._fallback or ""
# Rule 5: High urgency overrides everything → smallest model
if context.urgency > 0.8:
return _smallest_model(available) or available[0]
# Rule 1: Code detected → prefer model with code/coder in name
if context.has_code:
code_model = (
_find_model_by_tag(available, "code")
or _find_model_by_tag(available, "coder")
)
if code_model:
return code_model
# Fall through to larger model for code
return _largest_model(available) or available[0]
# Rule 2: Math detected → prefer larger model
if context.has_math:
return _largest_model(available) or available[0]
# Rule 3: Short simple query → prefer smaller model
if context.query_length < 50:
return _smallest_model(available) or available[0]
# Rule 4: Long/complex query → prefer larger model
if context.query_length > 500 or _REASONING_KEYWORDS.search(context.query):
return _largest_model(available) or available[0]
# Rule 6: Default fallback
if self._default and self._default in available:
return self._default
if self._fallback and self._fallback in available:
return self._fallback
return available[0]
class DefaultQueryAnalyzer(QueryAnalyzer):
"""Default query analyzer wrapping the heuristic build_routing_context function."""
def analyze(self, query: str, **kwargs: object) -> RoutingContext:
urgency = kwargs.get("urgency", 0.5)
if not isinstance(urgency, (int, float)):
urgency = 0.5
return build_routing_context(query, urgency=urgency)
__all__ = ["DefaultQueryAnalyzer", "HeuristicRouter", "build_routing_context"]
+14 -2
View File
@@ -1,9 +1,18 @@
"""Learning pillar — router policy and reward functions."""
"""Learning pillar — router policies, reward functions, and trace-driven learning."""
from __future__ import annotations
from openjarvis.learning._stubs import RewardFunction, RouterPolicy, RoutingContext
from openjarvis.learning._stubs import (
QueryAnalyzer,
RewardFunction,
RouterPolicy,
RoutingContext,
)
from openjarvis.learning.heuristic_reward import HeuristicRewardFunction
from openjarvis.learning.router import (
HeuristicRouter,
build_routing_context,
)
def ensure_registered() -> None:
@@ -56,8 +65,11 @@ def ensure_registered() -> None:
__all__ = [
"HeuristicRewardFunction",
"HeuristicRouter",
"QueryAnalyzer",
"RewardFunction",
"RouterPolicy",
"RoutingContext",
"build_routing_context",
"ensure_registered",
]
+19 -3
View File
@@ -1,4 +1,4 @@
"""Learning pillar ABCs and re-exports for backward compatibility."""
"""Learning pillar ABCs — router policies, reward functions, and learning policies."""
from __future__ import annotations
@@ -7,14 +7,29 @@ from typing import TYPE_CHECKING, Any, ClassVar, Dict
from openjarvis.core.registry import LearningRegistry # noqa: F401
# Re-export from canonical locations for backward compatibility
# Re-export from canonical location for backward compatibility
from openjarvis.core.types import RoutingContext # noqa: F401
from openjarvis.intelligence._stubs import RouterPolicy # noqa: F401
if TYPE_CHECKING:
pass
class RouterPolicy(ABC):
"""Model selection policy (used by the learning system)."""
@abstractmethod
def select_model(self, context: "RoutingContext") -> str:
"""Select the best model key for the given routing context."""
class QueryAnalyzer(ABC):
"""Query analysis for routing contexts."""
@abstractmethod
def analyze(self, query: str, **kwargs: object) -> "RoutingContext":
"""Analyze a query and return a RoutingContext."""
class RewardFunction(ABC):
"""Compute a scalar reward for a routing decision."""
@@ -56,6 +71,7 @@ __all__ = [
"IntelligenceLearningPolicy",
"LearningPolicy",
"LearningRegistry",
"QueryAnalyzer",
"RewardFunction",
"RouterPolicy",
"RoutingContext",
+1 -1
View File
@@ -6,7 +6,7 @@ from typing import Any
from openjarvis.core.registry import RouterPolicyRegistry
from openjarvis.core.types import RoutingContext
from openjarvis.intelligence._stubs import RouterPolicy
from openjarvis.learning._stubs import RouterPolicy
class GRPORouterPolicy(RouterPolicy):
+1 -1
View File
@@ -3,7 +3,7 @@
from __future__ import annotations
from openjarvis.core.registry import RouterPolicyRegistry
from openjarvis.intelligence.router import HeuristicRouter
from openjarvis.learning.router import HeuristicRouter
def ensure_registered() -> None:
+164
View File
@@ -0,0 +1,164 @@
"""Heuristic model router — selects the best model based on query characteristics."""
from __future__ import annotations
import re
from typing import List, Optional
from openjarvis.core.registry import ModelRegistry
from openjarvis.core.types import RoutingContext
from openjarvis.learning._stubs import QueryAnalyzer, RouterPolicy
# Detection patterns
_CODE_PATTERNS = re.compile(
r"```|`[^`]+`|\bdef\s|\bclass\s|\bimport\s|\bfunction\s|\bconst\s|\bvar\s|\blet\s|"
r"\bif\s*\(|->|=>|\{\s*\}|\bfor\s+\w+\s+in\s|#include|System\.out",
re.IGNORECASE,
)
_MATH_PATTERNS = re.compile(
r"\bsolve\b|\bintegral\b|\bequation\b|\bproof\b|\bderivative\b|\bmatrix\b|"
r"\btheorem\b|\bcalculate\b|\bcompute\b|\bsigma\b|\bsum\b|\blimit\b|\bprobability\b",
re.IGNORECASE,
)
_REASONING_KEYWORDS = re.compile(
r"\bexplain\b|\banalyze\b|\bcompare\b|\bwhy\b"
r"|\bstep[- ]by[- ]step\b|\breason\b|\bthink\b",
re.IGNORECASE,
)
def build_routing_context(query: str, *, urgency: float = 0.5) -> RoutingContext:
"""Populate a ``RoutingContext`` from a raw query string."""
return RoutingContext(
query=query,
query_length=len(query),
has_code=bool(_CODE_PATTERNS.search(query)),
has_math=bool(_MATH_PATTERNS.search(query)),
urgency=urgency,
)
def _model_size(key: str) -> float:
"""Return parameter count for a registered model, or 0 if not found."""
try:
spec = ModelRegistry.get(key)
return spec.parameter_count_b
except (KeyError, AttributeError):
return 0.0
def _find_model_by_tag(available: List[str], tag: str) -> Optional[str]:
"""Find the first available model whose key contains *tag* (case-insensitive)."""
tag_lower = tag.lower()
for key in available:
if tag_lower in key.lower():
return key
return None
def _largest_model(available: List[str]) -> Optional[str]:
"""Return the model with the largest parameter count from the available list."""
if not available:
return None
best = available[0]
best_size = _model_size(best)
for key in available[1:]:
size = _model_size(key)
if size > best_size:
best = key
best_size = size
return best
def _smallest_model(available: List[str]) -> Optional[str]:
"""Return the smallest-parameter model from *available*."""
if not available:
return None
best = available[0]
best_size = _model_size(best) or float("inf")
for key in available[1:]:
size = _model_size(key)
if 0 < size < best_size:
best = key
best_size = size
return best
class HeuristicRouter(RouterPolicy):
"""Rule-based model router.
Rules (applied in order):
1. Code detected → prefer model with "code"/"coder" in name
2. Math detected → prefer larger model
3. Short query (<50 chars, no code/math) → prefer smaller/faster model
4. Long/complex query (>500 chars OR reasoning keywords) → prefer larger model
5. High urgency (>0.8) → override to smaller model
6. Default fallback → default_model → fallback_model → first available
"""
def __init__(
self,
available_models: List[str] | None = None,
*,
default_model: str = "",
fallback_model: str = "",
) -> None:
self._available = available_models or []
self._default = default_model
self._fallback = fallback_model
@property
def available_models(self) -> List[str]:
return list(self._available)
def select_model(self, context: RoutingContext) -> str:
available = self._available or list(ModelRegistry.keys())
if not available:
return self._default or self._fallback or ""
# Rule 5: High urgency overrides everything → smallest model
if context.urgency > 0.8:
return _smallest_model(available) or available[0]
# Rule 1: Code detected → prefer model with code/coder in name
if context.has_code:
code_model = (
_find_model_by_tag(available, "code")
or _find_model_by_tag(available, "coder")
)
if code_model:
return code_model
# Fall through to larger model for code
return _largest_model(available) or available[0]
# Rule 2: Math detected → prefer larger model
if context.has_math:
return _largest_model(available) or available[0]
# Rule 3: Short simple query → prefer smaller model
if context.query_length < 50:
return _smallest_model(available) or available[0]
# Rule 4: Long/complex query → prefer larger model
if context.query_length > 500 or _REASONING_KEYWORDS.search(context.query):
return _largest_model(available) or available[0]
# Rule 6: Default fallback
if self._default and self._default in available:
return self._default
if self._fallback and self._fallback in available:
return self._fallback
return available[0]
class DefaultQueryAnalyzer(QueryAnalyzer):
"""Default query analyzer wrapping the heuristic build_routing_context function."""
def analyze(self, query: str, **kwargs: object) -> RoutingContext:
urgency = kwargs.get("urgency", 0.5)
if not isinstance(urgency, (int, float)):
urgency = 0.5
return build_routing_context(query, urgency=urgency)
__all__ = ["DefaultQueryAnalyzer", "HeuristicRouter", "build_routing_context"]
+1 -1
View File
@@ -14,7 +14,7 @@ from typing import Any, Dict, List, Optional
from openjarvis.core.registry import RouterPolicyRegistry
from openjarvis.core.types import RoutingContext
from openjarvis.intelligence._stubs import RouterPolicy
from openjarvis.learning._stubs import RouterPolicy
from openjarvis.traces.analyzer import TraceAnalyzer
# Query classification for grouping traces
+14 -7
View File
@@ -187,7 +187,9 @@ class Jarvis:
except ImportError:
pass
resolved = get_engine(self._config, self._engine_key)
pref = self._config.intelligence.preferred_engine
engine_key = self._engine_key or pref or None
resolved = get_engine(self._config, engine_key)
if resolved is None:
raise RuntimeError(
"No inference engine available. "
@@ -229,8 +231,8 @@ class Jarvis:
model: Optional[str] = None,
agent: Optional[str] = None,
tools: Optional[List[str]] = None,
temperature: float = 0.7,
max_tokens: int = 1024,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
context: bool = True,
) -> str:
"""Send a query and return the response text."""
@@ -252,8 +254,8 @@ class Jarvis:
model: Optional[str] = None,
agent: Optional[str] = None,
tools: Optional[List[str]] = None,
temperature: float = 0.7,
max_tokens: int = 1024,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
context: bool = True,
) -> Dict[str, Any]:
"""Send a query and return the full result dict.
@@ -261,6 +263,11 @@ class Jarvis:
Returns a dict with keys: content, usage, tool_results (if agent mode).
"""
self._ensure_engine()
if temperature is None:
temperature = self._config.intelligence.temperature
if max_tokens is None:
max_tokens = self._config.intelligence.max_tokens
model_name = model or self._model_override
# Resolve model via router if not specified
@@ -285,7 +292,7 @@ class Jarvis:
messages = [Message(role=Role.USER, content=query)]
# Memory context injection
if context and self._config.memory.context_injection:
if context and self._config.agent.context_from_memory:
messages = self._inject_context(query, messages)
result = instrumented_generate(
@@ -364,7 +371,7 @@ class Jarvis:
ctx = AgentContext()
# Context injection
if context and self._config.memory.context_injection:
if context and self._config.agent.context_from_memory:
try:
from openjarvis.cli.ask import _get_memory_backend
from openjarvis.memory.context import ContextConfig, inject_context
+11 -5
View File
@@ -38,16 +38,21 @@ class JarvisSystem:
query: str,
*,
context: bool = True,
temperature: float = 0.7,
max_tokens: int = 1024,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
agent: Optional[str] = None,
tools: Optional[List[str]] = None,
) -> Dict[str, Any]:
"""Execute a query through the system and return a result dict."""
if temperature is None:
temperature = self.config.intelligence.temperature
if max_tokens is None:
max_tokens = self.config.intelligence.max_tokens
messages = [Message(role=Role.USER, content=query)]
# Context injection from memory
if context and self.memory_backend and self.config.memory.context_injection:
if context and self.memory_backend and self.config.agent.context_from_memory:
try:
from openjarvis.tools.storage.context import (
ContextConfig,
@@ -299,7 +304,8 @@ class SystemBuilder:
"""Resolve the inference engine."""
from openjarvis.engine._discovery import get_engine
key = self._engine_key or config.engine.default
pref = config.intelligence.preferred_engine
key = self._engine_key or pref or config.engine.default
resolved = get_engine(config, key)
if resolved is None:
raise RuntimeError(
@@ -511,7 +517,7 @@ class SystemBuilder:
# 3. Determine which tool names to include
tool_names = self._tool_names
if tool_names is None:
raw = config.tools.enabled or config.agent.default_tools
raw = config.tools.enabled or config.agent.tools
if raw:
tool_names = [n.strip() for n in raw.split(",") if n.strip()]
else:
+2 -2
View File
@@ -83,7 +83,7 @@ class TestAskModelResolution:
cfg.telemetry.enabled = False
cfg.intelligence.default_model = ""
cfg.intelligence.fallback_model = ""
cfg.memory.context_injection = False
cfg.agent.context_from_memory = False
result = CliRunner().invoke(cli, ["ask", "Hello"])
assert result.exit_code == 0
@@ -107,6 +107,6 @@ class TestAskModelResolution:
cfg.telemetry.enabled = False
cfg.intelligence.default_model = ""
cfg.intelligence.fallback_model = "fallback-model"
cfg.memory.context_injection = False
cfg.agent.context_from_memory = False
result = CliRunner().invoke(cli, ["ask", "Hello"])
assert result.exit_code == 0
+215
View File
@@ -5,11 +5,14 @@ from __future__ import annotations
from pathlib import Path
from openjarvis.core.config import (
AgentConfig,
ChannelConfig,
EngineConfig,
GpuInfo,
HardwareInfo,
IntelligenceConfig,
JarvisConfig,
LearningConfig,
SecurityConfig,
generate_default_toml,
load_config,
@@ -26,6 +29,13 @@ class TestDefaults:
def test_engine_config_defaults(self) -> None:
ec = EngineConfig()
# Nested configs
assert ec.ollama.host == "http://localhost:11434"
assert ec.vllm.host == "http://localhost:8000"
assert ec.sglang.host == "http://localhost:30000"
assert ec.llamacpp.host == "http://localhost:8080"
assert ec.llamacpp.binary_path == ""
# Backward-compat properties still work
assert ec.ollama_host == "http://localhost:11434"
assert ec.vllm_host == "http://localhost:8000"
@@ -145,3 +155,208 @@ class TestChannelConfig:
def test_channel_config_in_default_toml(self) -> None:
output = generate_default_toml(HardwareInfo())
assert "[channel]" in output
# ---------------------------------------------------------------------------
# New config structure tests
# ---------------------------------------------------------------------------
class TestIntelligenceGenerationDefaults:
def test_generation_fields_exist(self) -> None:
ic = IntelligenceConfig()
assert ic.temperature == 0.7
assert ic.max_tokens == 1024
assert ic.top_p == 0.9
assert ic.top_k == 40
assert ic.repetition_penalty == 1.0
assert ic.stop_sequences == ""
def test_custom_generation_values(self) -> None:
ic = IntelligenceConfig(temperature=0.3, max_tokens=512, top_p=0.5)
assert ic.temperature == 0.3
assert ic.max_tokens == 512
assert ic.top_p == 0.5
class TestAgentConfigNew:
def test_new_fields(self) -> None:
ac = AgentConfig()
assert ac.objective == ""
assert ac.system_prompt == ""
assert ac.system_prompt_path == ""
assert ac.context_from_memory is True
assert ac.tools == ""
assert ac.max_turns == 10
def test_default_tools_backward_compat(self) -> None:
ac = AgentConfig()
ac.default_tools = "calculator,think"
assert ac.tools == "calculator,think"
assert ac.default_tools == "calculator,think"
def test_no_temperature_or_max_tokens(self) -> None:
ac = AgentConfig()
assert not hasattr(ac.__class__, "temperature") or isinstance(
getattr(ac.__class__, "temperature", None), property
) is False
class TestNestedEngineConfig:
def test_nested_access(self) -> None:
ec = EngineConfig()
assert ec.ollama.host == "http://localhost:11434"
assert ec.vllm.host == "http://localhost:8000"
assert ec.sglang.host == "http://localhost:30000"
assert ec.llamacpp.host == "http://localhost:8080"
assert ec.llamacpp.binary_path == ""
def test_backward_compat_setter(self) -> None:
ec = EngineConfig()
ec.ollama_host = "http://custom:1234"
assert ec.ollama.host == "http://custom:1234"
assert ec.ollama_host == "http://custom:1234"
def test_llamacpp_path_compat(self) -> None:
ec = EngineConfig()
ec.llamacpp_path = "/usr/bin/llama-server"
assert ec.llamacpp.binary_path == "/usr/bin/llama-server"
def test_loads_nested_toml(self, tmp_path: Path) -> None:
toml_file = tmp_path / "config.toml"
toml_file.write_text(
'[engine]\ndefault = "vllm"\n\n'
'[engine.ollama]\nhost = "http://custom:11434"\n\n'
'[engine.llamacpp]\nbinary_path = "/opt/llama"\n'
)
cfg = load_config(toml_file)
assert cfg.engine.default == "vllm"
assert cfg.engine.ollama.host == "http://custom:11434"
assert cfg.engine.llamacpp.binary_path == "/opt/llama"
def test_loads_old_flat_toml(self, tmp_path: Path) -> None:
"""Old flat engine keys still work via backward-compat properties."""
toml_file = tmp_path / "config.toml"
toml_file.write_text(
'[engine]\ndefault = "ollama"\n'
'ollama_host = "http://old:11434"\n'
'vllm_host = "http://old:8000"\n'
)
cfg = load_config(toml_file)
assert cfg.engine.ollama.host == "http://old:11434"
assert cfg.engine.vllm.host == "http://old:8000"
class TestNestedLearningConfig:
def test_defaults(self) -> None:
lc = LearningConfig()
assert lc.enabled is False
assert lc.update_interval == 100
assert lc.auto_update is False
assert lc.routing.policy == "heuristic"
assert lc.routing.min_samples == 5
assert lc.intelligence.policy == "none"
assert lc.agent.policy == "none"
assert lc.agent.max_icl_examples == 20
assert lc.metrics.accuracy_weight == 0.6
assert lc.metrics.latency_weight == 0.2
def test_backward_compat_default_policy(self) -> None:
lc = LearningConfig()
assert lc.default_policy == "heuristic"
lc.default_policy = "grpo"
assert lc.routing.policy == "grpo"
def test_backward_compat_intelligence_policy(self) -> None:
lc = LearningConfig()
lc.intelligence_policy = "sft"
assert lc.intelligence.policy == "sft"
def test_backward_compat_agent_policy(self) -> None:
lc = LearningConfig()
lc.agent_policy = "agent_advisor"
assert lc.agent.policy == "agent_advisor"
def test_backward_compat_reward_weights(self) -> None:
lc = LearningConfig()
lc.reward_weights = "latency=0.4,cost=0.3"
assert lc.metrics.latency_weight == 0.4
assert lc.metrics.cost_weight == 0.3
def test_loads_nested_toml(self, tmp_path: Path) -> None:
toml_file = tmp_path / "config.toml"
toml_file.write_text(
'[learning]\nenabled = true\nupdate_interval = 50\n\n'
'[learning.routing]\npolicy = "learned"\n\n'
'[learning.metrics]\nlatency_weight = 0.5\n'
)
cfg = load_config(toml_file)
assert cfg.learning.enabled is True
assert cfg.learning.update_interval == 50
assert cfg.learning.routing.policy == "learned"
assert cfg.learning.metrics.latency_weight == 0.5
def test_loads_old_flat_learning_toml(self, tmp_path: Path) -> None:
"""Old flat learning keys still work via backward-compat properties."""
toml_file = tmp_path / "config.toml"
toml_file.write_text(
'[learning]\ndefault_policy = "grpo"\n'
'intelligence_policy = "sft"\n'
'reward_weights = "latency=0.5"\n'
)
cfg = load_config(toml_file)
assert cfg.learning.routing.policy == "grpo"
assert cfg.learning.intelligence.policy == "sft"
assert cfg.learning.metrics.latency_weight == 0.5
class TestBackwardCompatMigration:
def test_agent_temperature_migrates_to_intelligence(self, tmp_path: Path) -> None:
toml_file = tmp_path / "config.toml"
toml_file.write_text(
'[agent]\ntemperature = 0.3\nmax_tokens = 512\n'
)
cfg = load_config(toml_file)
assert cfg.intelligence.temperature == 0.3
assert cfg.intelligence.max_tokens == 512
def test_memory_context_injection_migrates_to_agent(self, tmp_path: Path) -> None:
toml_file = tmp_path / "config.toml"
toml_file.write_text('[memory]\ncontext_injection = false\n')
cfg = load_config(toml_file)
assert cfg.agent.context_from_memory is False
def test_tools_storage_context_injection_migrates(self, tmp_path: Path) -> None:
toml_file = tmp_path / "config.toml"
toml_file.write_text(
'[tools.storage]\ncontext_injection = false\ndefault_backend = "faiss"\n'
)
cfg = load_config(toml_file)
assert cfg.agent.context_from_memory is False
assert cfg.tools.storage.default_backend == "faiss"
class TestGenerateDefaultTomlNew:
def test_nested_engine_sections(self) -> None:
hw = HardwareInfo()
toml_str = generate_default_toml(hw)
assert "[engine.ollama]" in toml_str
assert "[engine.vllm]" in toml_str
assert "[engine.sglang]" in toml_str
def test_intelligence_generation_params(self) -> None:
hw = HardwareInfo()
toml_str = generate_default_toml(hw)
assert "temperature = 0.7" in toml_str
assert "max_tokens = 1024" in toml_str
def test_agent_new_fields(self) -> None:
hw = HardwareInfo()
toml_str = generate_default_toml(hw)
assert "context_from_memory = true" in toml_str
def test_learning_nested_sections(self) -> None:
hw = HardwareInfo()
toml_str = generate_default_toml(hw)
assert "[learning.routing]" in toml_str
assert 'policy = "heuristic"' in toml_str
+15 -11
View File
@@ -15,21 +15,23 @@ class TestAgentConfig:
def test_defaults(self):
cfg = AgentConfig()
assert cfg.default_agent == "simple"
assert cfg.max_turns == 3
assert cfg.default_tools == ""
assert cfg.temperature == 0.7
assert cfg.max_tokens == 1024
assert cfg.max_turns == 10
assert cfg.tools == ""
assert cfg.default_tools == "" # backward-compat property
assert cfg.objective == ""
assert cfg.system_prompt == ""
assert cfg.system_prompt_path == ""
assert cfg.context_from_memory is True
def test_custom_values(self):
cfg = AgentConfig(
default_agent="orchestrator",
max_turns=5,
default_tools="calculator,think",
temperature=0.1,
max_tokens=512,
tools="calculator,think",
)
assert cfg.default_agent == "orchestrator"
assert cfg.default_tools == "calculator,think"
assert cfg.tools == "calculator,think"
assert cfg.default_tools == "calculator,think" # backward-compat
class TestServerConfig:
@@ -55,9 +57,11 @@ class TestJarvisConfig:
def test_agent_config_expanded(self):
cfg = JarvisConfig()
assert hasattr(cfg.agent, "default_tools")
assert hasattr(cfg.agent, "temperature")
assert hasattr(cfg.agent, "max_tokens")
assert hasattr(cfg.agent, "default_tools") # backward-compat property
assert hasattr(cfg.agent, "tools")
assert hasattr(cfg.agent, "objective")
assert hasattr(cfg.agent, "system_prompt")
assert hasattr(cfg.agent, "context_from_memory")
class TestGenerateDefaultToml:
+35 -13
View File
@@ -16,22 +16,31 @@ from openjarvis.core.config import (
class TestLearningConfig:
def test_defaults(self) -> None:
cfg = LearningConfig()
assert cfg.enabled is False
assert cfg.update_interval == 100
assert cfg.auto_update is False
assert cfg.routing.policy == "heuristic"
assert cfg.intelligence.policy == "none"
assert cfg.agent.policy == "none"
assert cfg.metrics.accuracy_weight == 0.6
# Backward-compat properties
assert cfg.default_policy == "heuristic"
assert cfg.reward_weights == ""
def test_custom_values(self) -> None:
cfg = LearningConfig(
default_policy="grpo",
reward_weights="latency=0.4,cost=0.3,efficiency=0.3",
)
assert cfg.default_policy == "grpo"
assert cfg.reward_weights == "latency=0.4,cost=0.3,efficiency=0.3"
def test_backward_compat_custom_values(self) -> None:
cfg = LearningConfig()
cfg.default_policy = "grpo"
cfg.reward_weights = "latency=0.4,cost=0.3,efficiency=0.3"
assert cfg.routing.policy == "grpo"
assert cfg.metrics.latency_weight == 0.4
assert cfg.metrics.cost_weight == 0.3
assert cfg.metrics.efficiency_weight == 0.3
def test_jarvis_config_has_learning(self) -> None:
cfg = JarvisConfig()
assert hasattr(cfg, "learning")
assert isinstance(cfg.learning, LearningConfig)
assert cfg.learning.default_policy == "heuristic"
assert cfg.learning.routing.policy == "heuristic"
assert cfg.learning.default_policy == "heuristic" # backward-compat
def test_toml_loading_with_learning(self, tmp_path: Path) -> None:
toml_file = tmp_path / "config.toml"
@@ -40,17 +49,30 @@ class TestLearningConfig:
'reward_weights = "latency=0.5"\n'
)
cfg = load_config(toml_file)
assert cfg.learning.default_policy == "grpo"
assert cfg.learning.reward_weights == "latency=0.5"
assert cfg.learning.routing.policy == "grpo"
assert cfg.learning.metrics.latency_weight == 0.5
def test_toml_loading_nested(self, tmp_path: Path) -> None:
toml_file = tmp_path / "config.toml"
toml_file.write_text(
'[learning]\nenabled = true\n\n'
'[learning.routing]\npolicy = "learned"\n\n'
'[learning.metrics]\nlatency_weight = 0.5\n'
)
cfg = load_config(toml_file)
assert cfg.learning.enabled is True
assert cfg.learning.routing.policy == "learned"
assert cfg.learning.metrics.latency_weight == 0.5
def test_toml_loading_without_learning(self, tmp_path: Path) -> None:
toml_file = tmp_path / "config.toml"
toml_file.write_text("[engine]\n")
cfg = load_config(toml_file)
assert cfg.learning.default_policy == "heuristic"
assert cfg.learning.routing.policy == "heuristic"
def test_generate_default_toml_includes_learning(self) -> None:
hw = HardwareInfo()
toml_str = generate_default_toml(hw)
assert "[learning]" in toml_str
assert 'default_policy = "heuristic"' in toml_str
assert "[learning.routing]" in toml_str
assert 'policy = "heuristic"' in toml_str
+39 -49
View File
@@ -1,70 +1,58 @@
"""Tests for Intelligence pillar ABCs."""
"""Tests for Intelligence pillar backward-compat shims."""
from __future__ import annotations
import pytest
from openjarvis.core.types import RoutingContext
from openjarvis.intelligence._stubs import QueryAnalyzer, RouterPolicy
from openjarvis.intelligence.router import DefaultQueryAnalyzer
class TestBackwardCompatShims:
"""Verify ABCs are still importable from intelligence._stubs."""
class _DummyRouter(RouterPolicy):
def select_model(self, context: RoutingContext) -> str:
return "test-model"
def test_router_policy_from_intelligence(self) -> None:
from openjarvis.intelligence._stubs import RouterPolicy
class _DummyAnalyzer(QueryAnalyzer):
def analyze(self, query: str, **kwargs: object) -> RoutingContext:
return RoutingContext(query=query, query_length=len(query))
class TestRouterPolicy:
def test_abc_cannot_instantiate(self) -> None:
with pytest.raises(TypeError):
RouterPolicy() # type: ignore[abstract]
def test_concrete_implementation(self) -> None:
router = _DummyRouter()
ctx = RoutingContext(query="hello")
assert router.select_model(ctx) == "test-model"
def test_query_analyzer_from_intelligence(self) -> None:
from openjarvis.intelligence._stubs import QueryAnalyzer
class TestQueryAnalyzer:
def test_abc_cannot_instantiate(self) -> None:
with pytest.raises(TypeError):
QueryAnalyzer() # type: ignore[abstract]
def test_concrete_implementation(self) -> None:
analyzer = _DummyAnalyzer()
ctx = analyzer.analyze("hello world")
assert ctx.query == "hello world"
assert ctx.query_length == 11
def test_same_class_as_learning(self) -> None:
from openjarvis.intelligence._stubs import QueryAnalyzer as IQA
from openjarvis.intelligence._stubs import RouterPolicy as IRP
from openjarvis.learning._stubs import QueryAnalyzer as LQA
from openjarvis.learning._stubs import RouterPolicy as LRP
assert IRP is LRP
assert IQA is LQA
def test_router_from_intelligence_module(self) -> None:
"""HeuristicRouter still importable from intelligence.router."""
from openjarvis.intelligence.router import (
HeuristicRouter,
build_routing_context,
)
ctx = build_routing_context("hello")
assert ctx.query == "hello"
router = HeuristicRouter(
available_models=[], default_model="m",
)
assert router.select_model(ctx) == "m"
def test_default_query_analyzer_from_intelligence(
self,
) -> None:
from openjarvis.intelligence.router import (
DefaultQueryAnalyzer,
)
class TestDefaultQueryAnalyzer:
def test_analyze_basic(self) -> None:
analyzer = DefaultQueryAnalyzer()
ctx = analyzer.analyze("Hello world")
assert ctx.query == "Hello world"
assert ctx.query_length == 11
assert ctx.has_code is False
assert ctx.has_math is False
def test_analyze_code_query(self) -> None:
analyzer = DefaultQueryAnalyzer()
ctx = analyzer.analyze("def hello(): pass")
assert ctx.has_code is True
def test_analyze_math_query(self) -> None:
analyzer = DefaultQueryAnalyzer()
ctx = analyzer.analyze("solve the integral of x^2")
assert ctx.has_math is True
def test_analyze_with_urgency(self) -> None:
analyzer = DefaultQueryAnalyzer()
ctx = analyzer.analyze("quick question", urgency=0.9)
assert ctx.urgency == 0.9
class TestRoutingContextInCoreTypes:
@@ -72,17 +60,19 @@ class TestRoutingContextInCoreTypes:
def test_import_from_core_types(self) -> None:
from openjarvis.core.types import RoutingContext as RC
ctx = RC(query="test", has_code=True)
assert ctx.query == "test"
assert ctx.has_code is True
def test_backward_compat_import(self) -> None:
"""RoutingContext should still be importable from learning._stubs."""
from openjarvis.learning._stubs import RoutingContext as RC
ctx = RC(query="compat")
assert ctx.query == "compat"
def test_router_policy_backward_compat(self) -> None:
"""RouterPolicy should still be importable from learning._stubs."""
from openjarvis.intelligence._stubs import RouterPolicy
from openjarvis.learning._stubs import RouterPolicy as RP
assert RP is RouterPolicy
+14 -87
View File
@@ -1,15 +1,21 @@
"""Tests for the heuristic model router."""
"""Backward-compat: verify router is still importable from intelligence.
The canonical tests live in tests/learning/test_router.py. This file
verifies the backward-compat shim in intelligence/router.py works.
"""
from __future__ import annotations
from openjarvis.core.registry import ModelRegistry
from openjarvis.core.types import ModelSpec
from openjarvis.intelligence.router import HeuristicRouter, build_routing_context
from openjarvis.intelligence.router import (
HeuristicRouter,
build_routing_context,
)
from openjarvis.learning._stubs import RoutingContext
def _register_models() -> None:
"""Register a small set of models for testing."""
ModelRegistry.register_value(
"small",
ModelSpec(
@@ -24,96 +30,17 @@ def _register_models() -> None:
parameter_count_b=70.0, context_length=131072,
),
)
ModelRegistry.register_value(
"coder",
ModelSpec(
model_id="coder", name="DeepSeek Coder",
parameter_count_b=16.0, context_length=131072,
),
)
class TestBuildRoutingContext:
def test_code_detection(self) -> None:
class TestShimImports:
def test_build_routing_context(self) -> None:
ctx = build_routing_context("def hello():\n pass")
assert ctx.has_code is True
assert ctx.has_math is False
def test_math_detection(self) -> None:
ctx = build_routing_context("solve the integral of x^2")
assert ctx.has_math is True
assert ctx.has_code is False
def test_length(self) -> None:
ctx = build_routing_context("Hi")
assert ctx.query_length == 2
def test_urgency_default(self) -> None:
ctx = build_routing_context("test")
assert ctx.urgency == 0.5
class TestHeuristicRouter:
def test_short_query_prefers_small(self) -> None:
_register_models()
router = HeuristicRouter(available_models=["small", "large", "coder"])
ctx = RoutingContext(query="Hi", query_length=2)
assert router.select_model(ctx) == "small"
def test_code_prefers_coder(self) -> None:
_register_models()
router = HeuristicRouter(available_models=["small", "large", "coder"])
ctx = RoutingContext(query="def foo():", query_length=10, has_code=True)
assert router.select_model(ctx) == "coder"
def test_math_prefers_large(self) -> None:
_register_models()
router = HeuristicRouter(available_models=["small", "large", "coder"])
ctx = RoutingContext(query="solve x", query_length=7, has_math=True)
assert router.select_model(ctx) == "large"
def test_long_query_prefers_large(self) -> None:
_register_models()
router = HeuristicRouter(available_models=["small", "large", "coder"])
ctx = RoutingContext(query="x" * 501, query_length=501)
assert router.select_model(ctx) == "large"
def test_high_urgency_overrides_to_small(self) -> None:
_register_models()
router = HeuristicRouter(available_models=["small", "large", "coder"])
ctx = RoutingContext(query="x" * 501, query_length=501, urgency=0.9)
assert router.select_model(ctx) == "small"
def test_fallback_chain(self) -> None:
def test_heuristic_router(self) -> None:
_register_models()
router = HeuristicRouter(
available_models=["small", "large"],
default_model="large",
fallback_model="small",
)
# Medium-length, no code/math, no reasoning → falls to default
ctx = RoutingContext(query="Tell me about cats", query_length=60)
assert router.select_model(ctx) == "large"
def test_no_available_models(self) -> None:
router = HeuristicRouter(
available_models=[], default_model="fallback-model"
)
ctx = RoutingContext(query="test", query_length=4)
assert router.select_model(ctx) == "fallback-model"
def test_reasoning_keywords_prefer_large(self) -> None:
_register_models()
router = HeuristicRouter(available_models=["small", "large"])
query = (
"Please explain step by step how the process"
" of photosynthesis works in plants"
)
ctx = build_routing_context(query)
assert router.select_model(ctx) == "large"
def test_code_without_coder_falls_to_large(self) -> None:
_register_models()
router = HeuristicRouter(available_models=["small", "large"])
ctx = RoutingContext(query="def foo():", query_length=10, has_code=True)
assert router.select_model(ctx) == "large"
ctx = RoutingContext(query="Hi", query_length=2)
assert router.select_model(ctx) == "small"
+19 -162
View File
@@ -1,178 +1,35 @@
"""Tests for router behavior with the extended model catalog."""
"""Backward-compat: verify router with model catalog via intelligence imports.
Canonical tests live in tests/learning/test_routing_models.py.
"""
from __future__ import annotations
import pytest
from openjarvis.intelligence.model_catalog import register_builtin_models
from openjarvis.intelligence.router import HeuristicRouter, build_routing_context
from openjarvis.intelligence.router import (
HeuristicRouter,
build_routing_context,
)
from openjarvis.learning._stubs import RoutingContext
# New local model keys for testing
NEW_LOCAL_MODELS = [
"gpt-oss:120b", # 117B total, 5.1B active, MoE
"qwen3:8b", # 8.2B, dense
"glm-4.7-flash", # 30B total, 3.0B active, MoE
"trinity-mini", # 26B total, 3.0B active, MoE
]
# Cloud model keys
CLOUD_MODELS = [
"gpt-5-mini",
"claude-opus-4-6",
"claude-sonnet-4-6",
"claude-haiku-4-5",
"gemini-2.5-pro",
"gemini-3-flash",
]
def _setup_models() -> None:
"""Register builtin models needed for the tests."""
register_builtin_models()
class TestRouterWithNewModels:
"""Router behavior when using the new local models."""
def test_short_query_routes_to_smallest(self) -> None:
_setup_models()
router = HeuristicRouter(available_models=NEW_LOCAL_MODELS)
ctx = RoutingContext(query="hi", query_length=2)
selected = router.select_model(ctx)
# qwen3:8b is 8.2B -- the smallest by parameter_count_b
assert selected == "qwen3:8b"
def test_code_query_routes_to_largest(self) -> None:
_setup_models()
router = HeuristicRouter(available_models=NEW_LOCAL_MODELS)
ctx = RoutingContext(
query="def merge_sort(arr):", query_length=22, has_code=True
)
selected = router.select_model(ctx)
# No model with "code"/"coder" in name, falls to largest → gpt-oss:120b (117B)
assert selected == "gpt-oss:120b"
def test_code_query_with_coder_available(self) -> None:
_setup_models()
models = NEW_LOCAL_MODELS + ["deepseek-coder-v2:16b"]
router = HeuristicRouter(available_models=models)
ctx = RoutingContext(
query="import numpy as np", query_length=18, has_code=True
)
selected = router.select_model(ctx)
assert selected == "deepseek-coder-v2:16b"
def test_math_query_routes_to_largest(self) -> None:
_setup_models()
router = HeuristicRouter(available_models=NEW_LOCAL_MODELS)
ctx = RoutingContext(
query="solve the integral of x^2 dx", query_length=29, has_math=True
)
selected = router.select_model(ctx)
assert selected == "gpt-oss:120b"
def test_long_context_routes_to_largest(self) -> None:
_setup_models()
router = HeuristicRouter(available_models=NEW_LOCAL_MODELS)
ctx = RoutingContext(query="x" * 501, query_length=501)
selected = router.select_model(ctx)
assert selected == "gpt-oss:120b"
def test_high_urgency_routes_to_smallest(self) -> None:
_setup_models()
router = HeuristicRouter(available_models=NEW_LOCAL_MODELS)
ctx = RoutingContext(
query="solve the integral of x^2", query_length=25,
has_math=True, urgency=0.9,
)
selected = router.select_model(ctx)
# High urgency overrides everything → smallest params → qwen3:8b (8.2B)
assert selected == "qwen3:8b"
def test_reasoning_query_routes_to_largest(self) -> None:
_setup_models()
router = HeuristicRouter(available_models=NEW_LOCAL_MODELS)
ctx = build_routing_context(
"Please explain step by step how neural networks learn"
)
selected = router.select_model(ctx)
assert selected == "gpt-oss:120b"
def test_medium_query_uses_default(self) -> None:
class TestShimRouterWithModels:
def test_short_query(self) -> None:
_setup_models()
router = HeuristicRouter(
available_models=NEW_LOCAL_MODELS,
default_model="glm-4.7-flash",
available_models=["qwen3:8b", "gpt-oss:120b"],
)
# Medium length query, no code/math/reasoning → rule 6 default
ctx = RoutingContext(query="Tell me about the weather today", query_length=60)
selected = router.select_model(ctx)
assert selected == "glm-4.7-flash"
class TestRouterCloudFallback:
"""Router behavior when only cloud models are available."""
def test_no_local_falls_to_cloud(self) -> None:
_setup_models()
router = HeuristicRouter(available_models=CLOUD_MODELS)
ctx = RoutingContext(query="hi", query_length=2)
selected = router.select_model(ctx)
# Should return one of the cloud models (smallest params)
assert selected in CLOUD_MODELS
def test_cloud_model_selection_with_math(self) -> None:
_setup_models()
router = HeuristicRouter(available_models=CLOUD_MODELS)
ctx = RoutingContext(query="solve x", query_length=7, has_math=True)
selected = router.select_model(ctx)
# All cloud models have parameter_count_b=0 so falls to first
assert selected in CLOUD_MODELS
def test_empty_models_returns_fallback(self) -> None:
router = HeuristicRouter(
available_models=[],
fallback_model="gpt-5-mini",
)
ctx = RoutingContext(query="hello", query_length=5)
assert router.select_model(ctx) == "gpt-5-mini"
class TestRouterParameterized:
"""Parametrized cross-product tests for model/query combinations."""
@pytest.mark.parametrize("query,expected_is_largest", [
("hi", False),
("solve the integral of sin(x)", True),
("def foo(): pass", True), # code → largest when no coder
("x" * 501, True),
])
def test_query_type_selects_expected_size(
self, query: str, expected_is_largest: bool,
) -> None:
_setup_models()
router = HeuristicRouter(available_models=NEW_LOCAL_MODELS)
ctx = build_routing_context(query)
selected = router.select_model(ctx)
if expected_is_largest:
assert selected == "gpt-oss:120b"
else:
assert selected == "qwen3:8b"
@pytest.mark.parametrize("model_id", NEW_LOCAL_MODELS)
def test_single_model_always_returns_it(self, model_id: str) -> None:
_setup_models()
router = HeuristicRouter(available_models=[model_id])
ctx = RoutingContext(query="hello world", query_length=11)
assert router.select_model(ctx) == model_id
@pytest.mark.parametrize("urgency", [0.85, 0.9, 1.0])
def test_high_urgency_always_smallest(self, urgency: float) -> None:
_setup_models()
router = HeuristicRouter(available_models=NEW_LOCAL_MODELS)
ctx = RoutingContext(
query="complex reasoning task", query_length=23,
has_math=True, urgency=urgency,
)
assert router.select_model(ctx) == "qwen3:8b"
def test_code_query(self) -> None:
_setup_models()
router = HeuristicRouter(
available_models=["qwen3:8b", "gpt-oss:120b"],
)
ctx = build_routing_context("def merge_sort(arr):")
assert router.select_model(ctx) == "gpt-oss:120b"
+1 -1
View File
@@ -3,8 +3,8 @@
from __future__ import annotations
from openjarvis.core.registry import RouterPolicyRegistry
from openjarvis.intelligence.router import HeuristicRouter
from openjarvis.learning.heuristic_policy import ensure_registered
from openjarvis.learning.router import HeuristicRouter
class TestHeuristicPolicy:
+144
View File
@@ -0,0 +1,144 @@
"""Tests for the heuristic model router (canonical location)."""
from __future__ import annotations
from openjarvis.core.registry import ModelRegistry
from openjarvis.core.types import ModelSpec
from openjarvis.learning._stubs import RoutingContext
from openjarvis.learning.router import (
HeuristicRouter,
build_routing_context,
)
def _register_models() -> None:
"""Register a small set of models for testing."""
ModelRegistry.register_value(
"small",
ModelSpec(
model_id="small", name="Small",
parameter_count_b=3.0, context_length=4096,
),
)
ModelRegistry.register_value(
"large",
ModelSpec(
model_id="large", name="Large",
parameter_count_b=70.0, context_length=131072,
),
)
ModelRegistry.register_value(
"coder",
ModelSpec(
model_id="coder", name="DeepSeek Coder",
parameter_count_b=16.0, context_length=131072,
),
)
class TestBuildRoutingContext:
def test_code_detection(self) -> None:
ctx = build_routing_context("def hello():\n pass")
assert ctx.has_code is True
assert ctx.has_math is False
def test_math_detection(self) -> None:
ctx = build_routing_context("solve the integral of x^2")
assert ctx.has_math is True
assert ctx.has_code is False
def test_length(self) -> None:
ctx = build_routing_context("Hi")
assert ctx.query_length == 2
def test_urgency_default(self) -> None:
ctx = build_routing_context("test")
assert ctx.urgency == 0.5
class TestHeuristicRouter:
def test_short_query_prefers_small(self) -> None:
_register_models()
router = HeuristicRouter(
available_models=["small", "large", "coder"],
)
ctx = RoutingContext(query="Hi", query_length=2)
assert router.select_model(ctx) == "small"
def test_code_prefers_coder(self) -> None:
_register_models()
router = HeuristicRouter(
available_models=["small", "large", "coder"],
)
ctx = RoutingContext(
query="def foo():", query_length=10, has_code=True,
)
assert router.select_model(ctx) == "coder"
def test_math_prefers_large(self) -> None:
_register_models()
router = HeuristicRouter(
available_models=["small", "large", "coder"],
)
ctx = RoutingContext(
query="solve x", query_length=7, has_math=True,
)
assert router.select_model(ctx) == "large"
def test_long_query_prefers_large(self) -> None:
_register_models()
router = HeuristicRouter(
available_models=["small", "large", "coder"],
)
ctx = RoutingContext(query="x" * 501, query_length=501)
assert router.select_model(ctx) == "large"
def test_high_urgency_overrides_to_small(self) -> None:
_register_models()
router = HeuristicRouter(
available_models=["small", "large", "coder"],
)
ctx = RoutingContext(
query="x" * 501, query_length=501, urgency=0.9,
)
assert router.select_model(ctx) == "small"
def test_fallback_chain(self) -> None:
_register_models()
router = HeuristicRouter(
available_models=["small", "large"],
default_model="large",
fallback_model="small",
)
# Medium-length, no code/math, no reasoning → falls to default
ctx = RoutingContext(
query="Tell me about cats", query_length=60,
)
assert router.select_model(ctx) == "large"
def test_no_available_models(self) -> None:
router = HeuristicRouter(
available_models=[], default_model="fallback-model",
)
ctx = RoutingContext(query="test", query_length=4)
assert router.select_model(ctx) == "fallback-model"
def test_reasoning_keywords_prefer_large(self) -> None:
_register_models()
router = HeuristicRouter(available_models=["small", "large"])
query = (
"Please explain step by step how the process"
" of photosynthesis works in plants"
)
ctx = build_routing_context(query)
assert router.select_model(ctx) == "large"
def test_code_without_coder_falls_to_large(self) -> None:
_register_models()
router = HeuristicRouter(
available_models=["small", "large"],
)
ctx = RoutingContext(
query="def foo():", query_length=10, has_code=True,
)
assert router.select_model(ctx) == "large"
+71
View File
@@ -0,0 +1,71 @@
"""Tests for RouterPolicy and QueryAnalyzer ABCs (canonical location)."""
from __future__ import annotations
import pytest
from openjarvis.core.types import RoutingContext
from openjarvis.learning._stubs import QueryAnalyzer, RouterPolicy
from openjarvis.learning.router import DefaultQueryAnalyzer
class _DummyRouter(RouterPolicy):
def select_model(self, context: RoutingContext) -> str:
return "test-model"
class _DummyAnalyzer(QueryAnalyzer):
def analyze(
self, query: str, **kwargs: object,
) -> RoutingContext:
return RoutingContext(
query=query, query_length=len(query),
)
class TestRouterPolicy:
def test_abc_cannot_instantiate(self) -> None:
with pytest.raises(TypeError):
RouterPolicy() # type: ignore[abstract]
def test_concrete_implementation(self) -> None:
router = _DummyRouter()
ctx = RoutingContext(query="hello")
assert router.select_model(ctx) == "test-model"
class TestQueryAnalyzer:
def test_abc_cannot_instantiate(self) -> None:
with pytest.raises(TypeError):
QueryAnalyzer() # type: ignore[abstract]
def test_concrete_implementation(self) -> None:
analyzer = _DummyAnalyzer()
ctx = analyzer.analyze("hello world")
assert ctx.query == "hello world"
assert ctx.query_length == 11
class TestDefaultQueryAnalyzer:
def test_analyze_basic(self) -> None:
analyzer = DefaultQueryAnalyzer()
ctx = analyzer.analyze("Hello world")
assert ctx.query == "Hello world"
assert ctx.query_length == 11
assert ctx.has_code is False
assert ctx.has_math is False
def test_analyze_code_query(self) -> None:
analyzer = DefaultQueryAnalyzer()
ctx = analyzer.analyze("def hello(): pass")
assert ctx.has_code is True
def test_analyze_math_query(self) -> None:
analyzer = DefaultQueryAnalyzer()
ctx = analyzer.analyze("solve the integral of x^2")
assert ctx.has_math is True
def test_analyze_with_urgency(self) -> None:
analyzer = DefaultQueryAnalyzer()
ctx = analyzer.analyze("quick question", urgency=0.9)
assert ctx.urgency == 0.9
+218
View File
@@ -0,0 +1,218 @@
"""Tests for router behavior with the extended model catalog."""
from __future__ import annotations
import pytest
from openjarvis.intelligence.model_catalog import register_builtin_models
from openjarvis.learning._stubs import RoutingContext
from openjarvis.learning.router import (
HeuristicRouter,
build_routing_context,
)
# New local model keys for testing
NEW_LOCAL_MODELS = [
"gpt-oss:120b", # 117B total, 5.1B active, MoE
"qwen3:8b", # 8.2B, dense
"glm-4.7-flash", # 30B total, 3.0B active, MoE
"trinity-mini", # 26B total, 3.0B active, MoE
]
# Cloud model keys
CLOUD_MODELS = [
"gpt-5-mini",
"claude-opus-4-6",
"claude-sonnet-4-6",
"claude-haiku-4-5",
"gemini-2.5-pro",
"gemini-3-flash",
]
def _setup_models() -> None:
"""Register builtin models needed for the tests."""
register_builtin_models()
class TestRouterWithNewModels:
"""Router behavior when using the new local models."""
def test_short_query_routes_to_smallest(self) -> None:
_setup_models()
router = HeuristicRouter(
available_models=NEW_LOCAL_MODELS,
)
ctx = RoutingContext(query="hi", query_length=2)
selected = router.select_model(ctx)
assert selected == "qwen3:8b"
def test_code_query_routes_to_largest(self) -> None:
_setup_models()
router = HeuristicRouter(
available_models=NEW_LOCAL_MODELS,
)
ctx = RoutingContext(
query="def merge_sort(arr):",
query_length=22,
has_code=True,
)
selected = router.select_model(ctx)
assert selected == "gpt-oss:120b"
def test_code_query_with_coder_available(self) -> None:
_setup_models()
models = NEW_LOCAL_MODELS + ["deepseek-coder-v2:16b"]
router = HeuristicRouter(available_models=models)
ctx = RoutingContext(
query="import numpy as np",
query_length=18,
has_code=True,
)
selected = router.select_model(ctx)
assert selected == "deepseek-coder-v2:16b"
def test_math_query_routes_to_largest(self) -> None:
_setup_models()
router = HeuristicRouter(
available_models=NEW_LOCAL_MODELS,
)
ctx = RoutingContext(
query="solve the integral of x^2 dx",
query_length=29,
has_math=True,
)
selected = router.select_model(ctx)
assert selected == "gpt-oss:120b"
def test_long_context_routes_to_largest(self) -> None:
_setup_models()
router = HeuristicRouter(
available_models=NEW_LOCAL_MODELS,
)
ctx = RoutingContext(query="x" * 501, query_length=501)
selected = router.select_model(ctx)
assert selected == "gpt-oss:120b"
def test_high_urgency_routes_to_smallest(self) -> None:
_setup_models()
router = HeuristicRouter(
available_models=NEW_LOCAL_MODELS,
)
ctx = RoutingContext(
query="solve the integral of x^2",
query_length=25,
has_math=True,
urgency=0.9,
)
selected = router.select_model(ctx)
assert selected == "qwen3:8b"
def test_reasoning_query_routes_to_largest(self) -> None:
_setup_models()
router = HeuristicRouter(
available_models=NEW_LOCAL_MODELS,
)
ctx = build_routing_context(
"Please explain step by step how neural networks"
" learn"
)
selected = router.select_model(ctx)
assert selected == "gpt-oss:120b"
def test_medium_query_uses_default(self) -> None:
_setup_models()
router = HeuristicRouter(
available_models=NEW_LOCAL_MODELS,
default_model="glm-4.7-flash",
)
ctx = RoutingContext(
query="Tell me about the weather today",
query_length=60,
)
selected = router.select_model(ctx)
assert selected == "glm-4.7-flash"
class TestRouterCloudFallback:
"""Router behavior when only cloud models are available."""
def test_no_local_falls_to_cloud(self) -> None:
_setup_models()
router = HeuristicRouter(available_models=CLOUD_MODELS)
ctx = RoutingContext(query="hi", query_length=2)
selected = router.select_model(ctx)
assert selected in CLOUD_MODELS
def test_cloud_model_selection_with_math(self) -> None:
_setup_models()
router = HeuristicRouter(available_models=CLOUD_MODELS)
ctx = RoutingContext(
query="solve x", query_length=7, has_math=True,
)
selected = router.select_model(ctx)
assert selected in CLOUD_MODELS
def test_empty_models_returns_fallback(self) -> None:
router = HeuristicRouter(
available_models=[],
fallback_model="gpt-5-mini",
)
ctx = RoutingContext(query="hello", query_length=5)
assert router.select_model(ctx) == "gpt-5-mini"
class TestRouterParameterized:
"""Parametrized tests for model/query combinations."""
@pytest.mark.parametrize(
"query,expected_is_largest",
[
("hi", False),
("solve the integral of sin(x)", True),
("def foo(): pass", True),
("x" * 501, True),
],
)
def test_query_type_selects_expected_size(
self,
query: str,
expected_is_largest: bool,
) -> None:
_setup_models()
router = HeuristicRouter(
available_models=NEW_LOCAL_MODELS,
)
ctx = build_routing_context(query)
selected = router.select_model(ctx)
if expected_is_largest:
assert selected == "gpt-oss:120b"
else:
assert selected == "qwen3:8b"
@pytest.mark.parametrize("model_id", NEW_LOCAL_MODELS)
def test_single_model_always_returns_it(
self, model_id: str,
) -> None:
_setup_models()
router = HeuristicRouter(available_models=[model_id])
ctx = RoutingContext(
query="hello world", query_length=11,
)
assert router.select_model(ctx) == model_id
@pytest.mark.parametrize("urgency", [0.85, 0.9, 1.0])
def test_high_urgency_always_smallest(
self, urgency: float,
) -> None:
_setup_models()
router = HeuristicRouter(
available_models=NEW_LOCAL_MODELS,
)
ctx = RoutingContext(
query="complex reasoning task",
query_length=23,
has_math=True,
urgency=urgency,
)
assert router.select_model(ctx) == "qwen3:8b"
+1 -1
View File
@@ -405,9 +405,9 @@ class TestAskFlowWithRouterPolicy:
"""Full ask flow with router policy (mocked)."""
def test_mocked_ask_with_registry_router(self):
from openjarvis.intelligence.router import HeuristicRouter
from openjarvis.learning._stubs import RoutingContext
from openjarvis.learning.heuristic_policy import ensure_registered
from openjarvis.learning.router import HeuristicRouter
ensure_registered()
router_cls = RouterPolicyRegistry.get("heuristic")