Rename: jarvisChat → cAIc (product name)

- jarvisChat/JarvisChat/jarvischat → cAIc/cAIc/caic (branded/lower)
- JARVISCHAT_ env vars → CAIC_
- jc- script/config prefix → caic-
- jarvis_rag → caic_rag
- jarvischat.db / volumes → caic.db / caic_*
- AMQP vhost/user jarvischat → caic
- Syslog, loggers, docstrings all updated
- 47 files, zero stale references, 148 tests pass
This commit is contained in:
gramps
2026-07-06 19:56:32 -07:00
parent 94e1cdae11
commit 90d2cf8326
47 changed files with 255 additions and 255 deletions
+8 -8
View File
@@ -1,4 +1,4 @@
# JarvisChat — Agents Guide
# cAIc — Agents Guide
## Run
@@ -60,8 +60,8 @@ Refactored from single-file (`app.py`) into modules under project root:
### Entrypoint / API keys
- `app.py` line 148: `uvicorn.run(app, ...)` when called directly
- `config.py` line 14: `LLAMA_SERVER_BASE` defaults to `http://192.168.50.108:8081` — llama-server on ultron, RPC-offloads GPU layers to jarvis :50052
- `config.py` line 17: `COMPLETIONS_API_KEY` read from `JARVISCHAT_COMPLETIONS_API_KEY` env var or auto-generates
- `config.py` line 14: `LLAMA_SERVER_BASE` defaults to `http://192.168.50.108:8081` — llama-server on coordinator, RPC-offloads GPU layers to worker :50052
- `config.py` line 17: `COMPLETIONS_API_KEY` read from `CAIC_COMPLETIONS_API_KEY` env var or auto-generates
- `config.py` line 13: `OLLAMA_BASE` is legacy/unused — all endpoints use `LLAMA_SERVER_BASE`
### Key flows
@@ -83,11 +83,11 @@ The upstream request includes `"logprobs": true`. `parse_llama_stream_chunk()` e
- `/api/ingest` is exempt from session auth — self-authenticates via Bearer token
- IP allowlist, rate limiting, origin checking, payload size limits — all enforced in `app.py` middleware
- Origin check applies to **all** `/api/` requests; returns `False` when both `Origin` and `Referer` are absent
- `JARVISCHAT_ADMIN_PIN` env var required on first boot (or `JARVISCHAT_ALLOW_DEFAULT_PIN=true`)
- `CAIC_ADMIN_PIN` env var required on first boot (or `CAIC_ALLOW_DEFAULT_PIN=true`)
### Database
- SQLite at `jarvischat.db`, auto-created by `init_db()` on startup via FastAPI `lifespan`
- SQLite at `caic.db`, auto-created by `init_db()` on startup via FastAPI `lifespan`
- `get_db()` opens new connection per request (no pool). Close after use.
- FTS5 virtual table `memories` for full-text search with BM25 ranking.
- `upload_context` table: auto-expiring document storage for chat context injection.
@@ -96,11 +96,11 @@ The upstream request includes `"logprobs": true`. `parse_llama_stream_chunk()` e
| Service | Required | Port |
|---------|----------|------|
| llama-server (ultron) | Yes | 8081 + RPC :50052 (jarvis GPU) |
| llama-server (coordinator) | Yes | 8081 + RPC :50052 (worker GPU) |
| SearXNG | No | 8888 |
| wttr.in | No | weather shortcut |
| rocm-smi | No | AMD GPU stats |
| Qdrant | No | 6333 (ultron) — RAG vector search |
| Qdrant | No | 6333 (coordinator) — RAG vector search |
### Config quirks
@@ -108,7 +108,7 @@ The upstream request includes `"logprobs": true`. `parse_llama_stream_chunk()` e
- `SUPPORTED_UPLOAD_TYPES` includes images (png/jpeg/gif/svg/webp) + text + PDF + JSON
- `UPLOAD_CONTEXT_EXPIRY_HOURS` = 1 hour
- Rate limits and payload caps in `config.py` — patch `security.RL_*` not `config.RL_*` for tests
- RAG embedding requests go to `EMBED_URL` at `/api/embeddings` (Ollama on jarvis :11434)
- RAG embedding requests go to `EMBED_URL` at `/api/embeddings` (Ollama on worker :11434)
### SSE Protocol
+8 -8
View File
@@ -9,7 +9,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
./venv/bin/uvicorn app:app --host 0.0.0.0 --port 8080 --reload
# Production (via systemd)
sudo systemctl restart jarvischat
sudo systemctl restart caic
# Direct run
./venv/bin/python app.py
@@ -24,7 +24,7 @@ sudo systemctl restart jarvischat
## Architecture
Modular FastAPI app — `app.py` wires routers, middleware, and lifespan. SQLite database auto-created at `jarvischat.db` on first run. No build step, single `templates/index.html`.
Modular FastAPI app — `app.py` wires routers, middleware, and lifespan. SQLite database auto-created at `caic.db` on first run. No build step, single `templates/index.html`.
### Request Flow: `/api/chat`
@@ -54,22 +54,22 @@ FTS5 virtual table (`memories`) in SQLite. `search_memories()` uses BM25 ranking
### Key Constants (`config.py`)
- `LLAMA_SERVER_BASE``http://192.168.50.108:8081` (ultron llama-server, RPC offloads to jarvis GPU)
- `LLAMA_SERVER_BASE``http://192.168.50.108:8081` (coordinator llama-server, RPC offloads to worker GPU)
- `SEARXNG_BASE``http://localhost:8888`
- `PERPLEXITY_THRESHOLD``15.0`
- `EMBED_URL``http://192.168.50.210:11434/api/embeddings` (Ollama on jarvis)
- `EMBED_URL``http://192.168.50.210:11434/api/embeddings` (Ollama on worker)
- `VERSION` — current version string
### External Services
| Service | Required | Port |
|---------|----------|------|
| **llama-server** (ultron) | Yes | 8081 + RPC :50052 (jarvis GPU) |
| **llama-server** (coordinator) | Yes | 8081 + RPC :50052 (worker GPU) |
| **SearXNG** | No | 8888 |
| **wttr.in** | No | weather shortcut |
| **rocm-smi** | No | AMD GPU stats |
| **Qdrant** (ultron) | No | 6333 — RAG vector search |
| **Ollama** (jarvis) | No | 11434 — embeddings only |
| **Qdrant** (coordinator) | No | 6333 — RAG vector search |
| **Ollama** (worker) | No | 11434 — embeddings only |
### Database
@@ -86,4 +86,4 @@ All streaming endpoints yield `data: {json}\n\n`. Key event shapes:
### Deployment
Runs as systemd service under user `jarvischat`, working directory `/opt/jarvischat`. Logs via syslog (`journalctl -u jarvischat`). Version bumps via git tag + commit, deployed via `git pull && systemctl restart jarvischat`.
Runs as systemd service under user `caic`, working directory `/opt/caic`. Logs via syslog (`journalctl -u caic`). Version bumps via git tag + commit, deployed via `git pull && systemctl restart caic`.
+22 -22
View File
@@ -1,8 +1,8 @@
# jarvisChat v0.14.0
# cAIc v0.14.0
You have a garage full of retired office PCs, a GPU that was mid-range when Obama was president, and a burning desire to chat with a language model without renting some billionaire's server farm. Congratulations — you've found your people.
jarvisChat is a chat UI that grew limbs. It started as a single-file Python script because OpenWebUI wouldn't install on Debian 13, and somewhere along the way it learned to file paperwork (file attachments), write things down (RAG ingest), boss around other computers (AMQP clustering), and check its own pulse (hardware self-assessment). It now does all the things you didn't ask for, plus a few you might actually use.
cAIc is a chat UI that grew limbs. It started as a single-file Python script because OpenWebUI wouldn't install on Debian 13, and somewhere along the way it learned to file paperwork (file attachments), write things down (RAG ingest), boss around other computers (AMQP clustering), and check its own pulse (hardware self-assessment). It now does all the things you didn't ask for, plus a few you might actually use.
Under the hood: FastAPI + SQLite + Jinja2 on Python 3.13. Distributes inference across mismatched hardware via llama.cpp RPC.
@@ -38,7 +38,7 @@ Developer wiki: [docs/wiki/Home.md](docs/wiki/Home.md)
### Terminal RAG Hook — `POST /api/ingest` (v0.11.0)
- Bearer token auth (same key as `/v1/chat/completions`)
- Chunking via shared `chunk_text()` helper, embed via Ollama, upsert to Qdrant
- `jc-ingest.sh` — PROMPT_COMMAND shell script for autonomous terminal history ingestion
- `caic-ingest.sh` — PROMPT_COMMAND shell script for autonomous terminal history ingestion
### v1.8.0 Foundation (refactor & fixes)
- **Modular refactor** — single-file `app.py` split into `config.py`, `db.py`, `auth.py`, `security.py`, `memory.py`, `search.py`, `rag.py`, `gpu.py`, and `routers/` package
@@ -64,7 +64,7 @@ Developer wiki: [docs/wiki/Home.md](docs/wiki/Home.md)
## File Structure
```
/opt/jarvischat/
/opt/caic/
├── amqp.py # aio-pika AMQP connection manager + subscribe/rebind
├── app.py # FastAPI app entry point
├── cluster.py # Cluster protocol: node registry, event log, ping/pong
@@ -112,45 +112,45 @@ Developer wiki: [docs/wiki/Home.md](docs/wiki/Home.md)
```bash
# Create directory and venv
sudo mkdir -p /opt/jarvischat
sudo chown $USER:$USER /opt/jarvischat
cd /opt/jarvischat
sudo mkdir -p /opt/caic
sudo chown $USER:$USER /opt/caic
cd /opt/caic
python3 -m venv venv
# Install dependencies
pip install fastapi uvicorn httpx psutil jinja2 python-multipart pypdf aio-pika
# Set admin PIN before first startup (4 digits)
export JARVISCHAT_ADMIN_PIN=4827
export CAIC_ADMIN_PIN=4827
# Create subdirectories
mkdir -p templates static
# Copy files
# (copy all .py files to /opt/jarvischat/)
# (copy routers/ directory to /opt/jarvischat/)
# (copy templates/index.html to /opt/jarvischat/templates/)
# (copy all .py files to /opt/caic/)
# (copy routers/ directory to /opt/caic/)
# (copy templates/index.html to /opt/caic/templates/)
```
WARNING: Do not use `1234` as your admin PIN unless you accept weak local security.
NOTE: First boot requires `JARVISCHAT_ADMIN_PIN` unless you explicitly opt into insecure fallback with `JARVISCHAT_ALLOW_DEFAULT_PIN=true`.
NOTE: First boot requires `CAIC_ADMIN_PIN` unless you explicitly opt into insecure fallback with `CAIC_ALLOW_DEFAULT_PIN=true`.
## Systemd Service
Create `/etc/systemd/system/jarvischat.service`:
Create `/etc/systemd/system/caic.service`:
```ini
[Unit]
Description=jarvisChat - Local Inference Web Interface
Description=cAIc - Local Inference Web Interface
After=network.target
[Service]
Type=simple
User=jarvischat
Group=jarvischat
WorkingDirectory=/opt/jarvischat
ExecStart=/opt/jarvischat/venv/bin/uvicorn app:app --host 0.0.0.0 --port 8080
User=caic
Group=caic
WorkingDirectory=/opt/caic
ExecStart=/opt/caic/venv/bin/uvicorn app:app --host 0.0.0.0 --port 8080
Restart=always
RestartSec=5
@@ -160,8 +160,8 @@ WantedBy=multi-user.target
```bash
sudo systemctl daemon-reload
sudo systemctl enable jarvischat
sudo systemctl start jarvischat
sudo systemctl enable caic
sudo systemctl start caic
```
## Memory Commands
@@ -171,7 +171,7 @@ In chat, natural language triggers memory operations:
| You say | What happens |
|---------|--------------|
| "remember that I prefer Rust over Go" | Stores as `preference` |
| "remember that JarvisChat runs on port 8080" | Stores as `infrastructure` |
| "remember that cAIc runs on port 8080" | Stores as `infrastructure` |
| "note that the deadline is Friday" | Stores as `general` |
| "forget about the deadline" | Removes matching memories |
@@ -317,4 +317,4 @@ MIT
## Repository
Gitea: `ssh://gitea@llgit.llamachile.tube:1319/gramps/jarvisChat.git`
Gitea: `ssh://gitea@llgit.llamachile.tube:1319/gramps/caic.git`
+50 -50
View File
@@ -1,4 +1,4 @@
# jarvisChat — OpenCode Prompt Sequence
# cAIc — OpenCode Prompt Sequence
# Generated: 2026-07-01
# Execute sequentially. Run full test suite after each task before proceeding.
# Test command: ./venv/bin/python -m pytest tests/ -v
@@ -7,13 +7,13 @@
## ~~TASK 1 — README Cleanup [DONE]~~
Review README.md in the current repo. Remove any node references other than `ultron` (192.168.50.108) and `jarvis` (192.168.50.210). Ensure all references to the project use the exact casing `jarvisChat` — not `Jarvischat`, `JarvisChat`, or `jarvischat`. Do not change any functional content, endpoint documentation, or architecture descriptions — this is a text cleanup only. After editing, verify the file renders cleanly as markdown. Commit with message: `docs: clean up node references and branding consistency`.
Review README.md in the current repo. Remove any node references other than `coordinator` (192.168.50.108) and `worker` (192.168.50.210). Ensure all references to the project use the exact casing `cAIc` — not `Jarvischat`, `JarvisChat`, or `jarvischat`. Do not change any functional content, endpoint documentation, or architecture descriptions — this is a text cleanup only. After editing, verify the file renders cleanly as markdown. Commit with message: `docs: clean up node references and branding consistency`.
No new tests required for this task.
---
## ~~TASK 2 — Qwen2.5-Coder llama-server Service on Ultron (Infrastructure) [DONE]~~
## ~~TASK 2 — Qwen2.5-Coder llama-server Service on Coordinator (Infrastructure) [DONE]~~
**Status: Systemd unit created, verified, and restored.**
@@ -24,8 +24,8 @@ This task originally defined creation of `/etc/systemd/system/llama-server-coder
1. **Task 13** — Phi-4-mini triage (`triage.py`) classifies the query as `general`, `code`, `search`, or `rag`
2. **Task 13**`select_node()` picks the best worker node; if the ideal model isn't active, it triggers a swap
3. **Task 14**`request_model_swap()` publishes `cmd.swap_model` via AMQP `jc.admin` exchange
4. **Task 12** — The node agent on jarvis receives the command, stops the current llama-server, starts the correct one, waits for health, and publishes `model_ready`
5. **Task 14**ultron receives `model_ready`, updates the cluster registry, and routes the query to the node
4. **Task 12** — The node agent on worker receives the command, stops the current llama-server, starts the correct one, waits for health, and publishes `model_ready`
5. **Task 14**coordinator receives `model_ready`, updates the cluster registry, and routes the query to the node
The swap is async and transparent — the user sees only latency. The UI (Task 15) shows a yellow "swapping" status dot during the transition.
@@ -39,7 +39,7 @@ No pytest tests required for this infrastructure task.
## ~~TASK 3 — Update OpenCode Config to Use Qwen on :8082 [DONE]~~
Update `/home/gramps/.config/opencode/opencode.jsonc` (on this machine, ultron) to point the configured provider at `http://127.0.0.1:8082/v1` instead of `http://127.0.0.1:8081/v1`. The model name in the config should be updated to reflect `qwen2.5-coder-14b` or whatever model ID the llama-server instance at :8082 reports via `/v1/models`. Verify the endpoint is reachable before writing the config change. Do not restart OpenCode — the config change takes effect on next session start.
Update `/home/gramps/.config/opencode/opencode.jsonc` (on this machine, coordinator) to point the configured provider at `http://127.0.0.1:8082/v1` instead of `http://127.0.0.1:8081/v1`. The model name in the config should be updated to reflect `qwen2.5-coder-14b` or whatever model ID the llama-server instance at :8082 reports via `/v1/models`. Verify the endpoint is reachable before writing the config change. Do not restart OpenCode — the config change takes effect on next session start.
No pytest tests required for this task.
@@ -52,7 +52,7 @@ No pytest tests required for this task.
This task implements the backend half of file/document attachment (TODO #21). The goal is dual-aspect upload: a file can be used as immediate chat context, ingested into the RAG corpus (Qdrant), or both.
**Add to `config.py`:**
- `UPLOAD_DIR` — path for temporary upload storage, default `/tmp/jarvischat_uploads`
- `UPLOAD_DIR` — path for temporary upload storage, default `/tmp/caic_uploads`
- `MAX_UPLOAD_BYTES` — max file size, default 20MB
- `SUPPORTED_UPLOAD_TYPES` — set of MIME types: `text/plain`, `text/markdown`, `application/pdf`, `application/json`, `text/x-python`, `text/html`
@@ -68,7 +68,7 @@ Behavior:
- Validate MIME type against `SUPPORTED_UPLOAD_TYPES` — return 415 if unsupported
- For PDF files, extract text using `pypdf` (add to requirements.txt)
- For all other types, read as UTF-8 text
- If mode includes `ingest`: chunk the extracted text into 512-token overlapping chunks (128-token overlap), generate embeddings via `EMBED_URL` (http://192.168.50.108:11434/api/embeddings, model mxbai-embed-large), upsert into Qdrant collection `jarvischat` with metadata `{source: filename, upload_date: iso_timestamp, type: "upload"}`
- If mode includes `ingest`: chunk the extracted text into 512-token overlapping chunks (128-token overlap), generate embeddings via `EMBED_URL` (http://192.168.50.108:11434/api/embeddings, model mxbai-embed-large), upsert into Qdrant collection `caic` with metadata `{source: filename, upload_date: iso_timestamp, type: "upload"}`
- If mode includes `context`: store the full extracted text in a new SQLite table `upload_context` with columns `(id INTEGER PRIMARY KEY, conversation_id TEXT, filename TEXT, content TEXT, created_at TEXT, expires_at TEXT)`. Context entries expire after 1 hour.
- Return JSON: `{filename, size_bytes, mode, chunks_ingested (if ingest), context_id (if context), message}`
@@ -120,7 +120,7 @@ Run full test suite. All existing tests must continue to pass.
## ~~TASK 6 — Roadmap I: Terminal Command RAG Hook [DONE]~~
**Status: `POST /api/ingest` with Bearer token auth, `chunk_text()` shared helper, `jc-ingest.sh` script. Committed `1ac21ad` (v0.11.0).**
**Status: `POST /api/ingest` with Bearer token auth, `chunk_text()` shared helper, `caic-ingest.sh` script. Committed `1ac21ad` (v0.11.0).**
This task implements autonomous RAG ingestion of significant terminal activity (TODO #23).
@@ -134,21 +134,21 @@ Implement `POST /api/ingest` (requires Bearer token auth — use same `COMPLETIO
Behavior:
- Chunk `content` into 512-token overlapping chunks (128-token overlap) — extract this logic into a shared helper `chunk_text(text, chunk_size=512, overlap=128)` in `rag.py` if not already present
- Generate embeddings via `EMBED_URL`
- Upsert into Qdrant collection `jarvischat` with metadata `{source, ingest_date: iso_timestamp, ...metadata}`
- Upsert into Qdrant collection `caic` with metadata `{source, ingest_date: iso_timestamp, ...metadata}`
- Return JSON: `{chunks_ingested, source, message}`
**Wire `ingest.router` into `app.py`.**
**Create `/home/gramps/bin/jc-ingest.sh` on jarvis (192.168.50.210)** — this is a shell script, not a Python file, and lives outside the repo. Write it to stdout/document it clearly so gramps can deploy it manually:
**Create `/usr/local/bin/caic-ingest.sh` on worker (192.168.50.210)** — this is a shell script, not a Python file, and lives outside the repo. Write it to stdout/document it clearly so gramps can deploy it manually:
```bash
#!/bin/bash
# jc-ingest.sh — pipe terminal commands into jarvisChat RAG
# caic-ingest.sh — pipe terminal commands into cAIc RAG
# Add to ~/.bashrc: export PROMPT_COMMAND="jc_capture"
# Function to call after significant commands
JC_URL="http://192.168.50.210:8080/api/ingest"
JC_TOKEN="${JARVISCHAT_COMPLETIONS_API_KEY}"
JC_TOKEN="${CAIC_COMPLETIONS_API_KEY}"
jc_capture() {
local cmd
@@ -215,7 +215,7 @@ Run full test suite. All existing tests must continue to pass.
## ~~TASK 8 — Roadmap K: RAG Corpus Management [DONE]~~
Qdrant collection `jarvischat` currently grows without bound. Implement score-based eviction with hysteresis, pinned sources, operational stats, and a flush command.
Qdrant collection `caic` currently grows without bound. Implement score-based eviction with hysteresis, pinned sources, operational stats, and a flush command.
### Config — add to `config.py`:
@@ -242,7 +242,7 @@ Lower score = evicted first. Tiebreak: `last_accessed` ASC (older wins).
```python
async def get_collection_count() -> int
# GET /collections/jarvischat → return vectors_count
# GET /collections/caic → return vectors_count
async def get_collection_stats() -> dict
# Return {vector_count, max_vectors, high_water, low_water, percent_full, pinned_sources}
@@ -298,7 +298,7 @@ Call `maybe_evict()` after each upsert batch completes in:
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/rag/stats` | Operational stats (see `get_rag_operational_stats()`) — admin required |
| POST | `/api/rag/flush` | Delete ALL points from the Qdrant `jarvischat` collection. Returns `{deleted_count, collection: "jarvischat", status: "flushed"}`. Admin required. |
| POST | `/api/rag/flush` | Delete ALL points from the Qdrant `caic` collection. Returns `{deleted_count, collection: "caic", status: "flushed"}`. Admin required. |
### In-memory eviction log:
@@ -325,18 +325,18 @@ Run full test suite. All existing tests must continue to pass.
---
## ~~TASK 9 — Roadmap N1: RabbitMQ Install and Service on Ultron (Infrastructure) [DONE]~~
## ~~TASK 9 — Roadmap N1: RabbitMQ Install and Service on Coordinator (Infrastructure) [DONE]~~
This task runs on ultron (this machine). Install RabbitMQ and verify it is operational.
This task runs on coordinator (this machine). Install RabbitMQ and verify it is operational.
Run the following steps:
1. `apt-get update && apt-get install -y rabbitmq-server`
2. `systemctl enable rabbitmq-server && systemctl start rabbitmq-server`
3. `systemctl status rabbitmq-server` — verify active/running
4. Enable the management plugin: `rabbitmq-plugins enable rabbitmq_management`
5. Create a dedicated jC vhost: `rabbitmqctl add_vhost jarvischat`
6. Create a dedicated user: `rabbitmqctl add_user jarvischat CHANGEME_PASSWORD` — generate a random 24-char alphanumeric password and record it
7. Grant permissions: `rabbitmqctl set_permissions -p jarvischat jarvischat ".*" ".*" ".*"`
5. Create a dedicated jC vhost: `rabbitmqctl add_vhost caic`
6. Create a dedicated user: `rabbitmqctl add_user caic CHANGEME_PASSWORD` — generate a random 24-char alphanumeric password and record it
7. Grant permissions: `rabbitmqctl set_permissions -p caic caic ".*" ".*" ".*"`
8. Verify management UI is reachable: `curl -s -u guest:guest http://localhost:15672/api/overview | python3 -m json.tool`
9. Delete default guest user: `rabbitmqctl delete_user guest`
@@ -344,7 +344,7 @@ Declare the two topic exchanges needed by jC:
- Exchange name: `jc.admin`, type: `topic`, durable: true
- Exchange name: `jc.system`, type: `topic`, durable: true
Use `rabbitmqadmin` or `curl` against the management API to declare exchanges. Verify both exchanges appear in: `curl -s -u jarvischat:{password} http://localhost:15672/api/exchanges/jarvischat`
Use `rabbitmqadmin` or `curl` against the management API to declare exchanges. Verify both exchanges appear in: `curl -s -u caic:{password} http://localhost:15672/api/exchanges/caic`
Write the generated RabbitMQ password to `/home/gramps/.jc_amqp_secret` with mode 600. This will be read by jC as an env var source in subsequent tasks.
@@ -354,12 +354,12 @@ No pytest tests required for this infrastructure task.
## ~~TASK 10 — Roadmap N2: AMQP Connection Layer in jC [DONE]~~
This task adds the core AMQP connection manager to jC. It must connect to RabbitMQ on ultron (localhost from jC's perspective since jC runs on ultron), handle reconnection, and provide a shared channel for all AMQP operations.
This task adds the core AMQP connection manager to jC. It must connect to RabbitMQ on coordinator (localhost from jC's perspective since jC runs on coordinator), handle reconnection, and provide a shared channel for all AMQP operations.
**Add to `requirements.txt`:** `aio-pika>=9.0.0`
**Add to `config.py`:**
- `AMQP_URL` — read from env `JARVISCHAT_AMQP_URL`, default `amqp://jarvischat:password@localhost:5672/jarvischat`. The actual password comes from `/home/gramps/.jc_amqp_secret` — read it at startup if the env var is not set.
- `AMQP_URL` — read from env `CAIC_AMQP_URL`, default `amqp://caic:password@localhost:5672/caic`. The actual password comes from `/home/gramps/.jc_amqp_secret` — read it at startup if the env var is not set.
- `AMQP_RECONNECT_DELAY` — seconds between reconnect attempts, default 5
- `AMQP_EXCHANGE_ADMIN``jc.admin`
- `AMQP_EXCHANGE_SYSTEM``jc.system`
@@ -423,7 +423,7 @@ Worker presence is assumed from registration onward. No periodic heartbeats —
**register** (worker → coordinator):
```json
{
"node_name": "jarvis",
"node_name": "worker01",
"node_type": "worker",
"ip": "192.168.50.210",
"capabilities": {
@@ -432,12 +432,12 @@ Worker presence is assumed from registration onward. No periodic heartbeats —
},
"active_model": {
"name": "llama3.1", "version": "latest", "quant": "Q4_K_M",
"path": "/home/jarvis/models/llama3.1-latest-Q4_K_M.gguf",
"path": "/var/lib/caic/models/llama3.1-latest-Q4_K_M.gguf",
"port": 8081
},
"inventory": [
{"name": "llama3.1", "version": "latest", "quant": "Q4_K_M",
"path": "/home/jarvis/models/llama3.1-latest-Q4_K_M.gguf", "port": 8081}
"path": "/var/lib/caic/models/llama3.1-latest-Q4_K_M.gguf", "port": 8081}
],
"status": "active"
}
@@ -446,7 +446,7 @@ Worker presence is assumed from registration onward. No periodic heartbeats —
**deregister** (worker → coordinator):
```json
{
"node_name": "jarvis",
"node_name": "worker01",
"reason": "shutdown",
"timestamp": "2026-07-06T12:00:00Z"
}
@@ -455,8 +455,8 @@ Worker presence is assumed from registration onward. No periodic heartbeats —
**ping** (coordinator → worker):
```json
{
"from": "ultron",
"node_name": "jarvis",
"from": "coordinator",
"node_name": "worker01",
"type": "ping",
"correlation_id": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-07-06T12:00:00Z"
@@ -467,7 +467,7 @@ Worker must respond within 5 seconds or the coordinator considers it absent.
**pong** (worker → coordinator):
```json
{
"node_name": "jarvis",
"node_name": "worker01",
"type": "pong",
"correlation_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "active",
@@ -485,8 +485,8 @@ Correlation ID matches the ping so the coordinator can pair request and response
Coordinator responds on `cluster.coordinator.response`:
```json
{
"coordinator_node": "ultron",
"cluster_nodes": ["jarvis"],
"coordinator_node": "coordinator",
"cluster_nodes": ["worker01"],
"timestamp": "2026-07-06T12:00:00Z"
}
```
@@ -494,7 +494,7 @@ Coordinator responds on `cluster.coordinator.response`:
**event** (worker → coordinator):
```json
{
"node_name": "jarvis",
"node_name": "worker01",
"severity": "info",
"message": "llama-server started with model llama3.1:latest",
"details": {"model": "llama3.1:latest", "port": 8081, "pid": 1234},
@@ -667,22 +667,22 @@ Run full test suite. All existing tests must continue to pass.
---
## TASK 12 — Roadmap N4: Worker Node Registration Publisher (Jarvis Side)
## TASK 12 — Roadmap N4: Worker Node Registration Publisher (Worker Side)
This task creates the worker node AMQP client that runs on jarvis (192.168.50.210). It is a standalone Python script — not part of the jC FastAPI app — that runs as a systemd service on jarvis.
This task creates the worker node AMQP client that runs on worker (192.168.50.210). It is a standalone Python script — not part of the jC FastAPI app — that runs as a systemd service on worker.
**Create `node_agent/agent.py`** in the repo (new directory).
### 12.1 Config & Inventory Discovery
On start, reads `/etc/jc-node-agent.conf` (INI format):
On start, reads `/etc/caic-node-agent.conf` (INI format):
- `node_name` — hostname, default from `socket.gethostname()`
- `node_ip` — LAN IP, default from socket
- `node_type``"worker"` (fixed)
- `capabilities` — comma-separated list, e.g. `llm,rag`
- `amqp_url` — RabbitMQ URL on ultron, e.g. `amqp://jarvischat:password@192.168.50.108:5672/jarvischat`
- `amqp_url` — RabbitMQ URL on coordinator, e.g. `amqp://caic:password@192.168.50.108:5672/caic`
- `llama_port` — port llama-server/llama-rpc is listening on, default 8081
- `models_dir` — path to GGUF model files, default `/home/gramps/models`
- `models_dir` — path to GGUF model files, default `/var/lib/caic/models`
- `active_model` — filename of currently active model (without path)
Discovers inventory by globbing `models_dir` for `*.gguf` files and parsing name/version/quant from filename using regex pattern: `{name}-{version}-{quant}.gguf` where quant matches `Q[0-9]+_K_[A-Z]+` or similar standard suffixes.
@@ -692,7 +692,7 @@ Discovers inventory by globbing `models_dir` for `*.gguf` files and parsing name
Publishes registration to `jc.admin`, routing key `node.{node_name}.register`:
```json
{
"node_name": "jarvis",
"node_name": "worker01",
"node_type": "worker",
"ip": "192.168.50.210",
"capabilities": ["llm"],
@@ -710,7 +710,7 @@ After admission: listens on `jc.admin`, routing key `node.{node_name}.ping`. On
```json
{
"node_name": "jarvis",
"node_name": "worker01",
"type": "pong",
"correlation_id": "<echoed from ping>",
"status": "active",
@@ -727,7 +727,7 @@ No periodic heartbeats. Worker sits idle between pings — coordinator only ping
Listens on `jc.admin`, routing key `node.{node_name}.cmd.swap_model`:
- Payload: `{model_filename: str}`
- Stops current llama-server: `systemctl stop llama-server`
- Updates `/etc/jc-node-agent.conf` active_model field
- Updates `/etc/caic-node-agent.conf` active_model field
- Starts llama-server: `systemctl start llama-server` (assumes service reads active_model from conf or ExecStart is updated)
- Waits for llama-server to be healthy: poll `http://localhost:{llama_port}/v1/models` every 2s, timeout 120s
- Publishes to `jc.system`, routing key `node.{node_name}.model_ready`:
@@ -740,7 +740,7 @@ Listens on `jc.admin`, routing key `node.{node_name}.cmd.swap_model`:
**Create `node_agent/requirements.txt`:** `aio-pika>=9.0.0`
**Document `/etc/jc-node-agent.conf` format** in a comment block at the top of `agent.py`.
**Document `/etc/caic-node-agent.conf` format** in a comment block at the top of `agent.py`.
**Write `tests/test_node_agent.py`** covering:
- Registration payload construction from config + model discovery — assert correct JSON shape
@@ -763,9 +763,9 @@ This task wires the cluster into jC's chat flow. When a query arrives at `/api/c
**Prerequisites:** Tasks 912 complete. At least one worker node admitted to cluster.
**Install Phi-4-mini on ultron (infrastructure step):**
- Download `Phi-4-mini-Instruct-Q4_K_M.gguf` from HuggingFace using `hf download microsoft/Phi-4-mini-instruct --include "*.Q4_K_M.gguf" --local-dir /home/gramps/models`
- Create `/etc/systemd/system/llama-server-triage.service` — same pattern as existing llama-server service but: port 8083, model path points to Phi-4-mini GGUF, no `--rpc` flag (runs entirely on ultron CPU/iGPU), description `Llama.cpp Server (Phi-4-mini — triage/routing)`
**Install Phi-4-mini on coordinator (infrastructure step):**
- Download `Phi-4-mini-Instruct-Q4_K_M.gguf` from HuggingFace using `hf download microsoft/Phi-4-mini-instruct --include "*.Q4_K_M.gguf" --local-dir /var/lib/jc/models`
- Create `/etc/systemd/system/llama-server-triage.service` — same pattern as existing llama-server service but: port 8083, model path points to Phi-4-mini GGUF, no `--rpc` flag (runs entirely on coordinator CPU/iGPU), description `Llama.cpp Server (Phi-4-mini — triage/routing)`
- `systemctl daemon-reload && systemctl enable llama-server-triage && systemctl start llama-server-triage`
- Verify: `curl -s http://localhost:8083/v1/models`
@@ -820,7 +820,7 @@ Run full test suite. All existing tests must continue to pass.
## TASK 14 — Roadmap N6: Model Swap Command Flow
This task implements the ultron-side logic for requesting a model swap on a worker node when the ideal model is not currently active.
This task implements the coordinator-side logic for requesting a model swap on a worker node when the ideal model is not currently active.
**Add to `cluster.py`:**
@@ -925,10 +925,10 @@ Commit all changes introduced across Tasks 915 with message: `feat: Roadmap N
### B3 — Docker distribution (v1.0 gate)
**Goal:** Ship jarvisChat as a `docker compose` stack so a single command stands up everything.
**Goal:** Ship cAIc as a `docker compose` stack so a single command stands up everything.
**Services to containerize:**
- jarvisChat (FastAPI app + SQLite)
- cAIc (FastAPI app + SQLite)
- SearXNG
- Qdrant
- RabbitMQ
@@ -936,7 +936,7 @@ Commit all changes introduced across Tasks 915 with message: `feat: Roadmap N
- Ollama (embeddings)
**Also needed:**
- `Dockerfile` for the jarvisChat app itself
- `Dockerfile` for the cAIc app itself
- `docker-compose.yml` with all services, volumes, networks, env vars
- Setup wizard script (run on first boot) that:
- Probes CPU vs GPU (reuses `hardware.py`)
+2 -2
View File
@@ -1,5 +1,5 @@
"""
JarvisChat — AMQP connection manager.
cAIc — AMQP connection manager.
Single persistent aio-pika connection with auto-reconnect.
"""
import asyncio
@@ -21,7 +21,7 @@ from config import (
get_amqp_url,
)
log = logging.getLogger("jarvischat")
log = logging.getLogger("caic")
_connection = None
_channel = None
+6 -6
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
JarvisChat - Entry point.
cAIc - Entry point.
Creates the FastAPI app, registers middleware, mounts all routers.
"""
import logging
@@ -43,10 +43,10 @@ import routers.rag_admin as rag_admin
import routers.cluster as cluster_router
# --- Logging ---
log = logging.getLogger("jarvischat")
log = logging.getLogger("caic")
log.setLevel(logging.DEBUG)
syslog_handler = logging.handlers.SysLogHandler(address="/dev/log")
syslog_handler.setFormatter(logging.Formatter("jarvischat[%(process)d]: %(levelname)s %(message)s"))
syslog_handler.setFormatter(logging.Formatter("caic[%(process)d]: %(levelname)s %(message)s"))
log.addHandler(syslog_handler)
BASE_DIR = Path(__file__).parent
@@ -55,7 +55,7 @@ templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
@asynccontextmanager
async def lifespan(app: FastAPI):
log.info(f"JarvisChat {VERSION} starting up")
log.info(f"cAIc {VERSION} starting up")
os.makedirs(UPLOAD_DIR, exist_ok=True)
init_db()
log.info(f"Memory system: {get_memory_count()} memories loaded")
@@ -75,11 +75,11 @@ async def lifespan(app: FastAPI):
log.warning("RAG_MAX_VECTORS <= 0 — RAG eviction disabled")
yield
log.info("JarvisChat shutting down")
log.info("cAIc shutting down")
await amqp_disconnect()
app = FastAPI(title="JarvisChat", lifespan=lifespan)
app = FastAPI(title="cAIc", lifespan=lifespan)
@app.exception_handler(Exception)
+2 -2
View File
@@ -1,5 +1,5 @@
"""
JarvisChat - Auth: session management, PIN verification, middleware, auth routes.
cAIc - Auth: session management, PIN verification, middleware, auth routes.
"""
import hashlib
import hmac
@@ -21,7 +21,7 @@ from security import (
read_json_body, hash_pin, customer_error_envelope, log_incident,
)
log = logging.getLogger("jarvischat")
log = logging.getLogger("caic")
router = APIRouter()
+2 -2
View File
@@ -1,5 +1,5 @@
"""
JarvisChat Cluster protocol implementation.
cAIc Cluster protocol implementation.
Maintains node registry, event log, coordinator state, and ping-based health checks.
"""
import asyncio
@@ -11,7 +11,7 @@ from datetime import datetime, timezone
from amqp import publish, subscribe
from config import AMQP_EXCHANGE_ADMIN, AMQP_EXCHANGE_SYSTEM
log = logging.getLogger("jarvischat")
log = logging.getLogger("caic")
CLUSTER_NODES: dict[str, dict] = {}
CLUSTER_EVENTS: deque = deque(maxlen=1000)
+12 -12
View File
@@ -1,5 +1,5 @@
"""
JarvisChat - Central configuration.
cAIc - Central configuration.
All constants, environment variables, limits, and skill registry live here.
"""
import os
@@ -7,23 +7,23 @@ import re
import ipaddress
import logging
log = logging.getLogger("jarvischat")
log = logging.getLogger("caic")
VERSION = "v0.14.0"
OLLAMA_BASE = os.environ.get("OLLAMA_BASE", "http://localhost:11434")
LLAMA_SERVER_BASE = os.environ.get("LLAMA_SERVER_BASE", "http://192.168.50.108:8081")
SEARXNG_BASE = "http://localhost:8888"
DEFAULT_MODEL = "llama3.1:latest"
COMPLETIONS_API_KEY = os.environ.get("JARVISCHAT_COMPLETIONS_API_KEY", "jc-sk-" + os.urandom(24).hex())
COMPLETIONS_API_KEY = os.environ.get("CAIC_COMPLETIONS_API_KEY", "caic-sk-" + os.urandom(24).hex())
# --- AMQP ---
AMQP_RECONNECT_DELAY = 5
AMQP_EXCHANGE_ADMIN = "jc.admin"
AMQP_EXCHANGE_SYSTEM = "jc.system"
AMQP_SECRET_PATH = "/home/gramps/.jc_amqp_secret"
AMQP_SECRET_PATH = "/home/gramps/.caic_amqp_secret"
def get_amqp_url() -> str:
url = os.environ.get("JARVISCHAT_AMQP_URL")
url = os.environ.get("CAIC_AMQP_URL")
if url:
return url
try:
@@ -31,22 +31,22 @@ def get_amqp_url() -> str:
pw = f.read().strip()
except (FileNotFoundError, OSError):
pw = "password"
return f"amqp://jarvischat:{pw}@localhost:5672/jarvischat"
return f"amqp://caic:{pw}@localhost:5672/caic"
# --- Auth ---
SESSION_TIMEOUT_SECONDS = 90
MAX_PIN_ATTEMPTS = 5
PIN_LOCKOUT_SECONDS = 300
ALLOW_DEFAULT_PIN = os.getenv("JARVISCHAT_ALLOW_DEFAULT_PIN", "false").lower() == "true"
ALLOW_DEFAULT_PIN = os.getenv("CAIC_ALLOW_DEFAULT_PIN", "false").lower() == "true"
TRUSTED_ORIGINS = {
origin.strip().rstrip("/")
for origin in os.getenv("JARVISCHAT_TRUSTED_ORIGINS", "").split(",")
for origin in os.getenv("CAIC_TRUSTED_ORIGINS", "").split(",")
if origin.strip()
}
DEFAULT_ALLOWED_CIDRS = "127.0.0.0/8,::1/128,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
ALLOWED_CIDRS_RAW = os.getenv("JARVISCHAT_ALLOWED_CIDRS", DEFAULT_ALLOWED_CIDRS)
ALLOWED_CIDRS_RAW = os.getenv("CAIC_ALLOWED_CIDRS", DEFAULT_ALLOWED_CIDRS)
TRUST_X_FORWARDED_FOR = (
os.getenv("JARVISCHAT_TRUST_X_FORWARDED_FOR", "false").lower() == "true"
os.getenv("CAIC_TRUST_X_FORWARDED_FOR", "false").lower() == "true"
)
# --- Rate limits ---
@@ -64,11 +64,11 @@ BODY_LIMIT_CHAT_BYTES = 128 * 1024
BODY_LIMIT_PROFILE_BYTES = 256 * 1024
# --- Upload ---
UPLOAD_DIR = "/tmp/jarvischat_uploads"
UPLOAD_DIR = "/tmp/caic_uploads"
MAX_UPLOAD_BYTES = 20 * 1024 * 1024
SUPPORTED_UPLOAD_TYPES = {"text/plain", "text/markdown", "application/pdf", "application/json", "text/x-python", "text/html", "image/png", "image/jpeg", "image/gif", "image/svg+xml", "image/webp"}
QDRANT_URL = "http://192.168.50.108:6333"
RAG_COLLECTION = "jarvis_rag"
RAG_COLLECTION = "caic_rag"
UPLOAD_CONTEXT_EXPIRY_HOURS = 1
BODY_LIMIT_UPLOAD_BYTES = MAX_UPLOAD_BYTES
+6 -6
View File
@@ -1,5 +1,5 @@
"""
JarvisChat - Database layer.
cAIc - Database layer.
Schema init, connection factory, settings helpers, skill state management.
"""
import logging
@@ -16,10 +16,10 @@ from config import (
MAX_SKILL_PROMPT_CHARS, ALLOWED_NETWORKS,
)
log = logging.getLogger("jarvischat")
log = logging.getLogger("caic")
BASE_DIR = Path(__file__).parent
DB_PATH = BASE_DIR / "jarvischat.db"
DB_PATH = BASE_DIR / "caic.db"
def get_db():
@@ -190,15 +190,15 @@ def init_db():
existing_pin_salt = conn.execute("SELECT value FROM settings WHERE key = 'admin_pin_salt'").fetchone()
if not existing_pin_hash or not existing_pin_salt:
from config import ALLOW_DEFAULT_PIN
configured_pin = os.getenv("JARVISCHAT_ADMIN_PIN", "").strip()
configured_pin = os.getenv("CAIC_ADMIN_PIN", "").strip()
if re.fullmatch(r"\d{4}", configured_pin):
seed_pin, pin_source = configured_pin, "env"
elif ALLOW_DEFAULT_PIN:
seed_pin, pin_source = "1234", "default"
else:
raise RuntimeError(
"Admin PIN bootstrap blocked: set JARVISCHAT_ADMIN_PIN to a 4-digit PIN "
"or set JARVISCHAT_ALLOW_DEFAULT_PIN=true."
"Admin PIN bootstrap blocked: set CAIC_ADMIN_PIN to a 4-digit PIN "
"or set CAIC_ALLOW_DEFAULT_PIN=true."
)
salt_hex, pin_hash_hex = hash_pin(seed_pin)
conn.execute("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", ("admin_pin_hash", pin_hash_hex))
+44 -44
View File
@@ -1,6 +1,6 @@
# Docker Distribution — Architecture & Planning
> **Part of B3 (v1.0 gate).** This document catalogs every service, volume, port, configuration, and decision needed to ship jarvisChat as a `docker compose` stack. It also defines extraction (setup) and back-out (uninstall) procedures so nothing is lost when reality disagrees with the plan.
> **Part of B3 (v1.0 gate).** This document catalogs every service, volume, port, configuration, and decision needed to ship cAIc as a `docker compose` stack. It also defines extraction (setup) and back-out (uninstall) procedures so nothing is lost when reality disagrees with the plan.
## 1. Stack Overview
@@ -15,10 +15,10 @@
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ jarvisChat (FastAPI) │ │
│ │ cAIc (FastAPI) │ │
│ │ :8080 (HTTP) │ │
│ │ │ │
│ │ SQLite ◄── jarvischat.db (volume) │ │
│ │ SQLite ◄── caic.db (volume) │ │
│ │ Uploads ◄── /app/uploads (volume) │ │
│ └──────────┬──────────────┬───────────────────────┘ │
│ │ │ │
@@ -37,7 +37,7 @@
| Service | Image | Role |
|---------|-------|------|
| **jarvisChat** | Custom `Dockerfile` | FastAPI app serving UI + API |
| **cAIc** | Custom `Dockerfile` | FastAPI app serving UI + API |
| **SearXNG** | `searxng/searxng:latest` | Privacy-respecting web search |
| **Qdrant** | `qdrant/qdrant:latest` | Vector database for RAG |
| **RabbitMQ** | `rabbitmq:4-management` | Message broker for AMQP cluster |
@@ -57,9 +57,9 @@
## 2. Service Catalog
### 2.1 jarvisChat (FastAPI app)
### 2.1 cAIc (FastAPI app)
**Image:** `jarvischat:latest` (built from `Dockerfile`)
**Image:** `caic:latest` (built from `Dockerfile`)
**Ports:**
| Container | Host | Purpose |
@@ -69,8 +69,8 @@
**Volumes:**
| Container path | Type | Purpose |
|----------------|------|---------|
| `/app/jarvischat.db` | named volume `jarvischat_data` | SQLite database |
| `/app/uploads` | named volume `jarvischat_uploads` | Uploaded files |
| `/app/caic.db` | named volume `caic_data` | SQLite database |
| `/app/uploads` | named volume `caic_uploads` | Uploaded files |
| `/app/hardware_state.json` | (inside volume) | Cached hardware probe |
**Dependencies:** Wait for SearXNG, Qdrant, RabbitMQ, llama-server, Ollama before serving.
@@ -153,7 +153,7 @@ QDRANT__SERVICE__GRPC_PORT=6334
**Environment:**
```env
RABBITMQ_DEFAULT_USER=jarvischat
RABBITMQ_DEFAULT_USER=caic
RABBITMQ_DEFAULT_PASS_FILE=/run/secrets/rabbitmq_password
RABBITMQ_DEFAULT_VHOST=/
```
@@ -227,9 +227,9 @@ LLAMA_ARG_RPC= # optional: comma-separated RPC endpoints
```env
# --- Secrets (auto-generated, change before production) ---
JARVISCHAT_ADMIN_PIN=
JARVISCHAT_COMPLETIONS_API_KEY=
JARVISCHAT_ALLOW_DEFAULT_PIN=false
CAIC_ADMIN_PIN=
CAIC_COMPLETIONS_API_KEY=
CAIC_ALLOW_DEFAULT_PIN=false
RABBITMQ_PASSWORD=
SEARXNG_SECRET_KEY=
@@ -257,9 +257,9 @@ LLAMA_CTX_SIZE=4096
OLLAMA_EMBED_MODEL=all-minilm:latest
# --- Network ---
JARVISCHAT_ALLOWED_CIDRS=127.0.0.0/8,::1/128,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
JARVISCHAT_TRUSTED_ORIGINS=
JARVISCHAT_TRUST_X_FORWARDED_FOR=false
CAIC_ALLOWED_CIDRS=127.0.0.0/8,::1/128,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
CAIC_TRUSTED_ORIGINS=
CAIC_TRUST_X_FORWARDED_FOR=false
```
### 3.2 Mapping of config.py → .env variable
@@ -272,24 +272,24 @@ Every config.py default that references an external service must accept a matchi
| `OLLAMA_BASE` | `OLLAMA_BASE` | Ollama |
| `SEARXNG_BASE` | `SEARXNG_BASE` | SearXNG |
| `QDRANT_URL` | `QDRANT_URL` | Qdrant |
| `COMPLETIONS_API_KEY` | `JARVISCHAT_COMPLETIONS_API_KEY` | — |
| `ALLOWED_CIDRS_RAW` | `JARVISCHAT_ALLOWED_CIDRS` | — |
| `TRUST_X_FORWARDED_FOR` | `JARVISCHAT_TRUST_X_FORWARDED_FOR` | — |
| `TRUSTED_ORIGINS` | `JARVISCHAT_TRUSTED_ORIGINS` | — |
| `COMPLETIONS_API_KEY` | `CAIC_COMPLETIONS_API_KEY` | — |
| `ALLOWED_CIDRS_RAW` | `CAIC_ALLOWED_CIDRS` | — |
| `TRUST_X_FORWARDED_FOR` | `CAIC_TRUST_X_FORWARDED_FOR` | — |
| `TRUSTED_ORIGINS` | `CAIC_TRUSTED_ORIGINS` | — |
| `RAG_MAX_VECTORS` | `RAG_MAX_VECTORS` | — (calc'd from RAM) |
### 3.3 Secrets management
| Secret | Generated by | Stored in | Mounted to |
|--------|-------------|-----------|------------|
| `JARVISCHAT_ADMIN_PIN` | User prompt | `.env` | jarvisChat container |
| `JARVISCHAT_COMPLETIONS_API_KEY` | Auto-generated, shown to user | `.env` | jarvisChat container |
| `CAIC_ADMIN_PIN` | User prompt | `.env` | cAIc container |
| `CAIC_COMPLETIONS_API_KEY` | Auto-generated, shown to user | `.env` | cAIc container |
| `RABBITMQ_PASSWORD` | Auto-generated | `.env` + Docker secret | RabbitMQ container |
| `SEARXNG_SECRET_KEY` | Auto-generated | `.env` | SearXNG container |
**Docker secrets approach:** Use `secrets:` in compose file for RabbitMQ password (mounted as file) rather than passing via env var, since `settings.yml` in SearXNG and RabbitMQ config can reference file-based secrets without env-var leakage.
### 3.4 Dockerfile for jarvisChat
### 3.4 Dockerfile for cAIc
```dockerfile
FROM python:3.13-slim-bookworm AS builder
@@ -321,12 +321,12 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"]
```yaml
services:
jarvischat:
caic:
build: .
ports: ["8080:8080"]
volumes:
- jarvischat_data:/app/jarvischat.db
- jarvischat_uploads:/app/uploads
- caic_data:/app/caic.db
- caic_uploads:/app/uploads
env_file: .env
depends_on:
searxng: { condition: service_started }
@@ -416,8 +416,8 @@ services:
restart: unless-stopped
volumes:
jarvischat_data:
jarvischat_uploads:
caic_data:
caic_uploads:
searxng_config:
qdrant_storage:
rabbitmq_data:
@@ -441,11 +441,11 @@ secrets:
| From | To | Port | Protocol |
|------|----|------|----------|
| jarvisChat | llama-server | 8081 | HTTP |
| jarvisChat | Ollama | 11434 | HTTP |
| jarvisChat | SearXNG | 8080 | HTTP |
| jarvisChat | Qdrant | 6333 | HTTP |
| jarvisChat | RabbitMQ | 5672 | AMQP |
| cAIc | llama-server | 8081 | HTTP |
| cAIc | Ollama | 11434 | HTTP |
| cAIc | SearXNG | 8080 | HTTP |
| cAIc | Qdrant | 6333 | HTTP |
| cAIc | RabbitMQ | 5672 | AMQP |
| RabbitMQ | (cluster peers) | 4369 | EPMD |
| RabbitMQ | (cluster peers) | 25672 | Inter-node |
@@ -453,7 +453,7 @@ secrets:
| Port | Service | Should expose? | Notes |
|------|---------|---------------|-------|
| 8080 | jarvisChat | ✅ Required | UI + API |
| 8080 | cAIc | ✅ Required | UI + API |
| 8888 | SearXNG | Optional | Only if user wants standalone search |
| 6333 | Qdrant | Optional | Only for external tooling |
| 5672 | RabbitMQ | Optional | Only for remote AMQP clients |
@@ -461,7 +461,7 @@ secrets:
| 8081 | llama-server | Optional | Only for external tooling |
| 11434 | Ollama | Optional | Only for external tooling |
**Design decision:** By default, only port 8080 (jarvisChat) is published. All other services remain on the internal compose network. Advanced users can opt-in by uncommenting `ports:` blocks.
**Design decision:** By default, only port 8080 (cAIc) is published. All other services remain on the internal compose network. Advanced users can opt-in by uncommenting `ports:` blocks.
### 5.3 Reverse proxy consideration
@@ -534,7 +534,7 @@ This is out of scope for v1.0 but documented for future.
./docker-deploy/
├── .env # All env vars (SECRET — add to .gitignore)
├── docker-compose.yml # Compose stack definition
├── Dockerfile # jarvisChat image build
├── Dockerfile # cAIc image build
├── secrets/
│ └── rabbitmq_password.txt # RabbitMQ password file
├── searxng/
@@ -564,9 +564,9 @@ Re-running `setup.sh`:
| Item | Removal method |
|------|---------------|
| Docker containers | `docker compose down -v` |
| Docker images | `docker rmi jarvischat:latest` (ask about other images) |
| Docker volumes | `docker volume rm jarvischat_data ...` (prompt first) |
| Network `jarvischat_default` | Removed with compose |
| Docker images | `docker rmi caic:latest` (ask about other images) |
| Docker volumes | `docker volume rm caic_data ...` (prompt first) |
| Network `caic_default` | Removed with compose |
| `.env` file | `rm .env` |
| `secrets/` directory | `rm -rf secrets/` |
| `searxng/` directory | `rm -rf searxng/` |
@@ -578,7 +578,7 @@ Re-running `setup.sh`:
| Item | Reason |
|------|--------|
| `./models/*.gguf` | User data — prompt for deletion |
| `jarvischat.db` (in volume) | Prompt: "Keep database snapshot?" |
| `caic.db` (in volume) | Prompt: "Keep database snapshot?" |
| `./uploads/` (in volume) | Prompt: "Keep uploaded files?" |
| Docker Engine itself | Not installed by this project — leave it |
@@ -596,12 +596,12 @@ Re-running `setup.sh`:
3. ASK: "Remove secrets/ and searxng/ directories?" (default no)
4. ASK: "Remove Docker images? (y/N)" (default no)
├── Y → docker rmi jarvischat:latest
├── Y → docker rmi caic:latest
├── Y → docker image ls | grep searxng/qdrant/rabbitmq → prompt per image
└── N → skip
5. ASK: "Keep database volume snapshot? (Y/n)" (default yes)
├── N → docker volume rm jarvischat_data
├── N → docker volume rm caic_data
└── Y → leave volume (can be reattached later)
6. ASK: "Remove model files from ./models/? (y/N)" (default no)
@@ -653,7 +653,7 @@ The Docker stack above defines the **coordinator** only. Workers (headless infer
### 9.1 What a worker runs
```
Worker machine (e.g. jarvis, Corsair)
Worker machine (e.g. worker01, worker02)
┌────────────────────────────────────┐
│ llama-server │
│ (single binary, no build needed) │
@@ -717,7 +717,7 @@ The broker-mediated model is the preferred architecture for this project because
- [ ] `Dockerfile` written and builds clean
- [ ] `docker-compose.yml` boots all containers
- [ ] jarvisChat container reaches all services (env vars resolve correctly)
- [ ] cAIc container reaches all services (env vars resolve correctly)
- [ ] SearXNG settings.yml generated correctly by setup.sh
- [ ] RabbitMQ password secret mounted correctly
- [ ] GPU (NVIDIA) passes through to llama-server container
@@ -736,7 +736,7 @@ The broker-mediated model is the preferred architecture for this project because
```
docker.md ← this file (planning doc)
Dockerfile ← jarvisChat image
Dockerfile ← cAIc image
docker-compose.yml ← full stack
.env.example ← template without secrets
setup.sh ← extraction wizard
+17 -17
View File
@@ -1,10 +1,10 @@
# Developer Architecture Guide
This document explains how JarvisChat is structured, the external services it integrates with, and the key architectural changes made during development.
This document explains how cAIc is structured, the external services it integrates with, and the key architectural changes made during development.
## 1. System Overview
JarvisChat is a single-process FastAPI service with a Jinja2 frontend and SQLite persistence. It connects to an external llama-server for inference and optionally to SearXNG (web search), Qdrant (vector search), and RabbitMQ (AMQP cluster messaging).
cAIc is a single-process FastAPI service with a Jinja2 frontend and SQLite persistence. It connects to an external llama-server for inference and optionally to SearXNG (web search), Qdrant (vector search), and RabbitMQ (AMQP cluster messaging).
### 1.1 Module Layout
@@ -29,11 +29,11 @@ Refactored from single-file (`app.py`) into modules under project root:
| Service | Required | Port | Purpose |
|---------|----------|------|---------|
| llama-server (ultron) | Yes | 8081 | LLM inference (OpenAI-compat), RPC offload to jarvis:50052 |
| llama-server (coordinator) | Yes | 8081 | LLM inference (OpenAI-compat), RPC offload to worker:50052 |
| SearXNG | No | 8888 | Privacy-respecting web search |
| Qdrant (ultron) | No | 6333 | Vector database for RAG |
| Ollama (jarvis) | No | 11434 | Embeddings for RAG chunk vectors |
| RabbitMQ (ultron) | No | 5672 | AMQP broker for cluster messaging |
| Qdrant (coordinator) | No | 6333 | Vector database for RAG |
| Ollama (worker) | No | 11434 | Embeddings for RAG chunk vectors |
| RabbitMQ (coordinator) | No | 5672 | AMQP broker for cluster messaging |
| rocm-smi | No | — | AMD GPU stats (host-level) |
### 1.3 Config Discovery
@@ -42,11 +42,11 @@ Key base URLs are configured via environment variables with sensible defaults:
| Variable | Default | Service |
|----------|---------|---------|
| `LLAMA_SERVER_BASE` | `http://192.168.50.108:8081` | llama-server on ultron |
| `LLAMA_SERVER_BASE` | `http://192.168.50.108:8081` | llama-server on coordinator |
| `OLLAMA_BASE` | `http://localhost:11434` | Legacy — all inference goes through LLAMA_SERVER_BASE |
| `SEARXNG_BASE` | `http://localhost:8888` | SearXNG |
| `QDRANT_URL` | `http://192.168.50.108:6333` | Qdrant on ultron |
| `JARVISCHAT_AMQP_URL` | `amqp://jarvischat:password@localhost:5672/jarvischat` | RabbitMQ |
| `QDRANT_URL` | `http://192.168.50.108:6333` | Qdrant on coordinator |
| `CAIC_AMQP_URL` | `amqp://caic:password@localhost:5672/caic` | RabbitMQ |
## 2. Request/Response Architecture
@@ -73,7 +73,7 @@ Key base URLs are configured via environment variables with sensible defaults:
1. Bearer token auth (same key as completions API)
2. Chunk text via shared `chunk_text()` helper (512-token chunks, 128-token overlap)
3. Embed via Ollama `/api/embeddings`
4. Upsert to Qdrant collection `jarvis_rag`
4. Upsert to Qdrant collection `caic_rag`
5. Trigger `maybe_evict()` if collection exceeds high-water mark
### 2.4 Upload Pipeline (`/api/upload`)
@@ -116,7 +116,7 @@ Design notes:
- Admin PIN hashed with PBKDF2-HMAC-SHA256 + salt
- Failed PIN attempts tracked per client IP (max 5, 300s lockout)
- Default PIN allowed only if JARVISCHAT_ALLOW_DEFAULT_PIN=true
- Default PIN allowed only if CAIC_ALLOW_DEFAULT_PIN=true
### 4.3 Browser and API Abuse Controls
@@ -141,8 +141,8 @@ Design notes:
### 5.1 Vector Search
- Qdrant collection `jarvis_rag` on ultron:6333
- Embeddings via Ollama on jarvis:11434 (`/api/embeddings`)
- Qdrant collection `caic_rag` on coordinator:6333
- Embeddings via Ollama on worker:11434 (`/api/embeddings`)
- Shared `chunk_text(text, chunk_size=512, overlap=128)` helper in rag.py
- Upload and ingest endpoints share the same chunk+embed+upsert pipeline
@@ -178,7 +178,7 @@ Eviction module at `eviction.py` (re-exported through `rag.py` for backward comp
### 6.1 Design Model: Broker-Mediated
JarvisChat uses a **broker-mediated** cluster design. This is the preferred architecture and is reflected in all implementation decisions below.
cAIc uses a **broker-mediated** cluster design. This is the preferred architecture and is reflected in all implementation decisions below.
**How it works:**
- A single RabbitMQ broker (or clustered set of brokers) acts as the central nervous system
@@ -208,7 +208,7 @@ Every physical machine in the cluster is classified by which services it runs. T
| **RabbitMQ server** | Required — hosts the broker | Not required — connects as AMQP client only |
| **RabbitMQ client (aio-pika)** | Required — publishes commands, consumes events | Required — consumes commands, publishes events |
| **FastAPI / uvicorn** | Required | Not needed |
| **SQLite** | Required — owns jarvischat.db | Not needed |
| **SQLite** | Required — owns caic.db | Not needed |
| **Qdrant** | Optional (recommended) — vector DB for RAG | Not needed |
| **SearXNG** | Optional — web search | Not needed |
| **llama-server** | Optional — can share its own GPU for inference | Required — this is why the worker exists |
@@ -220,7 +220,7 @@ Every physical machine in the cluster is classified by which services it runs. T
```
Coordinator Worker(s)
┌────────────────────┐ ┌─────────────────────────┐
jarvisChat │ │ llama-server │
cAIc │ │ llama-server │
│ (FastAPI + SQLite)│ │ (inference) │
│ RabbitMQ server │◄──AMQP───────│ aio-pika (agent) │
│ SearXNG (opt) │ persistent │ ROCm / CUDA (if GPU) │
@@ -243,7 +243,7 @@ Every RabbitMQ server belongs to a cluster. Currently only the coordinator runs
Pending implementation (Tasks 1015):
- `amqp.py` — aio-pika connection manager with reconnect
- Node agent on jarvis — registration, heartbeat, command consumer
- Node agent on worker — registration, heartbeat, command consumer
- `triage.py` — Phi-4-mini query classification (general/code/search/rag)
- Dynamic model swap via llama-server RPC
+3 -3
View File
@@ -1,4 +1,4 @@
# JarvisChat Current WiP Backlog
# cAIc Current WiP Backlog
Last updated: 2026-07-06
Owner: Gramps
@@ -9,9 +9,9 @@ Scope: Active roadmap items and backlog.
| Task | Description | Status |
|------|-------------|--------|
| Task 8 | RAG Corpus Management (score-based eviction, pinned sources, operational stats) | **DONE** |
| Task 9 | RabbitMQ install + exchange setup on ultron | **DONE** |
| Task 9 | RabbitMQ install + exchange setup on coordinator | **DONE** |
| Task 10 | AMQP connection layer in jC (`amqp.py`, aio-pika) | **NEXT** |
| Task 11 | Node agent on jarvis — registration, heartbeat, command listeners | Pending |
| Task 11 | Node agent on worker — registration, heartbeat, command listeners | Pending |
| Task 12 | AMQP wiring: inject context into chat/completions pipeline | Pending |
| Task 13 | Query triage via Phi-4-mini (`triage.py`) | Pending |
| Task 14 | Dynamic model swap — publish cmd, handle ready/failed | Pending |
+2 -2
View File
@@ -1,5 +1,5 @@
"""
JarvisChat Score-based RAG vector eviction with hysteresis.
cAIc Score-based RAG vector eviction with hysteresis.
"""
import asyncio
import logging
@@ -14,7 +14,7 @@ from config import (
RAG_ACCESS_WEIGHT, RAG_AGE_WEIGHT,
)
log = logging.getLogger("jarvischat")
log = logging.getLogger("caic")
eviction_lock = asyncio.Lock()
EVICTION_LOG: list[dict] = []
+2 -2
View File
@@ -1,11 +1,11 @@
"""
JarvisChat - AMD GPU stats via rocm-smi.
cAIc - AMD GPU stats via rocm-smi.
"""
import json
import logging
import subprocess
log = logging.getLogger("jarvischat")
log = logging.getLogger("caic")
def get_gpu_stats() -> dict:
+2 -2
View File
@@ -1,5 +1,5 @@
"""
JarvisChat Startup hardware self-assessment.
cAIc Startup hardware self-assessment.
"""
import asyncio
import json
@@ -12,7 +12,7 @@ import psutil
from config import LLAMA_SERVER_BASE, SEARXNG_BASE
log = logging.getLogger("jarvischat")
log = logging.getLogger("caic")
HARDWARE_STATE_PATH = Path("hardware_state.json")
_TIMEOUT_EXPIRED = subprocess.TimeoutExpired
+2 -2
View File
@@ -1,5 +1,5 @@
"""
JarvisChat - FTS5 memory system.
cAIc - FTS5 memory system.
CRUD, search, remember/forget command processing, topic detection.
"""
import logging
@@ -10,7 +10,7 @@ from typing import Optional
from db import get_db
from config import MAX_MEMORY_FACT_CHARS
log = logging.getLogger("jarvischat")
log = logging.getLogger("caic")
REMEMBER_PATTERNS = [
(r"remember that (.+)", "explicit"),
+2 -2
View File
@@ -1,5 +1,5 @@
"""
JarvisChat - RAG pipeline: Qdrant vector search + system prompt assembly.
cAIc - RAG pipeline: Qdrant vector search + system prompt assembly.
"""
import asyncio
import logging
@@ -11,7 +11,7 @@ from db import get_db, get_setting, list_skills_with_state, format_active_skills
from memory import search_memories
from config import MAX_SKILL_PROMPT_CHARS, QDRANT_URL, RAG_COLLECTION
log = logging.getLogger("jarvischat")
log = logging.getLogger("caic")
EMBED_URL = "http://192.168.50.210:11434"
EMBED_MODEL = "mxbai-embed-large"
+1 -1
View File
@@ -18,7 +18,7 @@ from search import (calculate_perplexity, is_uncertain, is_refusal,
from security import read_json_body, log_incident, BODY_LIMIT_CHAT_BYTES
from config import MAX_CHAT_MESSAGE_CHARS
log = logging.getLogger("jarvischat")
log = logging.getLogger("caic")
router = APIRouter()
+1 -1
View File
@@ -20,7 +20,7 @@ from db import get_db
from rag import build_system_prompt
from routers.chat import parse_llama_stream_chunk
log = logging.getLogger("jarvischat")
log = logging.getLogger("caic")
router = APIRouter()
+1 -1
View File
@@ -7,7 +7,7 @@ from db import get_db
from security import read_json_body, BODY_LIMIT_DEFAULT_BYTES
from config import DEFAULT_MODEL, MAX_CONVERSATION_TITLE_CHARS
log = logging.getLogger("jarvischat")
log = logging.getLogger("caic")
router = APIRouter()
+1 -1
View File
@@ -10,7 +10,7 @@ from config import COMPLETIONS_API_KEY
from eviction import maybe_evict
from rag import chunk_text, QDRANT_URL, EMBED_URL, EMBED_MODEL, RAG_COLLECTION
log = logging.getLogger("jarvischat")
log = logging.getLogger("caic")
router = APIRouter()
+1 -1
View File
@@ -12,7 +12,7 @@ from config import LLAMA_SERVER_BASE
from gpu import get_gpu_stats
from security import read_json_body, BODY_LIMIT_DEFAULT_BYTES
log = logging.getLogger("jarvischat")
log = logging.getLogger("caic")
router = APIRouter()
+1 -1
View File
@@ -8,7 +8,7 @@ from fastapi.responses import JSONResponse
from eviction import get_rag_operational_stats, EVICTION_LOG
from rag import QDRANT_URL, RAG_COLLECTION
log = logging.getLogger("jarvischat")
log = logging.getLogger("caic")
router = APIRouter()
+1 -1
View File
@@ -14,7 +14,7 @@ from search import query_searxng, format_search_results
from routers.chat import parse_llama_stream_chunk
from security import read_json_body, log_incident, BODY_LIMIT_CHAT_BYTES
log = logging.getLogger("jarvischat")
log = logging.getLogger("caic")
router = APIRouter()
+1 -1
View File
@@ -13,7 +13,7 @@ from db import get_db, insert_upload_context, list_upload_context_by_conversatio
from eviction import maybe_evict
from rag import chunk_text, QDRANT_URL, EMBED_URL, EMBED_MODEL, RAG_COLLECTION
log = logging.getLogger("jarvischat")
log = logging.getLogger("caic")
router = APIRouter()
+2 -2
View File
@@ -1,5 +1,5 @@
"""
JarvisChat - SearXNG integration, perplexity scoring, refusal/hedge detection.
cAIc - SearXNG integration, perplexity scoring, refusal/hedge detection.
"""
import logging
import math
@@ -10,7 +10,7 @@ import httpx
from config import SEARXNG_BASE, PERPLEXITY_THRESHOLD, REFUSAL_PATTERNS, HEDGE_PATTERNS
log = logging.getLogger("jarvischat")
log = logging.getLogger("caic")
def sanitize_outbound_url(url: str) -> str:
+2 -2
View File
@@ -1,5 +1,5 @@
"""
JarvisChat - Security utilities.
cAIc - Security utilities.
PIN hashing, audit logging, incident tracking, CSRF/origin checks,
rate limiting, request helpers.
"""
@@ -30,7 +30,7 @@ from config import (
import ipaddress
log = logging.getLogger("jarvischat")
log = logging.getLogger("caic")
SESSIONS: dict = {}
PIN_ATTEMPTS: dict = {}
+8 -8
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JarvisChat</title>
<title>cAIc</title>
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
@@ -263,7 +263,7 @@ body { font-family: var(--font-body); background: var(--bg-primary); color: var(
<div class="auth-screen" id="authScreen">
<div class="auth-card">
<div class="auth-title">JarvisChat Unlock</div>
<div class="auth-title">cAIc Unlock</div>
<div class="auth-subtitle">Enter 4-digit admin PIN to unlock advanced actions</div>
<div class="auth-warning">Security warning: PIN 1234 is weak. Use a non-trivial 4-digit PIN.</div>
<input id="pinInput" class="pin-input" type="password" inputmode="numeric" maxlength="4" autocomplete="off" />
@@ -276,8 +276,8 @@ body { font-family: var(--font-body); background: var(--bg-primary); color: var(
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
<img class="logo" src="/static/logo.png" alt="JarvisChat Logo" onerror="this.style.display='none'">
<h1>JarvisChat {{ version }}</h1>
<img class="logo" src="/static/logo.png" alt="cAIc Logo" onerror="this.style.display='none'">
<h1>cAIc {{ version }}</h1>
<div class="subtitle">🦙 local coding companion</div>
<div class="btn-row">
<button class="new-chat-btn" onclick="newChat()">+ New Chat</button>
@@ -332,7 +332,7 @@ body { font-family: var(--font-body); background: var(--bg-primary); color: var(
</div>
<div class="modal-section">
<h3>Web Search (SearXNG)</h3>
<p class="desc">When enabled, JarvisChat will automatically search the web if the model indicates uncertainty. Use the 🔍 button to force a web search.</p>
<p class="desc">When enabled, cAIc will automatically search the web if the model indicates uncertainty. Use the 🔍 button to force a web search.</p>
<div class="toggle-row">
<span class="toggle-label">Enable automatic web search</span>
<div class="toggle-switch on" id="searchToggle" onclick="toggleSearch()"></div>
@@ -388,7 +388,7 @@ body { font-family: var(--font-body); background: var(--bg-primary); color: var(
<div class="chat-container" id="chatContainer">
<div class="welcome-screen" id="welcomeScreen">
<div class="logo"></div>
<p>JarvisChat — your local coding companion.<br>Profile + Memory context injected automatically.<br>Web search kicks in when the model is uncertain.<br>Use 🔍 to force a web search.<br>Say "remember that..." to teach me things.</p>
<p>cAIc — your local coding companion.<br>Profile + Memory context injected automatically.<br>Web search kicks in when the model is uncertain.<br>Use 🔍 to force a web search.<br>Say "remember that..." to teach me things.</p>
</div>
</div>
<div class="input-area">
@@ -1109,7 +1109,7 @@ function newChat() {
function escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; }
function showWelcome() {
document.getElementById('chatContainer').innerHTML = '<div class="welcome-screen" id="welcomeScreen"><div class="logo"></div><p>JarvisChat — your local coding companion.<br>Profile + Memory context injected automatically.<br>Web search kicks in when the model is uncertain.<br>Use 🔍 to force a web search.<br>Say "remember that..." to teach me things.</p></div>';
document.getElementById('chatContainer').innerHTML = '<div class="welcome-screen" id="welcomeScreen"><div class="logo"></div><p>cAIc — your local coding companion.<br>Profile + Memory context injected automatically.<br>Web search kicks in when the model is uncertain.<br>Use 🔍 to force a web search.<br>Say "remember that..." to teach me things.</p></div>';
}
async function sendSearch() {
@@ -1479,7 +1479,7 @@ function addMessageToolbar(msgDiv) {
const blob = new Blob([t], { type: 'text/markdown' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `jarvischat-response-${Date.now()}.md`;
a.download = `caic-response-${Date.now()}.md`;
a.click();
URL.revokeObjectURL(a.href);
};
+2 -2
View File
@@ -9,8 +9,8 @@ from security import SESSIONS, PIN_ATTEMPTS
def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-test.db"
os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "caic-test.db"
SESSIONS.clear()
PIN_ATTEMPTS.clear()
db.init_db()
@@ -13,8 +13,8 @@ from security import SESSIONS, PIN_ATTEMPTS, RATE_EVENTS
def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-streaming.db"
os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "caic-streaming.db"
SESSIONS.clear()
PIN_ATTEMPTS.clear()
RATE_EVENTS.clear()
+3 -3
View File
@@ -13,8 +13,8 @@ from security import SESSIONS, PIN_ATTEMPTS, RATE_EVENTS
def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-completions.db"
os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "caic-completions.db"
SESSIONS.clear()
PIN_ATTEMPTS.clear()
RATE_EVENTS.clear()
@@ -22,7 +22,7 @@ def make_client(tmp_path: Path) -> TestClient:
return TestClient(app.app, raise_server_exceptions=False)
TEST_API_KEY = "test-sk-jarvischat-completions"
TEST_API_KEY = "test-sk-caic-completions"
def _auth_headers(extra: dict = None) -> dict:
+2 -2
View File
@@ -9,8 +9,8 @@ from security import SESSIONS, PIN_ATTEMPTS, RATE_EVENTS
def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-conversations.db"
os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "caic-conversations.db"
SESSIONS.clear()
PIN_ATTEMPTS.clear()
RATE_EVENTS.clear()
+2 -2
View File
@@ -12,8 +12,8 @@ from security import SESSIONS, PIN_ATTEMPTS, RATE_EVENTS
def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-errors.db"
os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "caic-errors.db"
SESSIONS.clear()
PIN_ATTEMPTS.clear()
RATE_EVENTS.clear()
+6 -6
View File
@@ -16,8 +16,8 @@ from security import SESSIONS, PIN_ATTEMPTS, RATE_EVENTS
def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-hardware.db"
os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "caic-hardware.db"
hardware.HARDWARE_STATE_PATH = tmp_path / "hardware_state.json"
SESSIONS.clear()
PIN_ATTEMPTS.clear()
@@ -72,7 +72,7 @@ def test_assess_hardware_all_services_reachable(tmp_path: Path, monkeypatch):
if "v1/models" in url:
return _MockGet(200, {"data": [{"id": "mistral-nemo:latest"}]})
if "6333" in url:
return _MockGet(200, {"result": {"collections": [{"name": "jarvischat"}]}})
return _MockGet(200, {"result": {"collections": [{"name": "caic"}]}})
if "8888" in url:
return _MockGet(200, {})
return _MockGet(200, {})
@@ -88,7 +88,7 @@ def test_assess_hardware_all_services_reachable(tmp_path: Path, monkeypatch):
assert state["llama_reachable"] is True
assert state["llama_models"] == ["mistral-nemo:latest"]
assert state["qdrant_reachable"] is True
assert state["qdrant_collections"] == ["jarvischat"]
assert state["qdrant_collections"] == ["caic"]
assert state["searxng_reachable"] is True
assert tmp_path.joinpath("hardware_state.json").exists()
@@ -130,7 +130,7 @@ def test_assess_hardware_llama_unreachable(tmp_path: Path, monkeypatch):
if "v1/models" in url:
raise httpx.ConnectError("refused")
if "6333" in url:
return _MockGet(200, {"result": {"collections": [{"name": "jarvischat"}]}})
return _MockGet(200, {"result": {"collections": [{"name": "caic"}]}})
if "8888" in url:
return _MockGet(200, {})
return _MockGet(200, {})
@@ -156,7 +156,7 @@ def test_get_hardware_endpoint(tmp_path: Path, monkeypatch):
if "v1/models" in url:
return _MockGet(200, {"data": [{"id": "mistral-nemo:latest"}]})
if "6333" in url:
return _MockGet(200, {"result": {"collections": [{"name": "jarvischat"}]}})
return _MockGet(200, {"result": {"collections": [{"name": "caic"}]}})
if "8888" in url:
return _MockGet(200, {})
return _MockGet(200, {})
+3 -3
View File
@@ -12,8 +12,8 @@ from security import SESSIONS, PIN_ATTEMPTS, RATE_EVENTS
def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-ingest.db"
os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "caic-ingest.db"
SESSIONS.clear()
PIN_ATTEMPTS.clear()
RATE_EVENTS.clear()
@@ -21,7 +21,7 @@ def make_client(tmp_path: Path) -> TestClient:
return TestClient(app.app, raise_server_exceptions=False)
TEST_API_KEY = "test-sk-jarvischat-ingest"
TEST_API_KEY = "test-sk-caic-ingest"
def _auth_headers() -> dict:
+2 -2
View File
@@ -9,8 +9,8 @@ from security import SESSIONS, PIN_ATTEMPTS, RATE_EVENTS, is_ip_allowed
def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-ip.db"
os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "caic-ip.db"
SESSIONS.clear()
PIN_ATTEMPTS.clear()
RATE_EVENTS.clear()
+2 -2
View File
@@ -10,8 +10,8 @@ from security import SESSIONS, PIN_ATTEMPTS, RATE_EVENTS
def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-memories.db"
os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "caic-memories.db"
SESSIONS.clear()
PIN_ATTEMPTS.clear()
RATE_EVENTS.clear()
+2 -2
View File
@@ -11,8 +11,8 @@ from security import SESSIONS, PIN_ATTEMPTS, RATE_EVENTS
def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-models.db"
os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "caic-models.db"
SESSIONS.clear()
PIN_ATTEMPTS.clear()
RATE_EVENTS.clear()
+2 -2
View File
@@ -9,8 +9,8 @@ from security import SESSIONS, PIN_ATTEMPTS, RATE_EVENTS
def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-presets.db"
os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "caic-presets.db"
SESSIONS.clear()
PIN_ATTEMPTS.clear()
RATE_EVENTS.clear()
+2 -2
View File
@@ -10,8 +10,8 @@ from security import SESSIONS, PIN_ATTEMPTS, RATE_EVENTS
def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-profile.db"
os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "caic-profile.db"
SESSIONS.clear()
PIN_ATTEMPTS.clear()
RATE_EVENTS.clear()
+3 -3
View File
@@ -14,8 +14,8 @@ from security import SESSIONS, PIN_ATTEMPTS, RATE_EVENTS
def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-rag-mgmt.db"
os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "caic-rag-mgmt.db"
SESSIONS.clear()
PIN_ATTEMPTS.clear()
RATE_EVENTS.clear()
@@ -54,7 +54,7 @@ class FakeAsyncClient:
pass
async def get(self, url, **kw):
if "/collections/jarvis_rag" in url:
if "/collections/caic_rag" in url:
return FakeResponse(200, {"result": {"vectors_count": 123}})
return FakeResponse(200)
+2 -2
View File
@@ -12,8 +12,8 @@ from security import SESSIONS, PIN_ATTEMPTS, RATE_EVENTS
def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-rate.db"
os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "caic-rate.db"
SESSIONS.clear()
PIN_ATTEMPTS.clear()
RATE_EVENTS.clear()
+2 -2
View File
@@ -13,8 +13,8 @@ from security import SESSIONS, PIN_ATTEMPTS, RATE_EVENTS
def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-search-route.db"
os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "caic-search-route.db"
SESSIONS.clear()
PIN_ATTEMPTS.clear()
RATE_EVENTS.clear()
+2 -2
View File
@@ -9,8 +9,8 @@ from security import SESSIONS, PIN_ATTEMPTS
def make_admin_client(tmp_path: Path) -> tuple[TestClient, dict[str, str]]:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-settings.db"
os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "caic-settings.db"
SESSIONS.clear()
PIN_ATTEMPTS.clear()
db.init_db()
+2 -2
View File
@@ -11,8 +11,8 @@ from security import SESSIONS, PIN_ATTEMPTS, RATE_EVENTS
def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-skills.db"
os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "caic-skills.db"
SESSIONS.clear()
PIN_ATTEMPTS.clear()
RATE_EVENTS.clear()
+2 -2
View File
@@ -12,8 +12,8 @@ from security import SESSIONS, PIN_ATTEMPTS, RATE_EVENTS
def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-upload.db"
os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "caic-upload.db"
SESSIONS.clear()
PIN_ATTEMPTS.clear()
RATE_EVENTS.clear()