Files
srbhr--resume-matcher/apps/backend/app/services/interview_prep.py
T
wehub-resource-sync 5bdf4cc89a
Publish Docker Image / publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:36 +08:00

129 lines
3.9 KiB
Python

"""Interview preparation generation service."""
import json
from typing import Any
from app.llm import (
complete_json,
get_llm_config,
get_model_name,
get_safe_max_tokens,
)
from app.prompts import INTERVIEW_PREP_PROMPT, get_language_name
from app.schemas import InterviewPrepData
_JOB_DESCRIPTION_PROMPT_CHAR_LIMIT = 12_000
_RESUME_DATA_PROMPT_CHAR_LIMIT = 30_000
_TRUNCATION_NOTICE = (
"[Content truncated for prompt length. Use only the visible evidence; "
"do not infer or invent omitted details.]"
)
def _truncate_text_for_prompt(value: str, max_chars: int) -> str:
"""Bound unstructured prompt input while making omissions explicit."""
if len(value) <= max_chars:
return value
return f"{value[:max_chars].rstrip()}\n\n{_TRUNCATION_NOTICE}"
def _truncate_json_value(
value: Any,
*,
max_string_chars: int,
max_list_items: int,
) -> Any:
if isinstance(value, str):
return _truncate_text_for_prompt(value, max_string_chars)
if isinstance(value, list):
truncated = [
_truncate_json_value(
item,
max_string_chars=max_string_chars,
max_list_items=max_list_items,
)
for item in value[:max_list_items]
]
if len(value) > max_list_items:
truncated.append(
{
"_prompt_truncation_notice": (
f"{len(value) - max_list_items} additional items omitted. "
"Do not infer omitted details."
)
}
)
return truncated
if isinstance(value, dict):
return {
key: _truncate_json_value(
item,
max_string_chars=max_string_chars,
max_list_items=max_list_items,
)
for key, item in value.items()
}
return value
def _serialize_resume_data_for_prompt(resume_data: dict[str, Any]) -> str:
resume_json = json.dumps(resume_data, ensure_ascii=False)
if len(resume_json) <= _RESUME_DATA_PROMPT_CHAR_LIMIT:
return resume_json
for max_string_chars, max_list_items in ((2_000, 30), (1_000, 20), (500, 10)):
bounded = _truncate_json_value(
resume_data,
max_string_chars=max_string_chars,
max_list_items=max_list_items,
)
bounded_json = json.dumps(bounded, ensure_ascii=False)
if len(bounded_json) <= _RESUME_DATA_PROMPT_CHAR_LIMIT:
return bounded_json
compact_snapshot = json.dumps(
_truncate_json_value(resume_data, max_string_chars=250, max_list_items=5),
ensure_ascii=False,
)
return json.dumps(
{
"_prompt_truncation_notice": _TRUNCATION_NOTICE,
"limited_resume_snapshot": _truncate_text_for_prompt(
compact_snapshot,
_RESUME_DATA_PROMPT_CHAR_LIMIT - 500,
),
},
ensure_ascii=False,
)
async def generate_interview_prep(
resume_data: dict[str, Any],
job_description: str,
language: str = "en",
) -> InterviewPrepData:
"""Generate structured interview preparation for a tailored resume."""
prompt = INTERVIEW_PREP_PROMPT.format(
job_description=_truncate_text_for_prompt(
job_description,
_JOB_DESCRIPTION_PROMPT_CHAR_LIMIT,
),
resume_data=_serialize_resume_data_for_prompt(resume_data),
output_language=get_language_name(language),
)
config = get_llm_config()
max_tokens = get_safe_max_tokens(get_model_name(config), requested=8192)
result = await complete_json(
prompt=prompt,
system_prompt=(
"You are a career interview coach. Output truthful, resume-grounded "
"interview preparation as JSON only."
),
max_tokens=max_tokens,
schema_type="interview_prep",
)
return InterviewPrepData.model_validate(result)