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 ## Run
@@ -60,8 +60,8 @@ Refactored from single-file (`app.py`) into modules under project root:
### Entrypoint / API keys ### Entrypoint / API keys
- `app.py` line 148: `uvicorn.run(app, ...)` when called directly - `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 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 `JARVISCHAT_COMPLETIONS_API_KEY` env var or auto-generates - `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` - `config.py` line 13: `OLLAMA_BASE` is legacy/unused — all endpoints use `LLAMA_SERVER_BASE`
### Key flows ### 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 - `/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 - 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 - 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 ### 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. - `get_db()` opens new connection per request (no pool). Close after use.
- FTS5 virtual table `memories` for full-text search with BM25 ranking. - FTS5 virtual table `memories` for full-text search with BM25 ranking.
- `upload_context` table: auto-expiring document storage for chat context injection. - `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 | | Service | Required | Port |
|---------|----------|------| |---------|----------|------|
| llama-server (ultron) | Yes | 8081 + RPC :50052 (jarvis GPU) | | llama-server (coordinator) | Yes | 8081 + RPC :50052 (worker GPU) |
| SearXNG | No | 8888 | | SearXNG | No | 8888 |
| wttr.in | No | weather shortcut | | wttr.in | No | weather shortcut |
| rocm-smi | No | AMD GPU stats | | rocm-smi | No | AMD GPU stats |
| Qdrant | No | 6333 (ultron) — RAG vector search | | Qdrant | No | 6333 (coordinator) — RAG vector search |
### Config quirks ### 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 - `SUPPORTED_UPLOAD_TYPES` includes images (png/jpeg/gif/svg/webp) + text + PDF + JSON
- `UPLOAD_CONTEXT_EXPIRY_HOURS` = 1 hour - `UPLOAD_CONTEXT_EXPIRY_HOURS` = 1 hour
- Rate limits and payload caps in `config.py` — patch `security.RL_*` not `config.RL_*` for tests - 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 ### 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 ./venv/bin/uvicorn app:app --host 0.0.0.0 --port 8080 --reload
# Production (via systemd) # Production (via systemd)
sudo systemctl restart jarvischat sudo systemctl restart caic
# Direct run # Direct run
./venv/bin/python app.py ./venv/bin/python app.py
@@ -24,7 +24,7 @@ sudo systemctl restart jarvischat
## Architecture ## 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` ### Request Flow: `/api/chat`
@@ -54,22 +54,22 @@ FTS5 virtual table (`memories`) in SQLite. `search_memories()` uses BM25 ranking
### Key Constants (`config.py`) ### 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` - `SEARXNG_BASE``http://localhost:8888`
- `PERPLEXITY_THRESHOLD``15.0` - `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 - `VERSION` — current version string
### External Services ### External Services
| Service | Required | Port | | Service | Required | Port |
|---------|----------|------| |---------|----------|------|
| **llama-server** (ultron) | Yes | 8081 + RPC :50052 (jarvis GPU) | | **llama-server** (coordinator) | Yes | 8081 + RPC :50052 (worker GPU) |
| **SearXNG** | No | 8888 | | **SearXNG** | No | 8888 |
| **wttr.in** | No | weather shortcut | | **wttr.in** | No | weather shortcut |
| **rocm-smi** | No | AMD GPU stats | | **rocm-smi** | No | AMD GPU stats |
| **Qdrant** (ultron) | No | 6333 — RAG vector search | | **Qdrant** (coordinator) | No | 6333 — RAG vector search |
| **Ollama** (jarvis) | No | 11434 — embeddings only | | **Ollama** (worker) | No | 11434 — embeddings only |
### Database ### Database
@@ -86,4 +86,4 @@ All streaming endpoints yield `data: {json}\n\n`. Key event shapes:
### Deployment ### 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. 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. 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) ### Terminal RAG Hook — `POST /api/ingest` (v0.11.0)
- Bearer token auth (same key as `/v1/chat/completions`) - Bearer token auth (same key as `/v1/chat/completions`)
- Chunking via shared `chunk_text()` helper, embed via Ollama, upsert to Qdrant - 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) ### 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 - **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 ## File Structure
``` ```
/opt/jarvischat/ /opt/caic/
├── amqp.py # aio-pika AMQP connection manager + subscribe/rebind ├── amqp.py # aio-pika AMQP connection manager + subscribe/rebind
├── app.py # FastAPI app entry point ├── app.py # FastAPI app entry point
├── cluster.py # Cluster protocol: node registry, event log, ping/pong ├── 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 ```bash
# Create directory and venv # Create directory and venv
sudo mkdir -p /opt/jarvischat sudo mkdir -p /opt/caic
sudo chown $USER:$USER /opt/jarvischat sudo chown $USER:$USER /opt/caic
cd /opt/jarvischat cd /opt/caic
python3 -m venv venv python3 -m venv venv
# Install dependencies # Install dependencies
pip install fastapi uvicorn httpx psutil jinja2 python-multipart pypdf aio-pika pip install fastapi uvicorn httpx psutil jinja2 python-multipart pypdf aio-pika
# Set admin PIN before first startup (4 digits) # Set admin PIN before first startup (4 digits)
export JARVISCHAT_ADMIN_PIN=4827 export CAIC_ADMIN_PIN=4827
# Create subdirectories # Create subdirectories
mkdir -p templates static mkdir -p templates static
# Copy files # Copy files
# (copy all .py files to /opt/jarvischat/) # (copy all .py files to /opt/caic/)
# (copy routers/ directory to /opt/jarvischat/) # (copy routers/ directory to /opt/caic/)
# (copy templates/index.html to /opt/jarvischat/templates/) # (copy templates/index.html to /opt/caic/templates/)
``` ```
WARNING: Do not use `1234` as your admin PIN unless you accept weak local security. 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 ## Systemd Service
Create `/etc/systemd/system/jarvischat.service`: Create `/etc/systemd/system/caic.service`:
```ini ```ini
[Unit] [Unit]
Description=jarvisChat - Local Inference Web Interface Description=cAIc - Local Inference Web Interface
After=network.target After=network.target
[Service] [Service]
Type=simple Type=simple
User=jarvischat User=caic
Group=jarvischat Group=caic
WorkingDirectory=/opt/jarvischat WorkingDirectory=/opt/caic
ExecStart=/opt/jarvischat/venv/bin/uvicorn app:app --host 0.0.0.0 --port 8080 ExecStart=/opt/caic/venv/bin/uvicorn app:app --host 0.0.0.0 --port 8080
Restart=always Restart=always
RestartSec=5 RestartSec=5
@@ -160,8 +160,8 @@ WantedBy=multi-user.target
```bash ```bash
sudo systemctl daemon-reload sudo systemctl daemon-reload
sudo systemctl enable jarvischat sudo systemctl enable caic
sudo systemctl start jarvischat sudo systemctl start caic
``` ```
## Memory Commands ## Memory Commands
@@ -171,7 +171,7 @@ In chat, natural language triggers memory operations:
| You say | What happens | | You say | What happens |
|---------|--------------| |---------|--------------|
| "remember that I prefer Rust over Go" | Stores as `preference` | | "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` | | "note that the deadline is Friday" | Stores as `general` |
| "forget about the deadline" | Removes matching memories | | "forget about the deadline" | Removes matching memories |
@@ -317,4 +317,4 @@ MIT
## Repository ## 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 # Generated: 2026-07-01
# Execute sequentially. Run full test suite after each task before proceeding. # Execute sequentially. Run full test suite after each task before proceeding.
# Test command: ./venv/bin/python -m pytest tests/ -v # Test command: ./venv/bin/python -m pytest tests/ -v
@@ -7,13 +7,13 @@
## ~~TASK 1 — README Cleanup [DONE]~~ ## ~~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. 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.** **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` 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 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 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` 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**ultron receives `model_ready`, updates the cluster registry, and routes the query to the node 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. 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]~~ ## ~~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. 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. 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`:** **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 - `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` - `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 - Validate MIME type against `SUPPORTED_UPLOAD_TYPES` — return 415 if unsupported
- For PDF files, extract text using `pypdf` (add to requirements.txt) - For PDF files, extract text using `pypdf` (add to requirements.txt)
- For all other types, read as UTF-8 text - 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. - 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}` - 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]~~ ## ~~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). 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: 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 - 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` - 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}` - Return JSON: `{chunks_ingested, source, message}`
**Wire `ingest.router` into `app.py`.** **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 ```bash
#!/bin/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" # Add to ~/.bashrc: export PROMPT_COMMAND="jc_capture"
# Function to call after significant commands # Function to call after significant commands
JC_URL="http://192.168.50.210:8080/api/ingest" JC_URL="http://192.168.50.210:8080/api/ingest"
JC_TOKEN="${JARVISCHAT_COMPLETIONS_API_KEY}" JC_TOKEN="${CAIC_COMPLETIONS_API_KEY}"
jc_capture() { jc_capture() {
local cmd 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]~~ ## ~~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`: ### Config — add to `config.py`:
@@ -242,7 +242,7 @@ Lower score = evicted first. Tiebreak: `last_accessed` ASC (older wins).
```python ```python
async def get_collection_count() -> int async def get_collection_count() -> int
# GET /collections/jarvischat → return vectors_count # GET /collections/caic → return vectors_count
async def get_collection_stats() -> dict async def get_collection_stats() -> dict
# Return {vector_count, max_vectors, high_water, low_water, percent_full, pinned_sources} # 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 | | Method | Endpoint | Description |
|--------|----------|-------------| |--------|----------|-------------|
| GET | `/api/rag/stats` | Operational stats (see `get_rag_operational_stats()`) — admin required | | 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: ### 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: Run the following steps:
1. `apt-get update && apt-get install -y rabbitmq-server` 1. `apt-get update && apt-get install -y rabbitmq-server`
2. `systemctl enable rabbitmq-server && systemctl start rabbitmq-server` 2. `systemctl enable rabbitmq-server && systemctl start rabbitmq-server`
3. `systemctl status rabbitmq-server` — verify active/running 3. `systemctl status rabbitmq-server` — verify active/running
4. Enable the management plugin: `rabbitmq-plugins enable rabbitmq_management` 4. Enable the management plugin: `rabbitmq-plugins enable rabbitmq_management`
5. Create a dedicated jC vhost: `rabbitmqctl add_vhost jarvischat` 5. Create a dedicated jC vhost: `rabbitmqctl add_vhost caic`
6. Create a dedicated user: `rabbitmqctl add_user jarvischat CHANGEME_PASSWORD` — generate a random 24-char alphanumeric password and record it 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 jarvischat jarvischat ".*" ".*" ".*"` 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` 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` 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.admin`, type: `topic`, durable: true
- Exchange name: `jc.system`, 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. 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]~~ ## ~~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 `requirements.txt`:** `aio-pika>=9.0.0`
**Add to `config.py`:** **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_RECONNECT_DELAY` — seconds between reconnect attempts, default 5
- `AMQP_EXCHANGE_ADMIN``jc.admin` - `AMQP_EXCHANGE_ADMIN``jc.admin`
- `AMQP_EXCHANGE_SYSTEM``jc.system` - `AMQP_EXCHANGE_SYSTEM``jc.system`
@@ -423,7 +423,7 @@ Worker presence is assumed from registration onward. No periodic heartbeats —
**register** (worker → coordinator): **register** (worker → coordinator):
```json ```json
{ {
"node_name": "jarvis", "node_name": "worker01",
"node_type": "worker", "node_type": "worker",
"ip": "192.168.50.210", "ip": "192.168.50.210",
"capabilities": { "capabilities": {
@@ -432,12 +432,12 @@ Worker presence is assumed from registration onward. No periodic heartbeats —
}, },
"active_model": { "active_model": {
"name": "llama3.1", "version": "latest", "quant": "Q4_K_M", "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 "port": 8081
}, },
"inventory": [ "inventory": [
{"name": "llama3.1", "version": "latest", "quant": "Q4_K_M", {"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" "status": "active"
} }
@@ -446,7 +446,7 @@ Worker presence is assumed from registration onward. No periodic heartbeats —
**deregister** (worker → coordinator): **deregister** (worker → coordinator):
```json ```json
{ {
"node_name": "jarvis", "node_name": "worker01",
"reason": "shutdown", "reason": "shutdown",
"timestamp": "2026-07-06T12:00:00Z" "timestamp": "2026-07-06T12:00:00Z"
} }
@@ -455,8 +455,8 @@ Worker presence is assumed from registration onward. No periodic heartbeats —
**ping** (coordinator → worker): **ping** (coordinator → worker):
```json ```json
{ {
"from": "ultron", "from": "coordinator",
"node_name": "jarvis", "node_name": "worker01",
"type": "ping", "type": "ping",
"correlation_id": "550e8400-e29b-41d4-a716-446655440000", "correlation_id": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-07-06T12:00:00Z" "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): **pong** (worker → coordinator):
```json ```json
{ {
"node_name": "jarvis", "node_name": "worker01",
"type": "pong", "type": "pong",
"correlation_id": "550e8400-e29b-41d4-a716-446655440000", "correlation_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "active", "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`: Coordinator responds on `cluster.coordinator.response`:
```json ```json
{ {
"coordinator_node": "ultron", "coordinator_node": "coordinator",
"cluster_nodes": ["jarvis"], "cluster_nodes": ["worker01"],
"timestamp": "2026-07-06T12:00:00Z" "timestamp": "2026-07-06T12:00:00Z"
} }
``` ```
@@ -494,7 +494,7 @@ Coordinator responds on `cluster.coordinator.response`:
**event** (worker → coordinator): **event** (worker → coordinator):
```json ```json
{ {
"node_name": "jarvis", "node_name": "worker01",
"severity": "info", "severity": "info",
"message": "llama-server started with model llama3.1:latest", "message": "llama-server started with model llama3.1:latest",
"details": {"model": "llama3.1:latest", "port": 8081, "pid": 1234}, "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). **Create `node_agent/agent.py`** in the repo (new directory).
### 12.1 Config & Inventory Discovery ### 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_name` — hostname, default from `socket.gethostname()`
- `node_ip` — LAN IP, default from socket - `node_ip` — LAN IP, default from socket
- `node_type``"worker"` (fixed) - `node_type``"worker"` (fixed)
- `capabilities` — comma-separated list, e.g. `llm,rag` - `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 - `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) - `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. 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`: Publishes registration to `jc.admin`, routing key `node.{node_name}.register`:
```json ```json
{ {
"node_name": "jarvis", "node_name": "worker01",
"node_type": "worker", "node_type": "worker",
"ip": "192.168.50.210", "ip": "192.168.50.210",
"capabilities": ["llm"], "capabilities": ["llm"],
@@ -710,7 +710,7 @@ After admission: listens on `jc.admin`, routing key `node.{node_name}.ping`. On
```json ```json
{ {
"node_name": "jarvis", "node_name": "worker01",
"type": "pong", "type": "pong",
"correlation_id": "<echoed from ping>", "correlation_id": "<echoed from ping>",
"status": "active", "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`: Listens on `jc.admin`, routing key `node.{node_name}.cmd.swap_model`:
- Payload: `{model_filename: str}` - Payload: `{model_filename: str}`
- Stops current llama-server: `systemctl stop llama-server` - 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) - 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 - 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`: - 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` **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: **Write `tests/test_node_agent.py`** covering:
- Registration payload construction from config + model discovery — assert correct JSON shape - 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. **Prerequisites:** Tasks 912 complete. At least one worker node admitted to cluster.
**Install Phi-4-mini on ultron (infrastructure step):** **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 /home/gramps/models` - 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 ultron CPU/iGPU), description `Llama.cpp Server (Phi-4-mini — triage/routing)` - 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` - `systemctl daemon-reload && systemctl enable llama-server-triage && systemctl start llama-server-triage`
- Verify: `curl -s http://localhost:8083/v1/models` - 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 ## 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`:** **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) ### 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:** **Services to containerize:**
- jarvisChat (FastAPI app + SQLite) - cAIc (FastAPI app + SQLite)
- SearXNG - SearXNG
- Qdrant - Qdrant
- RabbitMQ - RabbitMQ
@@ -936,7 +936,7 @@ Commit all changes introduced across Tasks 915 with message: `feat: Roadmap N
- Ollama (embeddings) - Ollama (embeddings)
**Also needed:** **Also needed:**
- `Dockerfile` for the jarvisChat app itself - `Dockerfile` for the cAIc app itself
- `docker-compose.yml` with all services, volumes, networks, env vars - `docker-compose.yml` with all services, volumes, networks, env vars
- Setup wizard script (run on first boot) that: - Setup wizard script (run on first boot) that:
- Probes CPU vs GPU (reuses `hardware.py`) - 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. Single persistent aio-pika connection with auto-reconnect.
""" """
import asyncio import asyncio
@@ -21,7 +21,7 @@ from config import (
get_amqp_url, get_amqp_url,
) )
log = logging.getLogger("jarvischat") log = logging.getLogger("caic")
_connection = None _connection = None
_channel = None _channel = None
+6 -6
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
JarvisChat - Entry point. cAIc - Entry point.
Creates the FastAPI app, registers middleware, mounts all routers. Creates the FastAPI app, registers middleware, mounts all routers.
""" """
import logging import logging
@@ -43,10 +43,10 @@ import routers.rag_admin as rag_admin
import routers.cluster as cluster_router import routers.cluster as cluster_router
# --- Logging --- # --- Logging ---
log = logging.getLogger("jarvischat") log = logging.getLogger("caic")
log.setLevel(logging.DEBUG) log.setLevel(logging.DEBUG)
syslog_handler = logging.handlers.SysLogHandler(address="/dev/log") 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) log.addHandler(syslog_handler)
BASE_DIR = Path(__file__).parent BASE_DIR = Path(__file__).parent
@@ -55,7 +55,7 @@ templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): 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) os.makedirs(UPLOAD_DIR, exist_ok=True)
init_db() init_db()
log.info(f"Memory system: {get_memory_count()} memories loaded") 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") log.warning("RAG_MAX_VECTORS <= 0 — RAG eviction disabled")
yield yield
log.info("JarvisChat shutting down") log.info("cAIc shutting down")
await amqp_disconnect() await amqp_disconnect()
app = FastAPI(title="JarvisChat", lifespan=lifespan) app = FastAPI(title="cAIc", lifespan=lifespan)
@app.exception_handler(Exception) @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 hashlib
import hmac import hmac
@@ -21,7 +21,7 @@ from security import (
read_json_body, hash_pin, customer_error_envelope, log_incident, read_json_body, hash_pin, customer_error_envelope, log_incident,
) )
log = logging.getLogger("jarvischat") log = logging.getLogger("caic")
router = APIRouter() 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. Maintains node registry, event log, coordinator state, and ping-based health checks.
""" """
import asyncio import asyncio
@@ -11,7 +11,7 @@ from datetime import datetime, timezone
from amqp import publish, subscribe from amqp import publish, subscribe
from config import AMQP_EXCHANGE_ADMIN, AMQP_EXCHANGE_SYSTEM from config import AMQP_EXCHANGE_ADMIN, AMQP_EXCHANGE_SYSTEM
log = logging.getLogger("jarvischat") log = logging.getLogger("caic")
CLUSTER_NODES: dict[str, dict] = {} CLUSTER_NODES: dict[str, dict] = {}
CLUSTER_EVENTS: deque = deque(maxlen=1000) 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. All constants, environment variables, limits, and skill registry live here.
""" """
import os import os
@@ -7,23 +7,23 @@ import re
import ipaddress import ipaddress
import logging import logging
log = logging.getLogger("jarvischat") log = logging.getLogger("caic")
VERSION = "v0.14.0" VERSION = "v0.14.0"
OLLAMA_BASE = os.environ.get("OLLAMA_BASE", "http://localhost:11434") 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") LLAMA_SERVER_BASE = os.environ.get("LLAMA_SERVER_BASE", "http://192.168.50.108:8081")
SEARXNG_BASE = "http://localhost:8888" SEARXNG_BASE = "http://localhost:8888"
DEFAULT_MODEL = "llama3.1:latest" 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 ---
AMQP_RECONNECT_DELAY = 5 AMQP_RECONNECT_DELAY = 5
AMQP_EXCHANGE_ADMIN = "jc.admin" AMQP_EXCHANGE_ADMIN = "jc.admin"
AMQP_EXCHANGE_SYSTEM = "jc.system" 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: def get_amqp_url() -> str:
url = os.environ.get("JARVISCHAT_AMQP_URL") url = os.environ.get("CAIC_AMQP_URL")
if url: if url:
return url return url
try: try:
@@ -31,22 +31,22 @@ def get_amqp_url() -> str:
pw = f.read().strip() pw = f.read().strip()
except (FileNotFoundError, OSError): except (FileNotFoundError, OSError):
pw = "password" pw = "password"
return f"amqp://jarvischat:{pw}@localhost:5672/jarvischat" return f"amqp://caic:{pw}@localhost:5672/caic"
# --- Auth --- # --- Auth ---
SESSION_TIMEOUT_SECONDS = 90 SESSION_TIMEOUT_SECONDS = 90
MAX_PIN_ATTEMPTS = 5 MAX_PIN_ATTEMPTS = 5
PIN_LOCKOUT_SECONDS = 300 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 = { TRUSTED_ORIGINS = {
origin.strip().rstrip("/") origin.strip().rstrip("/")
for origin in os.getenv("JARVISCHAT_TRUSTED_ORIGINS", "").split(",") for origin in os.getenv("CAIC_TRUSTED_ORIGINS", "").split(",")
if origin.strip() 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" 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 = ( 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 --- # --- Rate limits ---
@@ -64,11 +64,11 @@ BODY_LIMIT_CHAT_BYTES = 128 * 1024
BODY_LIMIT_PROFILE_BYTES = 256 * 1024 BODY_LIMIT_PROFILE_BYTES = 256 * 1024
# --- Upload --- # --- Upload ---
UPLOAD_DIR = "/tmp/jarvischat_uploads" UPLOAD_DIR = "/tmp/caic_uploads"
MAX_UPLOAD_BYTES = 20 * 1024 * 1024 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"} 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" QDRANT_URL = "http://192.168.50.108:6333"
RAG_COLLECTION = "jarvis_rag" RAG_COLLECTION = "caic_rag"
UPLOAD_CONTEXT_EXPIRY_HOURS = 1 UPLOAD_CONTEXT_EXPIRY_HOURS = 1
BODY_LIMIT_UPLOAD_BYTES = MAX_UPLOAD_BYTES 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. Schema init, connection factory, settings helpers, skill state management.
""" """
import logging import logging
@@ -16,10 +16,10 @@ from config import (
MAX_SKILL_PROMPT_CHARS, ALLOWED_NETWORKS, MAX_SKILL_PROMPT_CHARS, ALLOWED_NETWORKS,
) )
log = logging.getLogger("jarvischat") log = logging.getLogger("caic")
BASE_DIR = Path(__file__).parent BASE_DIR = Path(__file__).parent
DB_PATH = BASE_DIR / "jarvischat.db" DB_PATH = BASE_DIR / "caic.db"
def get_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() 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: if not existing_pin_hash or not existing_pin_salt:
from config import ALLOW_DEFAULT_PIN 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): if re.fullmatch(r"\d{4}", configured_pin):
seed_pin, pin_source = configured_pin, "env" seed_pin, pin_source = configured_pin, "env"
elif ALLOW_DEFAULT_PIN: elif ALLOW_DEFAULT_PIN:
seed_pin, pin_source = "1234", "default" seed_pin, pin_source = "1234", "default"
else: else:
raise RuntimeError( raise RuntimeError(
"Admin PIN bootstrap blocked: set JARVISCHAT_ADMIN_PIN to a 4-digit PIN " "Admin PIN bootstrap blocked: set CAIC_ADMIN_PIN to a 4-digit PIN "
"or set JARVISCHAT_ALLOW_DEFAULT_PIN=true." "or set CAIC_ALLOW_DEFAULT_PIN=true."
) )
salt_hex, pin_hash_hex = hash_pin(seed_pin) 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)) 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 # 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 ## 1. Stack Overview
@@ -15,10 +15,10 @@
│ │ │ │ │ │ │ │ │ │
│ ▼ ▼ ▼ │ │ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────┐ │ │ ┌──────────────────────────────────────────────────┐ │
│ │ jarvisChat (FastAPI) │ │ │ │ cAIc (FastAPI) │ │
│ │ :8080 (HTTP) │ │ │ │ :8080 (HTTP) │ │
│ │ │ │ │ │ │ │
│ │ SQLite ◄── jarvischat.db (volume) │ │ │ │ SQLite ◄── caic.db (volume) │ │
│ │ Uploads ◄── /app/uploads (volume) │ │ │ │ Uploads ◄── /app/uploads (volume) │ │
│ └──────────┬──────────────┬───────────────────────┘ │ │ └──────────┬──────────────┬───────────────────────┘ │
│ │ │ │ │ │ │ │
@@ -37,7 +37,7 @@
| Service | Image | Role | | 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 | | **SearXNG** | `searxng/searxng:latest` | Privacy-respecting web search |
| **Qdrant** | `qdrant/qdrant:latest` | Vector database for RAG | | **Qdrant** | `qdrant/qdrant:latest` | Vector database for RAG |
| **RabbitMQ** | `rabbitmq:4-management` | Message broker for AMQP cluster | | **RabbitMQ** | `rabbitmq:4-management` | Message broker for AMQP cluster |
@@ -57,9 +57,9 @@
## 2. Service Catalog ## 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:** **Ports:**
| Container | Host | Purpose | | Container | Host | Purpose |
@@ -69,8 +69,8 @@
**Volumes:** **Volumes:**
| Container path | Type | Purpose | | Container path | Type | Purpose |
|----------------|------|---------| |----------------|------|---------|
| `/app/jarvischat.db` | named volume `jarvischat_data` | SQLite database | | `/app/caic.db` | named volume `caic_data` | SQLite database |
| `/app/uploads` | named volume `jarvischat_uploads` | Uploaded files | | `/app/uploads` | named volume `caic_uploads` | Uploaded files |
| `/app/hardware_state.json` | (inside volume) | Cached hardware probe | | `/app/hardware_state.json` | (inside volume) | Cached hardware probe |
**Dependencies:** Wait for SearXNG, Qdrant, RabbitMQ, llama-server, Ollama before serving. **Dependencies:** Wait for SearXNG, Qdrant, RabbitMQ, llama-server, Ollama before serving.
@@ -153,7 +153,7 @@ QDRANT__SERVICE__GRPC_PORT=6334
**Environment:** **Environment:**
```env ```env
RABBITMQ_DEFAULT_USER=jarvischat RABBITMQ_DEFAULT_USER=caic
RABBITMQ_DEFAULT_PASS_FILE=/run/secrets/rabbitmq_password RABBITMQ_DEFAULT_PASS_FILE=/run/secrets/rabbitmq_password
RABBITMQ_DEFAULT_VHOST=/ RABBITMQ_DEFAULT_VHOST=/
``` ```
@@ -227,9 +227,9 @@ LLAMA_ARG_RPC= # optional: comma-separated RPC endpoints
```env ```env
# --- Secrets (auto-generated, change before production) --- # --- Secrets (auto-generated, change before production) ---
JARVISCHAT_ADMIN_PIN= CAIC_ADMIN_PIN=
JARVISCHAT_COMPLETIONS_API_KEY= CAIC_COMPLETIONS_API_KEY=
JARVISCHAT_ALLOW_DEFAULT_PIN=false CAIC_ALLOW_DEFAULT_PIN=false
RABBITMQ_PASSWORD= RABBITMQ_PASSWORD=
SEARXNG_SECRET_KEY= SEARXNG_SECRET_KEY=
@@ -257,9 +257,9 @@ LLAMA_CTX_SIZE=4096
OLLAMA_EMBED_MODEL=all-minilm:latest OLLAMA_EMBED_MODEL=all-minilm:latest
# --- Network --- # --- 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 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
JARVISCHAT_TRUSTED_ORIGINS= CAIC_TRUSTED_ORIGINS=
JARVISCHAT_TRUST_X_FORWARDED_FOR=false CAIC_TRUST_X_FORWARDED_FOR=false
``` ```
### 3.2 Mapping of config.py → .env variable ### 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 | | `OLLAMA_BASE` | `OLLAMA_BASE` | Ollama |
| `SEARXNG_BASE` | `SEARXNG_BASE` | SearXNG | | `SEARXNG_BASE` | `SEARXNG_BASE` | SearXNG |
| `QDRANT_URL` | `QDRANT_URL` | Qdrant | | `QDRANT_URL` | `QDRANT_URL` | Qdrant |
| `COMPLETIONS_API_KEY` | `JARVISCHAT_COMPLETIONS_API_KEY` | — | | `COMPLETIONS_API_KEY` | `CAIC_COMPLETIONS_API_KEY` | — |
| `ALLOWED_CIDRS_RAW` | `JARVISCHAT_ALLOWED_CIDRS` | — | | `ALLOWED_CIDRS_RAW` | `CAIC_ALLOWED_CIDRS` | — |
| `TRUST_X_FORWARDED_FOR` | `JARVISCHAT_TRUST_X_FORWARDED_FOR` | — | | `TRUST_X_FORWARDED_FOR` | `CAIC_TRUST_X_FORWARDED_FOR` | — |
| `TRUSTED_ORIGINS` | `JARVISCHAT_TRUSTED_ORIGINS` | — | | `TRUSTED_ORIGINS` | `CAIC_TRUSTED_ORIGINS` | — |
| `RAG_MAX_VECTORS` | `RAG_MAX_VECTORS` | — (calc'd from RAM) | | `RAG_MAX_VECTORS` | `RAG_MAX_VECTORS` | — (calc'd from RAM) |
### 3.3 Secrets management ### 3.3 Secrets management
| Secret | Generated by | Stored in | Mounted to | | Secret | Generated by | Stored in | Mounted to |
|--------|-------------|-----------|------------| |--------|-------------|-----------|------------|
| `JARVISCHAT_ADMIN_PIN` | User prompt | `.env` | jarvisChat container | | `CAIC_ADMIN_PIN` | User prompt | `.env` | cAIc container |
| `JARVISCHAT_COMPLETIONS_API_KEY` | Auto-generated, shown to user | `.env` | jarvisChat container | | `CAIC_COMPLETIONS_API_KEY` | Auto-generated, shown to user | `.env` | cAIc container |
| `RABBITMQ_PASSWORD` | Auto-generated | `.env` + Docker secret | RabbitMQ container | | `RABBITMQ_PASSWORD` | Auto-generated | `.env` + Docker secret | RabbitMQ container |
| `SEARXNG_SECRET_KEY` | Auto-generated | `.env` | SearXNG 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. **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 ```dockerfile
FROM python:3.13-slim-bookworm AS builder FROM python:3.13-slim-bookworm AS builder
@@ -321,12 +321,12 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"]
```yaml ```yaml
services: services:
jarvischat: caic:
build: . build: .
ports: ["8080:8080"] ports: ["8080:8080"]
volumes: volumes:
- jarvischat_data:/app/jarvischat.db - caic_data:/app/caic.db
- jarvischat_uploads:/app/uploads - caic_uploads:/app/uploads
env_file: .env env_file: .env
depends_on: depends_on:
searxng: { condition: service_started } searxng: { condition: service_started }
@@ -416,8 +416,8 @@ services:
restart: unless-stopped restart: unless-stopped
volumes: volumes:
jarvischat_data: caic_data:
jarvischat_uploads: caic_uploads:
searxng_config: searxng_config:
qdrant_storage: qdrant_storage:
rabbitmq_data: rabbitmq_data:
@@ -441,11 +441,11 @@ secrets:
| From | To | Port | Protocol | | From | To | Port | Protocol |
|------|----|------|----------| |------|----|------|----------|
| jarvisChat | llama-server | 8081 | HTTP | | cAIc | llama-server | 8081 | HTTP |
| jarvisChat | Ollama | 11434 | HTTP | | cAIc | Ollama | 11434 | HTTP |
| jarvisChat | SearXNG | 8080 | HTTP | | cAIc | SearXNG | 8080 | HTTP |
| jarvisChat | Qdrant | 6333 | HTTP | | cAIc | Qdrant | 6333 | HTTP |
| jarvisChat | RabbitMQ | 5672 | AMQP | | cAIc | RabbitMQ | 5672 | AMQP |
| RabbitMQ | (cluster peers) | 4369 | EPMD | | RabbitMQ | (cluster peers) | 4369 | EPMD |
| RabbitMQ | (cluster peers) | 25672 | Inter-node | | RabbitMQ | (cluster peers) | 25672 | Inter-node |
@@ -453,7 +453,7 @@ secrets:
| Port | Service | Should expose? | Notes | | Port | Service | Should expose? | Notes |
|------|---------|---------------|-------| |------|---------|---------------|-------|
| 8080 | jarvisChat | ✅ Required | UI + API | | 8080 | cAIc | ✅ Required | UI + API |
| 8888 | SearXNG | Optional | Only if user wants standalone search | | 8888 | SearXNG | Optional | Only if user wants standalone search |
| 6333 | Qdrant | Optional | Only for external tooling | | 6333 | Qdrant | Optional | Only for external tooling |
| 5672 | RabbitMQ | Optional | Only for remote AMQP clients | | 5672 | RabbitMQ | Optional | Only for remote AMQP clients |
@@ -461,7 +461,7 @@ secrets:
| 8081 | llama-server | Optional | Only for external tooling | | 8081 | llama-server | Optional | Only for external tooling |
| 11434 | Ollama | 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 ### 5.3 Reverse proxy consideration
@@ -534,7 +534,7 @@ This is out of scope for v1.0 but documented for future.
./docker-deploy/ ./docker-deploy/
├── .env # All env vars (SECRET — add to .gitignore) ├── .env # All env vars (SECRET — add to .gitignore)
├── docker-compose.yml # Compose stack definition ├── docker-compose.yml # Compose stack definition
├── Dockerfile # jarvisChat image build ├── Dockerfile # cAIc image build
├── secrets/ ├── secrets/
│ └── rabbitmq_password.txt # RabbitMQ password file │ └── rabbitmq_password.txt # RabbitMQ password file
├── searxng/ ├── searxng/
@@ -564,9 +564,9 @@ Re-running `setup.sh`:
| Item | Removal method | | Item | Removal method |
|------|---------------| |------|---------------|
| Docker containers | `docker compose down -v` | | Docker containers | `docker compose down -v` |
| Docker images | `docker rmi jarvischat:latest` (ask about other images) | | Docker images | `docker rmi caic:latest` (ask about other images) |
| Docker volumes | `docker volume rm jarvischat_data ...` (prompt first) | | Docker volumes | `docker volume rm caic_data ...` (prompt first) |
| Network `jarvischat_default` | Removed with compose | | Network `caic_default` | Removed with compose |
| `.env` file | `rm .env` | | `.env` file | `rm .env` |
| `secrets/` directory | `rm -rf secrets/` | | `secrets/` directory | `rm -rf secrets/` |
| `searxng/` directory | `rm -rf searxng/` | | `searxng/` directory | `rm -rf searxng/` |
@@ -578,7 +578,7 @@ Re-running `setup.sh`:
| Item | Reason | | Item | Reason |
|------|--------| |------|--------|
| `./models/*.gguf` | User data — prompt for deletion | | `./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?" | | `./uploads/` (in volume) | Prompt: "Keep uploaded files?" |
| Docker Engine itself | Not installed by this project — leave it | | 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) 3. ASK: "Remove secrets/ and searxng/ directories?" (default no)
4. ASK: "Remove Docker images? (y/N)" (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 ├── Y → docker image ls | grep searxng/qdrant/rabbitmq → prompt per image
└── N → skip └── N → skip
5. ASK: "Keep database volume snapshot? (Y/n)" (default yes) 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) └── Y → leave volume (can be reattached later)
6. ASK: "Remove model files from ./models/? (y/N)" (default no) 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 ### 9.1 What a worker runs
``` ```
Worker machine (e.g. jarvis, Corsair) Worker machine (e.g. worker01, worker02)
┌────────────────────────────────────┐ ┌────────────────────────────────────┐
│ llama-server │ │ llama-server │
│ (single binary, no build needed) │ │ (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 - [ ] `Dockerfile` written and builds clean
- [ ] `docker-compose.yml` boots all containers - [ ] `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 - [ ] SearXNG settings.yml generated correctly by setup.sh
- [ ] RabbitMQ password secret mounted correctly - [ ] RabbitMQ password secret mounted correctly
- [ ] GPU (NVIDIA) passes through to llama-server container - [ ] 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) docker.md ← this file (planning doc)
Dockerfile ← jarvisChat image Dockerfile ← cAIc image
docker-compose.yml ← full stack docker-compose.yml ← full stack
.env.example ← template without secrets .env.example ← template without secrets
setup.sh ← extraction wizard setup.sh ← extraction wizard
+17 -17
View File
@@ -1,10 +1,10 @@
# Developer Architecture Guide # 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 ## 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 ### 1.1 Module Layout
@@ -29,11 +29,11 @@ Refactored from single-file (`app.py`) into modules under project root:
| Service | Required | Port | Purpose | | 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 | | SearXNG | No | 8888 | Privacy-respecting web search |
| Qdrant (ultron) | No | 6333 | Vector database for RAG | | Qdrant (coordinator) | No | 6333 | Vector database for RAG |
| Ollama (jarvis) | No | 11434 | Embeddings for RAG chunk vectors | | Ollama (worker) | No | 11434 | Embeddings for RAG chunk vectors |
| RabbitMQ (ultron) | No | 5672 | AMQP broker for cluster messaging | | RabbitMQ (coordinator) | No | 5672 | AMQP broker for cluster messaging |
| rocm-smi | No | — | AMD GPU stats (host-level) | | rocm-smi | No | — | AMD GPU stats (host-level) |
### 1.3 Config Discovery ### 1.3 Config Discovery
@@ -42,11 +42,11 @@ Key base URLs are configured via environment variables with sensible defaults:
| Variable | Default | Service | | 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 | | `OLLAMA_BASE` | `http://localhost:11434` | Legacy — all inference goes through LLAMA_SERVER_BASE |
| `SEARXNG_BASE` | `http://localhost:8888` | SearXNG | | `SEARXNG_BASE` | `http://localhost:8888` | SearXNG |
| `QDRANT_URL` | `http://192.168.50.108:6333` | Qdrant on ultron | | `QDRANT_URL` | `http://192.168.50.108:6333` | Qdrant on coordinator |
| `JARVISCHAT_AMQP_URL` | `amqp://jarvischat:password@localhost:5672/jarvischat` | RabbitMQ | | `CAIC_AMQP_URL` | `amqp://caic:password@localhost:5672/caic` | RabbitMQ |
## 2. Request/Response Architecture ## 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) 1. Bearer token auth (same key as completions API)
2. Chunk text via shared `chunk_text()` helper (512-token chunks, 128-token overlap) 2. Chunk text via shared `chunk_text()` helper (512-token chunks, 128-token overlap)
3. Embed via Ollama `/api/embeddings` 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 5. Trigger `maybe_evict()` if collection exceeds high-water mark
### 2.4 Upload Pipeline (`/api/upload`) ### 2.4 Upload Pipeline (`/api/upload`)
@@ -116,7 +116,7 @@ Design notes:
- Admin PIN hashed with PBKDF2-HMAC-SHA256 + salt - Admin PIN hashed with PBKDF2-HMAC-SHA256 + salt
- Failed PIN attempts tracked per client IP (max 5, 300s lockout) - 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 ### 4.3 Browser and API Abuse Controls
@@ -141,8 +141,8 @@ Design notes:
### 5.1 Vector Search ### 5.1 Vector Search
- Qdrant collection `jarvis_rag` on ultron:6333 - Qdrant collection `caic_rag` on coordinator:6333
- Embeddings via Ollama on jarvis:11434 (`/api/embeddings`) - Embeddings via Ollama on worker:11434 (`/api/embeddings`)
- Shared `chunk_text(text, chunk_size=512, overlap=128)` helper in rag.py - Shared `chunk_text(text, chunk_size=512, overlap=128)` helper in rag.py
- Upload and ingest endpoints share the same chunk+embed+upsert pipeline - 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 ### 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:** **How it works:**
- A single RabbitMQ broker (or clustered set of brokers) acts as the central nervous system - 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 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 | | **RabbitMQ client (aio-pika)** | Required — publishes commands, consumes events | Required — consumes commands, publishes events |
| **FastAPI / uvicorn** | Required | Not needed | | **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 | | **Qdrant** | Optional (recommended) — vector DB for RAG | Not needed |
| **SearXNG** | Optional — web search | 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 | | **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) Coordinator Worker(s)
┌────────────────────┐ ┌─────────────────────────┐ ┌────────────────────┐ ┌─────────────────────────┐
jarvisChat │ │ llama-server │ cAIc │ │ llama-server │
│ (FastAPI + SQLite)│ │ (inference) │ │ (FastAPI + SQLite)│ │ (inference) │
│ RabbitMQ server │◄──AMQP───────│ aio-pika (agent) │ │ RabbitMQ server │◄──AMQP───────│ aio-pika (agent) │
│ SearXNG (opt) │ persistent │ ROCm / CUDA (if GPU) │ │ 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): Pending implementation (Tasks 1015):
- `amqp.py` — aio-pika connection manager with reconnect - `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) - `triage.py` — Phi-4-mini query classification (general/code/search/rag)
- Dynamic model swap via llama-server RPC - 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 Last updated: 2026-07-06
Owner: Gramps Owner: Gramps
@@ -9,9 +9,9 @@ Scope: Active roadmap items and backlog.
| Task | Description | Status | | Task | Description | Status |
|------|-------------|--------| |------|-------------|--------|
| Task 8 | RAG Corpus Management (score-based eviction, pinned sources, operational stats) | **DONE** | | 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 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 12 | AMQP wiring: inject context into chat/completions pipeline | Pending |
| Task 13 | Query triage via Phi-4-mini (`triage.py`) | Pending | | Task 13 | Query triage via Phi-4-mini (`triage.py`) | Pending |
| Task 14 | Dynamic model swap — publish cmd, handle ready/failed | 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 asyncio
import logging import logging
@@ -14,7 +14,7 @@ from config import (
RAG_ACCESS_WEIGHT, RAG_AGE_WEIGHT, RAG_ACCESS_WEIGHT, RAG_AGE_WEIGHT,
) )
log = logging.getLogger("jarvischat") log = logging.getLogger("caic")
eviction_lock = asyncio.Lock() eviction_lock = asyncio.Lock()
EVICTION_LOG: list[dict] = [] 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 json
import logging import logging
import subprocess import subprocess
log = logging.getLogger("jarvischat") log = logging.getLogger("caic")
def get_gpu_stats() -> dict: 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 asyncio
import json import json
@@ -12,7 +12,7 @@ import psutil
from config import LLAMA_SERVER_BASE, SEARXNG_BASE from config import LLAMA_SERVER_BASE, SEARXNG_BASE
log = logging.getLogger("jarvischat") log = logging.getLogger("caic")
HARDWARE_STATE_PATH = Path("hardware_state.json") HARDWARE_STATE_PATH = Path("hardware_state.json")
_TIMEOUT_EXPIRED = subprocess.TimeoutExpired _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. CRUD, search, remember/forget command processing, topic detection.
""" """
import logging import logging
@@ -10,7 +10,7 @@ from typing import Optional
from db import get_db from db import get_db
from config import MAX_MEMORY_FACT_CHARS from config import MAX_MEMORY_FACT_CHARS
log = logging.getLogger("jarvischat") log = logging.getLogger("caic")
REMEMBER_PATTERNS = [ REMEMBER_PATTERNS = [
(r"remember that (.+)", "explicit"), (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 asyncio
import logging 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 memory import search_memories
from config import MAX_SKILL_PROMPT_CHARS, QDRANT_URL, RAG_COLLECTION 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_URL = "http://192.168.50.210:11434"
EMBED_MODEL = "mxbai-embed-large" 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 security import read_json_body, log_incident, BODY_LIMIT_CHAT_BYTES
from config import MAX_CHAT_MESSAGE_CHARS from config import MAX_CHAT_MESSAGE_CHARS
log = logging.getLogger("jarvischat") log = logging.getLogger("caic")
router = APIRouter() router = APIRouter()
+1 -1
View File
@@ -20,7 +20,7 @@ from db import get_db
from rag import build_system_prompt from rag import build_system_prompt
from routers.chat import parse_llama_stream_chunk from routers.chat import parse_llama_stream_chunk
log = logging.getLogger("jarvischat") log = logging.getLogger("caic")
router = APIRouter() 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 security import read_json_body, BODY_LIMIT_DEFAULT_BYTES
from config import DEFAULT_MODEL, MAX_CONVERSATION_TITLE_CHARS from config import DEFAULT_MODEL, MAX_CONVERSATION_TITLE_CHARS
log = logging.getLogger("jarvischat") log = logging.getLogger("caic")
router = APIRouter() router = APIRouter()
+1 -1
View File
@@ -10,7 +10,7 @@ from config import COMPLETIONS_API_KEY
from eviction import maybe_evict from eviction import maybe_evict
from rag import chunk_text, QDRANT_URL, EMBED_URL, EMBED_MODEL, RAG_COLLECTION from rag import chunk_text, QDRANT_URL, EMBED_URL, EMBED_MODEL, RAG_COLLECTION
log = logging.getLogger("jarvischat") log = logging.getLogger("caic")
router = APIRouter() router = APIRouter()
+1 -1
View File
@@ -12,7 +12,7 @@ from config import LLAMA_SERVER_BASE
from gpu import get_gpu_stats from gpu import get_gpu_stats
from security import read_json_body, BODY_LIMIT_DEFAULT_BYTES from security import read_json_body, BODY_LIMIT_DEFAULT_BYTES
log = logging.getLogger("jarvischat") log = logging.getLogger("caic")
router = APIRouter() 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 eviction import get_rag_operational_stats, EVICTION_LOG
from rag import QDRANT_URL, RAG_COLLECTION from rag import QDRANT_URL, RAG_COLLECTION
log = logging.getLogger("jarvischat") log = logging.getLogger("caic")
router = APIRouter() 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 routers.chat import parse_llama_stream_chunk
from security import read_json_body, log_incident, BODY_LIMIT_CHAT_BYTES from security import read_json_body, log_incident, BODY_LIMIT_CHAT_BYTES
log = logging.getLogger("jarvischat") log = logging.getLogger("caic")
router = APIRouter() 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 eviction import maybe_evict
from rag import chunk_text, QDRANT_URL, EMBED_URL, EMBED_MODEL, RAG_COLLECTION from rag import chunk_text, QDRANT_URL, EMBED_URL, EMBED_MODEL, RAG_COLLECTION
log = logging.getLogger("jarvischat") log = logging.getLogger("caic")
router = APIRouter() 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 logging
import math import math
@@ -10,7 +10,7 @@ import httpx
from config import SEARXNG_BASE, PERPLEXITY_THRESHOLD, REFUSAL_PATTERNS, HEDGE_PATTERNS 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: 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, PIN hashing, audit logging, incident tracking, CSRF/origin checks,
rate limiting, request helpers. rate limiting, request helpers.
""" """
@@ -30,7 +30,7 @@ from config import (
import ipaddress import ipaddress
log = logging.getLogger("jarvischat") log = logging.getLogger("caic")
SESSIONS: dict = {} SESSIONS: dict = {}
PIN_ATTEMPTS: dict = {} PIN_ATTEMPTS: dict = {}
+8 -8
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <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="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache"> <meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0"> <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-screen" id="authScreen">
<div class="auth-card"> <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-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> <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" /> <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"> <aside class="sidebar" id="sidebar">
<div class="sidebar-header"> <div class="sidebar-header">
<img class="logo" src="/static/logo.png" alt="JarvisChat Logo" onerror="this.style.display='none'"> <img class="logo" src="/static/logo.png" alt="cAIc Logo" onerror="this.style.display='none'">
<h1>JarvisChat {{ version }}</h1> <h1>cAIc {{ version }}</h1>
<div class="subtitle">🦙 local coding companion</div> <div class="subtitle">🦙 local coding companion</div>
<div class="btn-row"> <div class="btn-row">
<button class="new-chat-btn" onclick="newChat()">+ New Chat</button> <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>
<div class="modal-section"> <div class="modal-section">
<h3>Web Search (SearXNG)</h3> <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"> <div class="toggle-row">
<span class="toggle-label">Enable automatic web search</span> <span class="toggle-label">Enable automatic web search</span>
<div class="toggle-switch on" id="searchToggle" onclick="toggleSearch()"></div> <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="chat-container" id="chatContainer">
<div class="welcome-screen" id="welcomeScreen"> <div class="welcome-screen" id="welcomeScreen">
<div class="logo"></div> <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> </div>
<div class="input-area"> <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 escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; }
function showWelcome() { 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() { async function sendSearch() {
@@ -1479,7 +1479,7 @@ function addMessageToolbar(msgDiv) {
const blob = new Blob([t], { type: 'text/markdown' }); const blob = new Blob([t], { type: 'text/markdown' });
const a = document.createElement('a'); const a = document.createElement('a');
a.href = URL.createObjectURL(blob); a.href = URL.createObjectURL(blob);
a.download = `jarvischat-response-${Date.now()}.md`; a.download = `caic-response-${Date.now()}.md`;
a.click(); a.click();
URL.revokeObjectURL(a.href); 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: def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234" os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-test.db" db.DB_PATH = tmp_path / "caic-test.db"
SESSIONS.clear() SESSIONS.clear()
PIN_ATTEMPTS.clear() PIN_ATTEMPTS.clear()
db.init_db() db.init_db()
@@ -13,8 +13,8 @@ from security import SESSIONS, PIN_ATTEMPTS, RATE_EVENTS
def make_client(tmp_path: Path) -> TestClient: def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234" os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-streaming.db" db.DB_PATH = tmp_path / "caic-streaming.db"
SESSIONS.clear() SESSIONS.clear()
PIN_ATTEMPTS.clear() PIN_ATTEMPTS.clear()
RATE_EVENTS.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: def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234" os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-completions.db" db.DB_PATH = tmp_path / "caic-completions.db"
SESSIONS.clear() SESSIONS.clear()
PIN_ATTEMPTS.clear() PIN_ATTEMPTS.clear()
RATE_EVENTS.clear() RATE_EVENTS.clear()
@@ -22,7 +22,7 @@ def make_client(tmp_path: Path) -> TestClient:
return TestClient(app.app, raise_server_exceptions=False) 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: 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: def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234" os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-conversations.db" db.DB_PATH = tmp_path / "caic-conversations.db"
SESSIONS.clear() SESSIONS.clear()
PIN_ATTEMPTS.clear() PIN_ATTEMPTS.clear()
RATE_EVENTS.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: def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234" os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-errors.db" db.DB_PATH = tmp_path / "caic-errors.db"
SESSIONS.clear() SESSIONS.clear()
PIN_ATTEMPTS.clear() PIN_ATTEMPTS.clear()
RATE_EVENTS.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: def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234" os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-hardware.db" db.DB_PATH = tmp_path / "caic-hardware.db"
hardware.HARDWARE_STATE_PATH = tmp_path / "hardware_state.json" hardware.HARDWARE_STATE_PATH = tmp_path / "hardware_state.json"
SESSIONS.clear() SESSIONS.clear()
PIN_ATTEMPTS.clear() PIN_ATTEMPTS.clear()
@@ -72,7 +72,7 @@ def test_assess_hardware_all_services_reachable(tmp_path: Path, monkeypatch):
if "v1/models" in url: if "v1/models" in url:
return _MockGet(200, {"data": [{"id": "mistral-nemo:latest"}]}) return _MockGet(200, {"data": [{"id": "mistral-nemo:latest"}]})
if "6333" in url: if "6333" in url:
return _MockGet(200, {"result": {"collections": [{"name": "jarvischat"}]}}) return _MockGet(200, {"result": {"collections": [{"name": "caic"}]}})
if "8888" in url: if "8888" in url:
return _MockGet(200, {}) return _MockGet(200, {})
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_reachable"] is True
assert state["llama_models"] == ["mistral-nemo:latest"] assert state["llama_models"] == ["mistral-nemo:latest"]
assert state["qdrant_reachable"] is True assert state["qdrant_reachable"] is True
assert state["qdrant_collections"] == ["jarvischat"] assert state["qdrant_collections"] == ["caic"]
assert state["searxng_reachable"] is True assert state["searxng_reachable"] is True
assert tmp_path.joinpath("hardware_state.json").exists() 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: if "v1/models" in url:
raise httpx.ConnectError("refused") raise httpx.ConnectError("refused")
if "6333" in url: if "6333" in url:
return _MockGet(200, {"result": {"collections": [{"name": "jarvischat"}]}}) return _MockGet(200, {"result": {"collections": [{"name": "caic"}]}})
if "8888" in url: if "8888" in url:
return _MockGet(200, {}) return _MockGet(200, {})
return _MockGet(200, {}) return _MockGet(200, {})
@@ -156,7 +156,7 @@ def test_get_hardware_endpoint(tmp_path: Path, monkeypatch):
if "v1/models" in url: if "v1/models" in url:
return _MockGet(200, {"data": [{"id": "mistral-nemo:latest"}]}) return _MockGet(200, {"data": [{"id": "mistral-nemo:latest"}]})
if "6333" in url: if "6333" in url:
return _MockGet(200, {"result": {"collections": [{"name": "jarvischat"}]}}) return _MockGet(200, {"result": {"collections": [{"name": "caic"}]}})
if "8888" in url: if "8888" in url:
return _MockGet(200, {}) return _MockGet(200, {})
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: def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234" os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-ingest.db" db.DB_PATH = tmp_path / "caic-ingest.db"
SESSIONS.clear() SESSIONS.clear()
PIN_ATTEMPTS.clear() PIN_ATTEMPTS.clear()
RATE_EVENTS.clear() RATE_EVENTS.clear()
@@ -21,7 +21,7 @@ def make_client(tmp_path: Path) -> TestClient:
return TestClient(app.app, raise_server_exceptions=False) 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: 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: def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234" os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-ip.db" db.DB_PATH = tmp_path / "caic-ip.db"
SESSIONS.clear() SESSIONS.clear()
PIN_ATTEMPTS.clear() PIN_ATTEMPTS.clear()
RATE_EVENTS.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: def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234" os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-memories.db" db.DB_PATH = tmp_path / "caic-memories.db"
SESSIONS.clear() SESSIONS.clear()
PIN_ATTEMPTS.clear() PIN_ATTEMPTS.clear()
RATE_EVENTS.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: def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234" os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-models.db" db.DB_PATH = tmp_path / "caic-models.db"
SESSIONS.clear() SESSIONS.clear()
PIN_ATTEMPTS.clear() PIN_ATTEMPTS.clear()
RATE_EVENTS.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: def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234" os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-presets.db" db.DB_PATH = tmp_path / "caic-presets.db"
SESSIONS.clear() SESSIONS.clear()
PIN_ATTEMPTS.clear() PIN_ATTEMPTS.clear()
RATE_EVENTS.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: def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234" os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-profile.db" db.DB_PATH = tmp_path / "caic-profile.db"
SESSIONS.clear() SESSIONS.clear()
PIN_ATTEMPTS.clear() PIN_ATTEMPTS.clear()
RATE_EVENTS.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: def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234" os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-rag-mgmt.db" db.DB_PATH = tmp_path / "caic-rag-mgmt.db"
SESSIONS.clear() SESSIONS.clear()
PIN_ATTEMPTS.clear() PIN_ATTEMPTS.clear()
RATE_EVENTS.clear() RATE_EVENTS.clear()
@@ -54,7 +54,7 @@ class FakeAsyncClient:
pass pass
async def get(self, url, **kw): 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, {"result": {"vectors_count": 123}})
return FakeResponse(200) 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: def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234" os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-rate.db" db.DB_PATH = tmp_path / "caic-rate.db"
SESSIONS.clear() SESSIONS.clear()
PIN_ATTEMPTS.clear() PIN_ATTEMPTS.clear()
RATE_EVENTS.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: def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234" os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-search-route.db" db.DB_PATH = tmp_path / "caic-search-route.db"
SESSIONS.clear() SESSIONS.clear()
PIN_ATTEMPTS.clear() PIN_ATTEMPTS.clear()
RATE_EVENTS.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]]: def make_admin_client(tmp_path: Path) -> tuple[TestClient, dict[str, str]]:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234" os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-settings.db" db.DB_PATH = tmp_path / "caic-settings.db"
SESSIONS.clear() SESSIONS.clear()
PIN_ATTEMPTS.clear() PIN_ATTEMPTS.clear()
db.init_db() 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: def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234" os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-skills.db" db.DB_PATH = tmp_path / "caic-skills.db"
SESSIONS.clear() SESSIONS.clear()
PIN_ATTEMPTS.clear() PIN_ATTEMPTS.clear()
RATE_EVENTS.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: def make_client(tmp_path: Path) -> TestClient:
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234" os.environ["CAIC_ADMIN_PIN"] = "1234"
db.DB_PATH = tmp_path / "jarvischat-upload.db" db.DB_PATH = tmp_path / "caic-upload.db"
SESSIONS.clear() SESSIONS.clear()
PIN_ATTEMPTS.clear() PIN_ATTEMPTS.clear()
RATE_EVENTS.clear() RATE_EVENTS.clear()