Skip to content

Commit c9be028

Browse files
Merge pull request #85 from goldlabelapps/staging
This pull request introduces several improvements and cleanups to the prompt handling API, particularly around LinkedIn profile analysis and prompt record management.
2 parents 9d6ced6 + 1bceae4 commit c9be028

4 files changed

Lines changed: 192 additions & 100 deletions

File tree

app/api/prompt/linkedin.py

Lines changed: 154 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import os
2+
13
from fastapi import APIRouter, Depends, HTTPException
24

35
from app.utils.api_key_auth import get_api_key
@@ -9,10 +11,22 @@
911

1012
@router.post("/prompt/linkedin")
1113
def linkedin_prompt_success(payload: dict, api_key: str = Depends(get_api_key)) -> dict:
12-
"""POST /prompt/linkedin: return cached completion for linkedinUrl when available."""
13-
linkedin_url = (payload.get("linkedinUrl") or "").strip()
14+
"""POST /prompt/linkedin: return cached completion or create a new Gemini analysis."""
15+
linkedin_url = (payload.get("linkedin_url") or payload.get("linkedinUrl") or "").strip()
1416
if not linkedin_url:
15-
raise HTTPException(status_code=400, detail="Missing 'linkedinUrl' in request body.")
17+
raise HTTPException(status_code=400, detail="Missing 'linkedin_url' in request body.")
18+
19+
prompt = (payload.get("prompt") or "").strip()
20+
if not prompt:
21+
prompt = (
22+
"Analyse this LinkedIn profile URL and provide a concise summary of the person, "
23+
"their role, company, seniority, likely responsibilities, and notable signals. "
24+
f"LinkedIn URL: {linkedin_url}"
25+
)
26+
27+
gemini_api_key = os.getenv("GEMINI_API_KEY")
28+
if not gemini_api_key:
29+
raise HTTPException(status_code=500, detail="Gemini API key not configured.")
1630

