From 9e028db735c82e5f12ef83565e3e2fa344a3d89a Mon Sep 17 00:00:00 2001 From: type-two Date: Wed, 24 Jun 2026 16:12:19 +1000 Subject: [PATCH] feat(connect): OpenRouter key + test in vault; AI agent integration plan - settings vault gains openrouter_api_key + openrouter_model (generic Connections page renders them); /settings/test/openrouter shows OpenRouter usage/limit. - RECORDGOD_AI_AGENT_PLAN.md: honest assessment + phased plan for a grounded, tool-calling AI assistant. Core stance: tools not free SQL; read-only + propose-never-commit; pgvector lives in DealGod (reached via its API), not recordgod-db; moat = grounded data, not the model. Co-Authored-By: Claude Opus 4.8 --- RECORDGOD_AI_AGENT_PLAN.md | 101 +++++++++++++++++++++++++++++++++++++ app/settings_routes.py | 15 ++++++ site/admin.html | 2 +- 3 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 RECORDGOD_AI_AGENT_PLAN.md diff --git a/RECORDGOD_AI_AGENT_PLAN.md b/RECORDGOD_AI_AGENT_PLAN.md new file mode 100644 index 0000000..cf1f6cf --- /dev/null +++ b/RECORDGOD_AI_AGENT_PLAN.md @@ -0,0 +1,101 @@ +# RecordGod AI assistant — honest assessment & plan (2026-06-24) + +OpenRouter key stored in the vault (`openrouter_api_key` + `openrouter_model`), reachable like every other +credential. This doc is the "durry and a short black" think: what's genuinely dope, what's hype, what breaks +things, and how to build it so it only helps. + +## The one-paragraph honest take + +The LLM is a **commodity** — anyone can call OpenRouter. The moat is that RecordGod can **ground** the model in +proprietary, structured, *vectorised* data: the full Discogs mirror, the live cross-store market intelligence in +DealGod (pgvector lives there, reached via the DealGod API — **RecordGod-db itself has no pgvector**), real sales +and stock. A WooCommerce plugin with a chatbot is a toy; "talk to your shop, and it answers with *your* numbers and +*the market's* numbers, then drafts the copy/newsletter/price for you to approve" is a product. **So the sell isn't +"AI" — it's "AI that can't lie to you because it's reading the database, and can't break anything because it can +only propose."** + +## The non-negotiable architecture: tools, not free SQL + +The exciting line — "it can run actual Postgres queries for the exact info it needs" — is right in spirit and a +foot-gun in the literal. The safe, equally-powerful pattern is a **tool-calling agent**: the model picks from a set +of vetted, parameterised functions; *our* code runs the query. The model never sees a raw `psql` prompt. + +- **Phase 1 — fixed read tools (safest, ~90% of the value).** `stock_query`, `release_lookup`, + `market_value`(DealGod API), `semantic_search`(DealGod pgvector API), `sales_summary`, `customer_wants`, + `catalog_health`. The model composes them; results are real rows, so it summarises rather than hallucinates. +- **Phase 2 — constrained text-to-SQL for power questions.** Only against a **read-only role**, over a curated set + of **VIEWS** (not raw tables), with the generated SQL parsed (reject anything not a single `SELECT`), a forced + `LIMIT`, and a `statement_timeout`. Even then, the fixed tools answer most real questions more reliably. +- **Never — free SQL execution.** One `UPDATE`/`DELETE`/`DROP` from a hallucination or an injected instruction and + the shop is down. Not happening. + +## Writes: propose, never commit + +RecordGod is *already* the right shape for this — intake **stages**, publish is a separate non-destructive step. +The AI just becomes another stager/proposer. Every write-flavoured capability lands in a **review queue** a human +approves; nothing the agent does is live without a click. + +- `propose_price(sku, price, reason)` → a `proposals` row → human applies. +- `propose_intake(...)` → `staged` inventory (already non-destructive). +- `draft_newsletter` / `draft_product_copy` / `draft_wantlist_emails` → text only, human sends. + +## The dope things, honestly rated + +**Tier A — genuine value, low risk, ship first:** +1. **Ask-your-data.** "DnB 12-inches under $10 not sold in 90 days?" → tools → a table. Huge for a shop owner. + Semantic angle (DealGod pgvector): "records with a *vibe* like this one," not just keyword. +2. **Product copy / hype spiels.** Feed the model the *real* artist/label/year/tracklist/condition + DealGod + rarity → a punchy description or social caption, grounded in facts. The killer low-risk content win. +3. **Newsletters.** "New techno arrivals + 3 staff picks" → query new stock → draft HTML → human reviews → send via + the existing mailer. Draft-only. +4. **Stock-hygiene assistant.** Surface Heal-sweep gaps, flag below-market/mispriced items (DealGod median), find + dupes. Read + propose. +5. **Intake helper.** "Box of these" → describe → semantic search the Discogs mirror → propose matches → human + confirms → stage. Pairs with the intake we built. + +**Tier B — valuable, needs the propose-gate:** +6. **Auto-pricing suggestions** (propose, not apply) using DealGod cross-store median × condition. +7. **Wantlist matching** — "these 4 customers want what just arrived" → draft the notify emails. + +**Tier C — flashy, lower ROI / higher risk:** +8. **Autonomous "site maintenance"** = write access. Honest answer: don't. Keep it propose + human-apply. Full + autonomy is exactly where it breaks shit. +9. **Free text-to-SQL on master.** Great demo, real foot-gun; covered better by fixed tools + Phase-2 read-only views. + +## Guardrails (so it only helps) + +| Risk | Guardrail | +|---|---| +| Destructive writes | Agent gets a **read-only DB role**; all writes are **propose → human apply** | +| Bad/expensive SQL (Phase 2) | read-only role · curated VIEWS only · parse-and-reject non-SELECT · forced LIMIT · `statement_timeout` | +| Tool abuse | hard **allowlist** of tools; no shell, no file write, no arbitrary HTTP | +| Hallucination | answers are **grounded** in returned rows; "AI draft — review" label on all output | +| Prompt injection (notes, customer msgs, web text are untrusted) | untrusted text can't trigger a write — the human gate absorbs it | +| Cost creep | per-call **cost + token log** (`ai_log`), daily **budget cap**, cheap models for bulk | +| PII leakage | customer tools return **de-identified** data unless a task needs more (matches DealGod privacy posture) | +| Over-trust | every agent action is **logged and reversible**; proposals show their reasoning + source rows | + +## Model routing (OpenRouter's real advantage) + +Route per task, don't pick one model: +- **Bulk / tool-calling / SQL reasoning** → DeepSeek V3 or Gemini 2.5 Flash (fast, dirt cheap, plenty smart). +- **Nuanced copy / newsletters** → Gemini Flash or a mid model; hype spiels don't need a frontier model. +- **Hard multi-step analysis** → route up to a stronger model (incl. Claude via OpenRouter) for that call only. +Store a default in `openrouter_model`; let specific tools override. + +## Build phases + +- **Phase 0 (done):** Connect page stores `openrouter_api_key` + `openrouter_model`; test button shows usage. +- **Phase 1:** `app/ai_routes.py` agent loop + the 7 read tools + `draft_*` tools. One `/admin` "Ask RecordGod" + panel (chat box → grounded answer + the tool trace). `ai_log` table (prompt, tools, tokens, cost, user, ts). + Everything read/draft. **This is the demo that sells.** +- **Phase 2:** `proposals` review queue + propose tools (price/intake/wantlist). Constrained text-to-SQL over + read-only VIEWS for power users. +- **Phase 3:** Scheduled agent jobs (weekly newsletter draft, daily mispriced-stock report) — still draft/propose. + +## Honest bottom line + +Worth building, and a real differentiator — **because of the data, not the model.** Start with read + draft +(Phase 1): all upside, no blast radius, and it's the version you can put in a sales demo. Add propose-and-apply +only behind the review queue. Keep the agent read-only and human-gated and it can't break the shop — it can only +make it faster to run. Links: [[openrouter-llm-backend]], [[recordgod-engine]], [[recordgod-intake-three-source]]. diff --git a/app/settings_routes.py b/app/settings_routes.py index 2543f32..ec901bc 100644 --- a/app/settings_routes.py +++ b/app/settings_routes.py @@ -37,6 +37,8 @@ KNOWN = { "google_sheet_id": "Intake Google Sheet ID (from its URL)", "google_sheet_name": "Intake sheet/tab name (e.g. Sheet1)", "google_column_map": "Intake sheet column map JSON (Google-Form positions)", + "openrouter_api_key": "OpenRouter API key (sk-or-…) — powers the RecordGod AI assistant (DeepSeek/Gemini/etc.)", + "openrouter_model": "OpenRouter default model (e.g. deepseek/deepseek-chat, google/gemini-2.5-flash)", } @@ -106,4 +108,17 @@ async def test_connection(service: str, ident=Depends(require_admin), db=Depends return {"ok": True, "connected": False, "detail": "DealGod /api/me not deployed yet"} return {"ok": True, "connected": False, "detail": f"HTTP {r.status_code}"} + if service == "openrouter": + key = await vault.get_secret(db, "openrouter_api_key") + if not key: + raise HTTPException(400, "save an OpenRouter API key first") + r = await c.get("https://openrouter.ai/api/v1/key", + headers={"Authorization": f"Bearer {key}"}) + if r.status_code == 200: + d = r.json().get("data", {}) + used, limit = d.get("usage"), d.get("limit") + return {"ok": True, "connected": True, + "as": f"${used} used" + (f" / ${limit} limit" if limit else " (no limit)")} + return {"ok": True, "connected": False, "detail": f"HTTP {r.status_code}"} + raise HTTPException(400, "unknown service") diff --git a/site/admin.html b/site/admin.html index 2ed0281..d5b954e 100644 --- a/site/admin.html +++ b/site/admin.html @@ -500,7 +500,7 @@ async function vConn(){ const m = $('#content'); m.innerHTML = '

Connections

Credentials — encrypted at rest, never shown back

loading…
' + '

Test connections

' - + ['discogs','woo','dealgod'].map(s=>`
`).join('') + + ['discogs','woo','dealgod','openrouter'].map(s=>`
`).join('') + '
'; const d = await fetch('/settings/secrets', { headers: hdr() }).then(r=>r.json()); $('#secrets').innerHTML = d.fields.map(f => {