Spaces:
Running
Running
| """ | |
| 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) | |