129 lines
3.9 KiB
Python
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)
|