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 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-06-24 16:12:19 +10:00
parent 9a0bf41fbe
commit 9e028db735
3 changed files with 117 additions and 1 deletions

101
RECORDGOD_AI_AGENT_PLAN.md Normal file
View File

@ -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]].

View File

@ -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")

View File

@ -500,7 +500,7 @@ async function vConn(){
const m = $('#content');
m.innerHTML = '<h2>Connections</h2><div class="card"><h3>Credentials <span class="soon">— encrypted at rest, never shown back</span></h3><div id="secrets" class="muted">loading…</div></div>'
+ '<div class="card"><h3>Test connections</h3>'
+ ['discogs','woo','dealgod'].map(s=>`<div class="conn"><button class="ghost" onclick="testConn('${s}')">Test ${s}</button><span id="c-${s}" class="soon"></span></div>`).join('')
+ ['discogs','woo','dealgod','openrouter'].map(s=>`<div class="conn"><button class="ghost" onclick="testConn('${s}')">Test ${s}</button><span id="c-${s}" class="soon"></span></div>`).join('')
+ '</div>';
const d = await fetch('/settings/secrets', { headers: hdr() }).then(r=>r.json());
$('#secrets').innerHTML = d.fields.map(f => {