""" 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)