feat(gemini): Skiva 1 – Gemini 2.5 Flash som lärar-tier för kylskåpsskanning

- Gemini-adapter bakom AamosClient-interface (AAMOS_MODE=gemini)
- Serversida/worker: hämtar bild, anropar Gemini, mappar mot canonical_ingredients
- Kostnad/tokens bokförs i ai_usage_counters; global dagsbudget via BudgetStore
- Redis-backed budget i worker, in-memory i tester
- Migration 0019: ai_cost_usd_microcents
- Hermetiska tester med inspelad fixture; separat pnpm eval:scan
- docs/09 uppdaterad ärligt: AAMOS-status, Gemini-flöde, säkerhet/kostnad
- REQUIRE_REAL=1 stödjer AAMOS_MODE=gemini; deploy-grind uppdaterad
This commit is contained in:
Sven (AAMOS AI)
2026-08-08 03:23:44 +07:00
parent 7894a1ec06
commit 050c958285
18 changed files with 1055 additions and 86 deletions
+102 -59
View File
@@ -1,60 +1,111 @@
# Del 9 AAMOS-integration
# Del 9 AI-integration (AAMOS + Gemini-lärar-tier)
AAMOS är er befintliga AI-plattform och nås **uteslutande via API** från appens
backend (beslut 2026-08-02). Mobilappen har aldrig direktkontakt och innehåller inga
AI-hemligheter (spec §31, §61.13).
> Uppdaterad 2026-08-08 efter scouting: AAMOS Core v1 är autentiseringsmässigt
> nåbar men saknar konfigurerad matmodell (`/v1/detect` faller tillbaka på YuNet
> ansiktsdetektering). Tills vidare används **Gemini 2.5 Flash** som
> lärar-tier för kylskåpsskanning, bakom samma `AamosClient`-gränssnitt.
> AAMOS/prod rörs inte.
Appen når AI **uteslutande via backend** (beslut 2026-08-02). Mobilappen har
aldrig direktkontakt och innehåller inga AI-hemligheter (spec §31, §61.13).
## Kontraktet (`packages/ai-contracts`)
Appen definierar ett versionerat, Zod-validerat kontrakt per uppgiftstyp. Input
valideras FÖRE nätverksanropet, output valideras EFTER ett svar som bryter kontraktet
behandlas som fel och når aldrig användardata. AAMOS kan därmed byta modeller,
prompts och leverantörer fritt bakom kontraktet ("Food API ska få stabila kontrakt
oavsett modell").
behandlas som fel och når aldrig användardata. Modeller, prompts och leverantörer kan
bytas fritt bakom kontraktet ("Food API ska få stabila kontrakt oavsett modell").
Antaget transport-API (justeras mot er AAMOS-dokumentation endast
`HttpAamosClient` berörs):
```text
POST {AAMOS_API_URL}/v1/tasks Authorization: Bearer {AAMOS_API_KEY}
→ AamosRequestEnvelope { taskId, taskType, contractVersion, input, metadata }
← AamosResponseEnvelope { taskId, status: ok|uncertain|failed, output,
modelVersion, promptVersion, latencyMs, costUsd }
GET {AAMOS_API_URL}/v1/health
```
┌─────────┐ signed image URL ┌──────────┐ AamosClient ┌─────────────────┐
│ App │ ───────────────────→ │ Food API │ ───────────────→│ Worker adapter │
└─────────┘ └──────────┘ └─────────────────┘
┌──────────────────┬──────────┴──────────┐
│ AAMOS_MODE=http │ AAMOS_MODE=gemini │
│ HttpAamosClient │ GeminiAamosClient │
│ → AAMOS Core v1 │ → Gemini 2.5 Flash │
└──────────────────┴─────────────────────┘
```
Metadata per anrop: `correlationId` (spårbarhet §58), `subjectRef`
(**pseudonymiserat** id aldrig e-post/namn, §56), `priority`, samt
`consentFlags {personalization, anonymizedImprovement, imageTraining}` så att AAMOS
kan upprätthålla samtyckesreglerna på sin sida också.
`createAamosClient()` väljer adapter utifrån `AAMOS_MODE`. Inga anropare behöver
ändras. App→Food API→leverantörsgränsen är oförändrad.
**Behövs från er:** AAMOS bas-URL + API-nyckel per miljö, och er endpoint-/schema-
dokumentation. Avviker kuvertformatet mappas det i `HttpAamosClient` kontrakten
utåt ändras inte.
## AAMOS Core v1-status
## Uppgiftstyper (15)
Reella endpoints (ingen `/v1/tasks` fiktion):
Bild/OCR: `ANALYZE_FRIDGE_IMAGE`, `ANALYZE_PANTRY_IMAGE`, `ANALYZE_MEAL_IMAGE`
(kcal-INTERVALL, aldrig exakt påstående), `READ_RECEIPT`, `READ_NUTRITION_LABEL`
(värden från etiketten inte modellens gissning), `READ_EXPIRY_DATE`.
Text/struktur: `NORMALIZE_PRODUCTS`, `DEDUPLICATE_INVENTORY`, `STRUCTURE_RECIPE_TEXT`,
`PARSE_CRAVING`, `MODERATE_RECIPE`.
Rådgivande: `GENERATE_RECIPE_OPTIONS` (granskas redaktionellt, §15), `RANK_RECIPES`
(får ordna om aldrig lägga till), `GENERATE_WEEK_PLAN`, `UPDATE_USER_MEMORY`.
```text
POST /v1/detect POST /v1/verify POST /v1/compare
POST /v1/segment POST /v1/classify POST /v1/authenticate
POST /v1/score POST /v1/explain POST /v1/extract
POST /v1/track GET /v1/health
```
Alla outputs bär `confidence`, och "osäker/okänd" är förstklassiga svar
(`canonicalIngredientId: null`, `requiresConfirmation: true`) spec §10/§61.4.
Scouting 2026-08-08 visade:
## Routing & specialister (spec §33)
- `amos-core.service` på server-2 anropar `http://localhost:3207`.
- Värdport `3207` ägs av `aamos-identity-rust`, inte AI-inferens.
- AI-inferenscontainern `amos-ai-inference:latest` exponerar **värdport 3209**.
- Port mismatch gör att AAMOS Core aldrig når inferens.
- Även efter hypotetisk port-fix saknas `FOOD`/`GROCERY`/`VISION_MODEL`
konfiguration i `.env.amos-core`; `/v1/detect` defaultar till `yunet`.
Rekommenderad routing inne i AAMOS: specialistmodell först (vanliga livsmedel,
nordiska förpackningar, kvitto-OCR, datum-OCR, portionssegmentering) → hög confidence:
svara; låg: extern generalist (Haiku 4.5-klass för volym, Sonnet-klass för svåra fall);
fortsatt osäkert: `status: "uncertain"` → appen frågar användaren. Appen skickar
`modelVersion`/`promptVersion` vidare in i `scan_jobs` och `ai_corrections` så att
varje datapunkt är spårbar till modellversion (§9).
**Beslut:** rör inte AAMOS-prod. AAMOS kan återaktiveras i framtiden när en
matmodell finns konfigurerad, via `HttpAamosClient`.
## Feedback → träning (spec §33)
## Gemini-lärar-tier (Skiva 1)
Aktiveras med `AAMOS_MODE=gemini` + `GEMINI_API_KEY`. Endast workern anropar
Gemini; API:et når aldrig nyckeln.
### Hanterade uppgiftstyper
- `ANALYZE_FRIDGE_IMAGE`
- `ANALYZE_PANTRY_IMAGE`
Övriga task types (`READ_RECEIPT`, `READ_NUTRITION_LABEL`, etc.) svarar
`failed` i Skiva 1 och faller tillbaka på appens befintliga flöden.
### Flöde
1. App laddar upp bild → Food API skapar `scan_job` (status `queued`).
2. Workern plockar jobbet, hämtar bilden från lagring (S3/mock).
3. `GeminiAamosClient` skickar bilden + svensk livsmedelsprompt till
`generativelanguage.googleapis.com/v1beta/models/{GEMINI_MODEL}:generateContent`.
4. Gemini svarar med JSON: produkt, varumärke, kvantitet, enhet, kategori,
bäst-före/sista-förbruk (om läsbart), konfidens, imageQualityIssues.
5. Workern mappar detekterade namn mot `canonical_ingredients` (mjuk matchning).
6. Resultatet sparas i `scan_jobs` som `awaiting_confirmation`.
7. Användaren granskar och bekräftar i appen. Aldrig auto-commit.
8. AI är aldrig facit för allergener/näring — mjölkprincipen orörd.
### Kostnadskontroll
- `scan_jobs.costUsd` lagrar verklig kostnad per anrop.
- `ai_usage_counters.ai_cost_usd_microcents` ackumulerar månadskostnad per
användare (utan PII).
- Hushållskvoten `aiScans` respekteras (befintlig entitlements-logik).
- Global dagsbudget `GEMINI_DAILY_BUDGET_USD` via `BudgetStore`. Workern använder
Redis-backed budget; tester använder in-memory budget. När budgeten är
förbrukad nekar adaptern nya anrop.
- Loggning: status, latens, tokens, kostnad, taskType. Ingen bild, inget
fritext, inga PII-fält.
### Säkerhet
- Nyckeln finns i `.env` på workerservrar (SSM i produktion) och checkas aldrig
in.
- App/API validerar att `AAMOS_MODE=gemini` kräver `GEMINI_API_KEY` i produktion.
- `REQUIRE_REAL=1` i deploy-grinden gör att start failar om nyckel saknas.
## Routing & specialister (framtida)
Skiva 2 kommer lägga premium-eskaleringsstier (Gemini Pro / GPT-4o / Claude)
internt i adaptern vid låg konfidens, med eval/guldset som beslutar billig vs
dyr modell. Fram till dess: Gemini 2.5 Flash för allt i Skiva 1.
## Feedback → träning (Skiva 3)
Varje användarkorrigering i granska-flödet sparas som `ai_corrections`
(AI-utdata + korrigering + modell/promptversion + **samtyckessnapshot**).
@@ -62,25 +113,17 @@ Veckojobbet `BUILD_TRAINING_SAMPLE` exporterar endast rader där
`anonymized_improvement = granted` vid korrigeringstillfället; bilder kräver
separat `image_training`-samtycke. Personligt minne är aldrig träningsdata (§32).
## Memory (spec §32)
Appens DB är den användarsynliga sanningen (`memory_items`). Nattjobbet skickar
veckans domänhändelser + befintliga minnesnycklar till `UPDATE_USER_MEMORY`; AAMOS
returnerar förslag (kind/key/sammanfattning/confidence/expires) som skrivs in men
poster som användaren verifierat eller pausat skrivs **aldrig** över. Användarens
rättelser blir `user_stated` med confidence 1 och vinner alltid (§30).
## Evals (spec §34)
`ai_eval_cases` (fast testbibliotek: kyl/frys/skafferi/tallrik/kvitton/etiketter/
allergifall/mörka bilder/överlapp/orimliga recept) + `ai_eval_runs` (precision, recall,
missade produkter, hallucinationer, allergifel, latens, kostnad, korrigeringsgrad).
Regel: **ingen modell- eller promptändring i AAMOS tas i produktion för appen utan
grön eval-körning**, synlig i adminpanelen (`/admin/v1/ai/eval-runs`). Corpus byggs
under beta ur anonymiserade, samtyckta exempel.
- `pnpm test`: hermetiska tester med inspelad Gemini-fixture, når aldrig nätet.
- `pnpm --filter @app/worker eval:scan`: live-eval mot litet guldset. Rapporterar
precision, recall, kostnad, latens. Kräver `AAMOS_MODE=gemini` + nyckel.
- Regel: **ingen modell-/promptändring tas i produktion utan grön eval**.
## Kostnadskontroll
## Lägen
`costUsd` per anrop loggas på scan_jobs → daglig kostnad per uppgiftstyp i admin;
larm vid spik (§58). Fair use-kvoterna (Del 10/13) begränsar exponeringen per användare.
Mock-läget (`AAMOS_MODE=mock`) finns för dev/test; produktion vägrar starta i mock.
| `AAMOS_MODE` | Användning |
|---|---|
| `mock` | dev/test utan nätverk. Produkten vägrar starta i mock. |
| `http` | Riktig AAMOS Core när matmodell finns. |
| `gemini` | Gemini 2.5 Flash som lärar-tier för kylskåpsskanning (Skiva 1). |