Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
PYTHON_KEY=
GEMINI_API_KEY=
RESEND_API_KEY=
BASE_URL=
Expand Down
14 changes: 9 additions & 5 deletions app/api/llm/llm.py
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()

Expand All @@ -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)
Comment on lines 13 to +14
Copy link

Copilot AI Apr 7, 2026

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.

Suggested change
page_size: int = Query(10, ge=1, le=100, description="Records per page")
, api_key: str = Depends(get_api_key)
page_size: int = Query(10, ge=1, le=100, description="Records per page"),
api_key: str = Depends(get_api_key)

Copilot uses AI. Check for mistakes.
) -> dict:
"""GET /llm: Paginated list of LLM completions."""
try:
Expand All @@ -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;
Expand All @@ -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()
]
Expand All @@ -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.")
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prospect_id is accepted from the request body but isn’t validated/cast. With the new FK constraint, non-integer values or non-existent IDs will make the insert fail later. Validate that prospect_id is an int (and optionally that the prospect exists) before attempting the insert.

Suggested change
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 uses AI. Check for mistakes.
api_key = os.getenv("GEMINI_API_KEY")
Expand Down Expand Up @@ -105,11 +109,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)
)
Comment on lines 109 to 117
Copy link

Copilot AI Apr 7, 2026

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.

Copilot uses AI. Check for mistakes.
record_id_row = cur.fetchone()
record_id = record_id_row[0] if record_id_row else None
Expand Down
2 changes: 2 additions & 0 deletions app/api/llm/sql/alter_add_prompt_code.sql
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;
2 changes: 2 additions & 0 deletions app/api/llm/sql/alter_add_prospect_id.sql
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);
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This migration adds a foreign key reference to prospects(id). If the prospects table does not exist yet in the target DB, this ALTER TABLE will fail. Consider creating prospects first (or splitting into: add column, then add FK constraint after verifying table existence) and document/encode the required migration order.

Suggested change
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 $$;

Copilot uses AI. Check for mistakes.
3 changes: 2 additions & 1 deletion app/api/llm/sql/create_table.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new prompt_code column is added via migrations in this PR, but it is missing from the base CREATE TABLE definition. Fresh environments created from create_table.sql will not have prompt_code unless migrations are also run, causing schema drift. Add prompt_code to this table definition to keep bootstrap and migrations consistent.

Suggested change
prospect_id INTEGER REFERENCES prospects(id)
prospect_id INTEGER REFERENCES prospects(id),
prompt_code TEXT

Copilot uses AI. Check for mistakes.
);
Copy link

Copilot AI Apr 7, 2026

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.

Suggested change
);
);
CREATE INDEX IF NOT EXISTS llm_prospect_id_id_desc_idx
ON llm (prospect_id, id DESC);

Copilot uses AI. Check for mistakes.
14 changes: 14 additions & 0 deletions app/api/llm/sql/empty_llm_table.py
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.")
25 changes: 0 additions & 25 deletions app/api/llm/sql/insert_llm_lorem.py

This file was deleted.

16 changes: 16 additions & 0 deletions app/api/llm/sql/run_alter_add_prompt_code.py
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.")
16 changes: 16 additions & 0 deletions app/api/llm/sql/run_alter_add_prospect_id.py
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.")
23 changes: 23 additions & 0 deletions app/api/prospects/prospects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block opens a second DB connection/cursor for llm, but if an exception is raised after llm_conn/llm_cur are created, they won’t be closed because there’s no finally. Wrap the llm cursor/connection in try/finally (or context managers) so resources are always released.

Suggested change
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 uses AI. Check for mistakes.
except Exception as llm_exc:
data["llm_records"] = []
Comment on lines +208 to +209
Copy link

Copilot AI Apr 7, 2026

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.

Copilot uses AI. Check for mistakes.
else:
data = None
meta = make_meta("error", f"No prospect found with id {id}")
Expand Down
40 changes: 40 additions & 0 deletions app/api/prospects/sql/create_table.sql
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
);
20 changes: 20 additions & 0 deletions app/api/prospects/sql/describe_prospects_table.py
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)
3 changes: 1 addition & 2 deletions app/api/root.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
22 changes: 22 additions & 0 deletions app/utils/api_key_auth.py
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
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_api_key is not actually reading the X-API-Key header: the api_key_header parameter here is treated by FastAPI as a query parameter, and the APIKeyHeader(...) dependency defined above is never used. This makes the API key check trivially bypassable (e.g., via ?api_key_header=...) and breaks the intended header-based authentication. Use api_key: str | None = Depends(api_key_header)/Security(api_key_header) to extract the header value and validate that instead.

Copilot uses AI. Check for mistakes.
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
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On invalid/missing API key (or missing PYTHON_KEY env var), this dependency returns a normal dict response instead of raising an HTTPException (e.g., 401/403). Because the /llm handler continues executing after the dependency returns, requests without a valid key will still receive data. Change this to raise HTTPException(status_code=..., detail=...) so the request is rejected early.

Suggested change
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,
)

Copilot uses AI. Check for mistakes.
Loading