jobhunt-platform/apps/api/app/llm.py

196 lines
No EOL
7.1 KiB
Python

"""LLM gateway integration.
Per the task spec: LLM usage via packages.llm_gateway. Since T2 hasn't built
the gateway yet, this module provides a minimal mock-mode interface that the
API endpoints call. When packages.llm_gateway is available (importable), it
delegates to the real gateway. Otherwise, it returns deterministic canned
outputs per task name.
Task names used by the API:
- cv_assist: returns suggestions for a CV section bullet
- cl_critique: returns critique comments for a cover letter
"""
from __future__ import annotations
import hashlib
import json
import os
import time
from typing import Any
# Try to import the real llm_gateway package
try:
from packages.llm_gateway.client import run_task as _real_run_task # type: ignore
HAS_REAL_GATEWAY = True
except Exception:
HAS_REAL_GATEWAY = False
def _has_api_key() -> bool:
return bool(
os.environ.get("LLM_PRIMARY_KEY")
or os.environ.get("OLLAMA_API_KEY")
)
# Deterministic mock outputs per task name
MOCK_OUTPUTS: dict[str, dict[str, Any]] = {
"cv_assist": {
"suggestions": [
"Improved bullet: Led cross-functional team of 8 to deliver feature X 2 weeks ahead of schedule.",
"Alternative: Streamlined process reducing cycle time by 30% via automation.",
]
},
"cl_critique": {
"comments": [
{
"quote": "I am writing to apply",
"suggestion": "Consider a stronger opening that references the specific role.",
"severity": "medium",
},
{
"quote": "I have experience",
"suggestion": "Quantify with a specific achievement rather than a generic claim.",
"severity": "low",
},
]
},
"score": {
"score": 72,
"rationale": {
"match": 0.72,
"factors": {"skills": 0.8, "location": 0.6, "experience": 0.75},
},
"red_flags": [],
},
"cv_extract": {
"drafts": [
{
"kind": "experience",
"title": "Software Engineer",
"org": "Extracted Company",
"location": "Malmo",
"start_date": "2020-01",
"end_date": None,
"bullets": ["Developed web applications", "Led team of 3"],
"tags": ["python", "javascript"],
},
{
"kind": "education",
"title": "MSc Computer Science",
"org": "Lund University",
"location": "Lund",
"start_date": "2016-09",
"end_date": "2018-06",
"bullets": ["Specialized in distributed systems"],
"tags": ["algorithms", "distributed systems"],
},
]
},
"interview_prep": {
"content": (
"# Interview Prep\n\n"
"## Q1: Tell us about yourself\n"
"**Suggested angle:** Highlight your experience with Python and FastAPI, "
"and your ability to deliver features on time.\n\n"
"## Q2: Why are you interested in this role?\n"
"**Suggested angle:** Reference the specific technologies mentioned in the "
"posting and your experience with similar stacks.\n\n"
"## Q3: Describe a challenging project\n"
"**Suggested angle:** Use the STAR method. Reference your experience building "
"distributed systems at Lund University.\n\n"
"## Q4: How do you handle tight deadlines?\n"
"**Suggested angle:** Mention your track record of delivering 2 weeks ahead "
"of schedule and your automation-first approach.\n\n"
"## Q5: What are your salary expectations?\n"
"**Suggested angle:** Research market rates for the Skane region. "
"Be prepared to give a range.\n\n"
"## Q6: Tell us about a time you failed\n"
"**Suggested angle:** Pick something real but not catastrophic. Show what "
"you learned and how you changed your approach.\n\n"
"## Q7: How do you stay current with technology?\n"
"**Suggested angle:** Mention your tags: python, javascript, distributed "
"systems. Talk about hands-on side projects.\n\n"
"## Q8: Describe your ideal work environment\n"
"**Suggested angle:** Be honest but flexible. Mention collaboration and "
"autonomy.\n\n"
"## Q9: What questions do you have for us?\n"
"**Suggested angle:** Ask about team structure, current projects, and "
"growth opportunities.\n\n"
"## Q10: Why should we hire you?\n"
"**Suggested angle:** Summarize your top 3 qualifications matching the "
"posting requirements. Be specific."
),
},
"email_classify": {
"classification": "interview_invite",
"state_proposal": "interviewing",
"reason": "The email mentions an interview invitation.",
},
"cv_tailor": {
"tailored_cv": {
"summary": "Senior Python Developer with 6+ years building scalable backend systems.",
"skills": [
"Python",
"Fast API",
"PostgreSQL",
"Docker",
"Kubernetes",
"AWS",
],
"experience": [
{
"company": "TechCorp",
"role": "Senior Backend Engineer",
"bullets": [
"Led migration of monolith to microservices using Fast API",
"Reduced API latency by 40% through query optimization and caching",
],
},
],
},
"change_log": [
{"action": "reordered", "detail": "Moved Python and Fast API to top of skills"},
{"action": "rephrased", "detail": "Rewrote first experience bullet to emphasize Fast API"},
],
},
"deadline_extract": {
"apply_by": None,
},
}
def run_task(
task: str,
prompt: str,
schema: dict[str, Any] | None = None,
telemetry_sink: callable | None = None,
) -> dict[str, Any]:
"""Run an LLM task. Uses mock mode when no API key is present.
Returns a dict with the task result. If a telemetry_sink callable is
provided, it is called with a dict of token/cost info.
"""
if HAS_REAL_GATEWAY and _has_api_key():
return _real_run_task(task, prompt, schema)
# Mock mode
time.sleep(0.01) # simulate latency
result = MOCK_OUTPUTS.get(task, {"result": "mock"})
# Validate against schema if provided (basic check)
# In real gateway this would be jsonschema validation
if telemetry_sink is not None:
telemetry_sink({
"task": task,
"model": "mock",
"provider": "mock",
"input_tokens": len(prompt) // 4, # rough estimate
"output_tokens": len(json.dumps(result)) // 4,
"cost_usd": 0.0,
"duration_ms": 10,
})
return result