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 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
- return HRAssistant(openai_api_key=api_key, pdf_directory="pdfs")
 
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:**\n"
47
- for source in response["sources"]:
48
- answer += f"- {source.get('filename', 'Brak nazwy pliku')}, strona {source.get('page', 'Brak numeru strony')}\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 = 1000, chunk_overlap: int = 200):
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 i poradnikach dla pracodawców.\n\n"
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. Bądź pomocny i profesjonalny\n"
226
- "7. Gdy to możliwe, podaj źródło informacji (nazwę dokumentu)\n\n"
 
 
 
 
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": 5}
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("Ładowanie dokumentów PDF...")
 
308
 
 
 
309
  pdf_files = self._list_pdf_files()
310
  if not pdf_files:
311
- raise ValueError(f"Nie znaleziono plików PDF w katalogu: {self.pdf_directory}")
312
-
313
- logger.info(f"Znaleziono {len(pdf_files)} plików PDF")
314
- all_documents = []
315
- for pdf_file in pdf_files:
316
- logger.info(f"Przetwarzanie: {pdf_file.name}")
317
- documents = self.chunker._extract_pdf_structure(str(pdf_file))
318
- for doc in documents:
319
- doc.metadata["filename"] = pdf_file.name
320
- doc.metadata["file_stem"] = pdf_file.stem
321
- all_documents.extend(documents)
322
- logger.info(f"Wyekstraktowano {len(all_documents)} sekcji")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 _reload_if_pdfs_changed(self):
332
  """
333
- Przeładowuje embeddingi jeśli pojawiły się nowe/zmienione PDF-y.
334
  """
335
- if self._pdfs_changed():
336
- logger.info("Wykryto nowe lub zmienione pliki PDF. Przeładowuję bazę wiedzy...")
337
- self._load_and_process_documents()
338
- self._setup_qa_chain()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
 
340
  def ask(self, question: str) -> Dict[str, Any]:
341
  """
342
  Zadaje pytanie asystentowi.
343
  """
344
  logger.info(f"Otrzymano pytanie: {question}")
345
- self._reload_if_pdfs_changed()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  try:
347
- result = self.qa_chain.invoke({
348
  "question": question,
349
  "chat_history": self.memory.chat_memory.messages
350
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  response = {
352
- "answer": result["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
- response["sources"].append(source_info)
 
 
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/