feat: incrementing counter file naming (01_name_company) + fix API key overwrite bug + better LLM logging
Browse files- backend/database.py +22 -4
- backend/llm.py +10 -4
- backend/main.py +11 -15
backend/database.py
CHANGED
|
@@ -114,7 +114,8 @@ CREATE TABLE IF NOT EXISTS global_jobs (
|
|
| 114 |
|
| 115 |
CREATE TABLE IF NOT EXISTS user_settings (
|
| 116 |
user_id INTEGER PRIMARY KEY,
|
| 117 |
-
use_default_api INTEGER DEFAULT 1
|
|
|
|
| 118 |
);
|
| 119 |
|
| 120 |
CREATE TABLE IF NOT EXISTS user_job_links (
|
|
@@ -194,7 +195,8 @@ CREATE TABLE IF NOT EXISTS global_jobs (
|
|
| 194 |
|
| 195 |
CREATE TABLE IF NOT EXISTS user_settings (
|
| 196 |
user_id INTEGER PRIMARY KEY,
|
| 197 |
-
use_default_api INTEGER DEFAULT 1
|
|
|
|
| 198 |
);
|
| 199 |
|
| 200 |
CREATE TABLE IF NOT EXISTS user_job_links (
|
|
@@ -290,6 +292,7 @@ def init_db():
|
|
| 290 |
_safe_alter(conn, "ALTER TABLE global_jobs ALTER COLUMN date_found TYPE TIMESTAMP USING date_found::timestamp")
|
| 291 |
_safe_alter(conn, "ALTER TABLE user_cv ADD COLUMN IF NOT EXISTS raw_text TEXT DEFAULT ''")
|
| 292 |
_safe_alter(conn, "ALTER TABLE users ADD COLUMN IF NOT EXISTS is_admin INTEGER DEFAULT 0")
|
|
|
|
| 293 |
_seed_categories(conn)
|
| 294 |
_seed_admin_user(conn)
|
| 295 |
else:
|
|
@@ -303,6 +306,10 @@ def init_db():
|
|
| 303 |
conn.execute("ALTER TABLE user_cv ADD COLUMN raw_text TEXT DEFAULT ''")
|
| 304 |
except _sqlite3.OperationalError:
|
| 305 |
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
_seed_categories(conn)
|
| 307 |
_seed_admin_user(conn)
|
| 308 |
|
|
@@ -454,6 +461,17 @@ def save_user_settings(user_id: int, settings: dict) -> None:
|
|
| 454 |
)
|
| 455 |
|
| 456 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 457 |
def get_api_keys(user_id: int) -> dict:
|
| 458 |
with get_db() as conn:
|
| 459 |
rows = _exec(conn, "SELECT provider, api_key FROM api_keys WHERE user_id = ?", (user_id,)).fetchall()
|
|
@@ -464,8 +482,8 @@ def get_effective_api_keys(user_id: int) -> dict:
|
|
| 464 |
from backend.config import DEFAULT_NVIDIA_KEY
|
| 465 |
keys = get_api_keys(user_id)
|
| 466 |
settings = get_user_settings(user_id)
|
| 467 |
-
if settings.get("use_default_api", True):
|
| 468 |
-
keys
|
| 469 |
return keys
|
| 470 |
|
| 471 |
|
|
|
|
| 114 |
|
| 115 |
CREATE TABLE IF NOT EXISTS user_settings (
|
| 116 |
user_id INTEGER PRIMARY KEY,
|
| 117 |
+
use_default_api INTEGER DEFAULT 1,
|
| 118 |
+
cv_gen_count INTEGER DEFAULT 0
|
| 119 |
);
|
| 120 |
|
| 121 |
CREATE TABLE IF NOT EXISTS user_job_links (
|
|
|
|
| 195 |
|
| 196 |
CREATE TABLE IF NOT EXISTS user_settings (
|
| 197 |
user_id INTEGER PRIMARY KEY,
|
| 198 |
+
use_default_api INTEGER DEFAULT 1,
|
| 199 |
+
cv_gen_count INTEGER DEFAULT 0
|
| 200 |
);
|
| 201 |
|
| 202 |
CREATE TABLE IF NOT EXISTS user_job_links (
|
|
|
|
| 292 |
_safe_alter(conn, "ALTER TABLE global_jobs ALTER COLUMN date_found TYPE TIMESTAMP USING date_found::timestamp")
|
| 293 |
_safe_alter(conn, "ALTER TABLE user_cv ADD COLUMN IF NOT EXISTS raw_text TEXT DEFAULT ''")
|
| 294 |
_safe_alter(conn, "ALTER TABLE users ADD COLUMN IF NOT EXISTS is_admin INTEGER DEFAULT 0")
|
| 295 |
+
_safe_alter(conn, "ALTER TABLE user_settings ADD COLUMN IF NOT EXISTS cv_gen_count INTEGER DEFAULT 0")
|
| 296 |
_seed_categories(conn)
|
| 297 |
_seed_admin_user(conn)
|
| 298 |
else:
|
|
|
|
| 306 |
conn.execute("ALTER TABLE user_cv ADD COLUMN raw_text TEXT DEFAULT ''")
|
| 307 |
except _sqlite3.OperationalError:
|
| 308 |
pass
|
| 309 |
+
try:
|
| 310 |
+
conn.execute("ALTER TABLE user_settings ADD COLUMN cv_gen_count INTEGER DEFAULT 0")
|
| 311 |
+
except _sqlite3.OperationalError:
|
| 312 |
+
pass
|
| 313 |
_seed_categories(conn)
|
| 314 |
_seed_admin_user(conn)
|
| 315 |
|
|
|
|
| 461 |
)
|
| 462 |
|
| 463 |
|
| 464 |
+
def get_and_increment_cv_gen_count(user_id: int) -> int:
|
| 465 |
+
with get_db() as conn:
|
| 466 |
+
rows = _exec(conn, "SELECT cv_gen_count FROM user_settings WHERE user_id = ?", (user_id,)).fetchall()
|
| 467 |
+
if not rows:
|
| 468 |
+
_exec(conn, "INSERT INTO user_settings (user_id, use_default_api, cv_gen_count) VALUES (?, 1, 1)")
|
| 469 |
+
return 1
|
| 470 |
+
current = int(rows[0]["cv_gen_count"] or 0)
|
| 471 |
+
_exec(conn, "UPDATE user_settings SET cv_gen_count = ? WHERE user_id = ?", (current + 1, user_id))
|
| 472 |
+
return current + 1
|
| 473 |
+
|
| 474 |
+
|
| 475 |
def get_api_keys(user_id: int) -> dict:
|
| 476 |
with get_db() as conn:
|
| 477 |
rows = _exec(conn, "SELECT provider, api_key FROM api_keys WHERE user_id = ?", (user_id,)).fetchall()
|
|
|
|
| 482 |
from backend.config import DEFAULT_NVIDIA_KEY
|
| 483 |
keys = get_api_keys(user_id)
|
| 484 |
settings = get_user_settings(user_id)
|
| 485 |
+
if settings.get("use_default_api", True) and DEFAULT_NVIDIA_KEY:
|
| 486 |
+
keys.setdefault("nvidia", DEFAULT_NVIDIA_KEY)
|
| 487 |
return keys
|
| 488 |
|
| 489 |
|
backend/llm.py
CHANGED
|
@@ -166,10 +166,16 @@ def _call_gemma(prompt: str, api_key: str, max_tokens: int = 512) -> str | None:
|
|
| 166 |
|
| 167 |
|
| 168 |
def _call_any(prompt: str, api_keys: dict, max_tokens: int = 512) -> str | None:
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
|
| 174 |
|
| 175 |
SKILL_TAXONOMY = {
|
|
|
|
| 166 |
|
| 167 |
|
| 168 |
def _call_any(prompt: str, api_keys: dict, max_tokens: int = 512) -> str | None:
|
| 169 |
+
if not any(api_keys.values()):
|
| 170 |
+
print("[llm] No API keys available — will use rule-based fallback")
|
| 171 |
+
result = _call_nim(prompt, api_keys.get("nvidia", ""), max_tokens)
|
| 172 |
+
if result: return result
|
| 173 |
+
result = _call_groq(prompt, api_keys.get("groq", ""), max_tokens)
|
| 174 |
+
if result: return result
|
| 175 |
+
result = _call_gemini(prompt, api_keys.get("gemini", ""), max_tokens)
|
| 176 |
+
if result: return result
|
| 177 |
+
result = _call_gemma(prompt, api_keys.get("gemini", ""), max_tokens)
|
| 178 |
+
return result
|
| 179 |
|
| 180 |
|
| 181 |
SKILL_TAXONOMY = {
|
backend/main.py
CHANGED
|
@@ -13,7 +13,7 @@ from backend.cron import run_nightly_scrape
|
|
| 13 |
from backend.database import (
|
| 14 |
init_db, create_user, get_user_by_email, get_user_by_id,
|
| 15 |
save_api_keys, get_api_keys, get_effective_api_keys,
|
| 16 |
-
save_user_settings, get_user_settings,
|
| 17 |
save_cv, get_cv, get_cv_default, save_cv_raw_text, get_cv_raw_text,
|
| 18 |
save_global_jobs, get_global_jobs, get_global_job, get_global_stats,
|
| 19 |
get_categories, classify_job_title,
|
|
@@ -27,20 +27,15 @@ from backend.cv_quality import score_cv_quality
|
|
| 27 |
from backend.docx_generator import generate_cv_docx, generate_cover_docx, generate_cv_pdf, generate_cover_pdf, generate_cv_preview_text, get_cv_profile
|
| 28 |
|
| 29 |
|
| 30 |
-
def _file_slug(name: str,
|
| 31 |
-
parts = [
|
| 32 |
if name:
|
| 33 |
parts.append(re.sub(r"[^a-zA-Z0-9]", "", name)[:20])
|
| 34 |
-
if title:
|
| 35 |
-
stitle = re.sub(r"[^a-zA-Z0-9 ]", "", title).strip()[:20]
|
| 36 |
-
if stitle:
|
| 37 |
-
words = stitle.split()
|
| 38 |
-
parts.append("".join(w.capitalize() for w in words[:3]))
|
| 39 |
if company:
|
| 40 |
scompany = re.sub(r"[^a-zA-Z0-9]", "", company)[:10]
|
| 41 |
if scompany:
|
| 42 |
parts.append(scompany)
|
| 43 |
-
return "_".join(parts) if parts else f"{
|
| 44 |
from backend.cv_diversity import randomize_tailored_cv
|
| 45 |
from backend.excel_export import export_jobs_to_excel
|
| 46 |
|
|
@@ -238,7 +233,8 @@ def make_cv(req: MakeCVRequest, current_user: dict = Depends(get_current_user)):
|
|
| 238 |
preview = generate_cv_preview_text(cv_data)
|
| 239 |
|
| 240 |
name = (cv_data.get("personal_info") or {}).get("name", "")
|
| 241 |
-
|
|
|
|
| 242 |
cv_path = str(GENERATED_DIR / f"{slug}_cv.docx")
|
| 243 |
cv_pdf_path = str(GENERATED_DIR / f"{slug}_cv.pdf")
|
| 244 |
profile = get_cv_profile(str(user_id))
|
|
@@ -659,8 +655,8 @@ def tailor_from_job_data(
|
|
| 659 |
tailored_cv = randomize_tailored_cv(tailored_cv, job.get("title", ""), seed=user_id)
|
| 660 |
|
| 661 |
name = (tailored_cv.get("personal_info") or {}).get("name", "")
|
| 662 |
-
|
| 663 |
-
slug = _file_slug(name, job.get("
|
| 664 |
cv_path = str(GENERATED_DIR / f"{slug}_cv.docx")
|
| 665 |
cover_path = str(GENERATED_DIR / f"{slug}_cover.docx")
|
| 666 |
cv_pdf_path = str(GENERATED_DIR / f"{slug}_cv.pdf")
|
|
@@ -673,7 +669,7 @@ def tailor_from_job_data(
|
|
| 673 |
|
| 674 |
return {
|
| 675 |
"status": "ok",
|
| 676 |
-
"match_score":
|
| 677 |
"provider": result.get("provider", "rule-based"),
|
| 678 |
"keywords_hit": result.get("keywords_hit", []),
|
| 679 |
"professional_summary": result.get("professional_summary", ""),
|
|
@@ -725,8 +721,8 @@ def tailor_job(job_id: int, current_user: dict = Depends(get_current_user)):
|
|
| 725 |
tailored_cv = randomize_tailored_cv(tailored_cv, job.get("title", ""), seed=user_id)
|
| 726 |
|
| 727 |
name = (tailored_cv.get("personal_info") or {}).get("name", "")
|
| 728 |
-
|
| 729 |
-
slug = _file_slug(name, job.get("
|
| 730 |
cv_path = str(GENERATED_DIR / f"{slug}_cv.docx")
|
| 731 |
cover_path = str(GENERATED_DIR / f"{slug}_cover.docx")
|
| 732 |
cv_pdf_path = str(GENERATED_DIR / f"{slug}_cv.pdf")
|
|
|
|
| 13 |
from backend.database import (
|
| 14 |
init_db, create_user, get_user_by_email, get_user_by_id,
|
| 15 |
save_api_keys, get_api_keys, get_effective_api_keys,
|
| 16 |
+
save_user_settings, get_user_settings, get_and_increment_cv_gen_count,
|
| 17 |
save_cv, get_cv, get_cv_default, save_cv_raw_text, get_cv_raw_text,
|
| 18 |
save_global_jobs, get_global_jobs, get_global_job, get_global_stats,
|
| 19 |
get_categories, classify_job_title,
|
|
|
|
| 27 |
from backend.docx_generator import generate_cv_docx, generate_cover_docx, generate_cv_pdf, generate_cover_pdf, generate_cv_preview_text, get_cv_profile
|
| 28 |
|
| 29 |
|
| 30 |
+
def _file_slug(name: str, company: str = "", counter: int = 0) -> str:
|
| 31 |
+
parts = [str(counter).zfill(2)]
|
| 32 |
if name:
|
| 33 |
parts.append(re.sub(r"[^a-zA-Z0-9]", "", name)[:20])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
if company:
|
| 35 |
scompany = re.sub(r"[^a-zA-Z0-9]", "", company)[:10]
|
| 36 |
if scompany:
|
| 37 |
parts.append(scompany)
|
| 38 |
+
return "_".join(parts) if parts else f"{str(counter).zfill(2)}_cv"
|
| 39 |
from backend.cv_diversity import randomize_tailored_cv
|
| 40 |
from backend.excel_export import export_jobs_to_excel
|
| 41 |
|
|
|
|
| 233 |
preview = generate_cv_preview_text(cv_data)
|
| 234 |
|
| 235 |
name = (cv_data.get("personal_info") or {}).get("name", "")
|
| 236 |
+
counter = get_and_increment_cv_gen_count(user_id)
|
| 237 |
+
slug = _file_slug(name, counter=counter)
|
| 238 |
cv_path = str(GENERATED_DIR / f"{slug}_cv.docx")
|
| 239 |
cv_pdf_path = str(GENERATED_DIR / f"{slug}_cv.pdf")
|
| 240 |
profile = get_cv_profile(str(user_id))
|
|
|
|
| 655 |
tailored_cv = randomize_tailored_cv(tailored_cv, job.get("title", ""), seed=user_id)
|
| 656 |
|
| 657 |
name = (tailored_cv.get("personal_info") or {}).get("name", "")
|
| 658 |
+
counter = get_and_increment_cv_gen_count(user_id)
|
| 659 |
+
slug = _file_slug(name, job.get("company", ""), counter=counter)
|
| 660 |
cv_path = str(GENERATED_DIR / f"{slug}_cv.docx")
|
| 661 |
cover_path = str(GENERATED_DIR / f"{slug}_cover.docx")
|
| 662 |
cv_pdf_path = str(GENERATED_DIR / f"{slug}_cv.pdf")
|
|
|
|
| 669 |
|
| 670 |
return {
|
| 671 |
"status": "ok",
|
| 672 |
+
"match_score": result.get("match_score", 0),
|
| 673 |
"provider": result.get("provider", "rule-based"),
|
| 674 |
"keywords_hit": result.get("keywords_hit", []),
|
| 675 |
"professional_summary": result.get("professional_summary", ""),
|
|
|
|
| 721 |
tailored_cv = randomize_tailored_cv(tailored_cv, job.get("title", ""), seed=user_id)
|
| 722 |
|
| 723 |
name = (tailored_cv.get("personal_info") or {}).get("name", "")
|
| 724 |
+
counter = get_and_increment_cv_gen_count(user_id)
|
| 725 |
+
slug = _file_slug(name, job.get("company", ""), counter=counter)
|
| 726 |
cv_path = str(GENERATED_DIR / f"{slug}_cv.docx")
|
| 727 |
cover_path = str(GENERATED_DIR / f"{slug}_cover.docx")
|
| 728 |
cv_pdf_path = str(GENERATED_DIR / f"{slug}_cv.pdf")
|