1731
conn = None
1832
cur = None
@@ -21,43 +35,164 @@ def linkedin_prompt_success(payload: dict, api_key: str = Depends(get_api_key))
2135
cur = conn.cursor()
2236
cur.execute(
2337
"""
24-
SELECT id, completion, time, model, data
25-
FROM prompt
26-
WHERE (data->>'linkedinUrl' = %s OR prompt ILIKE %s)
27-
ORDER BY id DESC
28-
LIMIT 1;
29-
""",
30-
(linkedin_url, f"%{linkedin_url}%"),
38+
SELECT EXISTS (
39+
SELECT 1
40+
FROM information_schema.columns
41+
WHERE table_schema = 'public'
42+
AND table_name = 'prompt'
43+
AND column_name = 'search_vector'
44+
);
45+
"""
3146
)
47+
exists_row = cur.fetchone()
48+
has_search_vector = bool(exists_row and exists_row[0])
49+
50+
if has_search_vector:
51+
cur.execute(
52+
"""
53+
SELECT id, prompt, completion, time, model, data
54+
FROM prompt
55+
WHERE (
56+
COALESCE(data->>'linkedin_url', data->>'linkedinUrl') = %s
57+
OR search_vector @@ plainto_tsquery('english', %s)
58+
OR prompt ILIKE %s
59+
)
60+
ORDER BY id DESC
61+
LIMIT 1;
62+
""",
63+
(linkedin_url, linkedin_url, f"%{linkedin_url}%"),
64+
)
65+
else:
66+
cur.execute(
67+
"""
68+
SELECT id, prompt, completion, time, model, data
69+
FROM prompt
70+
WHERE (COALESCE(data->>'linkedin_url', data->>'linkedinUrl') = %s OR prompt ILIKE %s)
71+
ORDER BY id DESC
72+
LIMIT 1;
73+
""",
74+
(linkedin_url, f"%{linkedin_url}%"),
75+
)
3276
row = cur.fetchone()
3377

3478
if row:
79+
cur.close()
80+
conn.close()
81+
cur = None
82+
conn = None
3583
return {
3684
"meta": make_meta("success", "LinkedIn URL already analysed"),
3785
"data": {
3886
"cached": True,
3987
"id": row[0],
40-
"linkedinUrl": linkedin_url,
41-
"completion": row[1],
42-
"time": row[2].isoformat() if row[2] else None,
43-
"model": row[3],
44-
"record_data": row[4],
88+
"linkedin_url": linkedin_url,
89+
"prompt": row[1],
90+
"completion": row[2],
91+
"time": row[3].isoformat() if row[3] else None,
92+
"model": row[4],
93+
"record_data": row[5],
4594
},
4695
}
4796

97+
cur.close()
98+
conn.close()
99+
cur = None
100+
conn = None
101+
102+
import json
103+
import logging
104+
import time as time_mod
105+
from app import __version__
106+
from google import genai
107+
108+
client = genai.Client(api_key=gemini_api_key)
109+
model_names = [
110+
"models/gemini-flash-latest",
111+
"models/gemini-1.5-pro",
112+
"models/gemini-1.5-flash",
113+
"models/gemini-1.0-pro",
114+
"models/gemini-pro",
115+
"models/gemini-pro-vision",
116+
]
117+
response = None
118+
completion = None
119+
used_model = None
120+
errors = {}
121+
start_time = time_mod.time()
122+
for model_name in model_names:
123+
try:
124+
response = client.models.generate_content(model=model_name, contents=prompt)
125+
completion = getattr(response, "text", None)
126+
if completion:
127+
used_model = model_name
128+
break
129+
except Exception as model_exc:
130+
errors[model_name] = str(model_exc)
131+
continue
132+
133+
duration = time_mod.time() - start_time
134+
if not completion:
135+
error_details = " | ".join([f"{name}: {message}" for name, message in errors.items()])
136+
raise Exception(
137+
"No available Gemini model succeeded for generate_content with your API key. "
138+
f"Details: {error_details}"
139+
)
140+
141+
record_id = None
142+
record_data = {
143+
"version": __version__,
144+
"linkedin_url": linkedin_url,
145+
}
146+
try:
147+
conn = get_db_connection_direct()
148+
cur = conn.cursor()
149+
data_blob = json.dumps(record_data)
150+
if has_search_vector:
151+
cur.execute(
152+
"""
153+
INSERT INTO prompt (prompt, completion, duration, model, data, search_vector)
154+
VALUES (%s, %s, %s, %s, %s, to_tsvector('english', %s || ' ' || %s))
155+
RETURNING id;
156+
""",
157+
(prompt, completion, duration, used_model, data_blob, prompt, completion)
158+
)
159+
else:
160+
cur.execute(
161+
"""
162+
INSERT INTO prompt (prompt, completion, duration, model, data)
163+
VALUES (%s, %s, %s, %s, %s)
164+
RETURNING id;
165+
""",
166+
(prompt, completion, duration, used_model, data_blob)
167+
)
168+
record_id_row = cur.fetchone()
169+
record_id = record_id_row[0] if record_id_row else None
170+
conn.commit()
171+
cur.close()
172+
conn.close()
173+
cur = None
174+
conn = None
175+
except Exception as db_exc:
176+
logging.error(f"Failed to insert prompt record: {db_exc}")
177+
48178
return {
49-
"meta": make_meta("warning", "LinkedIn URL not analysed yet"),
179+
"meta": make_meta("success", f"Gemini completion received from {used_model}"),
50180
"data": {
51181
"cached": False,
52-
"linkedinUrl": linkedin_url,
53-
"completion": None,
182+
"id": record_id,
183+
"linkedin_url": linkedin_url,
184+
"prompt": prompt,
185+
"completion": completion,
186+
"duration": duration,
187+
"model": used_model,
188+
"record_data": record_data,
54189
},
55190
}
56191
except HTTPException:
57192
raise
58193
except Exception as e:
59194
return {
60-
"meta": make_meta("error", f"DB error: {str(e)}"),
195+
"meta": make_meta("error", f"Gemini API error: {str(e)}"),
61196
"data": {},
62197
}
63198
finally:

app/api/prompt/prompt.py

Lines changed: 37 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -11,87 +11,47 @@ def get_prompt_records(
1111
request: Request,
1212
page: int = Query(1, ge=1, description="Page number (1-based)"),
1313
page_size: int = Query(10, ge=1, le=100, description="Records per page"),
14-
prospect_id: int = Query(None, description="Filter by prospect_id"),
1514
api_key: str = Depends(get_api_key)
1615
) -> dict:
1716
"""GET /prompt: Paginated list of prompt completions."""
1817
try:
1918
conn = get_db_connection_direct()
2019
cur = conn.cursor()
21-
if prospect_id is not None:
22-
# No pagination for single prospect_id lookup
23-
select_query = """
24-
SELECT id, prompt, completion, duration, time, data, model, prospect_id
25-
FROM prompt
26-
WHERE prospect_id = %s
27-
ORDER BY id DESC
28-
"""
29-
cur.execute(select_query, (prospect_id,))
30-
rows = cur.fetchall()
31-
records = [
32-
{
33-
"id": row[0],
34-
"prompt": row[1],
35-
"completion": row[2],
36-
"duration": row[3],
37-
"time": row[4].isoformat() if row[4] else None,
38-
"data": row[5],
39-
"model": row[6],
40-
"prospect_id": row[7],
41-
}
42-
for row in rows
43-
]
44-
cur.close()
45-
conn.close()
46-
if records:
47-
meta = make_meta("success", f"Found {len(records)} record(s) for prospect_id {prospect_id}")
48-
return {
49-
"meta": meta,
50-
"data": records,
51-
}
52-
else:
53-
meta = make_meta("warning", f"No records found for prospect_id {prospect_id}")
54-
return {
55-
"meta": meta,
56-
"data": [],
57-
}
58-
else:
59-
offset = (page - 1) * page_size
60-
cur.execute("SELECT COUNT(*) FROM prompt;")
61-
count_row = cur.fetchone()
62-
total = count_row[0] if count_row and count_row[0] is not None else 0
63-
cur.execute("""
64-
SELECT id, prompt, completion, duration, time, data, model, prospect_id
65-
FROM prompt
66-
ORDER BY id DESC
67-
LIMIT %s OFFSET %s;
68-
""", (page_size, offset))
69-
records = [
70-
{
71-
"id": row[0],
72-
"prompt": row[1],
73-
"completion": row[2],
74-
"duration": row[3],
75-
"time": row[4].isoformat() if row[4] else None,
76-
"data": row[5],
77-
"model": row[6],
78-
"prospect_id": row[7],
79-
}
80-
for row in cur.fetchall()
81-
]
82-
cur.close()
83-
conn.close()
84-
meta = make_meta("success", f"Prompt {len(records)} records (page {page})")
85-
return {
86-
"meta": meta,
87-
"data": {
88-
"page": page,
89-
"page_size": page_size,
90-
"total": total,
91-
"pages": (total + page_size - 1) // page_size,
92-
"data": records,
93-
},
20+
offset = (page - 1) * page_size
21+
cur.execute("SELECT COUNT(*) FROM prompt;")
22+
count_row = cur.fetchone()
23+
total = count_row[0] if count_row and count_row[0] is not None else 0
24+
cur.execute("""
25+
SELECT id, prompt, completion, duration, time, data, model
26+
FROM prompt
27+
ORDER BY id DESC
28+
LIMIT %s OFFSET %s;
29+
""", (page_size, offset))
30+
records = [
31+
{
32+
"id": row[0],
33+
"prompt": row[1],
34+
"completion": row[2],
35+
"duration": row[3],
36+
"time": row[4].isoformat() if row[4] else None,
37+
"data": row[5],
38+
"model": row[6],
9439
}
40+
for row in cur.fetchall()
41+
]
42+
cur.close()
43+
conn.close()
44+
meta = make_meta("success", f"Prompt {len(records)} records (page {page})")
45+
return {
46+
"meta": meta,
47+
"data": {
48+
"page": page,
49+
"page_size": page_size,
50+
"total": total,
51+
"pages": (total + page_size - 1) // page_size,
52+
"data": records,
53+
},
54+
}
9555
except Exception as e:
9656
meta = make_meta("error", f"DB error: {str(e)}")
9757
return {"meta": meta, "data": {}}
@@ -100,7 +60,6 @@ def get_prompt_records(
10060
def llm_post(payload: dict) -> dict:
10161
"""POST /prompt: send prompt to Gemini, returns completion google-genai SDK."""
10262
prompt = payload.get("prompt")
103-
prospect_id = payload.get("prospect_id")
10463
if not prompt:
10564
raise HTTPException(status_code=400, detail="Missing 'prompt' in request body.")
10665
api_key = os.getenv("GEMINI_API_KEY")
@@ -148,11 +107,11 @@ def llm_post(payload: dict) -> dict:
148107
cur = conn.cursor()
149108
cur.execute(
150109
"""
151-
INSERT INTO prompt (prompt, completion, duration, data, model, prospect_id)
152-
VALUES (%s, %s, %s, %s, %s, %s)
110+
INSERT INTO prompt (prompt, completion, duration, data, model)
111+
VALUES (%s, %s, %s, %s, %s)
153112
RETURNING id;
154113
""",
155-
(prompt, completion, duration, data_blob, used_model, prospect_id)
114+
(prompt, completion, duration, data_blob, used_model)
156115
)
157116
record_id_row = cur.fetchone()
158117
record_id = record_id_row[0] if record_id_row else None

app/api/prompt/sql/create_table.sql

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ CREATE TABLE IF NOT EXISTS prompt (
66
completion TEXT NOT NULL,
77
duration FLOAT,
88
time TIMESTAMPTZ DEFAULT NOW(),
9-
data JSONB,
109
model TEXT,
11-
prospect_id INTEGER REFERENCES prospects(id)
10+
data JSONB
1211
);

app/api/prompt/sql/drop_llm_table.sql

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)