@@ -32,11 +32,234 @@ uvicorn app.main:app --reload
3232
3333## Tech Stack
3434
35- - Framework/Server: FastAPI, Uvicorn
36- - Database & Vectors: PostgreSQL (Neon), pgvector
37- - AI/ML: OpenAI (chat), CLIP/OpenCLIP (image embeddings)
38- - Text-to-Speech: ElevenLabs
39- - Auth: Clerk (JWT)
40- - Cloud/Storage(Image): AWS S3 (+ CDN/presigned)
41- - DevOps: Docker, Docker Compose
42- - Config/Validation: Pydantic
35+ - FastAPI + Uvicorn: Python web framework and ASGI server
36+ - PostgreSQL (Neon) + pgvector: relational data + vector search
37+ - OpenAI + (CLIP/OpenCLIP): chat + image/text embeddings
38+ - ElevenLabs — TTS / voice replies
39+ - Clerk — JWT auth for API protection
40+ - AWS S3 — object storage + presigned uploads
41+ - Pydantic — config & schema validation
42+ - SQLAlchemy — ORM & sessions
43+
44+ ## Routes: Chat (` app/routers/chat.py ` )
45+
46+ ** Endpoint:** ` POST /api/chat `
47+ ** Auth:** ` Authorization: Bearer <Clerk JWT> ` (required)
48+ ** Does:** Runs the RAG pipeline and returns an answer plus supporting sources. Persists the turn as ` channel="text" ` .
49+
50+ ## Routes: Chat History (` app/routers/chat_history.py ` )
51+
52+ ** Base prefix:** ` /api/chat `
53+ ** Auth:** ` Authorization: Bearer <Clerk JWT> ` (required)
54+
55+ ### ` GET /api/chat/history `
56+
57+ Returns a paginated list of the user’s ** scan chats** with:
58+
59+ - ` scan_id ` , ` scan_title `
60+ - ` artwork_id ` , ` artwork_title `
61+ - ` artwork_image_url ` (auto-resolved to a usable URL)
62+ - ` created_at ` , ` last_message ` , ` last_message_at `
63+
64+ ## Route: Image Embedding (` app/routers/embed.py ` )
65+
66+ ** Endpoint:** ` POST /api/embed-image `
67+ ** Does:** Fetches an image from a URL, computes its embedding, and returns the vector + dimension.
68+
69+ ** Implementation Notes**
70+
71+ - Loads the image via ` app.helpers.common.load_image_from_url(...) ` .
72+ - Embeds with ` app.embeddings.embed_pil_image(...) ` .
73+ - Embedding model/dimension come from your embeddings module (see ` IMAGE_EMBEDDING_MODEL ` in ` app/config.py ` ).
74+
75+ ## Route: Image Search (` app/routers/search.py ` )
76+
77+ ** Endpoint:** ` POST /api/search-image `
78+ ** Auth:** ` Authorization: Bearer <Clerk JWT> ` (required)
79+ ** Uploads:** ` multipart/form-data ` with an image file field named ** ` file ` **
80+ ** Does:** Embeds the uploaded image, searches nearest artworks (pgvector), applies confidence/margin logic, persists a scan, and returns top-K neighbors + a ` scan_id ` .
81+
82+ ** Query Params**
83+
84+ - ` top_k ` : number of results to return (default from ` TOPK_DEFAULT ` , 1–100)
85+ - ` metric ` : similarity metric: ` cosine ` | ` l2 ` | ` ip ` (default ` METRIC_DEFAULT ` )
86+ - ` sim_threshold ` : min similarity for a match (default ` IMAGE_MATCH_SIM_THRESHOLD ` )
87+ - ` margin_threshold ` : min absolute margin ` (top1 - top2) ` when ` require_margin=true `
88+ - ` require_margin ` : enforce margin checks (` true/false ` , default ` IMAGE_MATCH_REQUIRE_MARGIN ` )
89+ - ` solo_threshold ` : min similarity to accept when only one candidate is present
90+ - ` high_conf_threshold ` : auto-accept if ` top1 >= high_conf_threshold `
91+ - ` margin_ratio_threshold ` : min ratio ` (top1 / top2) ` when margin is enforced
92+
93+ ## Route: Voice Chat (` app/routers/voice.py ` )
94+
95+ ** Endpoint:** ` POST /api/voice/chat `
96+ ** Auth:** ` Authorization: Bearer <Clerk JWT> ` (required)
97+ ** Does:** Converts speech to text (ASR), runs RAG chat, then returns ** TTS audio (base64)** + transcript + text answer. Persists the turn as ` channel="voice" ` .
98+
99+ ### Form Fields
100+
101+ - ** artwork_id** _ (string, required)_ : target artwork context
102+ - ** artist_id** _ (string, optional)_
103+ - ** scan_id** _ (string, optional)_ : tie to existing scan/session
104+ - ** prompt** _ (string, optional)_ : text prompt when no audio is sent
105+ - ** audio_file** _ (file, optional)_ : audio if present, ASR will produce ` transcript `
106+ - ** voice_id** _ (string, optional)_ : TTS voice (provider-specific)
107+ - ** metric** _ (string, optional)_ : ` cosine ` | ` l2 ` | ` ip ` (default: ` METRIC_DEFAULT ` )
108+ - ** sim_threshold** _ (float, optional)_ : ` [0,1] ` (default: ` TEXT_MATCH_SIM_THRESHOLD ` )
109+ - ** top_k** _ (int, optional)_ : retrieval candidates (default: ` 6 ` )
110+ - ** language_code** _ (string, optional)_ : ASR language hint (e.g., ` en ` , ` en-US ` )
111+
112+ ## Auth: Clerk JWT (` app/auth_clerk.py ` )
113+
114+ ** What it does:**
115+ Validates Clerk-issued JWTs on incoming requests and returns the current user (` {"user_id": sub, "claims": ...} ` ) for protected routes.
116+
117+ ** Headers supported**
118+
119+ - ` Authorization: Bearer <JWT> `
120+ - ` X-Client-Auth: Bearer <JWT> ` (fallback for mobile/web clients)
121+
122+ ** Env vars**
123+
124+ - ` CLERK_ISSUER ` ** (required)** : ` https://art-connect.org.clerk.accounts.dev `
125+
126+ ** How it works**
127+
128+ - Caches Clerk ** JWKS** for 5 minutes to verify signatures.
129+ - Verifies ` iss ` (issuer) and, if provided, ` aud ` (audience).
130+ - Extracts ` sub ` : returned as ` user_id ` for downstream usage.
131+ - Raises ` 401 ` on missing/bad bearer or invalid token.
132+
133+ ## Config (` app/config.py ` )
134+
135+ Central place for ** env-driven settings** (loaded via ` python-dotenv ` ).
136+ These control retrieval metrics, table/column names, thresholds, and model IDs.
137+
138+ ### What it does
139+
140+ - Reads ` .env ` and exposes constants used across routers/helpers.
141+ - Sets ** defaults** so the API can run locally without a huge .env.
142+ - Groups knobs for ** image search** , ** text RAG** , and ** DB schema** .
143+
144+ ### Key Env Vars (with defaults)
145+
146+ ** Models**
147+
148+ - ` MODEL_ID ` — image model id for local usage (default: ` openai/clip-vit-base-patch32 ` )
149+ - ` TEXT_EMBED_MODEL ` — text embedding model (default: ` text-embedding-3-small ` )
150+ - ` GEN_MODEL ` — chat/generation model (default: ` gpt-4o-mini ` )
151+ - ` OPENAI_API_KEY ` — required for OpenAI models
152+
153+ ** Database / Vectors**
154+
155+ - ` DATABASE_URL ` — Postgres connection string
156+ - ` PGVECTOR_PROBES ` — index probe count for ANN searches (default: ` 10 ` )
157+
158+ ** Image Embeddings (table/cols)**
159+
160+ - ` IMAGE_EMBED_TABLE ` (default: ` artwork_embeddings_image ` )
161+ - ` IMAGE_EMBED_VECTOR_COL ` (default: ` embedding ` )
162+ - ` IMAGE_EMBED_ARTWORK_ID_COL ` (default: ` artwork_id ` )
163+
164+ ** Retrieval Controls (shared)**
165+
166+ - ` METRIC_DEFAULT ` — ` cosine ` | ` l2 ` | ` ip ` (default: ` cosine ` )
167+ - ` TOPK_DEFAULT ` — default top-K (default: ` 5 ` )
168+
169+ ** Image Match Thresholds**
170+
171+ - ` IMAGE_MATCH_SIM_THRESHOLD ` (default: ` 0.70 ` )
172+ - ` IMAGE_MATCH_MARGIN_THRESHOLD ` (default: ` 0.10 ` )
173+ - ` IMAGE_MATCH_REQUIRE_MARGIN ` (` true|false ` , default: ` true ` )
174+ - ` IMAGE_MATCH_SOLO_THRESHOLD ` (default: ` 0.80 ` )
175+ - ` IMAGE_MATCH_HIGH_CONF_THRESHOLD ` (default: ` 0.90 ` )
176+ - ` IMAGE_MATCH_MARGIN_RATIO_THRESHOLD ` (default: ` 1.05 ` )
177+
178+ ** Domain Tables (read-joins)**
179+
180+ - ` ARTWORKS_TABLE ` (default: ` Artwork ` )
181+ - ` ARTWORKS_ID_COL ` (default: ` id ` )
182+ - ` ARTWORKS_ARTIST_ID_COL ` (default: ` artistId ` )
183+ - ` ARTISTS_TABLE ` (default: ` Artist ` )
184+ - ` ARTISTS_ID_COL ` (default: ` id ` )
185+ - ` ARTISTS_NAME_COL ` (default: ` name ` )
186+ - ` ARTWORKS_TITLE_COL ` (default: ` title ` )
187+ - ` ARTWORKS_DESC_COL ` (default: ` description ` )
188+ - ` ARTISTS_BIO_COL ` (default: ` bio ` )
189+
190+ ** Text Embeddings (RAG)**
191+
192+ - ` TEXT_EMBED_TABLE_ARTWORK ` (default: ` artwork_embeddings_text ` )
193+ - ` TEXT_EMBED_TABLE_ARTIST ` (default: ` artist_embeddings_text ` )
194+ - ` TEXT_EMBED_VECTOR_COL ` (default: ` embedding ` )
195+ - ` TEXT_EMBED_TEXT_COL ` (default: ` content ` )
196+ - ` TEXT_MATCH_SIM_THRESHOLD ` (default: ` 0.60 ` )
197+
198+ > ** Operator mapping (pgvector):**
199+ > ` cosine → <=> ` , ` l2 → <-> ` , ` ip → <#> ` ; code converts distance : similarity for thresholds.
200+
201+ ## Chat DB Pool (` app/db_chat.py ` )
202+
203+ ** What it does:**
204+ Provides a ** psycopg connection pool** and a ` get_chat_cursor() ` context manager for chat-related queries (scans/messages).
205+
206+ ** Env var**
207+
208+ - ` CHAT_DATABASE_URL ` ** (required)** : Postgres connection string for the ** chat** database.
209+
210+ ## App DB Pool (` app/db.py ` )
211+
212+ ** What it does:**
213+ Creates a ** psycopg connection pool** for the primary app database and exposes:
214+
215+ - ` get_cursor() ` : pooled cursor with ` pgvector ` registered and ` ivfflat.probes ` set
216+ - ` get_conn() ` : direct connection context (bypasses pool)
217+
218+ ** Env vars**
219+
220+ - ` DATABASE_URL ` ** (required)** : Postgres connection string
221+ - ` PGVECTOR_PROBES ` : ANN probe count for ` ivfflat ` (default from ` app/config.py ` )
222+
223+ ## Embeddings (` app/embeddings.py ` )
224+
225+ ** What it does**
226+
227+ - ** Image embeddings (CLIP):** Loads a CLIP model once, embeds a PIL image, ** L2-normalizes** features, and returns a Python list of floats.
228+ - ** Text embeddings (OpenAI):** Calls OpenAI Embeddings. Returns a list of floats.
229+
230+ ## LLM Wrapper (` app/llm.py ` )
231+
232+ ** What it does**
233+
234+ - Small helper around OpenAI Chat Completions to turn ** question + RAG context (+ optional history)** into an answer that stays grounded.
235+ - System Prompts
236+
237+ ## App Entry (` app/main.py ` )
238+
239+ ** What it does**
240+
241+ - Creates the FastAPI app, configures ** CORS** , exposes health & identity endpoints.
242+ - Registers feature routers: ** embed** , ** search** , ** chat** , ** voice** , ** chat history** .
243+
244+ ** Endpoints (no global ` /api ` prefix)**
245+
246+ - ` GET / ` — status payload (` name ` , ` version ` , ` python ` , ` server_time_utc ` , ` uptime ` , ` git_sha? ` )
247+ - ` GET /healthz ` — ` { "ok": true } `
248+ - ` GET /whoami ` — returns ` { "user_id": ... } ` (requires Clerk auth)
249+ - ** Routers included**
250+ - ` POST /embed-image ` — image to embedding
251+ - ` POST /search-image ` — image search (pgvector)
252+ - ` POST /chat ` — text chat (RAG)
253+ - ` POST /voice/chat ` — voice chat (ASR to RAG to TTS)
254+ - ` GET /chat/history ` — chat list
255+ - ` GET /chat/{scan_id}/messages ` — messages for a scan
256+
257+ ** CORS**
258+
259+ - Reads ` ALLOW_ORIGINS ` , splits by comma, and enables:
260+ - ` allow_credentials=True ` , ` allow_methods="*" ` , ` allow_headers="*" `
261+
262+ ## Schemas (` app/schemas.py ` )
263+
264+ Pydantic models that define request/response shapes for the API.
265+
0 commit comments