From f833b8ef9844e21af5241e22441d00c176531242 Mon Sep 17 00:00:00 2001 From: Wei Zang Date: Tue, 7 Apr 2026 13:04:59 +0100 Subject: [PATCH 1/2] Rename prospects 'flagged' module to 'flag' Rename app/api/prospects/flagged.py to app/api/prospects/flag.py and update imports/usages accordingly. Replace flagged_router with flag_router in app/api/prospects/__init__.py and app/api/routes.py, and include the updated router in the main routes. Also add a brief comment and minor formatting changes in the new flag.py. --- app/api/prospects/__init__.py | 2 +- app/api/prospects/{flagged.py => flag.py} | 4 +++- app/api/routes.py | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) rename app/api/prospects/{flagged.py => flag.py} (96%) diff --git a/app/api/prospects/__init__.py b/app/api/prospects/__init__.py index 24c986d..a957ddb 100644 --- a/app/api/prospects/__init__.py +++ b/app/api/prospects/__init__.py @@ -1,4 +1,4 @@ """Prospect Routes""" from .prospects import router as prospects_router -from .flagged import router as flagged_router +from .flag import router as flag_router diff --git a/app/api/prospects/flagged.py b/app/api/prospects/flag.py similarity index 96% rename from app/api/prospects/flagged.py rename to app/api/prospects/flag.py index b52dec3..e6d9c06 100644 --- a/app/api/prospects/flagged.py +++ b/app/api/prospects/flag.py @@ -8,7 +8,9 @@ base_url = os.getenv("BASE_URL", "http://localhost:8000") -# Refactored GET /prospects endpoint to return paginated, filtered, and ordered results + + +# Gel all flagged prspects @router.get("/prospects/flagged") def get_prospects( page: int = Query(1, ge=1, description="Page number (1-based)"), diff --git a/app/api/routes.py b/app/api/routes.py index a60b505..b49f7f1 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -9,13 +9,13 @@ from app.api.resend.resend import router as resend_router from app.api.llm.llm import router as llm_router from app.api.prospects.prospects import router as prospects_router -from app.api.prospects.flagged import router as flagged_router +from app.api.prospects.flag import router as flag_router from app.api.llm.llm import router as gemini_router router.include_router(root_router) router.include_router(resend_router) router.include_router(health_router) router.include_router(llm_router) -router.include_router(flagged_router) +router.include_router(flag_router) router.include_router(prospects_router) router.include_router(gemini_router) From e184bcec7e650535212b5122124cc9ac183bf37b Mon Sep 17 00:00:00 2001 From: Wei Zang Date: Wed, 8 Apr 2026 06:52:18 +0100 Subject: [PATCH 2/2] Add unflag-all, LLM filter, and type migration Add POST /unflag-all endpoint to reset all prospect flags (with a test). Extend /llm to accept prospect_id to return records for a specific prospect (no pagination) while keeping paginated listing. Add SQL migration and runner to add a 'type' TEXT column to the llm table. Bump package version to 2.1.6, increase prospects default page size to 100, and remove the deprecated /prospects/init endpoint. --- app/__init__.py | 2 +- app/api/llm/llm.py | 113 +++++++++++++------ app/api/llm/sql/alter_add_type_column.sql | 2 + app/api/llm/sql/run_alter_add_type_column.py | 21 ++++ app/api/prospects/flag.py | 24 +++- app/api/prospects/prospects.py | 92 +-------------- tests/test_routes.py | 11 ++ 7 files changed, 136 insertions(+), 129 deletions(-) create mode 100644 app/api/llm/sql/alter_add_type_column.sql create mode 100644 app/api/llm/sql/run_alter_add_type_column.py diff --git a/app/__init__.py b/app/__init__.py index 3942c5f..5ffc4af 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,4 +1,4 @@ """Python - FastAPI, Postgres, tsvector""" # Current Version -__version__ = "2.1.5" +__version__ = "2.1.6" diff --git a/app/api/llm/llm.py b/app/api/llm/llm.py index 0f06973..5f6f3a7 100644 --- a/app/api/llm/llm.py +++ b/app/api/llm/llm.py @@ -10,49 +10,88 @@ 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) + page_size: int = Query(10, ge=1, le=100, description="Records per page"), + prospect_id: int = Query(None, description="Filter by prospect_id"), + api_key: str = Depends(get_api_key) ) -> dict: """GET /llm: Paginated list of LLM completions.""" try: conn = get_db_connection_direct() cur = conn.cursor() - offset = (page - 1) * page_size - cur.execute("SELECT COUNT(*) FROM llm;") - 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, prospect_id - FROM llm - ORDER BY id DESC - LIMIT %s OFFSET %s; - """, (page_size, offset)) - records = [ - { - "id": row[0], - "prompt": row[1], - "completion": row[2], - "duration": row[3], - "time": row[4].isoformat() if row[4] else None, - "data": row[5], - "model": row[6], - "prospect_id": row[7], + if prospect_id is not None: + # No pagination for single prospect_id lookup + select_query = """ + SELECT id, prompt, completion, duration, time, data, model, prospect_id + FROM llm + WHERE prospect_id = %s + ORDER BY id DESC + """ + cur.execute(select_query, (prospect_id,)) + rows = cur.fetchall() + records = [ + { + "id": row[0], + "prompt": row[1], + "completion": row[2], + "duration": row[3], + "time": row[4].isoformat() if row[4] else None, + "data": row[5], + "model": row[6], + "prospect_id": row[7], + } + for row in rows + ] + cur.close() + conn.close() + if records: + meta = make_meta("success", f"Found {len(records)} record(s) for prospect_id {prospect_id}") + return { + "meta": meta, + "data": records, + } + else: + meta = make_meta("warning", f"No records found for prospect_id {prospect_id}") + return { + "meta": meta, + "data": [], + } + else: + offset = (page - 1) * page_size + cur.execute("SELECT COUNT(*) FROM llm;") + 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, prospect_id + FROM llm + ORDER BY id DESC + LIMIT %s OFFSET %s; + """, (page_size, offset)) + records = [ + { + "id": row[0], + "prompt": row[1], + "completion": row[2], + "duration": row[3], + "time": row[4].isoformat() if row[4] else None, + "data": row[5], + "model": row[6], + "prospect_id": row[7], + } + for row in cur.fetchall() + ] + cur.close() + conn.close() + meta = make_meta("success", f"LLM {len(records)} records (page {page})") + return { + "meta": meta, + "data": { + "page": page, + "page_size": page_size, + "total": total, + "pages": (total + page_size - 1) // page_size, + "data": records, + }, } - for row in cur.fetchall() - ] - cur.close() - conn.close() - meta = make_meta("success", f"LLM {len(records)} records (page {page})") - return { - "meta": meta, - "data": { - "page": page, - "page_size": page_size, - "total": total, - "pages": (total + page_size - 1) // page_size, - "data": records, - }, - } except Exception as e: meta = make_meta("error", f"DB error: {str(e)}") return {"meta": meta, "data": {}} diff --git a/app/api/llm/sql/alter_add_type_column.sql b/app/api/llm/sql/alter_add_type_column.sql new file mode 100644 index 0000000..b8999d7 --- /dev/null +++ b/app/api/llm/sql/alter_add_type_column.sql @@ -0,0 +1,2 @@ +-- Migration: Add 'type' column to llm table +ALTER TABLE llm ADD COLUMN IF NOT EXISTS type TEXT DEFAULT 'default'; diff --git a/app/api/llm/sql/run_alter_add_type_column.py b/app/api/llm/sql/run_alter_add_type_column.py new file mode 100644 index 0000000..884ce76 --- /dev/null +++ b/app/api/llm/sql/run_alter_add_type_column.py @@ -0,0 +1,21 @@ +# Run this script to add a 'type' column to the llm table if it doesn't exist +from app.utils.db import get_db_connection_direct + +def add_type_column(): + conn = get_db_connection_direct() + cur = conn.cursor() + try: + cur.execute(""" + ALTER TABLE llm ADD COLUMN IF NOT EXISTS type TEXT DEFAULT 'default'; + """) + conn.commit() + print("'type' column added to llm table (if not already present).") + except Exception as e: + print(f"Error adding 'type' column: {e}") + conn.rollback() + finally: + cur.close() + conn.close() + +if __name__ == "__main__": + add_type_column() diff --git a/app/api/prospects/flag.py b/app/api/prospects/flag.py index e6d9c06..432b0ad 100644 --- a/app/api/prospects/flag.py +++ b/app/api/prospects/flag.py @@ -1,13 +1,35 @@ + from app import __version__ import os from app.utils.make_meta import make_meta from fastapi import APIRouter, Query, Path, Body, HTTPException +from fastapi.responses import JSONResponse from app.utils.db import get_db_connection router = APIRouter() base_url = os.getenv("BASE_URL", "http://localhost:8000") - +# Route to unflag all prospects +@router.post("/unflag-all") +def unflag_all_prospects(): + """Reset all flags in the prospects table to false.""" + meta = make_meta("success", "All flags reset to false") + conn_gen = get_db_connection() + conn = next(conn_gen) + cur = conn.cursor() + try: + cur.execute("UPDATE prospects SET flag = FALSE WHERE flag IS TRUE;") + affected = cur.rowcount + conn.commit() + meta = make_meta("success", f"{affected} prospects unflagged.") + except Exception as e: + conn.rollback() + meta = make_meta("error", f"Failed to unflag all prospects: {str(e)}") + return JSONResponse(status_code=500, content={"meta": meta}) + finally: + cur.close() + conn.close() + return {"meta": meta} # Gel all flagged prspects diff --git a/app/api/prospects/prospects.py b/app/api/prospects/prospects.py index 1727174..353f83d 100644 --- a/app/api/prospects/prospects.py +++ b/app/api/prospects/prospects.py @@ -12,10 +12,10 @@ @router.get("/prospects") def get_prospects( page: int = Query(1, ge=1, description="Page number (1-based)"), - limit: int = Query(50, ge=1, le=500, description="Records per page (default 50, max 500)"), + limit: int = Query(100, ge=1, le=500, description="Records per page (default 100, max 500)"), search: str = Query(None, description="Search term for first or last name (case-insensitive, partial match)") ) -> dict: - """Return paginated, filtered, and ordered prospects (flagged first, then alphabetical by first_name), filtered by search if provided.""" + """Return paginated, filtered, and ordered prospects (then alphabetical by first_name), filtered by search if provided.""" meta = make_meta("success", "Read paginated prospects") conn_gen = get_db_connection() conn = next(conn_gen) @@ -80,94 +80,6 @@ class ProspectUpdate(BaseModel): flag: Optional[bool] = None hide: Optional[bool] = None - -# endpoint: /prospects/init -@router.get("/prospects/init") -def prospects_init() -> dict: - """Initialize prospects and return real total count.""" - meta = make_meta("success", "Init prospects") - conn_gen = get_db_connection() - conn = next(conn_gen) - cur = conn.cursor() - title = [] - total_unique_title = 0 - seniority = [] - total_unique_seniority = 0 - sub_departments = [] - total_unique_sub_departments = 0 - try: - cur.execute('SELECT COUNT(*) FROM prospects WHERE hide IS NOT TRUE;') - row = cur.fetchone() - total = row[0] if row is not None else 0 - - # Get unique titles and their counts (column is 'title') - cur.execute('SELECT title, COUNT(*) FROM prospects WHERE title IS NOT NULL AND hide IS NOT TRUE GROUP BY title ORDER BY COUNT(*) DESC;') - title_rows = cur.fetchall() - def slugify(text): - import re - text = str(text).lower() - text = re.sub(r'[^a-z0-9]+', '-', text) - return text.strip('-') - - title = [ - {"label": str(t[0]), "value": slugify(t[0])} - for t in title_rows - if t[0] is not None and str(t[0]).strip() != "" and slugify(t[0]) != "" - ] - total_unique_title = len(title) - - # Get unique seniority and their counts (column is 'seniority') - cur.execute('SELECT seniority, COUNT(*) FROM prospects WHERE seniority IS NOT NULL AND hide IS NOT TRUE GROUP BY seniority ORDER BY COUNT(*) DESC;') - seniority_rows = cur.fetchall() - seniority = [ - {"label": str(s[0]), "value": slugify(s[0])} - for s in seniority_rows - if s[0] is not None and str(s[0]).strip() != "" and slugify(s[0]) != "" - ] - total_unique_seniority = len(seniority) - - # Get unique sub_departments and their counts (column is 'sub_departments') - cur.execute('SELECT sub_departments, COUNT(*) FROM prospects WHERE sub_departments IS NOT NULL AND hide IS NOT TRUE GROUP BY sub_departments ORDER BY COUNT(*) DESC;') - sub_department_rows = cur.fetchall() - sub_departments = [ - {"label": str(sd[0]), "value": slugify(sd[0])} - for sd in sub_department_rows - if sd[0] is not None and str(sd[0]).strip() != "" and slugify(sd[0]) != "" - ] - total_unique_sub_departments = len(sub_departments) - except Exception: - total = 0 - title = [] - total_unique_title = 0 - seniority = [] - total_unique_seniority = 0 - sub_departments = [] - total_unique_sub_departments = 0 - finally: - cur.close() - conn.close() - data = { - "total": total, - "groups": { - "seniority": { - "total": total_unique_seniority, - "list": seniority - }, - "title": { - "total": total_unique_title, - "list": title - }, - "sub_departments": { - "total": total_unique_sub_departments, - "list": sub_departments - } - }, - "message": "This is a placeholder for prospects/init." - } - return {"meta": meta, "data": data} - - -# endpoint: /prospects/{id} # endpoint: /prospects/{id} @router.get("/prospects/{id}") def prospects_read_one(id: int = Path(..., description="ID of the prospect to retrieve")) -> dict: diff --git a/tests/test_routes.py b/tests/test_routes.py index 0cf778a..554c01d 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -21,4 +21,15 @@ def test_health_returns_ok() -> None: assert response.status_code == 200 assert response.json() == {"status": "ok"} +def test_unflag_all_prospects(): + """POST /unflag-all should reset all flags to false and return success meta.""" + # First, flag some prospects (if needed) - skipping setup for brevity + response = client.post("/unflag-all") + assert response.status_code == 200 + json_data = response.json() + # The meta dict uses 'severity' for status, not 'status' + assert json_data["meta"].get("severity") == "success" + # Accept any success message in the title + assert json_data["meta"].get("title", "").endswith("unflagged.") +