From bd215b2579328820f30a45aa618ce81e193c3024 Mon Sep 17 00:00:00 2001 From: Wei Zang Date: Mon, 6 Apr 2026 23:47:16 +0100 Subject: [PATCH 1/4] Add prospect_id to llm and migrations Associate LLM records with prospects by adding a prospect_id foreign key. Updated create_table.sql to include prospect_id REFERENCES prospects(id), added an ALTER TABLE migration SQL and a small run script to apply it, and updated app/api/llm/llm.py to return and persist prospect_id in SELECT and INSERT operations. Removed a deprecated insert_llm_lorem.py test script. --- app/api/llm/llm.py | 10 ++++---- app/api/llm/sql/alter_add_prospect_id.sql | 2 ++ app/api/llm/sql/create_table.sql | 3 ++- app/api/llm/sql/insert_llm_lorem.py | 25 -------------------- app/api/llm/sql/run_alter_add_prospect_id.py | 16 +++++++++++++ 5 files changed, 26 insertions(+), 30 deletions(-) create mode 100644 app/api/llm/sql/alter_add_prospect_id.sql delete mode 100644 app/api/llm/sql/insert_llm_lorem.py create mode 100644 app/api/llm/sql/run_alter_add_prospect_id.py diff --git a/app/api/llm/llm.py b/app/api/llm/llm.py index 050dcc9..cc897fa 100644 --- a/app/api/llm/llm.py +++ b/app/api/llm/llm.py @@ -20,7 +20,7 @@ def get_llm_records( count_row = cur.fetchone() total = count_row[0] if count_row and count_row[0] is not None else 0 cur.execute(""" - SELECT id, prompt, completion, duration, time, data, model + SELECT id, prompt, completion, duration, time, data, model, prospect_id FROM llm ORDER BY id DESC LIMIT %s OFFSET %s; @@ -34,6 +34,7 @@ def get_llm_records( "time": row[4].isoformat() if row[4] else None, "data": row[5], "model": row[6], + "prospect_id": row[7], } for row in cur.fetchall() ] @@ -58,6 +59,7 @@ def get_llm_records( def llm_post(payload: dict) -> dict: """POST /llm: send prompt to Gemini, returns completion google-genai SDK.""" prompt = payload.get("prompt") + prospect_id = payload.get("prospect_id") if not prompt: raise HTTPException(status_code=400, detail="Missing 'prompt' in request body.") api_key = os.getenv("GEMINI_API_KEY") @@ -105,11 +107,11 @@ def llm_post(payload: dict) -> dict: cur = conn.cursor() cur.execute( """ - INSERT INTO llm (prompt, completion, duration, data, model) - VALUES (%s, %s, %s, %s, %s) + INSERT INTO llm (prompt, completion, duration, data, model, prospect_id) + VALUES (%s, %s, %s, %s, %s, %s) RETURNING id; """, - (prompt, completion, duration, data_blob, used_model) + (prompt, completion, duration, data_blob, used_model, prospect_id) ) record_id_row = cur.fetchone() record_id = record_id_row[0] if record_id_row else None diff --git a/app/api/llm/sql/alter_add_prospect_id.sql b/app/api/llm/sql/alter_add_prospect_id.sql new file mode 100644 index 0000000..001a844 --- /dev/null +++ b/app/api/llm/sql/alter_add_prospect_id.sql @@ -0,0 +1,2 @@ +-- Migration: Add prospect_id column to llm table +ALTER TABLE llm ADD COLUMN IF NOT EXISTS prospect_id INTEGER REFERENCES prospects(id); \ No newline at end of file diff --git a/app/api/llm/sql/create_table.sql b/app/api/llm/sql/create_table.sql index 41c47ec..b53967d 100644 --- a/app/api/llm/sql/create_table.sql +++ b/app/api/llm/sql/create_table.sql @@ -7,5 +7,6 @@ CREATE TABLE IF NOT EXISTS llm ( duration FLOAT, time TIMESTAMPTZ DEFAULT NOW(), data JSONB, - model TEXT + model TEXT, + prospect_id INTEGER REFERENCES prospects(id) ); diff --git a/app/api/llm/sql/insert_llm_lorem.py b/app/api/llm/sql/insert_llm_lorem.py deleted file mode 100644 index ab0e9b8..0000000 --- a/app/api/llm/sql/insert_llm_lorem.py +++ /dev/null @@ -1,25 +0,0 @@ -import os -import psycopg2 -from dotenv import load_dotenv -import random - -load_dotenv() - -conn = psycopg2.connect( - host=os.getenv('DB_HOST'), - port=os.getenv('DB_PORT', '5432'), - dbname=os.getenv('DB_NAME'), - user=os.getenv('DB_USER'), - password=os.getenv('DB_PASSWORD'), -) -cur = conn.cursor() -lorem = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' -for i in range(5): - cur.execute( - 'INSERT INTO llm (vector, prompt, completion, duration, data) VALUES (%s, %s, %s, %s, %s);', - ([random.random() for _ in range(1536)], lorem, lorem, random.uniform(0.1, 2.0), '{}') - ) -conn.commit() -print('Inserted 5 records.') -cur.close() -conn.close() diff --git a/app/api/llm/sql/run_alter_add_prospect_id.py b/app/api/llm/sql/run_alter_add_prospect_id.py new file mode 100644 index 0000000..3393ce9 --- /dev/null +++ b/app/api/llm/sql/run_alter_add_prospect_id.py @@ -0,0 +1,16 @@ +import os +import sys +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../..'))) +from app.utils.db import get_db_connection_direct + +if __name__ == "__main__": + sql = """ + ALTER TABLE llm ADD COLUMN IF NOT EXISTS prospect_id INTEGER REFERENCES prospects(id); + """ + conn = get_db_connection_direct() + cur = conn.cursor() + cur.execute(sql) + conn.commit() + cur.close() + conn.close() + print("Migration complete: prospect_id column added to llm table.") From c388f5f213677dea12d86b25343cbe3a1e1479ae Mon Sep 17 00:00:00 2001 From: Wei Zang Date: Mon, 6 Apr 2026 23:52:12 +0100 Subject: [PATCH 2/4] Add script to clear llm table Add a small CLI script (app/api/llm/sql/empty_llm_table.py) that connects to the database via get_db_connection_direct and executes `DELETE FROM llm;`. The script adjusts sys.path to import the app utils, opens a cursor, commits the deletion, closes the connection, and prints a confirmation. Note: this is a destructive operation intended to purge all rows from the llm table. --- app/api/llm/sql/empty_llm_table.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 app/api/llm/sql/empty_llm_table.py diff --git a/app/api/llm/sql/empty_llm_table.py b/app/api/llm/sql/empty_llm_table.py new file mode 100644 index 0000000..8b7d7cc --- /dev/null +++ b/app/api/llm/sql/empty_llm_table.py @@ -0,0 +1,14 @@ +import os +import sys +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../..'))) +from app.utils.db import get_db_connection_direct + +if __name__ == "__main__": + sql = "DELETE FROM llm;" + conn = get_db_connection_direct() + cur = conn.cursor() + cur.execute(sql) + conn.commit() + cur.close() + conn.close() + print("All records deleted from llm table.") From d4dc23890f063451528623612f2a23413d9edebe Mon Sep 17 00:00:00 2001 From: Wei Zang Date: Tue, 7 Apr 2026 00:02:56 +0100 Subject: [PATCH 3/4] Include LLM records and add prospects SQL When reading a prospect, fetch related llm rows and attach them as data['llm_records'] (uses get_db_connection_direct and falls back to an empty list on error). Add SQL schema to create the prospects table (app/api/prospects/sql/create_table.sql) and a helper script to describe the prospects table columns (app/api/prospects/sql/describe_prospects_table.py). These changes expose related LLM data and provide the DB schema + inspection utility for the prospects resource. --- app/api/prospects/prospects.py | 23 +++++++++++ app/api/prospects/sql/create_table.sql | 40 +++++++++++++++++++ .../prospects/sql/describe_prospects_table.py | 20 ++++++++++ 3 files changed, 83 insertions(+) create mode 100644 app/api/prospects/sql/create_table.sql create mode 100644 app/api/prospects/sql/describe_prospects_table.py diff --git a/app/api/prospects/prospects.py b/app/api/prospects/prospects.py index 1302a66..1e6e62e 100644 --- a/app/api/prospects/prospects.py +++ b/app/api/prospects/prospects.py @@ -184,6 +184,29 @@ def prospects_read_one(id: int = Path(..., description="ID of the prospect to re if row is not None: columns = [desc[0] for desc in cur.description] data = dict(zip(columns, row)) + # Fetch related llm records + try: + from app.utils.db import get_db_connection_direct + llm_conn = get_db_connection_direct() + llm_cur = llm_conn.cursor() + llm_cur.execute("SELECT id, prompt, completion, duration, time, data, model FROM llm WHERE prospect_id = %s ORDER BY id DESC;", (id,)) + llm_records = [ + { + "id": r[0], + "prompt": r[1], + "completion": r[2], + "duration": r[3], + "time": r[4].isoformat() if r[4] else None, + "data": r[5], + "model": r[6], + } + for r in llm_cur.fetchall() + ] + llm_cur.close() + llm_conn.close() + data["llm_records"] = llm_records + except Exception as llm_exc: + data["llm_records"] = [] else: data = None meta = make_meta("error", f"No prospect found with id {id}") diff --git a/app/api/prospects/sql/create_table.sql b/app/api/prospects/sql/create_table.sql new file mode 100644 index 0000000..086db07 --- /dev/null +++ b/app/api/prospects/sql/create_table.sql @@ -0,0 +1,40 @@ +CREATE TABLE IF NOT EXISTS prospects ( + id SERIAL PRIMARY KEY, + first_name TEXT, + last_name TEXT, + title TEXT, + company_name TEXT, + email TEXT, + email_status TEXT, + primary_email_source TEXT, + primary_email_verification_source TEXT, + email_confidence TEXT, + primary_email_catchall_status TEXT, + primary_email_last_verified_at TEXT, + seniority TEXT, + sub_departments TEXT, + work_direct_phone TEXT, + home_phone TEXT, + mobile_phone TEXT, + corporate_phone TEXT, + other_phone TEXT, + do_not_call TEXT, + lists TEXT, + person_linkedin_url TEXT, + country TEXT, + subsidiary_of TEXT, + subsidiary_of_organization_id TEXT, + tertiary_email TEXT, + tertiary_email_source TEXT, + tertiary_email_status TEXT, + tertiary_email_verification_source TEXT, + primary_intent_topic TEXT, + primary_intent_score TEXT, + secondary_intent_topic TEXT, + secondary_intent_score TEXT, + qualify_contact TEXT, + cleaned TEXT, + search_vector TSVECTOR, + flag BOOLEAN DEFAULT FALSE, + hide BOOLEAN DEFAULT FALSE +); diff --git a/app/api/prospects/sql/describe_prospects_table.py b/app/api/prospects/sql/describe_prospects_table.py new file mode 100644 index 0000000..00b120a --- /dev/null +++ b/app/api/prospects/sql/describe_prospects_table.py @@ -0,0 +1,20 @@ +import os +import sys +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../..'))) +from app.utils.db import get_db_connection_direct + +if __name__ == "__main__": + sql = """ + SELECT column_name, data_type, is_nullable, column_default + FROM information_schema.columns + WHERE table_name = 'prospects' + ORDER BY ordinal_position; + """ + conn = get_db_connection_direct() + cur = conn.cursor() + cur.execute(sql) + columns = cur.fetchall() + cur.close() + conn.close() + for col in columns: + print(col) From ac114b247056d275edc30951babae40a31339c7a Mon Sep 17 00:00:00 2001 From: Wei Zang Date: Tue, 7 Apr 2026 09:18:50 +0100 Subject: [PATCH 4/4] Add API key auth and prompt_code DB migration Introduce API key authentication and a DB migration for LLM prompts. Changes include: added PYTHON_KEY to .env.sample; new app/utils/api_key_auth.py providing get_api_key (APIKeyHeader + dotenv) and used as a Depends in app/api/llm/llm.py; added SQL migration app/api/llm/sql/alter_add_prompt_code.sql and a runnable script app/api/llm/sql/run_alter_add_prompt_code.py to add a prompt_code TEXT column to the llm table; and a small reordering of the endpoints list in app/api/root.py to include the llm entry. These updates enable API key protected LLM endpoints and persist a prompt_code field in the database. --- .env.sample | 1 + app/api/llm/llm.py | 4 +++- app/api/llm/sql/alter_add_prompt_code.sql | 2 ++ app/api/llm/sql/run_alter_add_prompt_code.py | 16 ++++++++++++++ app/api/root.py | 3 +-- app/utils/api_key_auth.py | 22 ++++++++++++++++++++ 6 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 app/api/llm/sql/alter_add_prompt_code.sql create mode 100644 app/api/llm/sql/run_alter_add_prompt_code.py create mode 100644 app/utils/api_key_auth.py diff --git a/.env.sample b/.env.sample index 96c71b5..b596a82 100644 --- a/.env.sample +++ b/.env.sample @@ -1,3 +1,4 @@ +PYTHON_KEY= GEMINI_API_KEY= RESEND_API_KEY= BASE_URL= diff --git a/app/api/llm/llm.py b/app/api/llm/llm.py index cc897fa..0f06973 100644 --- a/app/api/llm/llm.py +++ b/app/api/llm/llm.py @@ -1,7 +1,8 @@ import os -from fastapi import APIRouter, HTTPException, Query, Request +from fastapi import APIRouter, HTTPException, Query, Request, Depends from app.utils.make_meta import make_meta from app.utils.db import get_db_connection_direct +from app.utils.api_key_auth import get_api_key router = APIRouter() @@ -10,6 +11,7 @@ def get_llm_records( request: Request, page: int = Query(1, ge=1, description="Page number (1-based)"), page_size: int = Query(10, ge=1, le=100, description="Records per page") + , api_key: str = Depends(get_api_key) ) -> dict: """GET /llm: Paginated list of LLM completions.""" try: diff --git a/app/api/llm/sql/alter_add_prompt_code.sql b/app/api/llm/sql/alter_add_prompt_code.sql new file mode 100644 index 0000000..80834b4 --- /dev/null +++ b/app/api/llm/sql/alter_add_prompt_code.sql @@ -0,0 +1,2 @@ +-- Migration: Add prompt_code column to llm table +ALTER TABLE llm ADD COLUMN IF NOT EXISTS prompt_code TEXT; \ No newline at end of file diff --git a/app/api/llm/sql/run_alter_add_prompt_code.py b/app/api/llm/sql/run_alter_add_prompt_code.py new file mode 100644 index 0000000..4b78232 --- /dev/null +++ b/app/api/llm/sql/run_alter_add_prompt_code.py @@ -0,0 +1,16 @@ +import os +import sys +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../..'))) +from app.utils.db import get_db_connection_direct + +if __name__ == "__main__": + sql = """ + ALTER TABLE llm ADD COLUMN IF NOT EXISTS prompt_code TEXT; + """ + conn = get_db_connection_direct() + cur = conn.cursor() + cur.execute(sql) + conn.commit() + cur.close() + conn.close() + print("Migration complete: prompt_code column added to llm table.") diff --git a/app/api/root.py b/app/api/root.py index cd8e87f..8967396 100644 --- a/app/api/root.py +++ b/app/api/root.py @@ -20,11 +20,10 @@ def root() -> dict: "time": epoch, } endpoints = [ - {"name": "llm", "url": f"{base_url}/llm"}, {"name": "docs", "url": f"{base_url}/docs"}, {"name": "resend", "url": f"{base_url}/resend"}, {"name": "health", "url": f"{base_url}/health"}, - {"name": "prompts", "url": f"{base_url}/prompts"}, {"name": "prospects", "url": f"{base_url}/prospects"}, + {"name": "llm", "url": f"{base_url}/llm"}, ] return {"meta": meta, "data": endpoints} diff --git a/app/utils/api_key_auth.py b/app/utils/api_key_auth.py new file mode 100644 index 0000000..c1fdf46 --- /dev/null +++ b/app/utils/api_key_auth.py @@ -0,0 +1,22 @@ +import os +from fastapi import Request, HTTPException, status +from app.utils.make_meta import make_meta +from fastapi.security.api_key import APIKeyHeader +from typing import Optional +from dotenv import load_dotenv + +load_dotenv() + +API_KEY = os.getenv("PYTHON_KEY") +API_KEY_NAME = "X-API-Key" +api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False) + +def get_api_key(request: Request, api_key_header: Optional[str] = ""): + if API_KEY is None: + meta = make_meta("error", "PYTHON_KEY not set") + return {"meta": meta, "data": []} + if api_key_header == API_KEY: + return api_key_header + else: + meta = make_meta("error", "Invalid PYTHON_KEY Key") + return {"meta": meta, "data": []}