update documentation for sdk, vault, gitea integration
This commit is contained in:
169
docs/conversations.md
Normal file
169
docs/conversations.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# Sol — Conversations API
|
||||
|
||||
The Conversations API path provides persistent, server-side conversation state per Matrix room via Mistral's Conversations API. Enable it with `agents.use_conversations_api = true` in `sol.toml`.
|
||||
|
||||
## lifecycle
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant M as Matrix Sync
|
||||
participant E as Evaluator
|
||||
participant R as Responder
|
||||
participant CR as ConversationRegistry
|
||||
participant API as Mistral Conversations API
|
||||
participant T as ToolRegistry
|
||||
participant DB as SQLite
|
||||
|
||||
M->>E: message event
|
||||
E-->>M: MustRespond/MaybeRespond
|
||||
|
||||
M->>R: generate_response_conversations()
|
||||
R->>CR: send_message(room_id, input, is_dm)
|
||||
|
||||
alt new room (no conversation)
|
||||
CR->>API: create_conversation(agent_id?, model, input)
|
||||
API-->>CR: ConversationResponse + conversation_id
|
||||
CR->>DB: upsert_conversation(room_id, conv_id, tokens)
|
||||
else existing room
|
||||
CR->>API: append_conversation(conv_id, input)
|
||||
API-->>CR: ConversationResponse
|
||||
CR->>DB: update_tokens(room_id, new_total)
|
||||
end
|
||||
|
||||
alt response contains function_calls
|
||||
loop up to max_tool_iterations (5)
|
||||
R->>T: execute(name, args)
|
||||
T-->>R: result string
|
||||
R->>CR: send_function_result(room_id, entries)
|
||||
CR->>API: append_conversation(conv_id, FunctionResult entries)
|
||||
API-->>CR: ConversationResponse
|
||||
alt more function_calls
|
||||
Note over R: continue loop
|
||||
else text response
|
||||
Note over R: break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
R-->>M: response text (or None)
|
||||
M->>M: send to Matrix room
|
||||
M->>M: fire-and-forget memory extraction
|
||||
```
|
||||
|
||||
## room-to-conversation mapping
|
||||
|
||||
Each Matrix room maps to exactly one Mistral conversation:
|
||||
|
||||
- **Group rooms**: one shared conversation per room (all participants' messages go to the same conversation)
|
||||
- **DMs**: one conversation per DM room (already unique per user pair in Matrix)
|
||||
|
||||
The mapping is stored in `ConversationRegistry.mapping` (HashMap in-memory, backed by SQLite `conversations` table).
|
||||
|
||||
## ConversationState
|
||||
|
||||
```rust
|
||||
struct ConversationState {
|
||||
conversation_id: String, // Mistral conversation ID
|
||||
estimated_tokens: u32, // running total from response.usage.total_tokens
|
||||
}
|
||||
```
|
||||
|
||||
Token estimates are incremented on each API response and persisted to SQLite.
|
||||
|
||||
## message formatting
|
||||
|
||||
Messages are formatted differently based on room type:
|
||||
|
||||
- **DMs**: raw text — `trigger_body` is sent directly
|
||||
- **Group rooms**: prefixed with Matrix user ID — `<@sienna:sunbeam.pt> what's for lunch?`
|
||||
|
||||
This is handled by the responder before calling `ConversationRegistry.send_message()`.
|
||||
|
||||
The `merge_user_messages()` helper (for buffered messages) follows the same pattern:
|
||||
|
||||
```rust
|
||||
// DM: "hello\nhow are you?"
|
||||
// Group: "<@sienna:sunbeam.pt> hello\n<@lonni:sunbeam.pt> how are you?"
|
||||
```
|
||||
|
||||
## compaction
|
||||
|
||||
When a conversation's `estimated_tokens` reaches the `compaction_threshold` (default 118,000 — ~90% of the 131K Mistral context window):
|
||||
|
||||
1. `ConversationRegistry.reset(room_id)` is called
|
||||
2. The mapping is removed from the in-memory HashMap
|
||||
3. The row is deleted from SQLite
|
||||
4. The next message creates a fresh conversation
|
||||
|
||||
This means conversation history is lost on compaction. The archive still has the full history, and memory notes persist independently.
|
||||
|
||||
## persistence
|
||||
|
||||
### startup recovery
|
||||
|
||||
On initialization, `ConversationRegistry::new()` calls `store.load_all_conversations()` to restore all room→conversation mappings from SQLite. This means conversations survive pod restarts.
|
||||
|
||||
### SQLite schema
|
||||
|
||||
```sql
|
||||
CREATE TABLE conversations (
|
||||
room_id TEXT PRIMARY KEY,
|
||||
conversation_id TEXT NOT NULL,
|
||||
estimated_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
```
|
||||
|
||||
### write operations
|
||||
|
||||
| Operation | When |
|
||||
|-----------|------|
|
||||
| `upsert_conversation` | New conversation created |
|
||||
| `update_tokens` | After each append (token count from API response) |
|
||||
| `delete_conversation` | On compaction reset |
|
||||
|
||||
## agent integration
|
||||
|
||||
Conversations can optionally use a Mistral agent (the orchestrator) instead of a bare model:
|
||||
|
||||
- If `agent_id` is set (via `set_agent_id()` at startup): new conversations are created with the agent
|
||||
- If `agent_id` is `None`: conversations use the `model` directly (fallback)
|
||||
|
||||
The agent provides Sol's personality, tool definitions, and delegation instructions. Without it, conversations still work but without agent-specific behavior.
|
||||
|
||||
```rust
|
||||
let req = CreateConversationRequest {
|
||||
inputs: message,
|
||||
model: if agent_id.is_none() { Some(self.model.clone()) } else { None },
|
||||
agent_id,
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
## error handling
|
||||
|
||||
- **API failure on create/append**: returns `Err(String)`, responder logs and returns `None` (no response sent to Matrix)
|
||||
- **Function result send failure**: logs error, returns `None`
|
||||
- **SQLite write failure**: logged as warning, in-memory state is still updated (will be lost on restart)
|
||||
|
||||
Sol never crashes on a conversation error — it simply doesn't respond.
|
||||
|
||||
## configuration
|
||||
|
||||
| Field | Default | Description |
|
||||
|-------|---------|-------------|
|
||||
| `agents.use_conversations_api` | `false` | Enable this path |
|
||||
| `agents.orchestrator_model` | `mistral-medium-latest` | Model for orchestrator agent |
|
||||
| `agents.compaction_threshold` | `118000` | Token limit before conversation reset |
|
||||
| `mistral.max_tool_iterations` | `5` | Max function call rounds per response |
|
||||
|
||||
## comparison with legacy path
|
||||
|
||||
| Aspect | Legacy | Conversations API |
|
||||
|--------|--------|------------------|
|
||||
| State management | Manual `Vec<ChatMessage>` per request | Server-side, persistent |
|
||||
| Memory injection | System prompt template | Agent instructions |
|
||||
| Tool calling | Chat completions tool_choice | Function calls via conversation entries |
|
||||
| Context window | Sliding window (configurable) | Full conversation history until compaction |
|
||||
| Multimodal | ContentPart::ImageUrl | Not yet supported (TODO in responder) |
|
||||
| Persistence | None (context rebuilt from archive) | SQLite + Mistral server |
|
||||
228
docs/deployment.md
Normal file
228
docs/deployment.md
Normal file
@@ -0,0 +1,228 @@
|
||||
# Sol — Kubernetes Deployment
|
||||
|
||||
Sol runs as a single-replica Deployment in the `matrix` namespace. SQLite is the persistence backend, so only one pod can run at a time (Recreate strategy).
|
||||
|
||||
## resource relationships
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph OpenBao
|
||||
vault[("secret/sol<br/>matrix-access-token<br/>matrix-device-id<br/>mistral-api-key")]
|
||||
end
|
||||
|
||||
subgraph "matrix namespace"
|
||||
vss[VaultStaticSecret<br/>sol-secrets]
|
||||
secret[Secret<br/>sol-secrets]
|
||||
cm[ConfigMap<br/>sol-config<br/>sol.toml + system_prompt.md]
|
||||
pvc[PVC<br/>sol-data<br/>1Gi RWO]
|
||||
deploy[Deployment<br/>sol]
|
||||
init[initContainer<br/>fix-permissions]
|
||||
pod[Container<br/>sol]
|
||||
end
|
||||
|
||||
vault --> |VSO sync| vss
|
||||
vss --> |creates| secret
|
||||
vss --> |rolloutRestartTargets| deploy
|
||||
deploy --> init
|
||||
init --> pod
|
||||
secret --> |env vars| pod
|
||||
cm --> |subPath mounts| pod
|
||||
pvc --> |/data| init
|
||||
pvc --> |/data| pod
|
||||
```
|
||||
|
||||
## manifests
|
||||
|
||||
All manifests are in `infrastructure/base/matrix/`.
|
||||
|
||||
### Deployment (`sol-deployment.yaml`)
|
||||
|
||||
```yaml
|
||||
strategy:
|
||||
type: Recreate # SQLite requires single-writer
|
||||
replicas: 1
|
||||
```
|
||||
|
||||
**initContainer** — `busybox` runs `chmod -R 777 /data && mkdir -p /data/matrix-state` to ensure the nonroot distroless container can write to the Longhorn PVC.
|
||||
|
||||
**Container** — `sol` image (distroless/cc-debian12:nonroot)
|
||||
|
||||
- Resources: 256Mi request / 512Mi limit memory, 100m CPU request
|
||||
- `enableServiceLinks: false` — avoids injecting service env vars that could conflict
|
||||
|
||||
**Environment variables** (from Secret `sol-secrets`):
|
||||
|
||||
| Env Var | Secret Key |
|
||||
|---------|-----------|
|
||||
| `SOL_MATRIX_ACCESS_TOKEN` | `matrix-access-token` |
|
||||
| `SOL_MATRIX_DEVICE_ID` | `matrix-device-id` |
|
||||
| `SOL_MISTRAL_API_KEY` | `mistral-api-key` |
|
||||
|
||||
Fixed env vars:
|
||||
|
||||
| Env Var | Value |
|
||||
|---------|-------|
|
||||
| `SOL_CONFIG` | `/etc/sol/sol.toml` |
|
||||
| `SOL_SYSTEM_PROMPT` | `/etc/sol/system_prompt.md` |
|
||||
|
||||
**Volume mounts:**
|
||||
|
||||
| Mount | Source | Details |
|
||||
|-------|--------|---------|
|
||||
| `/etc/sol/sol.toml` | ConfigMap `sol-config` | subPath: `sol.toml`, readOnly |
|
||||
| `/etc/sol/system_prompt.md` | ConfigMap `sol-config` | subPath: `system_prompt.md`, readOnly |
|
||||
| `/data` | PVC `sol-data` | read-write |
|
||||
|
||||
### PVC (`sol-deployment.yaml`, second document)
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: sol-data
|
||||
namespace: matrix
|
||||
spec:
|
||||
accessModes: [ReadWriteOnce]
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
```
|
||||
|
||||
Uses the default StorageClass (Longhorn).
|
||||
|
||||
### VaultStaticSecret (`vault-secrets.yaml`)
|
||||
|
||||
```yaml
|
||||
apiVersion: secrets.hashicorp.com/v1beta1
|
||||
kind: VaultStaticSecret
|
||||
metadata:
|
||||
name: sol-secrets
|
||||
namespace: matrix
|
||||
spec:
|
||||
vaultAuthRef: vso-auth
|
||||
mount: secret
|
||||
type: kv-v2
|
||||
path: sol
|
||||
refreshAfter: 60s
|
||||
rolloutRestartTargets:
|
||||
- kind: Deployment
|
||||
name: sol
|
||||
destination:
|
||||
name: sol-secrets
|
||||
create: true
|
||||
overwrite: true
|
||||
```
|
||||
|
||||
The `rolloutRestartTargets` field means VSO will automatically restart the Sol deployment when secrets change in OpenBao.
|
||||
|
||||
Three keys synced from OpenBao `secret/sol`:
|
||||
|
||||
- `matrix-access-token`
|
||||
- `matrix-device-id`
|
||||
- `mistral-api-key`
|
||||
|
||||
## `/data` mount layout
|
||||
|
||||
```
|
||||
/data/
|
||||
├── sol.db SQLite database (conversations + agents tables, WAL mode)
|
||||
└── matrix-state/ Matrix SDK sqlite state store (E2EE keys, sync tokens)
|
||||
```
|
||||
|
||||
Both are created automatically. The initContainer ensures directory permissions are correct for the nonroot container.
|
||||
|
||||
## secrets in OpenBao
|
||||
|
||||
Store secrets at `secret/sol` in OpenBao KV v2:
|
||||
|
||||
```sh
|
||||
# Via sunbeam seed (automated), or manually:
|
||||
openbao kv put secret/sol \
|
||||
matrix-access-token="syt_..." \
|
||||
matrix-device-id="DEVICE_ID" \
|
||||
mistral-api-key="..."
|
||||
```
|
||||
|
||||
These are synced to K8s Secret `sol-secrets` by the Vault Secrets Operator.
|
||||
|
||||
## build and deploy
|
||||
|
||||
```sh
|
||||
# Build only (local Docker image)
|
||||
sunbeam build sol
|
||||
|
||||
# Build + push to registry
|
||||
sunbeam build sol --push
|
||||
|
||||
# Build + push + deploy (apply manifests + rollout restart)
|
||||
sunbeam build sol --push --deploy
|
||||
```
|
||||
|
||||
The Docker build cross-compiles to `x86_64-unknown-linux-gnu` on macOS. The final image is `gcr.io/distroless/cc-debian12:nonroot` (~30MB).
|
||||
|
||||
## startup sequence
|
||||
|
||||
1. Initialize `tracing_subscriber` with `RUST_LOG` env filter (default: `sol=info`)
|
||||
2. Load config from `SOL_CONFIG` path
|
||||
3. Load system prompt from `SOL_SYSTEM_PROMPT` path
|
||||
4. Read 3 secret env vars (`SOL_MATRIX_ACCESS_TOKEN`, `SOL_MATRIX_DEVICE_ID`, `SOL_MISTRAL_API_KEY`)
|
||||
5. Build Matrix client with E2EE sqlite store, restore session
|
||||
6. Connect to OpenSearch, ensure archive + memory indices exist
|
||||
7. Initialize Mistral client
|
||||
8. Build components: Personality, ConversationManager, ToolRegistry, Indexer, Evaluator, Responder
|
||||
9. Backfill conversation context from archive (if `backfill_on_join` enabled)
|
||||
10. Open SQLite database (fallback to in-memory on failure)
|
||||
11. Initialize AgentRegistry + ConversationRegistry (load persisted state from SQLite)
|
||||
12. If `use_conversations_api` enabled: ensure orchestrator agent exists on Mistral server
|
||||
13. Backfill reactions from Matrix room timelines
|
||||
14. Start background index flush task
|
||||
15. Start Matrix sync loop
|
||||
16. If SQLite failed: send `*sneezes*` to all joined rooms
|
||||
17. Log "Sol is running", wait for SIGINT
|
||||
|
||||
## monitoring
|
||||
|
||||
Sol uses `tracing` with structured fields. Default log level: `sol=info`.
|
||||
|
||||
Key log events:
|
||||
|
||||
| Event | Level | Fields |
|
||||
|-------|-------|--------|
|
||||
| Response sent | info | `room`, `len`, `is_dm` |
|
||||
| Tool execution | info | `tool`, `id`, `args` |
|
||||
| Engagement evaluation | info | `sender`, `rule`, `relevance`, `threshold` |
|
||||
| Memory extraction | debug | `count`, `user` |
|
||||
| Conversation created | info | `room`, `conversation_id` |
|
||||
| Agent restored/created | info | `agent_id`, `name` |
|
||||
| Backfill complete | info | `rooms`, `messages` / `reactions` |
|
||||
|
||||
Set `RUST_LOG=sol=debug` for verbose output including tool results, evaluation prompts, and memory details.
|
||||
|
||||
## troubleshooting
|
||||
|
||||
**Pod won't start / CrashLoopBackOff:**
|
||||
|
||||
```sh
|
||||
sunbeam logs matrix/sol
|
||||
```
|
||||
|
||||
Common causes:
|
||||
- Missing secrets (env vars not set) — check `sunbeam k8s get secret sol-secrets -n matrix -o yaml`
|
||||
- ConfigMap not applied — check `sunbeam k8s get cm sol-config -n matrix`
|
||||
- PVC not bound — check `sunbeam k8s get pvc -n matrix`
|
||||
|
||||
**SQLite recovery failure (*sneezes*):**
|
||||
|
||||
If Sol sends `*sneezes*` on startup, it means the SQLite database at `/data/sol.db` couldn't be opened. Sol falls back to in-memory state. Check PVC mount and file permissions:
|
||||
|
||||
```sh
|
||||
sunbeam k8s exec -n matrix deployment/sol -- ls -la /data/
|
||||
```
|
||||
|
||||
**Matrix sync errors:**
|
||||
|
||||
Sol auto-joins rooms on invite (3 retries with exponential backoff). If it can't join, check homeserver connectivity and access token validity.
|
||||
|
||||
**Agent creation failure:**
|
||||
|
||||
If the orchestrator agent can't be created, Sol falls back to model-only conversations (no agent). Check Mistral API key and quota.
|
||||
Reference in New Issue
Block a user