gb/prompts/always.md
Rodrigo Rodriguez (Pragmatismo) 25ea1965a4
Some checks failed
BotServer CI / build (push) Failing after 59s
Implement THINK KB keyword for explicit knowledge base reasoning
- Add botserver/src/basic/keywords/think_kb.rs with structured KB search
- Register THINK KB in keywords module and BASIC engine
- Add comprehensive documentation in ALWAYS.md and botbook
- Include confidence scoring, multi-KB support, and error handling
- Add unit tests and example usage script
2026-03-16 08:03:18 -03:00

9.9 KiB

Always-On Memory KB — Implementation Plan

What This Is

The Google always-on-memory-agent runs 3 sub-agents continuously:

  • IngestAgent — extracts structured facts from any input (text, files, images, audio, video)
  • ConsolidateAgent — runs on a timer, finds connections between memories, generates insights
  • QueryAgent — answers questions by synthesizing all memories with citations

The key insight: no vector DB, no embeddings — just an LLM that reads, thinks, and writes structured memory to SQLite. It's active, not passive.

We integrate this as a new KB mode in the existing BASIC keyword system:

USE KB "customer-notes" ALWAYS ON

This turns a KB into a living memory that continuously ingests, consolidates, and is always available in every session for that bot — no USE KB needed per session.


Architecture in GB Context

BASIC script:
  USE KB "notes" ALWAYS ON
        │
        ▼
always_on_kb table (bot_id, kb_name, mode=always_on, consolidate_interval_mins)
        │
        ├── IngestWorker (tokio task per bot)
        │     watches: /opt/gbo/data/{bot}.gbai/{kb}.gbkb/inbox/
        │     on new file → LLM extract → kb_memories table
        │
        ├── ConsolidateWorker (tokio interval per bot)
        │     reads: unconsolidated kb_memories
        │     LLM finds connections → kb_consolidations table
        │
        └── QueryEnhancer (in BotOrchestrator::stream_response)
              before LLM call → fetch relevant memories + consolidations
              inject as system context

Database Migration

File: botserver/migrations/6.2.6-always-on-kb/up.sql

-- Always-on KB configuration per bot
CREATE TABLE IF NOT EXISTS always_on_kb (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
    kb_name TEXT NOT NULL,
    consolidate_interval_mins INTEGER NOT NULL DEFAULT 30,
    is_active BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE(bot_id, kb_name)
);

