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)