615 lines
23 KiB
Python
615 lines
23 KiB
Python
"""AI service for CV parsing, matching, and generation using GLM-5.2 via Ollama Cloud."""
|
|
import json
|
|
import re
|
|
from datetime import datetime, date
|
|
from dateutil.relativedelta import relativedelta
|
|
from openai import OpenAI
|
|
from config import get_settings
|
|
|
|
settings = get_settings()
|
|
|
|
# LLM client
|
|
_client = None
|
|
|
|
def get_client():
|
|
global _client
|
|
if _client is None:
|
|
_client = OpenAI(
|
|
api_key=settings.llm_api_key,
|
|
base_url=settings.llm_base_url,
|
|
)
|
|
return _client
|
|
|
|
|
|
def llm_chat(messages, temperature=0.3, max_tokens=4000, response_format=None):
|
|
"""Call the LLM with messages and return the response text."""
|
|
client = get_client()
|
|
kwargs = {
|
|
"model": settings.llm_model,
|
|
"messages": messages,
|
|
"temperature": temperature,
|
|
"max_tokens": max_tokens,
|
|
}
|
|
if response_format:
|
|
kwargs["response_format"] = response_format
|
|
resp = client.chat.completions.create(**kwargs)
|
|
return resp.choices[0].message.content
|
|
|
|
|
|
def extract_json(text):
|
|
"""Extract JSON from LLM response, handling markdown code blocks, truncation, and common LLM JSON errors."""
|
|
json_str = text.strip()
|
|
if json_str.startswith("```"):
|
|
json_str = re.sub(r'^```(?:json)?\s*', '', json_str)
|
|
json_str = re.sub(r'\s*```$', '', json_str)
|
|
|
|
# First attempt: parse as-is
|
|
try:
|
|
return json.loads(json_str)
|
|
except json.JSONDecodeError:
|
|
pass
|
|
|
|
# Second attempt: fix truncated JSON by closing open braces/brackets
|
|
try:
|
|
open_b = json_str.count('{')
|
|
close_b = json_str.count('}')
|
|
open_arr = json_str.count('[')
|
|
close_arr = json_str.count(']')
|
|
if open_b > close_b or open_arr > close_arr:
|
|
json_str = json_str.rstrip()
|
|
json_str = re.sub(r'[\s,]*"[^"]*":\s*"?[^",}\]]*$', '', json_str)
|
|
json_str += '}' * max(0, open_b - json_str.count('}'))
|
|
json_str += ']' * max(0, open_arr - json_str.count(']'))
|
|
return json.loads(json_str)
|
|
except json.JSONDecodeError:
|
|
pass
|
|
|
|
# Third attempt: fix common LLM JSON errors
|
|
# - Unescaped newlines inside string values
|
|
# - Single quotes instead of double quotes
|
|
# - Trailing commas before closing brackets
|
|
# - Unescaped quotes inside string values (the "Expecting ',' delimiter" error)
|
|
try:
|
|
fixed = json_str
|
|
# Remove trailing commas
|
|
fixed = re.sub(r',\s*([}\]])', r'\1', fixed)
|
|
# Fix unescaped newlines inside strings
|
|
fixed = fixed.replace('\\\n', '\\n')
|
|
return json.loads(fixed)
|
|
except json.JSONDecodeError:
|
|
pass
|
|
|
|
# Fourth attempt: line-by-line repair — find the error location and try to fix it
|
|
try:
|
|
return json.loads(json_str, strict=False)
|
|
except json.JSONDecodeError as e:
|
|
# Last resort: try to salvage by removing the problematic section
|
|
# and parsing what we can
|
|
lines = json_str.split('\n')
|
|
if e.lineno and e.lineno <= len(lines):
|
|
# Remove the problematic line and try again
|
|
repaired = '\n'.join(lines[:e.lineno-1] + lines[e.lineno:])
|
|
try:
|
|
return json.loads(repaired, strict=False)
|
|
except json.JSONDecodeError:
|
|
pass
|
|
|
|
# If all else fails, raise with context
|
|
error_line = lines[e.lineno-1] if e.lineno and e.lineno <= len(lines) else '?'
|
|
raise ValueError(f"JSON parse failed at line {e.lineno}: {e.msg}\nProblem line: {error_line[:200]}\nFull response length: {len(text)} chars")
|
|
|
|
|
|
# ============================================================
|
|
# CV PARSING
|
|
# ============================================================
|
|
|
|
PARSE_PROMPT = """You are a CV/resume parser. Analyze the following CV text and extract structured information.
|
|
|
|
Return ONLY a valid JSON object with this exact structure:
|
|
{
|
|
"first_name": "",
|
|
"last_name": "",
|
|
"email": "",
|
|
"phone": "",
|
|
"address": "",
|
|
"linkedin": "",
|
|
"github": "",
|
|
"website": "",
|
|
"summary": "",
|
|
"skills": [
|
|
{
|
|
"skill_name": "",
|
|
"skill_category": "Programming|Database|Cloud|DevOps|Tools|Framework|Language|Soft Skill|Other",
|
|
"proficiency": "Expert|Advanced|Intermediate|Beginner",
|
|
"start_date": "YYYY-MM-DD or YYYY-01-01 if only year is known",
|
|
"end_date": "YYYY-MM-DD or null if still active/present"
|
|
}
|
|
],
|
|
"experience": [
|
|
{
|
|
"company": "",
|
|
"position": "",
|
|
"location": "",
|
|
"start_date": "YYYY-MM-DD or null",
|
|
"end_date": "YYYY-MM-DD or null if current",
|
|
"description": "",
|
|
"achievements": ["bullet point 1", "bullet point 2"],
|
|
"skills_used": ["skill1", "skill2"]
|
|
}
|
|
],
|
|
"education": [
|
|
{
|
|
"institution": "",
|
|
"degree": "",
|
|
"field_of_study": "",
|
|
"start_date": "YYYY-MM-DD or null",
|
|
"end_date": "YYYY-MM-DD or null",
|
|
"grade": "",
|
|
"description": ""
|
|
}
|
|
],
|
|
"certifications": [
|
|
{
|
|
"name": "",
|
|
"issuer": "",
|
|
"issue_date": "YYYY-MM-DD or null",
|
|
"expiry_date": "YYYY-MM-DD or null",
|
|
"credential_id": ""
|
|
}
|
|
]
|
|
}
|
|
|
|
Rules:
|
|
- Extract dates as accurately as possible. If only a year is mentioned, use YYYY-01-01.
|
|
- If a skill or job is described as "present" or "current", set end_date to null.
|
|
- For skills, infer the start_date from when they first appear in experience/education.
|
|
- Be thorough — extract ALL skills, experience entries, education, and certifications.
|
|
- If a field is not found, use empty string "" or null as appropriate.
|
|
- Return ONLY the JSON, no other text.
|
|
|
|
CV Text to parse:
|
|
---
|
|
__CV_TEXT__
|
|
---"""
|
|
|
|
def parse_cv(cv_text: str) -> dict:
|
|
"""Parse a CV's raw text into structured data using AI."""
|
|
max_chars = 12000
|
|
if len(cv_text) > max_chars:
|
|
cv_text = cv_text[:max_chars] + "\n[...truncated...]"
|
|
|
|
prompt = PARSE_PROMPT.replace("__CV_TEXT__", cv_text)
|
|
messages = [
|
|
{"role": "system", "content": "You are a precise CV parser that outputs only valid JSON. Make sure all string values are properly escaped (no unescaped quotes, no unescaped newlines)."},
|
|
{"role": "user", "content": prompt}
|
|
]
|
|
|
|
# Retry with increasing max_tokens in case of truncation
|
|
for max_tokens in [4000, 6000, 8000]:
|
|
try:
|
|
resp = llm_chat(messages, temperature=0.1, max_tokens=max_tokens)
|
|
return extract_json(resp)
|
|
except (ValueError, json.JSONDecodeError) as e:
|
|
if max_tokens == 8000:
|
|
raise
|
|
# Try again with more tokens
|
|
continue
|
|
|
|
|
|
# ============================================================
|
|
# DYNAMIC EXPERIENCE CALCULATION
|
|
# ============================================================
|
|
|
|
def calculate_years_experience(start_date, end_date, reference_date=None):
|
|
"""Calculate years of experience dynamically based on reference date.
|
|
|
|
If end_date is None (still active), use reference_date (or today) as the end.
|
|
Returns a float representing years of experience.
|
|
"""
|
|
if reference_date is None:
|
|
reference_date = date.today()
|
|
elif isinstance(reference_date, str):
|
|
reference_date = datetime.strptime(reference_date, "%Y-%m-%d").date()
|
|
|
|
if isinstance(start_date, str):
|
|
start_date = datetime.strptime(start_date, "%Y-%m-%d").date()
|
|
|
|
if end_date is None:
|
|
end_date = reference_date
|
|
elif isinstance(end_date, str):
|
|
end_date = datetime.strptime(end_date, "%Y-%m-%d").date()
|
|
|
|
# Cap end_date at reference_date (don't count future time)
|
|
if end_date > reference_date:
|
|
end_date = reference_date
|
|
|
|
if start_date > end_date:
|
|
return 0.0
|
|
|
|
delta = relativedelta(end_date, start_date)
|
|
years = delta.years + delta.months / 12.0
|
|
return round(years, 1)
|
|
|
|
|
|
def calculate_skill_years(skill_start, skill_end, reference_date=None):
|
|
"""Calculate years for a specific skill."""
|
|
return calculate_years_experience(skill_start, skill_end, reference_date)
|
|
|
|
|
|
# ============================================================
|
|
# CV MATCHING
|
|
# ============================================================
|
|
|
|
MATCH_PROMPT = """You are a recruitment AI that matches candidates to job requirements.
|
|
|
|
Given a candidate's structured CV data and a list of position requirements, determine how well
|
|
the candidate matches each position.
|
|
|
|
Return ONLY a valid JSON array:
|
|
[
|
|
{
|
|
"position_title": "",
|
|
"match_score": 0-100,
|
|
"matching_skills": ["skill1", "skill2"],
|
|
"missing_skills": ["skill3"],
|
|
"experience_analysis": "Brief analysis of experience relevance",
|
|
"recommendation": "strong_match|moderate_match|weak_match|no_match",
|
|
"reasoning": "Detailed explanation"
|
|
}
|
|
]
|
|
|
|
Calculate experience years dynamically as of __REFERENCE_DATE__.
|
|
|
|
Candidate CV Data (JSON):
|
|
__CANDIDATE_DATA__
|
|
|
|
Position Requirements (JSON):
|
|
__REQUIREMENTS__
|
|
|
|
Return ONLY the JSON array, no other text."""
|
|
|
|
def match_candidate_to_requirements(candidate_data: dict, requirements: list, reference_date: date = None) -> list:
|
|
"""Match a candidate against a list of position requirements."""
|
|
ref_str = (reference_date or date.today()).isoformat()
|
|
|
|
prompt = (MATCH_PROMPT
|
|
.replace("__REFERENCE_DATE__", ref_str)
|
|
.replace("__CANDIDATE_DATA__", json.dumps(candidate_data, default=str))
|
|
.replace("__REQUIREMENTS__", json.dumps(requirements, default=str)))
|
|
|
|
messages = [
|
|
{"role": "system", "content": "You are a precise recruitment matching AI that outputs only valid JSON."},
|
|
{"role": "user", "content": prompt}
|
|
]
|
|
resp = llm_chat(messages, temperature=0.2, max_tokens=3000)
|
|
return extract_json(resp)
|
|
|
|
|
|
# ============================================================
|
|
# CV GENERATION (regenerate CV aligned to requirement)
|
|
# ============================================================
|
|
|
|
GENERATE_PROMPT = """You are an expert CV writer who regenerates CVs to align with specific job requirements.
|
|
|
|
IMPORTANT RULES:
|
|
1. DO NOT invent or fabricate skills, experience, or qualifications the candidate does not have.
|
|
2. DO reorder and emphasize relevant experience that matches the requirement.
|
|
3. DO rephrase descriptions to highlight relevant aspects without lying.
|
|
4. DO use dynamic experience calculations based on the generation date: __GENERATION_DATE__
|
|
5. DO format the CV according to the template structure provided.
|
|
6. DO ensure all dates and durations are accurate and dynamically calculated.
|
|
7. DO NOT change job titles to something the candidate didn't do. You can rephrase but not invent.
|
|
|
|
For each skill, calculate years of experience dynamically:
|
|
- If a skill started in 2020-01-01 and is still active (end_date=null),
|
|
and the generation date is __GENERATION_DATE__, the experience is calculated from start to generation date.
|
|
- If a skill has both start and end dates, calculate the duration between them.
|
|
|
|
Template Structure:
|
|
__TEMPLATE_STRUCTURE__
|
|
|
|
Candidate Data (with full CV):
|
|
__CANDIDATE_DATA__
|
|
|
|
Target Position Requirement:
|
|
__REQUIREMENT__
|
|
|
|
Generate the CV content. Return a JSON object:
|
|
{
|
|
"generated_content": "The full formatted CV as HTML or structured text",
|
|
"generated_data": {
|
|
"skills_with_years": [
|
|
{
|
|
"skill_name": "",
|
|
"years": 0.0,
|
|
"start_date": "",
|
|
"end_date": "",
|
|
"calculated_as_of": "__GENERATION_DATE__"
|
|
}
|
|
],
|
|
"experience_highlights": ["relevant achievement 1", "relevant achievement 2"],
|
|
"summary_aligned": "A summary paragraph aligned to the requirement"
|
|
},
|
|
"changes_made": "List of what was changed/reordered/emphasized (for transparency)"
|
|
}
|
|
|
|
Return ONLY the JSON, no other text."""
|
|
|
|
def generate_aligned_cv(
|
|
candidate_data: dict,
|
|
requirement: dict,
|
|
template_structure: dict,
|
|
generation_date: date = None
|
|
) -> dict:
|
|
"""Generate a CV aligned to a specific requirement."""
|
|
gen_date_str = (generation_date or date.today()).isoformat()
|
|
|
|
prompt = (GENERATE_PROMPT
|
|
.replace("__GENERATION_DATE__", gen_date_str)
|
|
.replace("__TEMPLATE_STRUCTURE__", json.dumps(template_structure, default=str))
|
|
.replace("__CANDIDATE_DATA__", json.dumps(candidate_data, default=str))
|
|
.replace("__REQUIREMENT__", json.dumps(requirement, default=str)))
|
|
|
|
messages = [
|
|
{"role": "system", "content": "You are an expert CV writer that outputs only valid JSON."},
|
|
{"role": "user", "content": prompt}
|
|
]
|
|
resp = llm_chat(messages, temperature=0.4, max_tokens=4000)
|
|
return extract_json(resp)
|
|
|
|
|
|
# ============================================================
|
|
# TEMPLATE GENERATION
|
|
# ============================================================
|
|
|
|
TEMPLATE_GEN_PROMPT = """You are a CV template designer. Based on the user's description, create a CV template structure.
|
|
|
|
The template structure defines the sections and layout of a CV. Return a JSON object:
|
|
{
|
|
"sections": [
|
|
{
|
|
"name": "Section Name",
|
|
"type": "header|summary|experience|education|skills|certifications|projects|custom",
|
|
"order": 1,
|
|
"fields": ["field1", "field2"],
|
|
"styling": {
|
|
"show_years": true,
|
|
"group_by_category": true,
|
|
"show_proficiency": false,
|
|
"max_items": null
|
|
}
|
|
}
|
|
],
|
|
"styling": "CSS string for the template, dark or light theme"
|
|
}
|
|
|
|
User description of desired template:
|
|
__DESCRIPTION__
|
|
|
|
If the user wants a specific industry or role focus, tailor the template accordingly.
|
|
Return ONLY the JSON, no other text."""
|
|
|
|
def generate_template(description: str) -> dict:
|
|
"""Generate a CV template from a text description using AI."""
|
|
prompt = TEMPLATE_GEN_PROMPT.replace("__DESCRIPTION__", description)
|
|
messages = [
|
|
{"role": "system", "content": "You are a CV template designer that outputs only valid JSON."},
|
|
{"role": "user", "content": prompt}
|
|
]
|
|
|
|
# Retry with increasing max_tokens if JSON parsing fails
|
|
for attempt, max_tok in enumerate([2000, 3000, 4000]):
|
|
try:
|
|
resp = llm_chat(messages, temperature=0.5, max_tokens=max_tok)
|
|
return extract_json(resp)
|
|
except json.JSONDecodeError as e:
|
|
if attempt < 2:
|
|
# Retry with more tokens
|
|
continue
|
|
# Last attempt failed — try to salvage partial JSON
|
|
try:
|
|
# Attempt to fix truncated JSON by closing braces
|
|
json_str = resp.strip()
|
|
if json_str.startswith("```"):
|
|
json_str = re.sub(r'^```(?:json)?\s*', '', json_str)
|
|
json_str = re.sub(r'\s*```$', '', json_str)
|
|
# Count open vs close braces
|
|
open_b = json_str.count('{')
|
|
close_b = json_str.count('}')
|
|
open_arr = json_str.count('[')
|
|
close_arr = json_str.count(']')
|
|
# Append missing closers
|
|
json_str += '}' * (open_b - close_b)
|
|
json_str += ']' * (open_arr - close_arr)
|
|
return json.loads(json_str)
|
|
except:
|
|
raise ValueError(f"AI returned invalid JSON (truncated at {max_tok} tokens). Please try again with a shorter description.")
|
|
|
|
|
|
# ============================================================
|
|
# CHAT (general AI interaction)
|
|
# ============================================================
|
|
|
|
SYSTEM_PROMPT = """You are an AI assistant for a CV/Candidate management system. You help users with:
|
|
1. Analyzing CVs in the database
|
|
2. Matching candidates to job requirements
|
|
3. Creating and modifying CV templates
|
|
4. Generating tailored CVs for specific positions
|
|
5. General questions about the candidate database
|
|
|
|
You have access to the system's database. When the user asks about candidates or requirements,
|
|
use the provided context to answer accurately.
|
|
|
|
Be concise and direct. When suggesting actions, format them clearly."""
|
|
|
|
def chat(user_message: str, conversation_history: list = None, context: str = "") -> str:
|
|
"""General AI chat with optional context."""
|
|
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
|
|
|
|
if context:
|
|
messages.append({"role": "system", "content": f"Context:\n{context}"})
|
|
|
|
if conversation_history:
|
|
for msg in conversation_history[-10:]: # last 10 messages
|
|
messages.append({"role": msg["role"], "content": msg["content"]})
|
|
|
|
messages.append({"role": "user", "content": user_message})
|
|
|
|
return llm_chat(messages, temperature=0.5, max_tokens=2000)
|
|
|
|
|
|
# ============================================================
|
|
# BATCH FUNCTIONS — extract requirements, match candidates, batch chat
|
|
# ============================================================
|
|
|
|
EXTRACT_REQUIREMENTS_PROMPT = """You are a requirements analyst. Analyze the following document and extract the job positions and their requirements.
|
|
|
|
Return ONLY a valid JSON object with this structure:
|
|
{
|
|
"batch_name": "Short name for this batch (from the document title or first heading)",
|
|
"description": "Brief description of what this batch is for (1-2 sentences)",
|
|
"positions": [
|
|
{
|
|
"job_title": "Position title",
|
|
"num_positions": 1,
|
|
"required_skills": ["skill1", "skill2"],
|
|
"required_years": 5,
|
|
"required_certs": ["cert1"],
|
|
"nice_to_have": ["skill3"],
|
|
"disqualifiers": ["something that would disqualify a candidate"],
|
|
"description": "Brief description of the role"
|
|
}
|
|
]
|
|
}
|
|
|
|
Rules:
|
|
- Extract ALL positions mentioned in the document.
|
|
- If a field is not specified, use empty string "" or empty array [].
|
|
- Be precise about required skills — distinguish must-haves from nice-to-haves.
|
|
- If no positions are found, return an empty positions array.
|
|
- Return ONLY the JSON, no other text.
|
|
|
|
Document text:
|
|
---
|
|
__DOC_TEXT__
|
|
---"""
|
|
|
|
def extract_requirements(doc_text: str) -> dict:
|
|
"""Extract job requirements from a document using AI."""
|
|
max_chars = 15000
|
|
if len(doc_text) > max_chars:
|
|
doc_text = doc_text[:max_chars] + "\n[...truncated...]"
|
|
|
|
prompt = EXTRACT_REQUIREMENTS_PROMPT.replace("__DOC_TEXT__", doc_text)
|
|
messages = [
|
|
{"role": "system", "content": "You are a requirements analyst that outputs only valid JSON. Make sure all string values are properly escaped."},
|
|
{"role": "user", "content": prompt}
|
|
]
|
|
|
|
for max_tokens in [4000, 6000, 8000]:
|
|
try:
|
|
resp = llm_chat(messages, temperature=0.1, max_tokens=max_tokens)
|
|
return extract_json(resp)
|
|
except (ValueError, json.JSONDecodeError):
|
|
if max_tokens == 8000:
|
|
raise
|
|
continue
|
|
|
|
|
|
MATCH_PROMPT = """You are a CV matching specialist. You are given a job position's requirements and a list of candidates with their skills, experience, education, and certifications.
|
|
|
|
For each candidate, determine:
|
|
1. A match score (0-100) based on how well they meet the requirements
|
|
2. Whether they are a good fit (score >= 60)
|
|
3. A brief reasoning for the match or mismatch
|
|
4. If they are a close match (score 50-79), suggest how their CV could be realigned (reworded) to better fit — without fabricating skills they don't have
|
|
|
|
Return ONLY a valid JSON array of matching candidates:
|
|
[
|
|
{
|
|
"candidate_id": "the UUID",
|
|
"candidate_name": "First Last",
|
|
"match_score": 85,
|
|
"fit": true,
|
|
"reasoning": "Strong match: has 7 years Python, AWS cert, Kubernetes experience",
|
|
"realignment_suggestion": "Emphasize microservices architecture experience over generic 'distributed systems' wording; reorder skills to put AWS and Kubernetes first"
|
|
}
|
|
]
|
|
|
|
Only include candidates with match_score >= 40. Sort by match_score descending.
|
|
Return ONLY the JSON array, no other text.
|
|
|
|
Position Requirements:
|
|
__POSITION__
|
|
|
|
Candidates:
|
|
__CANDIDATES__"""
|
|
|
|
def match_candidates_for_position(position: dict, candidates: list) -> list:
|
|
"""Match candidates against a single position's requirements using AI."""
|
|
# Build a compact summary of each candidate for the LLM
|
|
cand_summaries = []
|
|
for c in candidates:
|
|
skills = ", ".join([s.get("skill_name", "") for s in c.get("skills", [])[:15]])
|
|
exp = "; ".join([f"{e.get('position','')} at {e.get('company','')}" for e in c.get("experience", [])[:5]])
|
|
certs = ", ".join([cert.get("name", "") for cert in c.get("certifications", [])[:5]])
|
|
edu = "; ".join([f"{e.get('degree','')} {e.get('field_of_study','')} at {e.get('institution','')}" for e in c.get("education", [])[:3]])
|
|
|
|
cand_summaries.append(f"""Candidate ID: {c.get('id', 'N/A')}
|
|
Name: {c.get('first_name', '')} {c.get('last_name', '')}
|
|
Skills: {skills}
|
|
Experience: {exp}
|
|
Certifications: {certs}
|
|
Education: {edu}
|
|
Summary: {c.get('summary', '')[:200]}""")
|
|
|
|
position_json = json.dumps(position, indent=2)
|
|
candidates_text = "\n\n---\n\n".join(cand_summaries)
|
|
|
|
prompt = MATCH_PROMPT.replace("__POSITION__", position_json).replace("__CANDIDATES__", candidates_text)
|
|
messages = [
|
|
{"role": "system", "content": "You are a CV matching specialist that outputs only valid JSON. Make sure all string values are properly escaped."},
|
|
{"role": "user", "content": prompt}
|
|
]
|
|
|
|
for max_tokens in [4000, 6000, 8000]:
|
|
try:
|
|
resp = llm_chat(messages, temperature=0.1, max_tokens=max_tokens)
|
|
result = extract_json(resp)
|
|
if isinstance(result, list):
|
|
return result
|
|
elif isinstance(result, dict) and 'candidates' in result:
|
|
return result['candidates']
|
|
return [result]
|
|
except (ValueError, json.JSONDecodeError):
|
|
if max_tokens == 8000:
|
|
raise
|
|
continue
|
|
return []
|
|
|
|
|
|
BATCH_CHAT_SYSTEM = """You are an AI assistant helping a user manage a CV batch. You have context about the batch (name, description, positions, and matched candidates).
|
|
|
|
You can help the user with:
|
|
1. Discussing which candidates are best for specific positions
|
|
2. Suggesting CV realignment — rewording a candidate's experience to better match a position (without fabricating)
|
|
3. Answering questions about candidate qualifications
|
|
4. Recommending which candidates to approve or remove
|
|
|
|
Be concise and direct. When suggesting changes, be specific about what to change and why.
|
|
|
|
Batch context:
|
|
__BATCH_CONTEXT__"""
|
|
|
|
def batch_chat(user_message: str, batch_context: str, conversation_history: list = None) -> str:
|
|
"""Context-aware chat for a specific batch."""
|
|
system_content = BATCH_CHAT_SYSTEM.replace("__BATCH_CONTEXT__", batch_context)
|
|
messages = [{"role": "system", "content": system_content}]
|
|
|
|
if conversation_history:
|
|
for msg in conversation_history[-10:]:
|
|
messages.append({"role": msg["role"], "content": msg["content"]})
|
|
|
|
messages.append({"role": "user", "content": user_message})
|
|
|
|
return llm_chat(messages, temperature=0.5, max_tokens=3000) |