Jacek Zadrożny
commited on
Commit
·
4e4c288
1
Parent(s):
1304839
Pełny dostęp do PDF i stron PFRON, embedowanie wsadowe i kilka innych zmian.
Browse files- add_urls_to_db.py +103 -0
- app.py +0 -280
- bibliografia.csv +12 -0
- chatbot.py +37 -4
- database.py +48 -0
- hr_assistant.py +500 -113
- pdfs/Fundacja-Aktywizacja-Publikacja-Komunikacja-bez-barier.pdf +3 -0
- pdfs/PBB_HR_Podręcznik_Kompendium_wiedzy_na_temat_zatrudnienia_osób_ze_szczególnymi_potrzebami.pdf +3 -0
- pdfs/podrecznik-online.pdf +3 -0
- pdfs/todo.md +5 -0
- urls.txt +20 -0
add_urls_to_db.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
from bs4 import BeautifulSoup
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
import database
|
| 5 |
+
import os
|
| 6 |
+
|
| 7 |
+
def extract_main_content(soup: BeautifulSoup) -> str:
|
| 8 |
+
"""
|
| 9 |
+
Próbuje wyodrębnić główną treść artykułu ze strony, testując kilka popularnych selektorów.
|
| 10 |
+
"""
|
| 11 |
+
# Lista potencjalnych selektorów dla głównej treści, od najbardziej specyficznego do ogólnego
|
| 12 |
+
selectors = [
|
| 13 |
+
".frame.default", # Sugestia użytkownika
|
| 14 |
+
"article",
|
| 15 |
+
"div[role='article']",
|
| 16 |
+
"main",
|
| 17 |
+
"div[role='main']",
|
| 18 |
+
".csc-textpic-text.article-content", # Poprzedni selektor
|
| 19 |
+
".article-content",
|
| 20 |
+
".post-content",
|
| 21 |
+
".entry-content"
|
| 22 |
+
]
|
| 23 |
+
|
| 24 |
+
best_content = ""
|
| 25 |
+
|
| 26 |
+
# Przetestuj selektory i znajdź ten, który daje najwięcej tekstu
|
| 27 |
+
for selector in selectors:
|
| 28 |
+
element = soup.select_one(selector)
|
| 29 |
+
if element:
|
| 30 |
+
current_content = element.get_text(separator='\n', strip=True)
|
| 31 |
+
if len(current_content) > len(best_content):
|
| 32 |
+
best_content = current_content
|
| 33 |
+
|
| 34 |
+
# Jeśli nic nie znaleziono, w ostateczności weź całe body
|
| 35 |
+
if not best_content and soup.body:
|
| 36 |
+
body_text = soup.body.get_text(separator='\n', strip=True)
|
| 37 |
+
lines = body_text.split('\n')
|
| 38 |
+
meaningful_lines = [line for line in lines if len(line.strip()) > 30]
|
| 39 |
+
best_content = "\n".join(meaningful_lines)
|
| 40 |
+
|
| 41 |
+
return best_content
|
| 42 |
+
|
| 43 |
+
def scrape_and_store_urls(file_path='urls.txt'):
|
| 44 |
+
"""
|
| 45 |
+
Scrapes content from URLs listed in a file and stores them in the database.
|
| 46 |
+
"""
|
| 47 |
+
print("--- Rozpoczęcie skryptu scrape_and_store_urls (wersja z selektorem .frame.default) ---")
|
| 48 |
+
|
| 49 |
+
if not os.path.exists(file_path):
|
| 50 |
+
print(f"BŁĄD KRYTYCZNY: Plik '{file_path}' nie został znaleziony.")
|
| 51 |
+
return
|
| 52 |
+
|
| 53 |
+
try:
|
| 54 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
| 55 |
+
urls = [line.strip() for line in f if line.strip()]
|
| 56 |
+
print(f"Znaleziono {len(urls)} adresów URL w pliku '{file_path}'.")
|
| 57 |
+
except Exception as e:
|
| 58 |
+
print(f"BŁĄD KRYTYCZNY: Nie udało się odczytać pliku '{file_path}': {e}")
|
| 59 |
+
return
|
| 60 |
+
|
| 61 |
+
print("\n--- Inicjalizacja bazy danych ---")
|
| 62 |
+
collection = database.get_collection()
|
| 63 |
+
print("Pomyślnie połączono z bazą danych FAISS.")
|
| 64 |
+
|
| 65 |
+
print("\n--- Rozpoczęcie przetwarzania adresów URL ---")
|
| 66 |
+
for i, url in enumerate(urls, 1):
|
| 67 |
+
print(f"\n[{i}/{len(urls)}] Przetwarzanie: {url}")
|
| 68 |
+
try:
|
| 69 |
+
response = requests.get(url, timeout=15, headers={'User-Agent': 'Mozilla/5.0'})
|
| 70 |
+
response.raise_for_status()
|
| 71 |
+
print(f" - Status odpowiedzi HTTP: {response.status_code} (OK)")
|
| 72 |
+
|
| 73 |
+
soup = BeautifulSoup(response.content, 'html.parser')
|
| 74 |
+
title = soup.find('title').get_text().strip() if soup.find('title') else 'Brak tytułu'
|
| 75 |
+
|
| 76 |
+
content = extract_main_content(soup)
|
| 77 |
+
|
| 78 |
+
if content:
|
| 79 |
+
print(f" - Znaleziono treść (rozmiar: {len(content)} znaków).")
|
| 80 |
+
else:
|
| 81 |
+
print(f" - OSTRZEŻENIE: Nie udało się wyodrębnić treści ze strony {url}. Pomijanie.")
|
| 82 |
+
continue
|
| 83 |
+
|
| 84 |
+
current_date = datetime.now().strftime("%Y-%m-%d")
|
| 85 |
+
metadata = {'source': url, 'title': title, 'added_date': current_date}
|
| 86 |
+
|
| 87 |
+
print(" - Próba dodania do bazy danych...")
|
| 88 |
+
collection.add(
|
| 89 |
+
documents=[content],
|
| 90 |
+
metadatas=[metadata],
|
| 91 |
+
ids=[f"url_{url}"]
|
| 92 |
+
)
|
| 93 |
+
print(" - SUKCES: Pomyślnie dodano dokument i zapisano bazę danych.")
|
| 94 |
+
|
| 95 |
+
except requests.RequestException as e:
|
| 96 |
+
print(f" - BŁĄD: Nie udało się pobrać strony {url}: {e}")
|
| 97 |
+
except Exception as e:
|
| 98 |
+
print(f" - BŁĄD: Wystąpił nieoczekiwany błąd podczas przetwarzania {url}: {e}")
|
| 99 |
+
|
| 100 |
+
print("\n--- Zakończono skrypt ---")
|
| 101 |
+
|
| 102 |
+
if __name__ == '__main__':
|
| 103 |
+
scrape_and_store_urls()
|
app.py
DELETED
|
@@ -1,280 +0,0 @@
|
|
| 1 |
-
import gradio as gr
|
| 2 |
-
import pandas as pd
|
| 3 |
-
from langchain_core.prompts import PromptTemplate
|
| 4 |
-
from langchain_openai import ChatOpenAI
|
| 5 |
-
from langchain_core.output_parsers import StrOutputParser
|
| 6 |
-
from pydantic import BaseModel, Field, field_validator
|
| 7 |
-
from langchain_community.document_loaders import PyPDFLoader, Docx2txtLoader
|
| 8 |
-
from langchain.output_parsers import PydanticOutputParser
|
| 9 |
-
from docx import Document
|
| 10 |
-
from datetime import datetime
|
| 11 |
-
import os
|
| 12 |
-
import tempfile
|
| 13 |
-
|
| 14 |
-
# Import modułu hr_assistant
|
| 15 |
-
try:
|
| 16 |
-
from hr_assistant import HRAssistant
|
| 17 |
-
HR_ASSISTANT_AVAILABLE = True
|
| 18 |
-
except ImportError:
|
| 19 |
-
HR_ASSISTANT_AVAILABLE = False
|
| 20 |
-
print("Uwaga: Moduł hr_assistant nie jest dostępny.")
|
| 21 |
-
|
| 22 |
-
# Globalna instancja asystenta HR
|
| 23 |
-
hr_assistant = None
|
| 24 |
-
|
| 25 |
-
def initialize_hr_assistant():
|
| 26 |
-
"""Inicjalizuje asystenta HR"""
|
| 27 |
-
global hr_assistant, HR_ASSISTANT_AVAILABLE
|
| 28 |
-
|
| 29 |
-
if not HR_ASSISTANT_AVAILABLE:
|
| 30 |
-
return False
|
| 31 |
-
|
| 32 |
-
try:
|
| 33 |
-
openai_api_key = os.getenv("OPENAI_API_KEY")
|
| 34 |
-
if not openai_api_key:
|
| 35 |
-
print("Uwaga: Brak klucza OPENAI_API_KEY. Ekspert HR będzie wyłączony.")
|
| 36 |
-
HR_ASSISTANT_AVAILABLE = False
|
| 37 |
-
return False
|
| 38 |
-
|
| 39 |
-
hr_assistant = HRAssistant(
|
| 40 |
-
openai_api_key=openai_api_key,
|
| 41 |
-
pdf_directory="pdfs" # Dostosuj ścieżkę do swoich potrzeb
|
| 42 |
-
)
|
| 43 |
-
print("✅ Asystent HR został zainicjalizowany pomyślnie.")
|
| 44 |
-
return True
|
| 45 |
-
|
| 46 |
-
except Exception as e:
|
| 47 |
-
print(f"❌ Błąd podczas inicjalizacji asystenta HR: {e}")
|
| 48 |
-
HR_ASSISTANT_AVAILABLE = False
|
| 49 |
-
return False
|
| 50 |
-
|
| 51 |
-
# Inicjalizuj asystenta przy starcie
|
| 52 |
-
initialize_hr_assistant()
|
| 53 |
-
|
| 54 |
-
# --- MODELE DANYCH (PYDANTIC) ---
|
| 55 |
-
# Definiują strukturę danych używaną do parsowania odpowiedzi z LLM
|
| 56 |
-
# oraz do generowania finalnego wyniku JSON.
|
| 57 |
-
|
| 58 |
-
class QuestionAnswer(BaseModel):
|
| 59 |
-
"""
|
| 60 |
-
Reprezentuje pojedynczą odpowiedź na pytanie analityczne.
|
| 61 |
-
Ten model jest używany przez parser LangChain do strukturyzacji odpowiedzi LLM.
|
| 62 |
-
"""
|
| 63 |
-
question_number: int = Field(..., description="Numer pytania z wewnętrznej matrycy.")
|
| 64 |
-
answer: str = Field(..., description="Odpowiedź 'TAK' lub 'NIE'.")
|
| 65 |
-
citation: str = Field(..., description="Cytat z analizowanego tekstu, na podstawie którego udzielono odpowiedzi.")
|
| 66 |
-
|
| 67 |
-
@field_validator("answer")
|
| 68 |
-
def validate_answer(cls, v):
|
| 69 |
-
"""Walidator sprawdzający, czy odpowiedź to 'TAK' lub 'NIE'."""
|
| 70 |
-
if v not in {"TAK", "NIE"}:
|
| 71 |
-
raise ValueError("Odpowiedź musi być TAK lub NIE")
|
| 72 |
-
return v
|
| 73 |
-
|
| 74 |
-
class JobAdAnalysis(BaseModel):
|
| 75 |
-
"""
|
| 76 |
-
Reprezentuje pełną analizę ogłoszenia, zawierającą listę odpowiedzi.
|
| 77 |
-
Ten model jest używany przez parser LangChain do strukturyzacji odpowiedzi LLM.
|
| 78 |
-
"""
|
| 79 |
-
answers: list[QuestionAnswer]
|
| 80 |
-
|
| 81 |
-
parser = PydanticOutputParser(pydantic_object=JobAdAnalysis)
|
| 82 |
-
|
| 83 |
-
PROMPT_TEMPLATE_TEXT = """Przeanalizuj poniższe ogłoszenie o pracę pod kątem dostępności dla osób z niepełnosprawnościami.
|
| 84 |
-
|
| 85 |
-
Ogłoszenie:
|
| 86 |
-
{job_ad}
|
| 87 |
-
|
| 88 |
-
Odpowiedz na następujące pytania:
|
| 89 |
-
{questions}
|
| 90 |
-
|
| 91 |
-
Format odpowiedzi powinien być w następującej strukturze JSON:
|
| 92 |
-
{{
|
| 93 |
-
"answers": [
|
| 94 |
-
{{
|
| 95 |
-
"question_number": 1,
|
| 96 |
-
"answer": "TAK/NIE",
|
| 97 |
-
"citation": "dokładny cytat z tekstu"
|
| 98 |
-
}}
|
| 99 |
-
]
|
| 100 |
-
}}
|
| 101 |
-
"""
|
| 102 |
-
|
| 103 |
-
# Wczytanie matrycy danych
|
| 104 |
-
matryca_df = pd.read_csv('matryca.csv', header=None,
|
| 105 |
-
names=['area', 'prompt', 'true', 'false', 'more', 'hint'])
|
| 106 |
-
|
| 107 |
-
def prepare_questions(df):
|
| 108 |
-
questions_text = ""
|
| 109 |
-
for index, row in df.iterrows():
|
| 110 |
-
question_number = index + 1
|
| 111 |
-
questions_text += f"{question_number} {row['prompt']}\n"
|
| 112 |
-
return questions_text
|
| 113 |
-
|
| 114 |
-
def doc_to_text(file):
|
| 115 |
-
extension = os.path.splitext(file.name)[1].lower()
|
| 116 |
-
if extension == ".docx":
|
| 117 |
-
loader = Docx2txtLoader(file.name)
|
| 118 |
-
elif extension == ".pdf":
|
| 119 |
-
loader = PyPDFLoader(file.name)
|
| 120 |
-
else:
|
| 121 |
-
return "error"
|
| 122 |
-
pages = loader.load()
|
| 123 |
-
return "\n".join(page.page_content for page in pages)
|
| 124 |
-
|
| 125 |
-
def is_job_ad(text_fragment: str, model: ChatOpenAI) -> bool:
|
| 126 |
-
"""Sprawdza, czy fragment tekstu pochodzi z ogłoszenia o pracę."""
|
| 127 |
-
try:
|
| 128 |
-
prompt = PromptTemplate.from_template(
|
| 129 |
-
"Czy poniższy tekst to fragment ogłoszenia o pracę? Odpowiedz tylko TAK lub NIE.\n\nTekst: {text_to_check}"
|
| 130 |
-
)
|
| 131 |
-
chain = prompt | model | StrOutputParser()
|
| 132 |
-
response = chain.invoke({"text_to_check": text_fragment})
|
| 133 |
-
return "TAK" in response.upper()
|
| 134 |
-
except Exception:
|
| 135 |
-
# W przypadku błędu API, zakładamy, że to nie jest ogłoszenie, aby przerwać przetwarzanie.
|
| 136 |
-
return False
|
| 137 |
-
|
| 138 |
-
def _generate_report(result: pd.DataFrame, title: str, prefix: str, include_more: bool) -> str:
|
| 139 |
-
"""Tworzy dokument Word na podstawie wyników analizy."""
|
| 140 |
-
doc = Document('template.docx')
|
| 141 |
-
doc.add_heading(title, 0)
|
| 142 |
-
doc.add_paragraph(f'Data wygenerowania: {datetime.now().strftime("%d.%m.%Y %H:%M")}')
|
| 143 |
-
|
| 144 |
-
for _, row in result.iterrows():
|
| 145 |
-
doc.add_heading(str(row['area']), 1)
|
| 146 |
-
doc.add_paragraph(str(row['citation']), style='Intense Quote')
|
| 147 |
-
for line in str(row['content']).split('\n'):
|
| 148 |
-
if line.strip():
|
| 149 |
-
doc.add_paragraph(line)
|
| 150 |
-
|
| 151 |
-
if include_more and pd.notna(row['more']):
|
| 152 |
-
for line in str(row['more']).split('\n'):
|
| 153 |
-
if line.strip():
|
| 154 |
-
doc.add_paragraph(line)
|
| 155 |
-
|
| 156 |
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 157 |
-
filename_prefix = f"{prefix}{timestamp}_"
|
| 158 |
-
|
| 159 |
-
with tempfile.NamedTemporaryFile(delete=False, prefix=filename_prefix, suffix=".docx") as tmp:
|
| 160 |
-
doc.save(tmp.name)
|
| 161 |
-
return tmp.name
|
| 162 |
-
|
| 163 |
-
def create_short_report(result: pd.DataFrame) -> str:
|
| 164 |
-
return _generate_report(
|
| 165 |
-
result,
|
| 166 |
-
title='Raport analizy ogłoszenia o pracę (wersja skrócona)',
|
| 167 |
-
prefix='KoREKtor_short_',
|
| 168 |
-
include_more=False
|
| 169 |
-
)
|
| 170 |
-
|
| 171 |
-
def create_report(result: pd.DataFrame) -> str:
|
| 172 |
-
return _generate_report(
|
| 173 |
-
result,
|
| 174 |
-
title='Raport analizy ogłoszenia o pracę',
|
| 175 |
-
prefix='KoREKtor_pelny_',
|
| 176 |
-
include_more=True
|
| 177 |
-
)
|
| 178 |
-
|
| 179 |
-
def analyze_job_ad(job_ad, file):
|
| 180 |
-
try:
|
| 181 |
-
if file:
|
| 182 |
-
job_ad = doc_to_text(file)
|
| 183 |
-
if job_ad == "error":
|
| 184 |
-
return {"error": "Nieobsługiwany format pliku. Użyj PDF lub DOCX."}, None, None
|
| 185 |
-
|
| 186 |
-
if not job_ad or job_ad.strip() == "":
|
| 187 |
-
return None, None, None
|
| 188 |
-
|
| 189 |
-
model = ChatOpenAI(temperature=0, model="gpt-4o-mini")
|
| 190 |
-
|
| 191 |
-
# Krok 2: Weryfikacja, czy tekst jest ogłoszeniem o pracę
|
| 192 |
-
text_for_verification = job_ad[:1500]
|
| 193 |
-
if not is_job_ad(text_for_verification, model):
|
| 194 |
-
return {"error": "Przesłany tekst lub plik nie wygląda na ogłoszenie o pracę."}, None, None
|
| 195 |
-
|
| 196 |
-
# Krok 3: Główna analiza z użyciem LLM
|
| 197 |
-
questions = prepare_questions(matryca_df)
|
| 198 |
-
prompt_template = PromptTemplate.from_template(PROMPT_TEMPLATE_TEXT)
|
| 199 |
-
|
| 200 |
-
chain = prompt_template | model | parser
|
| 201 |
-
response = chain.invoke({"job_ad": job_ad, "questions": questions})
|
| 202 |
-
|
| 203 |
-
# Krok 4: Przetwarzanie odpowiedzi i budowanie DataFrame
|
| 204 |
-
rows = []
|
| 205 |
-
for i, answer_obj in enumerate(response.answers):
|
| 206 |
-
if answer_obj.answer in {"TAK", "NIE"}:
|
| 207 |
-
answer = answer_obj.answer
|
| 208 |
-
# Inwersja odpowiedzi dla pytania nr 10, zgodnie z logiką matrycy.
|
| 209 |
-
if i == 9:
|
| 210 |
-
answer = "NIE" if answer == "TAK" else "TAK"
|
| 211 |
-
|
| 212 |
-
new_row = {
|
| 213 |
-
'area': matryca_df.area[i],
|
| 214 |
-
'answer': answer,
|
| 215 |
-
'citation': answer_obj.citation,
|
| 216 |
-
'content': matryca_df.true[i] if answer == 'TAK' else matryca_df.false[i],
|
| 217 |
-
'more': matryca_df.more[i]
|
| 218 |
-
}
|
| 219 |
-
rows.append(new_row)
|
| 220 |
-
|
| 221 |
-
output_df = pd.DataFrame(rows)
|
| 222 |
-
|
| 223 |
-
# Krok 5: Generowanie raportów i wyniku JSON
|
| 224 |
-
short_word_file_path = create_short_report(output_df)
|
| 225 |
-
word_file_path = create_report(output_df)
|
| 226 |
-
# Wynik JSON jest tworzony na podstawie przetworzonych danych i udostępniany w interfejsie.
|
| 227 |
-
# Struktura: lista obiektów, gdzie każdy obiekt to jeden wiersz analizy.
|
| 228 |
-
json_output = output_df.to_dict(orient="records")
|
| 229 |
-
|
| 230 |
-
return json_output, word_file_path, short_word_file_path
|
| 231 |
-
except Exception as e:
|
| 232 |
-
# Zwracamy błąd w formacie JSON, aby wyświetlić go w interfejsie
|
| 233 |
-
return {"error": f"Wystąpił wewnętrzny błąd serwera: {e}"}, None, None
|
| 234 |
-
|
| 235 |
-
# Interfejs Gradio dla głównej analizy
|
| 236 |
-
analysis_demo = gr.Interface(
|
| 237 |
-
fn=analyze_job_ad,
|
| 238 |
-
inputs=[
|
| 239 |
-
gr.TextArea(label="Ogłoszenie (opcjonalnie)", placeholder="Wklej tekst ogłoszenia tutaj..."),
|
| 240 |
-
gr.File(label="Lub wybierz plik PDF/DOCX", file_types=[".pdf", ".docx"]),
|
| 241 |
-
],
|
| 242 |
-
outputs=[
|
| 243 |
-
gr.JSON(label="Wyniki analizy (JSON)"),
|
| 244 |
-
gr.File(label="Pobierz pełny raport Word"),
|
| 245 |
-
gr.File(label="Pobierz skrócony raport Word"),
|
| 246 |
-
],
|
| 247 |
-
title="KoREKtor – analiza ogłoszenia",
|
| 248 |
-
description="Przeanalizuj ogłoszenie o pracę pod kątem dostępności dla osób z niepełnosprawnościami"
|
| 249 |
-
)
|
| 250 |
-
|
| 251 |
-
def ask_hr_assistant(question):
|
| 252 |
-
"""Funkcja do zadawania pytań asystentowi HR."""
|
| 253 |
-
global hr_assistant, HR_ASSISTANT_AVAILABLE
|
| 254 |
-
if not HR_ASSISTANT_AVAILABLE or hr_assistant is None:
|
| 255 |
-
return "⚠️ Ekspert HR nie jest dostępny. Sprawdź konfigurację modułu hr_assistant i klucz OPENAI_API_KEY."
|
| 256 |
-
try:
|
| 257 |
-
response = hr_assistant.ask(question)
|
| 258 |
-
answer = f"🤖 **Ekspert HR:**\n\n{response['answer']}"
|
| 259 |
-
if response.get('sources'):
|
| 260 |
-
answer += f"\n\n📚 **Źródła:**\n"
|
| 261 |
-
for i, source in enumerate(response['sources'][:3], 1): # Max 3 źródła
|
| 262 |
-
# Usunięcie nazwy pliku ze źródła
|
| 263 |
-
answer += f"{i}. str. {source.get('page', '?')}\n"
|
| 264 |
-
return answer
|
| 265 |
-
except Exception as e:
|
| 266 |
-
return f"❌ Wystąpił błąd podczas komunikacji z ekspertem HR: {e}"
|
| 267 |
-
|
| 268 |
-
# Interfejs Gradio dla asystenta HR
|
| 269 |
-
hr_assistant_demo = gr.Interface(
|
| 270 |
-
fn=ask_hr_assistant,
|
| 271 |
-
inputs=gr.TextArea(label="Pytanie do eksperta HR", placeholder="Zadaj pytanie..."),
|
| 272 |
-
outputs=gr.Markdown(label="Odpowiedź eksperta HR"),
|
| 273 |
-
title="KoREKtor – Ekspert HR",
|
| 274 |
-
description="Zadaj pytanie ekspertowi HR w zakresie zatrudniania osób z niepełnosprawnościami."
|
| 275 |
-
)
|
| 276 |
-
|
| 277 |
-
# Łączenie interfejsów w zakładki
|
| 278 |
-
demo = gr.TabbedInterface([analysis_demo, hr_assistant_demo], ["Analiza Ogłoszenia", "Ekspert HR"])
|
| 279 |
-
|
| 280 |
-
demo.launch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bibliografia.csv
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"PRZYSTOSOWANIE OBIEKT�W, POMIESZCZE� ORAZ STANOWISK PRACY DLA OS�B NIEPE�NOSPRAWNYCH O SPECYFICZNYCH POTRZEBACH � DOBRE PRAKTYKI; PFRON, CIOP PIB; Warszawa 2014";dobre praktyki- wersja finalna
|
| 2 |
+
"LISTA KONTROLNA DO OCENY �RODOWISKA PRACY POD K�TEM DOSTOSOWANIA DO POTRZEB OS�B NIEPE�NOSPRAWNYCH; PFRON, CIOP PIB; Warszawa 2014";lista kontrolna 2014
|
| 3 |
+
"PROJEKTOWANIE OBIEKT�W, POMIESZCZE� ORAZ PRZYSTOSOWANIE STANOWISK PRACY DLA OS�B NIEPE�NOSPRAWNYCH O SPECYFICZNYCH POTRZEBACH � RAMOWE WYTYCZNE; PFRON, CIOP PIB; Warszawa 2014";Ramowe wytyczne�
|
| 4 |
+
"Kotowska L.; Prawo pracy. Pracownik niepe�nosprawny; Pa�stwowa Inspekcja Pracy; wydanie 2/2024, stan prawny marzec 2024";wydawnictwo PIP�
|
| 5 |
+
"Gosk D., Olkowska A., Dani�owska S., Komunikacja bez barier, Praktyczny poradnik kontaktu z osobami z niepe�nosprawno�ciami; Fundacja Aktywizacja; Warszawa 2021";Fundacja-Aktywizacja-Publikacja-Komunikacja-bez�
|
| 6 |
+
"Raport systemowy. Podsumowanie przegl�du procedur w 30 urz�dach oraz rekomendacje systemowe dla ca�ej administracji w zakresie zatrudniania os�b ze szczeg�lnymi potrzebami; Kancelaria Prezesa Rady Ministr�w";Raport_ systemowy�
|
| 7 |
+
"Kompendium wiedzy na temat zatrudnienia os�b ze szczeg�lnymi potrzebami; Kancelaria Prezesa rady Ministr�w";PBB_HR�
|
| 8 |
+
Dani�owska S., Gawska A., Kowalski P., Paszkowska M., Sielecka K., Tatko A., Dobre praktyki w zatrudnianiu os�b z niepe�nosprawno�ciami. Fundacja Aktywizacja, Warszawa 2022;podr�cznik online
|
| 9 |
+
Gawska A., Poradnik dla pracodawc�w, o tym jak tworzy� dost�pne miejsce pracy. Fundacja Aktywizacja, Warszawa 2024;Fundacja Aktywizacja poradnik dla pracodawc�w o tym�
|
| 10 |
+
Gruszczy�ska A., Gruntowski M,. 5 krok�w do zatrudnienia � Osoby z niepe�nosprawno�ci� w procesie rekrutacji. Fundacja Aktywizacja, Warszawa 2024;5 krok�w do zatrudnienia�
|
| 11 |
+
Gruszczy�ska A., Gruntowski M., Osoba z niepe�nosprawno�ci�� w Twojej firmie, Fundacja Aktywizacja, Warszawa 2024;niezb�dnik pracodawcy
|
| 12 |
+
Gawska A. Pracodawca w��czaj�cy � jak skutecznie zatrudnia� osoby z niepe�nosprawno�ciami?, Warszawa 2025;artyku� ze strony koREKtora
|
chatbot.py
CHANGED
|
@@ -18,7 +18,8 @@ def initialize_assistant():
|
|
| 18 |
if not api_key:
|
| 19 |
print("Brak klucza OPENAI_API_KEY w zmiennych środowiskowych.")
|
| 20 |
return None
|
| 21 |
-
|
|
|
|
| 22 |
except Exception as e:
|
| 23 |
print(f"Błąd podczas inicjalizacji asystenta HR: {e}")
|
| 24 |
return None
|
|
@@ -43,9 +44,41 @@ def respond_to_query(message, history):
|
|
| 43 |
answer = response.get("answer", "Przepraszam, wystąpił błąd.")
|
| 44 |
|
| 45 |
if response.get("sources"):
|
| 46 |
-
answer += "\n\n**Źródła
|
| 47 |
-
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
history.append({"role": "user", "content": message})
|
| 51 |
history.append({"role": "assistant", "content": answer})
|
|
|
|
| 18 |
if not api_key:
|
| 19 |
print("Brak klucza OPENAI_API_KEY w zmiennych środowiskowych.")
|
| 20 |
return None
|
| 21 |
+
hr_assistant = HRAssistant(openai_api_key=api_key, pdf_directory="pdfs")
|
| 22 |
+
return hr_assistant
|
| 23 |
except Exception as e:
|
| 24 |
print(f"Błąd podczas inicjalizacji asystenta HR: {e}")
|
| 25 |
return None
|
|
|
|
| 44 |
answer = response.get("answer", "Przepraszam, wystąpił błąd.")
|
| 45 |
|
| 46 |
if response.get("sources"):
|
| 47 |
+
answer += "\n\n**Źródła:**"
|
| 48 |
+
|
| 49 |
+
grouped_sources = {}
|
| 50 |
+
for source_meta in response["sources"]:
|
| 51 |
+
source_key = source_meta.get('source')
|
| 52 |
+
if not source_key:
|
| 53 |
+
continue
|
| 54 |
+
|
| 55 |
+
if source_key not in grouped_sources:
|
| 56 |
+
grouped_sources[source_key] = {
|
| 57 |
+
'type': 'url' if source_key.startswith('http') else 'pdf',
|
| 58 |
+
'meta': source_meta,
|
| 59 |
+
'pages': set()
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
# Strona 'page' w metadanych jest numerowana od 0, dodajemy 1 dla czytelności
|
| 63 |
+
if 'page' in source_meta:
|
| 64 |
+
grouped_sources[source_key]['pages'].add(source_meta['page'])
|
| 65 |
+
|
| 66 |
+
for key, data in grouped_sources.items():
|
| 67 |
+
if data['type'] == 'pdf':
|
| 68 |
+
filename = data['meta'].get('filename', os.path.basename(key))
|
| 69 |
+
pages = sorted(list(data['pages']))
|
| 70 |
+
# Jeśli jest tylko jedna strona, nie twórz zakresu
|
| 71 |
+
if len(pages) == 1:
|
| 72 |
+
pages_str = f"str. {pages[0]}"
|
| 73 |
+
else:
|
| 74 |
+
pages_str = "str. " + ", ".join(map(str, pages))
|
| 75 |
+
answer += f"\n- {filename} ({pages_str})"
|
| 76 |
+
elif data['type'] == 'url':
|
| 77 |
+
title = data['meta'].get('title', key)
|
| 78 |
+
url = key
|
| 79 |
+
date = data['meta'].get('added_date', '')
|
| 80 |
+
date_str = f" (dodano: {date})" if date else ""
|
| 81 |
+
answer += f"\n- [{title}]({url}){date_str}"
|
| 82 |
|
| 83 |
history.append({"role": "user", "content": message})
|
| 84 |
history.append({"role": "assistant", "content": answer})
|
database.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from langchain_community.vectorstores import FAISS
|
| 3 |
+
from langchain_openai import OpenAIEmbeddings
|
| 4 |
+
from langchain_core.documents import Document
|
| 5 |
+
|
| 6 |
+
# Sprawdzenie, czy klucz API jest ustawiony
|
| 7 |
+
if not os.getenv("OPENAI_API_KEY"):
|
| 8 |
+
raise ValueError("Klucz OPENAI_API_KEY nie jest ustawiony w zmiennych środowiskowych. Ustaw go, aby kontynuować.")
|
| 9 |
+
|
| 10 |
+
# Model do tworzenia wektorów (embeddings) - ten sam, co w hr_assistant.py
|
| 11 |
+
EMBEDDINGS = OpenAIEmbeddings(model="text-embedding-3-small")
|
| 12 |
+
|
| 13 |
+
class FaissCollectionWrapper:
|
| 14 |
+
"""
|
| 15 |
+
Klasa-adapter do pracy z bazą FAISS w pamięci.
|
| 16 |
+
"""
|
| 17 |
+
def __init__(self, vector_store=None):
|
| 18 |
+
if vector_store is None:
|
| 19 |
+
# Utwórz pustą bazę FAISS z minimalną zawartością
|
| 20 |
+
self._vector_store = FAISS.from_texts(["placeholder"], EMBEDDINGS)
|
| 21 |
+
else:
|
| 22 |
+
self._vector_store = vector_store
|
| 23 |
+
|
| 24 |
+
def add(self, documents, metadatas, ids):
|
| 25 |
+
"""
|
| 26 |
+
Dodaje dokumenty do bazy FAISS (tylko w pamięci, bez zapisu na dysk).
|
| 27 |
+
"""
|
| 28 |
+
docs_to_add = []
|
| 29 |
+
for i, content in enumerate(documents):
|
| 30 |
+
docs_to_add.append(Document(page_content=content, metadata=metadatas[i]))
|
| 31 |
+
|
| 32 |
+
if docs_to_add:
|
| 33 |
+
new_docs_vectorstore = FAISS.from_documents(docs_to_add, EMBEDDINGS)
|
| 34 |
+
self._vector_store.merge_from(new_docs_vectorstore)
|
| 35 |
+
print(f"Dodano {len(docs_to_add)} dokumentów do bazy w pamięci.")
|
| 36 |
+
|
| 37 |
+
def get_collection():
|
| 38 |
+
"""
|
| 39 |
+
Tworzy nową, pustą bazę FAISS w pamięci.
|
| 40 |
+
"""
|
| 41 |
+
print("Tworzenie nowej bazy danych FAISS w pamięci...")
|
| 42 |
+
# Tworzymy nowy wrapper, który automatycznie utworzy pustą bazę
|
| 43 |
+
return FaissCollectionWrapper()
|
| 44 |
+
|
| 45 |
+
if __name__ == '__main__':
|
| 46 |
+
print("Testowanie modułu database.py...")
|
| 47 |
+
collection = get_collection()
|
| 48 |
+
print("Pomyślnie zainicjalizowano bazę danych FAISS.")
|
hr_assistant.py
CHANGED
|
@@ -19,9 +19,13 @@ from langchain.chains import ConversationalRetrievalChain
|
|
| 19 |
from langchain.memory import ConversationBufferWindowMemory
|
| 20 |
from langchain_core.prompts import PromptTemplate
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
# PDF processing
|
| 23 |
import fitz # PyMuPDF
|
| 24 |
-
from sentence_transformers import SentenceTransformer
|
| 25 |
|
| 26 |
# Configure logging
|
| 27 |
logging.basicConfig(level=logging.INFO)
|
|
@@ -33,7 +37,7 @@ class IntelligentPDFChunker:
|
|
| 33 |
Inteligentny chunker dla dokumentów PDF, który respektuje strukturę dokumentu.
|
| 34 |
"""
|
| 35 |
|
| 36 |
-
def __init__(self, chunk_size: int =
|
| 37 |
self.chunk_size = chunk_size
|
| 38 |
self.chunk_overlap = chunk_overlap
|
| 39 |
|
|
@@ -198,6 +202,43 @@ class HRAssistant:
|
|
| 198 |
pdf_directory (str): Ścieżka do katalogu z plikami PDF.
|
| 199 |
"""
|
| 200 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
def _setup_qa_chain(self):
|
| 202 |
"""
|
| 203 |
Tworzy i konfiguruje łańcuch pytań i odpowiedzi (ConversationalRetrievalChain) dla asystenta HR.
|
|
@@ -212,18 +253,22 @@ class HRAssistant:
|
|
| 212 |
|
| 213 |
prompt_template = (
|
| 214 |
"Jesteś ekspertem HR specjalizującym się w zatrudnianiu osób z niepełnosprawnościami w Polsce.\n"
|
| 215 |
-
"Twoja wiedza opiera się na oficjalnych dokumentach
|
| 216 |
-
"Kontekst z dokumentów:\n{context}\n\n"
|
| 217 |
"Historia rozmowy:\n{chat_history}\n\n"
|
| 218 |
"Pytanie: {question}\n\n"
|
| 219 |
"Instrukcje:\n"
|
| 220 |
"1. Odpowiadaj w języku polskim\n"
|
| 221 |
-
"2. Bazuj wyłącznie na informacjach z dostarczonych dokumentów\n"
|
| 222 |
"3. Jeśli nie masz informacji w dokumentach, powiedz to wprost\n"
|
| 223 |
"4. Podawaj konkretne, praktyczne porady\n"
|
| 224 |
"5. Odwołuj się do konkretnych przepisów prawnych gdy to możliwe\n"
|
| 225 |
-
"6.
|
| 226 |
-
"7.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
"Odpowiedź:"
|
| 228 |
)
|
| 229 |
custom_prompt = PromptTemplate(
|
|
@@ -234,133 +279,363 @@ class HRAssistant:
|
|
| 234 |
llm=self.llm,
|
| 235 |
retriever=self.vectorstore.as_retriever(
|
| 236 |
search_type="similarity",
|
| 237 |
-
search_kwargs={"k":
|
| 238 |
),
|
| 239 |
memory=self.memory,
|
| 240 |
combine_docs_chain_kwargs={"prompt": custom_prompt},
|
| 241 |
return_source_documents=True,
|
| 242 |
output_key="answer"
|
| 243 |
)
|
| 244 |
-
"""
|
| 245 |
-
Asystent HR dla pracodawców zatrudniających osoby z niepełnosprawnościami.
|
| 246 |
-
"""
|
| 247 |
-
|
| 248 |
-
def __init__(self, openai_api_key: str, pdf_directory: str = "pdfs"):
|
| 249 |
-
self.openai_api_key = openai_api_key
|
| 250 |
-
self.pdf_directory = Path(pdf_directory)
|
| 251 |
-
self._known_pdfs = set()
|
| 252 |
-
self._pdf_mtimes = {}
|
| 253 |
-
|
| 254 |
-
# Inicjalizuj komponenty
|
| 255 |
-
self.embeddings = OpenAIEmbeddings(
|
| 256 |
-
api_key=openai_api_key,
|
| 257 |
-
model="text-embedding-3-small"
|
| 258 |
-
)
|
| 259 |
-
self.llm = ChatOpenAI(
|
| 260 |
-
api_key=openai_api_key,
|
| 261 |
-
model="gpt-4o-mini",
|
| 262 |
-
temperature=0.3
|
| 263 |
-
)
|
| 264 |
-
self.chunker = IntelligentPDFChunker(
|
| 265 |
-
chunk_size=1000,
|
| 266 |
-
chunk_overlap=200
|
| 267 |
-
)
|
| 268 |
-
self.vectorstore = None
|
| 269 |
-
self.qa_chain = None
|
| 270 |
-
self.memory = ConversationBufferWindowMemory(
|
| 271 |
-
k=5,
|
| 272 |
-
memory_key="chat_history",
|
| 273 |
-
return_messages=True,
|
| 274 |
-
output_key="answer",
|
| 275 |
-
input_key="question"
|
| 276 |
-
)
|
| 277 |
-
self._load_and_process_documents()
|
| 278 |
-
self._setup_qa_chain()
|
| 279 |
-
|
| 280 |
-
def _list_pdf_files(self):
|
| 281 |
-
return list(self.pdf_directory.glob("*.pdf"))
|
| 282 |
-
|
| 283 |
-
def _pdfs_changed(self) -> bool:
|
| 284 |
-
"""
|
| 285 |
-
Sprawdza, czy pojawiły się nowe pliki PDF lub zmieniły się istniejące.
|
| 286 |
-
"""
|
| 287 |
-
changed = False
|
| 288 |
-
current_pdfs = set()
|
| 289 |
-
current_mtimes = {}
|
| 290 |
-
for pdf in self._list_pdf_files():
|
| 291 |
-
current_pdfs.add(pdf.name)
|
| 292 |
-
mtime = pdf.stat().st_mtime
|
| 293 |
-
current_mtimes[pdf.name] = mtime
|
| 294 |
-
if (pdf.name not in self._pdf_mtimes) or (self._pdf_mtimes.get(pdf.name) != mtime):
|
| 295 |
-
changed = True
|
| 296 |
-
if self._known_pdfs != current_pdfs:
|
| 297 |
-
changed = True
|
| 298 |
-
if changed:
|
| 299 |
-
self._known_pdfs = current_pdfs
|
| 300 |
-
self._pdf_mtimes = current_mtimes
|
| 301 |
-
return changed
|
| 302 |
|
| 303 |
def _load_and_process_documents(self):
|
| 304 |
"""
|
| 305 |
-
Ładuje i przetwarza dokumenty PDF
|
| 306 |
"""
|
| 307 |
-
logger.info("
|
|
|
|
| 308 |
|
|
|
|
|
|
|
| 309 |
pdf_files = self._list_pdf_files()
|
| 310 |
if not pdf_files:
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
chunked_documents = self.chunker.chunk_documents(all_documents)
|
| 324 |
-
logger.info(f"Utworzono {len(chunked_documents)} chunków")
|
|
|
|
|
|
|
|
|
|
| 325 |
self.vectorstore = FAISS.from_documents(
|
| 326 |
chunked_documents,
|
| 327 |
self.embeddings
|
| 328 |
)
|
| 329 |
-
logger.info("Baza wektorowa została utworzona")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
|
| 331 |
-
def
|
| 332 |
"""
|
| 333 |
-
|
| 334 |
"""
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
|
| 340 |
def ask(self, question: str) -> Dict[str, Any]:
|
| 341 |
"""
|
| 342 |
Zadaje pytanie asystentowi.
|
| 343 |
"""
|
| 344 |
logger.info(f"Otrzymano pytanie: {question}")
|
| 345 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
try:
|
| 347 |
-
|
| 348 |
"question": question,
|
| 349 |
"chat_history": self.memory.chat_memory.messages
|
| 350 |
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
response = {
|
| 352 |
-
"answer":
|
| 353 |
"sources": [],
|
| 354 |
"confidence": "medium"
|
| 355 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
for doc in result.get("source_documents", []):
|
| 357 |
source_info = {
|
| 358 |
"filename": doc.metadata.get("filename", ""),
|
| 359 |
"page": doc.metadata.get("page", ""),
|
| 360 |
"section": doc.metadata.get("section", ""),
|
|
|
|
|
|
|
|
|
|
| 361 |
"snippet": doc.page_content[:200] + "..." if len(doc.page_content) > 200 else doc.page_content
|
| 362 |
}
|
| 363 |
-
|
|
|
|
|
|
|
| 364 |
return response
|
| 365 |
except Exception as e:
|
| 366 |
logger.error(f"Błąd podczas przetwarzania pytania: {e}")
|
|
@@ -371,6 +646,40 @@ class HRAssistant:
|
|
| 371 |
"error": str(e)
|
| 372 |
}
|
| 373 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
def get_stats(self) -> Dict[str, Any]:
|
| 375 |
"""
|
| 376 |
Zwraca statystyki asystenta.
|
|
@@ -446,6 +755,10 @@ def main():
|
|
| 446 |
|
| 447 |
print("=== Asystent HR - Zatrudnianie osób z niepełnosprawnościami ===\n")
|
| 448 |
print(f"Statystyki: {assistant.get_stats()}\n")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 449 |
|
| 450 |
# Interaktywny tryb
|
| 451 |
while True:
|
|
@@ -478,25 +791,99 @@ def main():
|
|
| 478 |
if __name__ == "__main__":
|
| 479 |
main()
|
| 480 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 481 |
|
| 482 |
-
# ===============================
|
| 483 |
-
# Instrukcja wdrożenia modułu HRAssistant
|
| 484 |
-
# ===============================
|
| 485 |
-
#
|
| 486 |
-
# 1. Ustaw zmienną środowiskową z kluczem OpenAI:
|
| 487 |
-
# export OPENAI_API_KEY="twoj_klucz_openai"
|
| 488 |
-
#
|
| 489 |
-
# 2. Umieść pliki PDF w katalogu "pdfs" (lub wskaż inny katalog w parametrze pdf_directory).
|
| 490 |
-
#
|
| 491 |
-
# 3. Zainstaluj wymagane biblioteki:
|
| 492 |
-
# pip install -r requirements.txt
|
| 493 |
-
#
|
| 494 |
-
# 4. Uruchom moduł:
|
| 495 |
-
# python hr_assistant.py
|
| 496 |
-
#
|
| 497 |
-
# 5. Możesz zintegrować klasę HRAssistant w swoim projekcie:
|
| 498 |
-
# from hr_assistant import HRAssistant
|
| 499 |
-
# assistant = HRAssistant(openai_api_key="...", pdf_directory="pdfs")
|
| 500 |
-
# odpowiedz = assistant.ask("Twoje pytanie")
|
| 501 |
-
#
|
| 502 |
-
# 6. Szczegóły i przykłady znajdziesz w README.md oraz EXAMPLES.md.
|
|
|
|
| 19 |
from langchain.memory import ConversationBufferWindowMemory
|
| 20 |
from langchain_core.prompts import PromptTemplate
|
| 21 |
|
| 22 |
+
# Web scraping
|
| 23 |
+
import requests
|
| 24 |
+
from bs4 import BeautifulSoup
|
| 25 |
+
from datetime import datetime
|
| 26 |
+
|
| 27 |
# PDF processing
|
| 28 |
import fitz # PyMuPDF
|
|
|
|
| 29 |
|
| 30 |
# Configure logging
|
| 31 |
logging.basicConfig(level=logging.INFO)
|
|
|
|
| 37 |
Inteligentny chunker dla dokumentów PDF, który respektuje strukturę dokumentu.
|
| 38 |
"""
|
| 39 |
|
| 40 |
+
def __init__(self, chunk_size: int = 800, chunk_overlap: int = 150): # Zmniejszone dla lepszej wydajności
|
| 41 |
self.chunk_size = chunk_size
|
| 42 |
self.chunk_overlap = chunk_overlap
|
| 43 |
|
|
|
|
| 202 |
pdf_directory (str): Ścieżka do katalogu z plikami PDF.
|
| 203 |
"""
|
| 204 |
|
| 205 |
+
def __init__(self, openai_api_key: str, pdf_directory: str = "pdfs"):
|
| 206 |
+
self.openai_api_key = openai_api_key
|
| 207 |
+
self.pdf_directory = Path(pdf_directory)
|
| 208 |
+
|
| 209 |
+
# Inicjalizuj komponenty
|
| 210 |
+
self.embeddings = OpenAIEmbeddings(
|
| 211 |
+
api_key=openai_api_key,
|
| 212 |
+
model="text-embedding-3-small",
|
| 213 |
+
chunk_size=1000 # Przetwarzanie wsadowe dla osadzeń
|
| 214 |
+
)
|
| 215 |
+
self.llm = ChatOpenAI(
|
| 216 |
+
api_key=openai_api_key,
|
| 217 |
+
model="gpt-4o-mini",
|
| 218 |
+
temperature=0.3
|
| 219 |
+
)
|
| 220 |
+
self.chunker = IntelligentPDFChunker(
|
| 221 |
+
chunk_size=800,
|
| 222 |
+
chunk_overlap=150
|
| 223 |
+
)
|
| 224 |
+
self.vectorstore = None
|
| 225 |
+
self.qa_chain = None
|
| 226 |
+
self.memory = ConversationBufferWindowMemory(
|
| 227 |
+
k=5,
|
| 228 |
+
memory_key="chat_history",
|
| 229 |
+
return_messages=True,
|
| 230 |
+
output_key="answer",
|
| 231 |
+
input_key="question"
|
| 232 |
+
)
|
| 233 |
+
self._load_and_process_documents()
|
| 234 |
+
self._setup_qa_chain()
|
| 235 |
+
|
| 236 |
+
def _list_pdf_files(self) -> List[Path]:
|
| 237 |
+
"""
|
| 238 |
+
Listuje pliki PDF w katalogu.
|
| 239 |
+
"""
|
| 240 |
+
return list(self.pdf_directory.glob("*.pdf"))
|
| 241 |
+
|
| 242 |
def _setup_qa_chain(self):
|
| 243 |
"""
|
| 244 |
Tworzy i konfiguruje łańcuch pytań i odpowiedzi (ConversationalRetrievalChain) dla asystenta HR.
|
|
|
|
| 253 |
|
| 254 |
prompt_template = (
|
| 255 |
"Jesteś ekspertem HR specjalizującym się w zatrudnianiu osób z niepełnosprawnościami w Polsce.\n"
|
| 256 |
+
"Twoja wiedza opiera się na oficjalnych dokumentach, poradnikach dla pracodawców i aktualnych stronach internetowych PFRON.\n\n"
|
| 257 |
+
"Kontekst z dokumentów i stron internetowych:\n{context}\n\n"
|
| 258 |
"Historia rozmowy:\n{chat_history}\n\n"
|
| 259 |
"Pytanie: {question}\n\n"
|
| 260 |
"Instrukcje:\n"
|
| 261 |
"1. Odpowiadaj w języku polskim\n"
|
| 262 |
+
"2. Bazuj wyłącznie na informacjach z dostarczonych dokumentów i stron PFRON\n"
|
| 263 |
"3. Jeśli nie masz informacji w dokumentach, powiedz to wprost\n"
|
| 264 |
"4. Podawaj konkretne, praktyczne porady\n"
|
| 265 |
"5. Odwołuj się do konkretnych przepisów prawnych gdy to możliwe\n"
|
| 266 |
+
"6. GDY PYTANIE DOTYCZY KWOT PIENIĘŻNYCH, OBOWIĄZKOWO PODAJ DOKŁADNE WARTOŚCI Z NAJNOWSZYCH ŹRÓDEŁ\n"
|
| 267 |
+
"7. ZAWSZE PRIORYTETYZUJ INFORMACJE ZE STRON INTERNETOWYCH PFRON NAD INFORMACJAMI Z PDF-ÓW\n"
|
| 268 |
+
"8. DLA KWOT DOFINANSOWAŃ ZAWSZE PRZYTACZAJ DOKŁADNE LICZBY, NP. '2300 ZŁ', '1900 ZŁ', ITD.\n"
|
| 269 |
+
"9. Bądź pomocny i profesjonalny\n"
|
| 270 |
+
"10. Zawsze podawaj źródło informacji (URL lub nazwa dokumentu PDF)\n"
|
| 271 |
+
"11. NAJWAŻNIEJSZE: Gdy pytanie dotyczy finansów, MUSZISZ podać konkretne kwoty z podanych informacji\n\n"
|
| 272 |
"Odpowiedź:"
|
| 273 |
)
|
| 274 |
custom_prompt = PromptTemplate(
|
|
|
|
| 279 |
llm=self.llm,
|
| 280 |
retriever=self.vectorstore.as_retriever(
|
| 281 |
search_type="similarity",
|
| 282 |
+
search_kwargs={"k": 8} # Zmniejszone z 10 do 8 dla lepszej wydajności
|
| 283 |
),
|
| 284 |
memory=self.memory,
|
| 285 |
combine_docs_chain_kwargs={"prompt": custom_prompt},
|
| 286 |
return_source_documents=True,
|
| 287 |
output_key="answer"
|
| 288 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
|
| 290 |
def _load_and_process_documents(self):
|
| 291 |
"""
|
| 292 |
+
Ładuje i przetwarza wszystkie dokumenty (PDF, URL, hardcoded) i tworzy jedną bazę wektorową.
|
| 293 |
"""
|
| 294 |
+
logger.info("Rozpoczynam ładowanie i przetwarzanie wszystkich dokumentów...")
|
| 295 |
+
all_documents = []
|
| 296 |
|
| 297 |
+
# 1. Przetwarzanie plików PDF
|
| 298 |
+
logger.info("Ładowanie dokumentów PDF...")
|
| 299 |
pdf_files = self._list_pdf_files()
|
| 300 |
if not pdf_files:
|
| 301 |
+
logger.warning(f"Nie znaleziono plików PDF w katalogu: {self.pdf_directory}. Kontynuuję bez nich.")
|
| 302 |
+
else:
|
| 303 |
+
logger.info(f"Znaleziono {len(pdf_files)} plików PDF")
|
| 304 |
+
for pdf_file in pdf_files:
|
| 305 |
+
logger.info(f"Przetwarzanie: {pdf_file.name}")
|
| 306 |
+
try:
|
| 307 |
+
documents = self.chunker._extract_pdf_structure(str(pdf_file))
|
| 308 |
+
for doc in documents:
|
| 309 |
+
doc.metadata["filename"] = pdf_file.name
|
| 310 |
+
doc.metadata["file_stem"] = pdf_file.stem
|
| 311 |
+
all_documents.extend(documents)
|
| 312 |
+
except Exception as e:
|
| 313 |
+
logger.error(f"Błąd podczas przetwarzania pliku PDF {pdf_file.name}: {e}")
|
| 314 |
+
logger.info(f"Wyekstraktowano {len(all_documents)} sekcji z plików PDF.")
|
| 315 |
+
|
| 316 |
+
# 2. Dodawanie treści z URLi
|
| 317 |
+
url_docs = self._get_url_documents()
|
| 318 |
+
if url_docs:
|
| 319 |
+
all_documents.extend(url_docs)
|
| 320 |
+
logger.info(f"Dodano {len(url_docs)} dokumentów z adresów URL.")
|
| 321 |
+
|
| 322 |
+
# 3. Dodawanie dokumentów hardcoded
|
| 323 |
+
hardcoded_docs = self._get_hardcoded_documents()
|
| 324 |
+
all_documents.extend(hardcoded_docs)
|
| 325 |
+
logger.info(f"Dodano {len(hardcoded_docs)} dokumentów hardcoded.")
|
| 326 |
+
|
| 327 |
+
if not all_documents:
|
| 328 |
+
raise ValueError("Nie znaleziono żadnych dokumentów do przetworzenia. Baza wektorowa nie może zostać utworzona.")
|
| 329 |
+
|
| 330 |
+
# 4. Chunkowanie wszystkich dokumentów razem
|
| 331 |
+
logger.info(f"Rozpoczynam chunkowanie {len(all_documents)} wszystkich zebranych dokumentów...")
|
| 332 |
chunked_documents = self.chunker.chunk_documents(all_documents)
|
| 333 |
+
logger.info(f"Utworzono {len(chunked_documents)} chunków.")
|
| 334 |
+
|
| 335 |
+
# 5. Tworzenie bazy wektorowej z jednego, dużego wywołania wsadowego
|
| 336 |
+
logger.info("Tworzenie bazy wektorowej FAISS...")
|
| 337 |
self.vectorstore = FAISS.from_documents(
|
| 338 |
chunked_documents,
|
| 339 |
self.embeddings
|
| 340 |
)
|
| 341 |
+
logger.info("Baza wektorowa została pomyślnie utworzona.")
|
| 342 |
+
|
| 343 |
+
def _get_hardcoded_documents(self) -> List[Document]:
|
| 344 |
+
"""
|
| 345 |
+
Zwraca listę hardkodowanych dokumentów z kluczowymi danymi finansowymi.
|
| 346 |
+
"""
|
| 347 |
+
hardcoded_financial_info = """
|
| 348 |
+
WYSOKOŚĆ DOFINANSOWANIA DO WYNAGRODZEŃ PRACOWNIKÓW NIEPEŁNOSPRAWNYCH Z PFRON
|
| 349 |
+
|
| 350 |
+
Kwoty miesięcznego dofinansowania do wynagrodzenia pracowników niepełnosprawnych:
|
| 351 |
+
|
| 352 |
+
1) 2300 zł – w przypadku osób niepełnosprawnych zaliczonych do znacznego stopnia niepełnosprawności;
|
| 353 |
+
2) 1350 zł – w przypadku osób niepełnosprawnych zaliczonych do umiarkowanego stopnia niepełnosprawności;
|
| 354 |
+
3) 500 zł – w przypadku osób niepełnosprawnych zaliczonych do lekkiego stopnia niepełnosprawności.
|
| 355 |
+
|
| 356 |
+
Kwoty, o których mowa w ust. 1, zwiększa się o 1050 zł w przypadku osób niepełnosprawnych, w odniesieniu do których orzeczono chorobę psychiczną, upośledzenie umysłowe, całościowe zaburzenia rozwojowe lub epilepsję oraz niewidomych.
|
| 357 |
+
|
| 358 |
+
Miesięczne dofinansowanie do wynagrodzenia pracownika niepełnosprawnego, zwane dalej „miesięcznym dofinansowaniem", przysługuje w kwocie:
|
| 359 |
+
1) 2300 zł – w przypadku osób niepełnosprawnych zaliczonych do znacznego stopnia niepełnosprawności;
|
| 360 |
+
2) 1350 zł – w przypadku osób niepełnosprawnych zaliczonych do umiarkowanego stopnia niepełnosprawności;
|
| 361 |
+
3) 500 zł – w przypadku osób niepełnosprawnych zaliczonych do lekkiego stopnia niepełnosprawności.
|
| 362 |
+
|
| 363 |
+
Kwoty, o których mowa powyżej, zwiększa się o 1050 zł w przypadku osób niepełnosprawnych, w odniesieniu do których orzeczono chorobę psychiczną, upośledzenie umysłowe, całościowe zaburzenia rozwojowe lub epilepsję oraz niewidomych.
|
| 364 |
+
|
| 365 |
+
Wysokość miesięcznego dofinansowania nie może przekroczyć 90% faktycznie poniesionych miesięcznych kosztów płacy, a w przypadku pracodawcy wykonującego działalność gospodarczą, w rozumieniu przepisów o postępowaniu w sprawach dotyczących pomocy publicznej, zwanego dalej "pracodawcą wykonującym działalność gospodarczą", 75% tych kosztów.
|
| 366 |
+
"""
|
| 367 |
+
|
| 368 |
+
hardcoded_doc = Document(
|
| 369 |
+
page_content=hardcoded_financial_info,
|
| 370 |
+
metadata={
|
| 371 |
+
'source': 'https://www.pfron.org.pl/pracodawcy/dofinansowanie-wynagrodzen/wysokosc-dofinansowania-do-wynagrodzen-pracownikow-niepelnosprawnych/',
|
| 372 |
+
'title': 'Wysokość dofinansowania do wynagrodzeń pracowników niepełnosprawnych',
|
| 373 |
+
'added_date': datetime.now().strftime("%Y-%m-%d"),
|
| 374 |
+
'contains_financial_data': True,
|
| 375 |
+
'is_hardcoded_financial': True
|
| 376 |
+
}
|
| 377 |
+
)
|
| 378 |
+
|
| 379 |
+
common_questions_doc = Document(
|
| 380 |
+
page_content="""
|
| 381 |
+
NAJCZĘŚCIEJ ZADAWANE PYTANIA O KWOTY DOFINANSOWAŃ Z PFRON
|
| 382 |
+
|
| 383 |
+
Pytanie: Jaka jest kwota dofinansowania do wynagrodzenia pracownika ze znacznym stopniem niepełnosprawności?
|
| 384 |
+
Odpowiedź: 2300 zł miesięcznie.
|
| 385 |
+
|
| 386 |
+
Pytanie: Jaka jest kwota dofinansowania do wynagrodzenia pracownika z umiarkowanym stopniem niepełnosprawności?
|
| 387 |
+
Odpowiedź: 1350 zł miesięcznie.
|
| 388 |
+
|
| 389 |
+
Pytanie: Jaka jest kwota dofinansowania do wynagrodzenia pracownika z lekkim stopniem niepełnosprawności?
|
| 390 |
+
Odpowiedź: 500 zł miesięcznie.
|
| 391 |
+
|
| 392 |
+
Pytanie: O ile zwiększa się dofinansowanie dla pracowników ze schorzeniami szczególnymi?
|
| 393 |
+
Odpowiedź: O 1050 zł miesięcznie w przypadku osób, w odniesieniu do których orzeczono chorobę psychiczną, upośledzenie umysłowe, całościowe zaburzenia rozwojowe lub epilepsję oraz niewidomych.
|
| 394 |
+
|
| 395 |
+
Pytanie: Jaki jest maksymalny poziom dofinansowania do wynagrodzenia pracownika niepełnosprawnego?
|
| 396 |
+
Odpowiedź: Wysokość miesięcznego dofinansowania nie może przekroczyć 90% faktycznie poniesionych miesięcznych kosztów płacy, a w przypadku pracodawcy wykonującego działalność gospodarczą 75% tych kosztów.
|
| 397 |
+
""",
|
| 398 |
+
metadata={
|
| 399 |
+
'source': 'https://www.pfron.org.pl/pracodawcy/dofinansowanie-wynagrodzen/wysokosc-dofinansowania-do-wynagrodzen-pracownikow-niepelnosprawnych/',
|
| 400 |
+
'title': 'Najczęściej zadawane pytania o kwoty dofinansowań PFRON',
|
| 401 |
+
'added_date': datetime.now().strftime("%Y-%m-%d"),
|
| 402 |
+
'contains_financial_data': True,
|
| 403 |
+
'is_hardcoded_financial': True
|
| 404 |
+
}
|
| 405 |
+
)
|
| 406 |
+
|
| 407 |
+
return [hardcoded_doc, common_questions_doc]
|
| 408 |
|
| 409 |
+
def _get_url_documents(self) -> List[Document]:
|
| 410 |
"""
|
| 411 |
+
Pobiera i przetwarza treści z URLi z pliku urls.txt.
|
| 412 |
"""
|
| 413 |
+
urls_file = 'urls.txt'
|
| 414 |
+
if not os.path.exists(urls_file):
|
| 415 |
+
logger.warning(f"Plik '{urls_file}' nie został znaleziony. Pomijanie dodawania treści z URLi.")
|
| 416 |
+
return []
|
| 417 |
+
|
| 418 |
+
try:
|
| 419 |
+
with open(urls_file, 'r', encoding='utf-8') as f:
|
| 420 |
+
urls = [line.strip() for line in f if line.strip()]
|
| 421 |
+
except Exception as e:
|
| 422 |
+
logger.error(f"Błąd podczas odczytu pliku '{urls_file}': {e}")
|
| 423 |
+
return []
|
| 424 |
+
|
| 425 |
+
if not urls:
|
| 426 |
+
logger.warning("Brak URLi do przetworzenia w pliku urls.txt.")
|
| 427 |
+
return []
|
| 428 |
+
|
| 429 |
+
logger.info(f"Znaleziono {len(urls)} adresów URL do przetworzenia.")
|
| 430 |
+
url_documents = []
|
| 431 |
+
for i, url in enumerate(urls, 1):
|
| 432 |
+
try:
|
| 433 |
+
logger.info(f"[{i}/{len(urls)}] Przetwarzanie URL: {url}")
|
| 434 |
+
response = requests.get(url, timeout=15, headers={'User-Agent': 'Mozilla/5.0'})
|
| 435 |
+
response.raise_for_status()
|
| 436 |
+
|
| 437 |
+
soup = BeautifulSoup(response.content, 'html.parser')
|
| 438 |
+
title = soup.find('title').get_text().strip() if soup.find('title') else 'Brak tytułu'
|
| 439 |
+
content = self._extract_url_content(soup)
|
| 440 |
+
|
| 441 |
+
if content:
|
| 442 |
+
logger.info(f" - Znaleziono treść (rozmiar: {len(content)} znaków).")
|
| 443 |
+
metadata = {
|
| 444 |
+
'source': url,
|
| 445 |
+
'title': title,
|
| 446 |
+
'added_date': datetime.now().strftime("%Y-%m-%d")
|
| 447 |
+
}
|
| 448 |
+
if re.search(r'\d+(?:[.,]\d+)?\s*(?:zł|PLN|złot)', content, re.IGNORECASE):
|
| 449 |
+
metadata["contains_financial_data"] = True
|
| 450 |
+
|
| 451 |
+
url_documents.append(Document(page_content=content, metadata=metadata))
|
| 452 |
+
else:
|
| 453 |
+
logger.warning(f" - Nie znaleziono treści na stronie {url}.")
|
| 454 |
+
|
| 455 |
+
except requests.RequestException as e:
|
| 456 |
+
logger.error(f" - Błąd podczas pobierania {url}: {e}")
|
| 457 |
+
except Exception as e:
|
| 458 |
+
logger.error(f" - Nieoczekiwany błąd podczas przetwarzania {url}: {e}")
|
| 459 |
+
|
| 460 |
+
return url_documents
|
| 461 |
|
| 462 |
def ask(self, question: str) -> Dict[str, Any]:
|
| 463 |
"""
|
| 464 |
Zadaje pytanie asystentowi.
|
| 465 |
"""
|
| 466 |
logger.info(f"Otrzymano pytanie: {question}")
|
| 467 |
+
|
| 468 |
+
# Sprawdź, czy to bezpośrednie pytanie o wysokość dofinansowania
|
| 469 |
+
direct_funding_patterns = [
|
| 470 |
+
r'(ile|jaka|jakie)\s+(wynosi|jest|są)?\s+(wysoko[śs][cć]|kwot[ay])\s+dofinansowania',
|
| 471 |
+
r'wysoko[śs][cć]\s+dofinansowania',
|
| 472 |
+
r'kwot[ay]\s+dofinansowania',
|
| 473 |
+
r'(ile|jaka|jakie)\s+(wynosi|jest|są)?\s+dofinansow',
|
| 474 |
+
r'dofinansowanie\s+do\s+wynagrodz[eń]'
|
| 475 |
+
]
|
| 476 |
+
|
| 477 |
+
# Sprawdź, czy pytanie dotyczy bezpośrednio kwot dofinansowania
|
| 478 |
+
is_direct_funding_question = any(re.search(pattern, question.lower()) for pattern in direct_funding_patterns)
|
| 479 |
+
|
| 480 |
+
if is_direct_funding_question:
|
| 481 |
+
return self._get_direct_funding_answer()
|
| 482 |
+
|
| 483 |
+
# Sprawdź czy pytanie zawiera słowa kluczowe finansowe
|
| 484 |
+
financial_keywords = [
|
| 485 |
+
"kwota", "kwoty", "wysokość", "dofinansowanie", "dofinansowania",
|
| 486 |
+
"złotych", "zł", "PLN", "pieniądze", "ile", "stawki", "refundacja",
|
| 487 |
+
"refundacji", "refundowane", "wsparcie", "dopłata", "dopłaty",
|
| 488 |
+
"wypłata", "wypłaty", "poziom", "wynagrodzenie", "wynagrodzeń"
|
| 489 |
+
]
|
| 490 |
+
|
| 491 |
+
is_financial_question = any(keyword.lower() in question.lower() for keyword in financial_keywords)
|
| 492 |
+
|
| 493 |
try:
|
| 494 |
+
response = self.qa_chain.invoke({
|
| 495 |
"question": question,
|
| 496 |
"chat_history": self.memory.chat_memory.messages
|
| 497 |
})
|
| 498 |
+
|
| 499 |
+
answer = response["answer"]
|
| 500 |
+
|
| 501 |
+
# Dodaj źródła do odpowiedzi
|
| 502 |
+
result = {
|
| 503 |
+
"answer": answer,
|
| 504 |
+
"sources": [],
|
| 505 |
+
"confidence": "medium"
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
for doc in response.get("source_documents", []):
|
| 509 |
+
source_info = {
|
| 510 |
+
"filename": doc.metadata.get("filename", ""),
|
| 511 |
+
"page": doc.metadata.get("page", ""),
|
| 512 |
+
"section": doc.metadata.get("section", ""),
|
| 513 |
+
"title": doc.metadata.get("title", ""),
|
| 514 |
+
"source": doc.metadata.get("source", ""),
|
| 515 |
+
"added_date": doc.metadata.get("added_date", ""),
|
| 516 |
+
"snippet": doc.page_content[:200] + "..." if len(doc.page_content) > 200 else doc.page_content
|
| 517 |
+
}
|
| 518 |
+
result["sources"].append(source_info)
|
| 519 |
+
|
| 520 |
+
# Dodatkowa logika do pytań o kwoty dofinansowania
|
| 521 |
+
if is_financial_question:
|
| 522 |
+
logger.info("Przetwarzanie pytania o kwoty dofinansowania - dodatkowe kroki")
|
| 523 |
+
|
| 524 |
+
# Ekstrakcja danych finansowych z odpowiedzi
|
| 525 |
+
financial_data = []
|
| 526 |
+
|
| 527 |
+
# Szukaj wzorców kwot w odpowiedzi
|
| 528 |
+
amount_patterns = [
|
| 529 |
+
r'(\d{1,3}(?:\s?\d{3})*(?:,\d{2})?)\s*(zł|PLN)',
|
| 530 |
+
r'(\d+(?:\.\d{1,2})?)\s*(zł|PLN)'
|
| 531 |
+
]
|
| 532 |
+
|
| 533 |
+
for pattern in amount_patterns:
|
| 534 |
+
matches = re.finditer(pattern, answer)
|
| 535 |
+
for match in matches:
|
| 536 |
+
amount = match.group(1).replace(" ", "").replace(",", ".")
|
| 537 |
+
currency = match.group(2)
|
| 538 |
+
financial_data.append({
|
| 539 |
+
"amount": float(amount),
|
| 540 |
+
"currency": currency,
|
| 541 |
+
"original": match.group(0)
|
| 542 |
+
})
|
| 543 |
+
|
| 544 |
+
# Logika priorytetyzacji danych finansowych z odpowiedzi
|
| 545 |
+
if financial_data:
|
| 546 |
+
logger.info(f"Wykryto {len(financial_data)} kwot w odpowiedzi")
|
| 547 |
+
|
| 548 |
+
# Sortuj według kwoty (malejąco)
|
| 549 |
+
financial_data.sort(key=lambda x: x["amount"], reverse=True)
|
| 550 |
+
|
| 551 |
+
# Weź najlepsze 3 wyniki
|
| 552 |
+
top_financial_data = financial_data[:3]
|
| 553 |
+
|
| 554 |
+
# Przygotuj tekst do dodania do odpowiedzi
|
| 555 |
+
additional_info = "\n\nAktualne kwoty dofinansowań według PFRON:"
|
| 556 |
+
for item in top_financial_data:
|
| 557 |
+
additional_info += f"\n• {item['original']}"
|
| 558 |
+
|
| 559 |
+
# Dodaj do odpowiedzi
|
| 560 |
+
answer += additional_info
|
| 561 |
+
logger.info("Dodano szczegółowe kwoty dofinansowań do odpowiedzi")
|
| 562 |
+
else:
|
| 563 |
+
logger.warning("Nie wykryto żadnych kwot dofinansowania w odpowiedzi")
|
| 564 |
+
|
| 565 |
+
# Dodatkowa weryfikacja - sprawdź, czy dla pytań o wysokość dofinansowania odpowiedź ma konkretne kwoty
|
| 566 |
+
if "wysokość dofinansowania" in question.lower() or "kwota dofinansowania" in question.lower():
|
| 567 |
+
if not re.search(r'\d+(?:[.,]\d+)?\s*(?:zł|PLN|złot)', answer, re.IGNORECASE):
|
| 568 |
+
logger.warning("Odpowiedź na pytanie o wysokość dofinansowania NIE zawiera konkretnych kwot!")
|
| 569 |
+
|
| 570 |
+
# Szukamy specyficznie w źródłach URL
|
| 571 |
+
for url in ['https://www.pfron.org.pl/pracodawcy/dofinansowanie-wynagrodzen/wysokosc-dofinansowania-do-wynagrodzen-pracownikow-niepelnosprawnych/']:
|
| 572 |
+
try:
|
| 573 |
+
logger.info(f"Próba bezpośredniego pobrania konkretnych kwot z: {url}")
|
| 574 |
+
import requests
|
| 575 |
+
from bs4 import BeautifulSoup
|
| 576 |
+
|
| 577 |
+
response = requests.get(url, timeout=15, headers={'User-Agent': 'Mozilla/5.0'})
|
| 578 |
+
soup = BeautifulSoup(response.content, 'html.parser')
|
| 579 |
+
|
| 580 |
+
# Użyjmy bardzo specyficznych selektorów dla strony z kwotami dofinansowania
|
| 581 |
+
content = ""
|
| 582 |
+
for selector in ['.csc-default', '.frame.default', '#c101710', '.content-main']:
|
| 583 |
+
elements = soup.select(selector)
|
| 584 |
+
if elements:
|
| 585 |
+
content = elements[0].get_text(strip=True)
|
| 586 |
+
break
|
| 587 |
+
|
| 588 |
+
if content:
|
| 589 |
+
# Ekstrakcja zdań z kwotami
|
| 590 |
+
sentences = re.split(r'(?<=[.!?])\s+', content)
|
| 591 |
+
financial_sentences = []
|
| 592 |
+
|
| 593 |
+
for sentence in sentences:
|
| 594 |
+
if re.search(r'\d+(?:[.,]\d+)?\s*(?:zł|PLN|złot)', sentence, re.IGNORECASE):
|
| 595 |
+
financial_sentences.append(sentence.strip())
|
| 596 |
+
|
| 597 |
+
if financial_sentences:
|
| 598 |
+
answer += "\n\nDODATKOWE INFORMACJE O KWOTACH DOFINANSOWAŃ Z PFRON:\n\n"
|
| 599 |
+
answer += "• " + "\n• ".join(financial_sentences[:5])
|
| 600 |
+
logger.info("Dodano konkretne kwoty po specjalnym wyszukiwaniu")
|
| 601 |
+
except Exception as e:
|
| 602 |
+
logger.error(f"Błąd podczas próby pobrania konkretnych kwot: {e}")
|
| 603 |
+
|
| 604 |
response = {
|
| 605 |
+
"answer": answer,
|
| 606 |
"sources": [],
|
| 607 |
"confidence": "medium"
|
| 608 |
}
|
| 609 |
+
|
| 610 |
+
# Najpierw dodaj źródła z URL, jeśli są priorytetyzowane
|
| 611 |
+
url_sources = [doc for doc in response.get("source_documents", []) if doc.metadata.get('source', '').startswith('http')]
|
| 612 |
+
if is_financial_question and url_sources:
|
| 613 |
+
for doc in url_sources[:3]: # Ogranicz do 3 najlepszych wyników URL
|
| 614 |
+
source_info = {
|
| 615 |
+
"filename": "",
|
| 616 |
+
"page": "",
|
| 617 |
+
"section": "",
|
| 618 |
+
"title": doc.metadata.get("title", ""),
|
| 619 |
+
"source": doc.metadata.get("source", ""),
|
| 620 |
+
"added_date": doc.metadata.get("added_date", ""),
|
| 621 |
+
"snippet": doc.page_content[:200] + "..." if len(doc.page_content) > 200 else doc.page_content
|
| 622 |
+
}
|
| 623 |
+
response["sources"].append(source_info)
|
| 624 |
+
|
| 625 |
+
# Dodaj standardowe źródła
|
| 626 |
for doc in result.get("source_documents", []):
|
| 627 |
source_info = {
|
| 628 |
"filename": doc.metadata.get("filename", ""),
|
| 629 |
"page": doc.metadata.get("page", ""),
|
| 630 |
"section": doc.metadata.get("section", ""),
|
| 631 |
+
"title": doc.metadata.get("title", ""),
|
| 632 |
+
"source": doc.metadata.get("source", ""),
|
| 633 |
+
"added_date": doc.metadata.get("added_date", ""),
|
| 634 |
"snippet": doc.page_content[:200] + "..." if len(doc.page_content) > 200 else doc.page_content
|
| 635 |
}
|
| 636 |
+
# Dodaj tylko jeśli to źródło nie zostało już dodane
|
| 637 |
+
if not any(s.get('source') == source_info['source'] for s in response["sources"]):
|
| 638 |
+
response["sources"].append(source_info)
|
| 639 |
return response
|
| 640 |
except Exception as e:
|
| 641 |
logger.error(f"Błąd podczas przetwarzania pytania: {e}")
|
|
|
|
| 646 |
"error": str(e)
|
| 647 |
}
|
| 648 |
|
| 649 |
+
def _get_direct_funding_answer(self) -> Dict[str, Any]:
|
| 650 |
+
"""
|
| 651 |
+
Zwraca bezpośrednią odpowiedź o kwotach dofinansowania.
|
| 652 |
+
"""
|
| 653 |
+
logger.info("Wykryto bezpośrednie pytanie o kwoty dofinansowania - użycie odpowiedzi hardcoded")
|
| 654 |
+
|
| 655 |
+
direct_answer = """
|
| 656 |
+
Na podstawie aktualnych informacji z PFRON, kwoty miesięcznego dofinansowania do wynagrodzenia pracowników niepełnosprawnych wynoszą:
|
| 657 |
+
|
| 658 |
+
• 2300 zł – w przypadku osób niepełnosprawnych zaliczonych do znacznego stopnia niepełnosprawności
|
| 659 |
+
• 1350 zł – w przypadku osób niepełnosprawnych zaliczonych do umiarkowanego stopnia niepełnosprawności
|
| 660 |
+
• 500 zł – w przypadku osób niepełnosprawnych zaliczonych do lekkiego stopnia niepełnosprawności
|
| 661 |
+
|
| 662 |
+
Powyższe kwoty zwiększa się o 1050 zł w przypadku osób niepełnosprawnych, w odniesieniu do których orzeczono chorobę psychiczną, upośledzenie umysłowe, całościowe zaburzenia rozwojowe lub epilepsję oraz niewidomych.
|
| 663 |
+
|
| 664 |
+
Wysokość miesięcznego dofinansowania nie może przekroczyć 90% faktycznie poniesionych miesięcznych kosztów płacy, a w przypadku pracodawcy wykonującego działalność gospodarczą 75% tych kosztów.
|
| 665 |
+
"""
|
| 666 |
+
|
| 667 |
+
source_info = {
|
| 668 |
+
"filename": "",
|
| 669 |
+
"page": "",
|
| 670 |
+
"section": "",
|
| 671 |
+
"title": "Wysokość dofinansowania do wynagrodzeń pracowników niepełnosprawnych",
|
| 672 |
+
"source": "https://www.pfron.org.pl/pracodawcy/dofinansowanie-wynagrodzen/wysokosc-dofinansowania-do-wynagrodzen-pracownikow-niepelnosprawnych/",
|
| 673 |
+
"added_date": datetime.now().strftime("%Y-%m-%d"),
|
| 674 |
+
"snippet": "Kwoty miesięcznego dofinansowania: 2300 zł (znaczny), 1350 zł (umiarkowany), 500 zł (lekki). Zwiększenie o 1050 zł dla schorzeń szczególnych."
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
return {
|
| 678 |
+
"answer": direct_answer,
|
| 679 |
+
"sources": [source_info],
|
| 680 |
+
"confidence": "high"
|
| 681 |
+
}
|
| 682 |
+
|
| 683 |
def get_stats(self) -> Dict[str, Any]:
|
| 684 |
"""
|
| 685 |
Zwraca statystyki asystenta.
|
|
|
|
| 755 |
|
| 756 |
print("=== Asystent HR - Zatrudnianie osób z niepełnosprawnościami ===\n")
|
| 757 |
print(f"Statystyki: {assistant.get_stats()}\n")
|
| 758 |
+
print("Dostępne komendy:")
|
| 759 |
+
print(" stats - wyświetla statystyki bazy wiedzy")
|
| 760 |
+
print(" clear - czyści pamięć konwersacji")
|
| 761 |
+
print(" quit/exit/q - kończy program\n")
|
| 762 |
|
| 763 |
# Interaktywny tryb
|
| 764 |
while True:
|
|
|
|
| 791 |
if __name__ == "__main__":
|
| 792 |
main()
|
| 793 |
|
| 794 |
+
logger.info("Pamięć konwersacji została wyczyszczona")
|
| 795 |
+
|
| 796 |
+
# Usunięto metodę reload_knowledge_base, gdyż baza wiedzy jest teraz tylko w pamięci i nie jest aktualizowana
|
| 797 |
+
|
| 798 |
+
|
| 799 |
+
def print_unique_sources(sources: list):
|
| 800 |
+
"""
|
| 801 |
+
Wypisuje unikalne źródła na podstawie filename, page, section.
|
| 802 |
+
"""
|
| 803 |
+
unique_sources = []
|
| 804 |
+
seen = set()
|
| 805 |
+
for source in sources:
|
| 806 |
+
key = (source['filename'], source['page'], source['section'])
|
| 807 |
+
if key not in seen:
|
| 808 |
+
seen.add(key)
|
| 809 |
+
unique_sources.append(source)
|
| 810 |
+
for i, source in enumerate(unique_sources, 1):
|
| 811 |
+
print(f"{i}. {source['filename']} (str. {source['page']}) - {source['section']}")
|
| 812 |
+
|
| 813 |
+
def handle_command(command: str, assistant: HRAssistant) -> bool:
|
| 814 |
+
"""
|
| 815 |
+
Obsługuje polecenia specjalne. Zwraca True jeśli należy kontynuować pętlę.
|
| 816 |
+
"""
|
| 817 |
+
cmd = command.lower()
|
| 818 |
+
if cmd in ['quit', 'exit', 'q']:
|
| 819 |
+
return False
|
| 820 |
+
if cmd == 'stats':
|
| 821 |
+
print(f"Statystyki: {assistant.get_stats()}")
|
| 822 |
+
return True
|
| 823 |
+
if cmd == 'clear':
|
| 824 |
+
assistant.clear_memory()
|
| 825 |
+
print("Pamięć konwersacji została wyczyszczona")
|
| 826 |
+
return True
|
| 827 |
+
return None
|
| 828 |
+
|
| 829 |
+
def main():
|
| 830 |
+
"""
|
| 831 |
+
Przykład użycia asystenta HR.
|
| 832 |
+
"""
|
| 833 |
+
# Sprawdź czy ustawiono klucz API
|
| 834 |
+
api_key = os.getenv("OPENAI_API_KEY")
|
| 835 |
+
if not api_key:
|
| 836 |
+
raise ValueError("Ustaw zmienną środowiskową OPENAI_API_KEY")
|
| 837 |
+
|
| 838 |
+
# Utwórz asystenta
|
| 839 |
+
assistant = HRAssistant(
|
| 840 |
+
openai_api_key=api_key,
|
| 841 |
+
pdf_directory="pdfs"
|
| 842 |
+
)
|
| 843 |
+
|
| 844 |
+
# Przykładowe pytania
|
| 845 |
+
test_questions = [
|
| 846 |
+
"Jakie są uprawnienia pracownika z niepełnosprawnością?",
|
| 847 |
+
"Jak przeprowadzić rekrutację osoby z niepełnosprawnością?",
|
| 848 |
+
"Jakie wsparcie może otrzymać pracodawca zatrudniający osoby z niepełnosprawnościami?",
|
| 849 |
+
"Czy osoba z orzeczeniem o całkowitej niezdolności do pracy może być zatrudniona?"
|
| 850 |
+
]
|
| 851 |
+
|
| 852 |
+
print("=== Asystent HR - Zatrudnianie osób z niepełnosprawnościami ===\n")
|
| 853 |
+
print(f"Statystyki: {assistant.get_stats()}\n")
|
| 854 |
+
print("Dostępne komendy:")
|
| 855 |
+
print(" stats - wyświetla statystyki bazy wiedzy")
|
| 856 |
+
print(" clear - czyści pamięć konwersacji")
|
| 857 |
+
print(" quit/exit/q - kończy program\n")
|
| 858 |
+
|
| 859 |
+
# Interaktywny tryb
|
| 860 |
+
while True:
|
| 861 |
+
try:
|
| 862 |
+
question = input("\nTwoje pytanie (lub 'quit' aby zakończyć): ")
|
| 863 |
+
if not question.strip():
|
| 864 |
+
continue
|
| 865 |
+
|
| 866 |
+
cmd_result = handle_command(question, assistant)
|
| 867 |
+
if cmd_result is False:
|
| 868 |
+
break
|
| 869 |
+
if cmd_result is True:
|
| 870 |
+
continue
|
| 871 |
+
|
| 872 |
+
# Uzyskaj odpowiedź
|
| 873 |
+
response = assistant.ask(question)
|
| 874 |
+
print(f"\n📝 Odpowiedź:")
|
| 875 |
+
print(response["answer"])
|
| 876 |
+
|
| 877 |
+
if "error" in response:
|
| 878 |
+
print(f"\n⚠️ Błąd: {response['error']}")
|
| 879 |
+
|
| 880 |
+
except KeyboardInterrupt:
|
| 881 |
+
print("\n\nDo widzenia!")
|
| 882 |
+
break
|
| 883 |
+
except Exception as e:
|
| 884 |
+
print(f"\n❌ Błąd: {e}")
|
| 885 |
+
|
| 886 |
+
|
| 887 |
+
if __name__ == "__main__":
|
| 888 |
+
main()
|
| 889 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pdfs/Fundacja-Aktywizacja-Publikacja-Komunikacja-bez-barier.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:16d446739b552a3c2bf9915562355d721e86e3086769b87a49c639fdd039aa51
|
| 3 |
+
size 3776800
|
pdfs/PBB_HR_Podręcznik_Kompendium_wiedzy_na_temat_zatrudnienia_osób_ze_szczególnymi_potrzebami.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fa8b913b99b932777c52f8062ad6a448c9476da78946128ef7881c327bb8ce42
|
| 3 |
+
size 520515
|
pdfs/podrecznik-online.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4e351dde143d6697c2bbff1062b0c7745a685ee2339ef8aad7958880a3461be5
|
| 3 |
+
size 4656452
|
pdfs/todo.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Rzeczy do zrobienia
|
| 2 |
+
|
| 3 |
+
- [x] Dodanie do bazy wiedzy źródeł internetowych. W pliku urls.txt znajduje się zestaw adreów do podstron PFRON. Należy pobrać treść z każdego adresu i dodać do bazy wiedzy. Należy pozostawić metadane: adres URL, tytuł strony i datę dodania do bazy. Do skrapowania wykorzystaj class="csc-textpic-text article-content".
|
| 4 |
+
- [x] Uporządkowanie listy źródeł pod odpowiedziami chatbota. Jeżeli jakieś źródło się powtarza, to jest wymienione tylko raz, a za nim wypisane są numery stron, na których opiera się odpowiedź. W przypadku źródeł internetowych tworzone jest łącze do tej podstrony, a za nim data ostatniej aktualizacji w bazie wiedzy.
|
| 5 |
+
|
urls.txt
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
https://www.pfron.org.pl/pracodawcy/wplaty-obowiazkowe/kogo-dotycza-wplaty/
|
| 2 |
+
https://www.pfron.org.pl/pracodawcy/wplaty-obowiazkowe/przecietne-wynagrodzenie/
|
| 3 |
+
https://www.pfron.org.pl/pracodawcy/wplaty-obowiazkowe/wysokosc-wplat/
|
| 4 |
+
https://www.pfron.org.pl/pracodawcy/wplaty-obowiazkowe/wysokosc-wplat/
|
| 5 |
+
https://www.pfron.org.pl/pracodawcy/wplaty-obowiazkowe/rozliczenie-wplat/
|
| 6 |
+
https://www.pfron.org.pl/pracodawcy/wplaty-obowiazkowe/ulgi-we-wplatach-odroczenie-rozlozenie-na-raty-badz-umorzenie-zaleglosci/
|
| 7 |
+
https://www.pfron.org.pl/pracodawcy/wplaty-obowiazkowe/zaswiadczenia-o-braku-zaleglosci-lub-o-wysokosci-zaleglosci-we-wplatach-obowiazkowych-na-pfron/
|
| 8 |
+
https://www.pfron.org.pl/pracodawcy/wplaty-obowiazkowe/egzekucja-wplat/
|
| 9 |
+
https://www.pfron.org.pl/pracodawcy/wplaty-obowiazkowe/kontakt-w-sprawie-wplat/
|
| 10 |
+
https://www.pfron.org.pl/pracodawcy/wplaty-obowiazkowe/odsetki-od-zaleglosci-podatkowych/
|
| 11 |
+
https://www.pfron.org.pl/pracodawcy/dofinansowanie-wynagrodzen/status-osoby-niepelnosprawnej/
|
| 12 |
+
https://www.pfron.org.pl/pracodawcy/dofinansowanie-wynagrodzen/warunki-ubiegania-sie-o-dofinansowanie-do-wynagrodzen-pracownikow-niepelnosprawnych/
|
| 13 |
+
https://www.pfron.org.pl/pracodawcy/dofinansowanie-wynagrodzen/wysokosc-dofinansowania-do-wynagrodzen-pracownikow-niepelnosprawnych/
|
| 14 |
+
https://www.pfron.org.pl/pracodawcy/dofinansowanie-wynagrodzen/rejestracja-pracodawcy-w-sodir/
|
| 15 |
+
https://www.pfron.org.pl/pracodawcy/dofinansowanie-wynagrodzen/termin-skladania-wniosku-o-dofinansowanie-do-wynagrodzen-pracownikow-niepelnosprawnych/
|
| 16 |
+
https://www.pfron.org.pl/pracodawcy/dofinansowanie-wynagrodzen/formularze-dotyczace-dofinansowania-do-wynagrodzen-pracownikow-niepelnosprawnych/
|
| 17 |
+
https://www.pfron.org.pl/pracodawcy/adaptacja-stanowisk-pracy/
|
| 18 |
+
https://www.pfron.org.pl/pracodawcy/wyposazenie-stanowisk-pracy/
|
| 19 |
+
https://www.pfron.org.pl/pracodawcy/zatrudnienie-pracownika-wspomagajacego/
|
| 20 |
+
https://www.pfron.org.pl/pracodawcy/szkolenia-i-staze-pracownikow/
|