166 lines
No EOL
6 KiB
Python
166 lines
No EOL
6 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.",
|
|
},
|
|
}
|
|
|
|
|
|
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 |