Spaces:
Running
Running
File size: 7,270 Bytes
60730db 227fa25 60730db 227fa25 7f90cd1 60730db 227fa25 60730db 227fa25 60730db 227fa25 60730db 227fa25 60730db 227fa25 60730db 227fa25 60730db 227fa25 60730db 227fa25 60730db 227fa25 60730db 227fa25 60730db 227fa25 60730db 227fa25 60730db 227fa25 60730db | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | """
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)
|