-- Individual memory entries (from IngestAgent)
CREATE TABLE IF NOT EXISTS kb_memories (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
    kb_name TEXT NOT NULL,
    summary TEXT NOT NULL,
    entities JSONB NOT NULL DEFAULT '[]',
    topics JSONB NOT NULL DEFAULT '[]',
    importance FLOAT NOT NULL DEFAULT 0.5,
    source TEXT,                          -- file path or "api" or "chat"
    is_consolidated BOOLEAN NOT NULL DEFAULT false,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_kb_memories_bot_kb ON kb_memories(bot_id, kb_name);
CREATE INDEX idx_kb_memories_consolidated ON kb_memories(is_consolidated) WHERE is_consolidated = false;
CREATE INDEX idx_kb_memories_importance ON kb_memories(importance DESC);

-- Consolidation insights (from ConsolidateAgent)
CREATE TABLE IF NOT EXISTS kb_consolidations (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
    kb_name TEXT NOT NULL,
    insight TEXT NOT NULL,
    memory_ids JSONB NOT NULL DEFAULT '[]',   -- UUIDs of source memories
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_kb_consolidations_bot_kb ON kb_consolidations(bot_id, kb_name);

New BASIC Keyword

File: botserver/src/basic/keywords/always_on_kb.rs

// Registers: USE KB "name" ALWAYS ON
// Syntax: ["USE", "KB", "$expr$", "ALWAYS", "ON"]
pub fn register_always_on_kb_keyword(
    engine: &mut Engine,
    state: Arc<AppState>,
    session: Arc<UserSession>,
) -> Result<(), Box<EvalAltResult>> {
    engine.register_custom_syntax(
        ["USE", "KB", "$expr$", "ALWAYS", "ON"],
        true,
        move |context, inputs| {
            let kb_name = context.eval_expression_tree(&inputs[0])?.to_string();
            let bot_id = session.bot_id;
            let pool = state.conn.clone();

            std::thread::spawn(move || {
                let mut conn = pool.get()?;
                diesel::sql_query(
                    "INSERT INTO always_on_kb (bot_id, kb_name) VALUES ($1, $2)
                     ON CONFLICT (bot_id, kb_name) DO UPDATE SET is_active = true"
                )
                .bind::<diesel::sql_types::Uuid, _>(bot_id)
                .bind::<diesel::sql_types::Text, _>(&kb_name)
                .execute(&mut conn)
            }).join().ok();

            Ok(Dynamic::UNIT)
        },
    )?;
    Ok(())
}

Register in botserver/src/basic/mod.rs alongside register_use_kb_keyword.


AlwaysOnKbService

File: botserver/src/learn/always_on_kb.rs

Three async tasks per active always-on KB:

IngestWorker

// Watches: /opt/gbo/data/{bot_name}.gbai/{kb_name}.gbkb/inbox/
// On new file:
//   1. Read file content (text/pdf/image via existing FileContentExtractor)
//   2. Call LLM: "Extract: summary, entities[], topics[], importance(0-1)"
//   3. INSERT INTO kb_memories
//   4. Move file to /processed/
async fn ingest_worker(bot_id: Uuid, kb_name: String, state: Arc<AppState>)

ConsolidateWorker

// Runs every `consolidate_interval_mins`
// 1. SELECT * FROM kb_memories WHERE bot_id=$1 AND kb_name=$2 AND is_consolidated=false LIMIT 20
// 2. Call LLM: "Find connections and generate insights from these memories: {memories}"
// 3. INSERT INTO kb_consolidations (insight, memory_ids)
// 4. UPDATE kb_memories SET is_consolidated=true WHERE id IN (...)
async fn consolidate_worker(bot_id: Uuid, kb_name: String, interval_mins: u64, state: Arc<AppState>)

QueryEnhancer (inject into BotOrchestrator)

// Called in BotOrchestrator::stream_response before LLM call
// 1. SELECT always_on_kb WHERE bot_id=$1 AND is_active=true
// 2. For each: fetch top-N memories by importance + recent consolidations
// 3. Prepend to system prompt:
//    "## Memory Context\n{memories}\n## Insights\n{consolidations}"
pub async fn build_memory_context(bot_id: Uuid, pool: &DbPool) -> String

Service Startup

// In botserver/src/main_module/server.rs, after bootstrap:
// SELECT * FROM always_on_kb WHERE is_active=true
// For each row: tokio::spawn ingest_worker + consolidate_worker
pub async fn start_always_on_kb_workers(state: Arc<AppState>)

HTTP API

File: botserver/src/learn/always_on_kb.rs (add routes)

GET  /api/kb/always-on                    → list all always-on KBs for bot
POST /api/kb/always-on/ingest             → { kb_name, text, source }  manual ingest
POST /api/kb/always-on/consolidate        → { kb_name }  trigger manual consolidation
GET  /api/kb/always-on/:kb_name/memories  → list memories (paginated)
GET  /api/kb/always-on/:kb_name/insights  → list consolidation insights
DELETE /api/kb/always-on/:kb_name/memory/:id → delete a memory
POST /api/kb/always-on/:kb_name/clear     → delete all memories

BotOrchestrator Integration

File: botserver/src/core/bot/mod.rs

In stream_response(), before building the LLM prompt:

// Inject always-on memory context
let memory_ctx = always_on_kb::build_memory_context(bot_id, &state.conn).await;
if !memory_ctx.is_empty() {
    system_prompt = format!("{}\n\n{}", memory_ctx, system_prompt);
}

Inbox File Watcher

The IngestWorker uses the existing LocalFileMonitor pattern from botserver/src/drive/local_file_monitor.rs.

Watch path: /opt/gbo/data/{bot_name}.gbai/{kb_name}.gbkb/inbox/

Supported types (reuse existing FileContentExtractor): .txt, .md, .pdf, .json, .csv, .png, .jpg, .mp3, .wav, .mp4


LLM Prompts

Ingest prompt

Extract structured information from this content.
Return JSON: { "summary": "...", "entities": [...], "topics": [...], "importance": 0.0-1.0 }
Content: {content}

Consolidate prompt

You are a memory consolidation agent. Review these memories and find connections.
Return JSON: { "insight": "...", "connected_memory_ids": [...] }
Memories: {memories_json}

Use the existing LlmClient in botserver/src/llm/mod.rs with the bot's configured model.


BASIC Usage Examples

' Enable always-on memory for this bot
USE KB "meeting-notes" ALWAYS ON

' Manual ingest from BASIC (optional)
' (future keyword: ADD MEMORY "text" TO KB "meeting-notes")

' Query is automatic — memories injected into every LLM call
TALK "What did we discuss last week?"
' → LLM sees memory context automatically

File Structure

botserver/
├── migrations/
│   └── 6.2.6-always-on-kb/
│       ├── up.sql      ← always_on_kb, kb_memories, kb_consolidations tables
│       └── down.sql
├── src/
│   ├── basic/keywords/
│   │   └── always_on_kb.rs   ← USE KB "x" ALWAYS ON keyword
│   ├── learn/
│   │   └── always_on_kb.rs   ← IngestWorker, ConsolidateWorker, QueryEnhancer, HTTP API
│   └── main_module/
│       └── server.rs         ← start_always_on_kb_workers() on startup

Implementation Order

  1. Migration (up.sql) — tables
  2. always_on_kb.rs keyword — register syntax
  3. learn/always_on_kb.rsbuild_memory_context() + HTTP API
  4. IngestWorker + ConsolidateWorker tokio tasks
  5. BotOrchestrator integration — inject memory context
  6. start_always_on_kb_workers() in server startup
  7. Register keyword in basic/mod.rs
  8. Botbook doc + i18n keys

Key Differences from Google's Implementation

Google ADK GB Implementation
Python + ADK framework Rust + existing AppState
SQLite PostgreSQL (existing pool)
Gemini Flash-Lite only Any configured LLM via LlmClient
Standalone process Embedded tokio tasks in botserver
File inbox only File inbox + HTTP API + future chat auto-ingest
Manual query Auto-injected into every bot LLM call
No BASIC integration USE KB "x" ALWAYS ON keyword