/ 01 — Audit
The architecture is sound. The wiring inside it is not yet honest.
v1 runs — FastAPI, a LangGraph workflow, SQLAlchemy memory. But two nodes fetch real data then discard it, so the LLM fabricates the rest. This page is the audit of what exists, the verdict, and the v2 that makes fabrication impossible by construction.
/ 02 — Runtime flow
Request pipeline
flowchart TD
LM(["landmark"]):::ok --> ST["POST /chat
JWT-authenticated request"]:::ok
ST --> WF["execute_chat_workflow()
LangGraph"]:::ok
WF --> N1["N1 · GET_LOCATION_AND_LOCALITY
Mapbox geocode → lon/lat + localities · cached"]:::ok
N1 -.parallel.-> N2["N2 · FIND_NEARBY_SCHOOLS
Foursquare ICSE · fetched, then printed and discarded · returns {}"]:::bad
N1 -.parallel.-> N3["N3 · FIND_NEARBY_PLACES
Foursquare metro · fetched, then printed and discarded · returns {}"]:::bad
N1 --> N4["N4 · GET_AQI
Open-Meteo for target coords · cached"]:::ok
N4 --> N5["N5 · RANK_BY_AQI
Open-Meteo per locality → sorted ascending"]:::ok
N5 -. edge commented out .-> N6["N6 · GET_HOUSE_RENTALS
Apify housing.com · signature mismatch · disabled"]:::dead
N2 --> LLM
N3 --> LLM
N5 --> LLM["LLM · Kimi K2.6 advisory
prompt gets coords + AQI + ranked only
schools/metro claims hardcoded → fabricates"]:::bad
LLM --> EN[("END · save_house_rental() → ChatResponse
persists user_location + top_matches JSON")]:::ok
classDef ok fill:#15102a,stroke:#f4a93b,stroke-width:1.5px,color:#f5ede0;
classDef bad fill:#1c1635,stroke:#d96a4a,stroke-width:1.5px,color:#f5ede0;
classDef dead fill:#15102a,stroke:#8a7f99,stroke-width:1px,color:#8a7f99,stroke-dasharray:4 3;
/ 03 — Schema
Data model
SQLAlchemy. backend/database.py. JSON columns imply Postgres. No migrations yet — create_all() runs at import. The relationships below, then v2's collections after the migration.
V1 — NOW · SQLAlchemy
erDiagram
User ||--o{ HouseRental : "owns (nullable FK)"
User {
int id PK
str username UK
str email UK
str password_hash "bcrypt"
}
HouseRental {
int id PK
int user_id FK "nullable"
str user_location
json top_matches
json top_matches_coordinates
}
APICache {
int id PK
str api_name
text cache_key UK
json response_data
datetime created_at
}
V2 — POCKETBASE · per-user via user = @request.auth.id
erDiagram
users ||--o{ searches : "owns"
users ||--|| profile : "one per user"
users {
string id PK
string email UK
string password "built-in auth"
}
searches {
string id PK
string user FK
string landmark
float lat
float lon
json aqi
json ranked
json schools "REAL — honesty fix"
json metro "REAL — honesty fix"
text advisory
date created
}
profile {
string id PK
string user FK
json saved "localities"
json notes
}
cache {
string id PK
string api_name
string cache_key UK
json data
date created
}
Detailed v1 field types (the tables being migrated away from):
HouseRental
APICache
/ 04 — Migration
Target architecture — v2
The flow is linear. It doesn't need LangGraph,
FastAPI, SQLAlchemy, hand-rolled JWT, or external Postgres. Collapse to one PocketBase binary + one hook + a
vanilla PWA — the stack already shipped at og.maahaa.dev. Full plan in docs/MIGRATION.md.
| Concern | v1 — now | v2 — PocketBase |
|---|---|---|
| Server | FastAPI + uvicorn | pocketbase serve — one binary |
| DB / ORM | SQLAlchemy + external Postgres | embedded SQLite + collections |
| Auth | hand-rolled JWT + bcrypt | PocketBase built-in bug 0.2 gone |
| Orchestration | LangGraph graph | sequential calls in one hook bug 0.1 gone |
| LLM | OpenAI SDK (Python) | $http.send in hook, server key only |
| Frontend | none — CLI + JSON | vanilla PWA, same origin, iOS-ready |
v2 — one linear hook, fabrication impossible
flowchart TD
A[landmark] --> B["POST /api/advisory
PocketBase auth"]
B --> G[geocode
Nominatim / Ola]
G --> O1[Overpass metro
cached]
G --> O2[Overpass schools
cached]
G --> AQ[Open-Meteo AQI
per locality]
O1 --> ASM[assemble REAL data]
O2 --> ASM
AQ --> RK[rank ascending] --> ASM
ASM --> LLM["LLM via $http.send
prompt = ONLY verified fields"]
LLM --> S[(searches collection
ranked + schools + metro + advisory)]
LLM --> R[JSON to PWA]
- Both bugs die by construction. One linear hook assembles real schools/metro then calls the LLM with
only verified fields — no
return {}to drop data (fabrication, 0.1). Hand-rolled auth is gone, so no secret to leak (0.2). - Deletes more than it adds. Out:
backend/(5 files), workflow.py, chatbot.py, and 6 deps (fastapi, uvicorn, sqlalchemy, python-jose, passlib, langgraph). In: one binary, one hook, one PWA, one deploy.sh. - Honest trade-off. It's a rewrite (Python → JS in the hook), ~1 focused session. SQLite not Postgres (fine at this scale). LangGraph would only earn its keep if the flow turned non-linear — it hasn't.
/ 05 — Read next