csob-matching / rag_engine.py
fihus's picture
Update rag_engine.py
227fa25 verified
"""
rag_engine.py – RAG Engine s Ollama (lokalni LLM) + JobBERT matching
AI Matching Assistant for Open Positions (CSOB)
Architektura:
1. Uzivatel zada dotaz + volitelne CV
2. Ollama syntetizuje "idealní cílový profil" z CV + dotazu
3. JobBERT zakoduje profil → 768d vektor
4. FAISS najde top-K pozic
5. Ollama vygeneruje pratelskou odpoved s doporucenimi
Ollama API: http://localhost:11434/api/generate
Model: llama3.2 (lokalne nainstalovany)
"""
import json
import re
import requests
import time
from dataclasses import dataclass
# =============================================================================
# Ollama klient
# =============================================================================
GROQ_URL = "https://api.groq.com/openai/v1/chat/completions"
# Větší model (70B) – lépe drží jazyk a instrukce. Na Groq zdarma, ~2s latence.
GROQ_MODEL = "llama-3.3-70b-versatile"
import os
GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "")
# System prompt vynucující češtinu – posílá se každému volání
CZECH_SYSTEM = (
"Jsi AI asistent pro český bankovní sektor (ČSOB). "
"KRITICKY: Odpovídáš VÝHRADNĚ v češtině. "
"Bez ohledu na jazyk vstupu (český, anglický, slovenský) VŽDY odpovíš ČESKY. "
"Nikdy nepoužívej angličtinu ani žádný jiný jazyk. "
"Technické termíny ponecháš v originálu jen pokud nemají přirozený český ekvivalent."
)
def check_llm_available() -> bool:
return bool(GROQ_API_KEY)
def llm_generate(
prompt: str,
temperature: float = 0.3,
max_tokens: int = 1024,
system: str = CZECH_SYSTEM,
) -> str:
"""Groq chat completion s volitelným system promptem."""
messages = []
if system:
messages.append({"role": "system", "content": system})
messages.append({"role": "user", "content": prompt})
try:
r = requests.post(
GROQ_URL,
headers={"Authorization": f"Bearer {GROQ_API_KEY}"},
json={
"model": GROQ_MODEL,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
},
timeout=60,
)
r.raise_for_status()
return r.json()["choices"][0]["message"]["content"].strip()
except Exception as e:
return f"[LLM error] {e}"
# =============================================================================
# Profil synteza (CV + dotaz → idealní profil pro embedding)
# =============================================================================
PROFILE_SYNTHESIS_PROMPT = """POVINNÉ: Celá odpověď bude POUZE v češtině. Žádná angličtina.
Tvým úkolem je vytvořit krátký „ideální cílový profil" zaměstnance na základě jeho dotazu a životopisu. Tento profil se použije k vyhledání vhodných pozic v česky psaném korpusu ČSOB, proto **musí** být česky.
DOTAZ ZAMĚSTNANCE:
{query}
{cv_section}
INSTRUKCE:
Vytvoř český profil (3–5 vět, max 200 slov), který shrnuje:
1. Jakou roli/pozici zaměstnanec hledá – používej české názvy pozic (např. „analytik dat", nikoli „data analyst")
2. Klíčové dovednosti a zkušenosti – v češtině
3. V jakém oboru/směru chce pokračovat – v češtině
Technické termíny (Python, SQL, Power BI) ponech v originálu. Vše ostatní musí být česky.
PROFIL (česky, bez jakéhokoliv anglického slova mimo technologie):"""
def synthesize_profile(query: str, cv_text: str = "",
use_ollama: bool = True) -> str:
"""
Syntetizuj "idealní cílový profil" z dotazu + CV.
Pokud Ollama neni dostupna, vrati primo dotaz + CV shrnutí.
"""
if not use_ollama or not check_llm_available():
# Fallback: primo spojit dotaz + CV extract
parts = [query]
if cv_text:
# Zkratit CV na klicove dovednosti
parts.append(cv_text[:500])
return " | ".join(parts)
cv_section = ""
if cv_text:
cv_section = f"ŽIVOTOPIS ZAMĚSTNANCE:\n{cv_text[:1500]}"
prompt = PROFILE_SYNTHESIS_PROMPT.format(
query=query,
cv_section=cv_section,
)
return llm_generate(prompt, temperature=0.1, max_tokens=400, system=CZECH_SYSTEM)
# =============================================================================
# Generovani odpovedi (pozice → přátelská HR odpověď)
# =============================================================================
RESPONSE_GENERATION_PROMPT = """POVINNÉ: Celá odpověď bude POUZE v češtině. Žádná angličtina.
Jsi přátelský HR asistent banky ČSOB. Zaměstnanec hledá nové kariérní příležitosti uvnitř firmy.
PROFIL ZAMĚSTNANCE:
{profile}
NALEZENÉ POZICE (seřazené podle relevance):
{positions_text}
INSTRUKCE:
1. Odpověz zaměstnanci česky, přátelsky a povzbudivě (použij tykání)
2. Doporuč mu top 3–5 pozic z nalezených (s vysvětlením PROČ se na ně hodí)
3. U každé pozice uveď: český název pozice (tak, jak je v seznamu), skóre shody, krátké zdůvodnění
4. Pokud zaměstnanec zmiňuje změnu oboru, povzbuď ho a vysvětli přenositelné dovednosti
5. Pokud je k dispozici URL, uveď ho jako odkaz
6. Maximální délka: 300 slov
ODPOVĚĎ (česky):"""
def generate_response(profile: str, positions: list[dict],
use_ollama: bool = True) -> str:
"""
Vygeneruj přátelskou HR odpověď na základě profilu a nalezených pozic.
Pokud Ollama neni dostupna, vrati strukturovany text bez LLM.
"""
# Formatovani pozic
pos_lines = []
for i, pos in enumerate(positions[:10], 1):
score = pos.get("score", 0)
title = pos.get("title", "?")
desc = pos.get("description", "")[:200]
url = pos.get("url", "")
line = f"{i}. {title} (shoda: {score:.0%})"
if desc:
line += f"\n Popis: {desc}..."
if url:
line += f"\n URL: {url}"
pos_lines.append(line)
positions_text = "\n".join(pos_lines) if pos_lines else "Žádné pozice nenalezeny."
if not use_ollama or not check_llm_available():
# Fallback: strukturovany text bez LLM
return _fallback_response(profile, positions)
prompt = RESPONSE_GENERATION_PROMPT.format(
profile=profile,
positions_text=positions_text,
)
return llm_generate(prompt, temperature=0.3, max_tokens=800, system=CZECH_SYSTEM)
def _fallback_response(profile: str, positions: list[dict]) -> str:
"""Fallback odpoved kdyz Ollama neni dostupna."""
lines = ["**Nalezene pozice pro vas profil:**\n"]
for i, pos in enumerate(positions[:5], 1):
score = pos.get("score", 0)
title = pos.get("title", "?")
desc = pos.get("description", "")[:150]
url = pos.get("url", "")
lines.append(f"### {i}. {title}")
lines.append(f"**Shoda:** {score:.0%}")
if desc:
lines.append(f"{desc}...")
if url:
lines.append(f"[Odkaz na pozici]({url})")
lines.append("")
if not positions:
lines.append("Bohužel jsem nenašel žádné vhodné pozice. Zkuste upřesnit váš dotaz.")
return "\n".join(lines)