-
Notifications
You must be signed in to change notification settings - Fork 0
This pull request introduces several enhancements and database schema changes to support associating LLM completions with prospects, improve API security, and add utility scripts for database management. #66
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| PYTHON_KEY= | ||
| GEMINI_API_KEY= | ||
| RESEND_API_KEY= | ||
| BASE_URL= | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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: | ||||||||||||||||
|
|
@@ -20,7 +22,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 +36,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 +61,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.") | ||||||||||||||||
|
||||||||||||||||
| raise HTTPException(status_code=400, detail="Missing 'prompt' in request body.") | |
| raise HTTPException(status_code=400, detail="Missing 'prompt' in request body.") | |
| if prospect_id is not None: | |
| try: | |
| prospect_id = int(prospect_id) | |
| except (TypeError, ValueError): | |
| raise HTTPException(status_code=400, detail="Invalid 'prospect_id' in request body. It must be an integer.") |
Copilot
AI
Apr 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This insert can now fail due to the new prospect_id FK (e.g., invalid/non-existent prospect), but failures are only logged and the endpoint still returns a "success" response. Consider validating prospect_id up front and/or returning an error (or clearly signaling persistence failure) when the DB insert fails so callers don’t get a misleading success result.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| -- Migration: Add prompt_code column to llm table | ||
| ALTER TABLE llm ADD COLUMN IF NOT EXISTS prompt_code TEXT; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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); | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
| ALTER TABLE llm ADD COLUMN IF NOT EXISTS prospect_id INTEGER REFERENCES prospects(id); | |
| ALTER TABLE llm ADD COLUMN IF NOT EXISTS prospect_id INTEGER; | |
| DO $$ | |
| BEGIN | |
| IF EXISTS ( | |
| SELECT 1 | |
| FROM information_schema.tables | |
| WHERE table_schema = current_schema() | |
| AND table_name = 'prospects' | |
| ) AND NOT EXISTS ( | |
| SELECT 1 | |
| FROM pg_constraint | |
| WHERE conname = 'llm_prospect_id_fkey' | |
| ) THEN | |
| ALTER TABLE llm | |
| ADD CONSTRAINT llm_prospect_id_fkey | |
| FOREIGN KEY (prospect_id) REFERENCES prospects(id); | |
| END IF; | |
| END $$; |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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) | ||||||||||||
|
||||||||||||
| prospect_id INTEGER REFERENCES prospects(id) | |
| prospect_id INTEGER REFERENCES prospects(id), | |
| prompt_code TEXT |
Copilot
AI
Apr 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Queries in prospects_read_one filter llm by prospect_id and order by id. Without an index on llm.prospect_id, this will devolve into sequential scans as the table grows. Add an index (e.g., on (prospect_id, id DESC) or at least prospect_id) via a migration/bootstrap SQL.
| ); | |
| ); | |
| CREATE INDEX IF NOT EXISTS llm_prospect_id_id_desc_idx | |
| ON llm (prospect_id, id DESC); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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.") |
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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.") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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.") |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+190
to
+207
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 | |
| llm_conn = None | |
| llm_cur = None | |
| try: | |
| 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() | |
| ] | |
| data["llm_records"] = llm_records | |
| finally: | |
| if llm_cur is not None: | |
| llm_cur.close() | |
| if llm_conn is not None: | |
| llm_conn.close() |
Copilot
AI
Apr 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The except Exception as llm_exc: here silently drops the error and returns an empty list, which makes production failures to fetch llm_records hard to detect. At minimum, log llm_exc (and still return an empty list) so this doesn’t fail invisibly.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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: | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+11
to
+15
|
||||||||||||||||||||||||||||||||||||||
| 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": []} | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+17
to
+22
|
||||||||||||||||||||||||||||||||||||||
| 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": []} | |
| raise HTTPException( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail=meta, | |
| ) | |
| if api_key_header == API_KEY: | |
| return api_key_header | |
| else: | |
| meta = make_meta("error", "Invalid PYTHON_KEY Key") | |
| raise HTTPException( | |
| status_code=status.HTTP_401_UNAUTHORIZED, | |
| detail=meta, | |
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The parameter list formatting here (line break followed by a leading comma) is inconsistent and will be reformatted/flagged by common linters/formatters. Move the comma to the end of the previous line so parameters are consistently formatted.