Skip to content

Commit 3f2449e

Browse files
committed
Enhance prompt caching; remove LinkedIn route
Bump version to 2.3.0, remove the LinkedIn-specific prompt endpoint and its router, and update routing/root listings accordingly. Rewrite /prompt handler to add SHA-256 prompt hashing, prefer exact-hash/text cache hits, and fall back to a tsvector rank-based match; store prompt_hash in the record data and populate search_vector on insert when supported. Improve response payloads to include cached/duration/model fields and ensure DB cursors/connections are closed in finally. Add tests for prompt behavior with mocked DB and GenAI client (tests/test_prompt.py) and update queue test expectations to the new filters structure (tests/test_queue.py).
1 parent 78eadf4 commit 3f2449e

8 files changed

Lines changed: 252 additions & 221 deletions

File tree

app/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
"""Python - FastAPI, Postgres, tsvector"""
22

33
# Current Version
4-
__version__ = "2.2.9"
4+
__version__ = "2.3.0"

app/api/prompt/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
"""Prompt Routes"""
22

33
from .prompt import router as prompt_router
4-
from .linkedin import router as linkedin_router
54
from .empty import router as empty_router

app/api/prompt/linkedin.py

Lines changed: 0 additions & 201 deletions
This file was deleted.

app/api/prompt/prompt.py

Lines changed: 111 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import hashlib
23
from fastapi import APIRouter, HTTPException, Depends
34
from app.utils.make_meta import make_meta
45
from app.utils.db import get_db_connection_direct
@@ -41,15 +42,84 @@ def get_prompt_table_metadata(api_key: str = Depends(get_api_key)) -> dict:
4142

4243
@router.post("/prompt")
4344
def llm_post(payload: dict) -> dict:
44-
"""POST /prompt: send prompt to Gemini, returns completion google-genai SDK."""
45-
prompt = payload.get("prompt")
45+
"""POST /prompt: send prompt to Gemini with DB-backed caching."""
46+
prompt = (payload.get("prompt") or "").strip()
4647
if not prompt:
4748
raise HTTPException(status_code=400, detail="Missing 'prompt' in request body.")
49+
4850
api_key = os.getenv("GEMINI_API_KEY")
4951
if not api_key:
5052
raise HTTPException(status_code=500, detail="Gemini API key not configured.")
53+
54+
prompt_hash = hashlib.sha256(prompt.encode("utf-8")).hexdigest()
55+
conn = None
56+
cur = None
5157
import logging
5258
try:
59+
conn = get_db_connection_direct()
60+
cur = conn.cursor()
61+
cur.execute(
62+
"""
63+
SELECT EXISTS (
64+
SELECT 1
65+
FROM information_schema.columns
66+
WHERE table_schema = 'public'
67+
AND table_name = 'prompt'
68+
AND column_name = 'search_vector'
69+
);
70+
"""
71+
)
72+
exists_row = cur.fetchone()
73+
has_search_vector = bool(exists_row and exists_row[0])
74+
75+
# Fast/safe cache hit: exact prompt hash or exact prompt text.
76+
cur.execute(
77+
"""
78+
SELECT id, prompt, completion, time, model
79+
FROM prompt
80+
WHERE COALESCE(data->>'prompt_hash', '') = %s OR prompt = %s
81+
ORDER BY id DESC
82+
LIMIT 1;
83+
""",
84+
(prompt_hash, prompt),
85+
)
86+
row = cur.fetchone()
87+
88+
# Fallback cache hit when tsvector exists and query terms match strongly.
89+
if not row and has_search_vector:
90+
cur.execute(
91+
"""
92+
SELECT id, prompt, completion, time, model,
93+
ts_rank_cd(search_vector, plainto_tsquery('english', %s)) AS rank
94+
FROM prompt
95+
WHERE search_vector @@ plainto_tsquery('english', %s)
96+
ORDER BY rank DESC, id DESC
97+
LIMIT 1;
98+
""",
99+
(prompt, prompt),
100+
)
101+
rank_row = cur.fetchone()
102+
if rank_row and rank_row[5] is not None and float(rank_row[5]) >= 0.35:
103+
row = rank_row[:5]
104+
105+
cur.close()
106+
conn.close()
107+
cur = None
108+
conn = None
109+
110+
if row:
111+
return {
112+
"meta": make_meta("success", "Prompt returned from cache"),
113+
"data": {
114+
"cached": True,
115+
"prompt_id": row[0],
116+
"prompt": row[1],
117+
"completion": row[2],
118+
"time": row[3].isoformat() if row[3] else None,
119+
"model": row[4],
120+
},
121+
}
122+
53123
from google import genai
54124
import time as time_mod
55125
client = genai.Client(api_key=api_key)
@@ -85,17 +155,31 @@ def llm_post(payload: dict) -> dict:
85155
try:
86156
import json
87157
from app import __version__
88-
data_blob = json.dumps({"version": __version__})
158+
record_data = {
159+
"version": __version__,
160+
"prompt_hash": prompt_hash,
161+
}
162+
data_blob = json.dumps(record_data)
89163
conn = get_db_connection_direct()
90164
cur = conn.cursor()
91-
cur.execute(
92-
"""
93-
INSERT INTO prompt (prompt, completion, duration, data, model)
94-
VALUES (%s, %s, %s, %s, %s)
95-
RETURNING id;
96-
""",
97-
(prompt, completion, duration, data_blob, used_model)
98-
)
165+
if has_search_vector:
166+
cur.execute(
167+
"""
168+
INSERT INTO prompt (prompt, completion, duration, data, model, search_vector)
169+
VALUES (%s, %s, %s, %s, %s, to_tsvector('english', %s || ' ' || %s))
170+
RETURNING id;
171+
""",
172+
(prompt, completion, duration, data_blob, used_model, prompt, completion)
173+
)
174+
else:
175+
cur.execute(
176+
"""
177+
INSERT INTO prompt (prompt, completion, duration, data, model)
178+
VALUES (%s, %s, %s, %s, %s)
179+
RETURNING id;
180+
""",
181+
(prompt, completion, duration, data_blob, used_model)
182+
)
99183
record_id_row = cur.fetchone()
100184
record_id = record_id_row[0] if record_id_row else None
101185
conn.commit()
@@ -105,8 +189,23 @@ def llm_post(payload: dict) -> dict:
105189
# Log DB error but do not fail the API response
106190
logging.error(f"Failed to insert prompt record: {db_exc}")
107191
meta = make_meta("success", f"Gemini completion received from {used_model}")
108-
return {"meta": meta, "data": {"id": record_id, "prompt": prompt, "completion": completion}}
192+
return {
193+
"meta": meta,
194+
"data": {
195+
"cached": False,
196+
"id": record_id,
197+
"prompt": prompt,
198+
"completion": completion,
199+
"duration": duration,
200+
"model": used_model,
201+
},
202+
}
109203
except Exception as e:
110204
meta = make_meta("error", f"Gemini API error: {str(e)}")
111205
return {"meta": meta, "data": {}}
206+
finally:
207+
if cur:
208+
cur.close()
209+
if conn:
210+
conn.close()
112211

app/api/root.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ def root() -> dict:
3131
"name": "Prompt°",
3232
"endpoints": [
3333
{"name": "list", "url": f"{base_url}/prompt"},
34-
{"name": "linkedin", "url": f"{base_url}/prompt/linkedin"},
3534
]
3635
},
3736
{

app/api/routes.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from app.utils.health import router as health_router
1010
from app.utils.notify.resend import router as resend_router
1111
from app.api.prompt.prompt import router as prompt_router
12-
from app.api.prompt.linkedin import router as linkedin_router
1312
from app.api.prompt.empty import router as prompts_empty_router
1413
from app.api.prospects.prospects import router as prospects_router
1514
from app.api.orders.orders import router as orders_router
@@ -19,7 +18,6 @@
1918
router.include_router(resend_router)
2019
router.include_router(health_router)
2120
router.include_router(prompt_router)
22-
router.include_router(linkedin_router)
2321
router.include_router(prompts_empty_router)
2422
router.include_router(prospects_router)
2523
router.include_router(orders_router)

0 commit comments

Comments
 (0)