jaczad commited on
Commit
6805005
·
1 Parent(s): 4e4c288

Bibliografia już się wyświetla prawidłowo

Browse files
.DS_Store ADDED
Binary file (6.15 kB). View file
 
README.md CHANGED
@@ -11,4 +11,53 @@ license: cc-by-sa-4.0
11
  short_description: Asystent zatrudniania osób z niepełnosprawnościami
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  short_description: Asystent zatrudniania osób z niepełnosprawnościami
12
  ---
13
 
14
+
15
+ # Asystent HR dla pracodawców zatrudniających osoby z niepełnosprawnościami
16
+
17
+ ## Funkcjonalności
18
+ - Wykorzystuje dokumenty PDF jako bazę wiedzy, przetwarza je na wektorową bazę danych (FAISS) w pamięci
19
+ - Pozwala na zadawanie pytań w języku polskim z konwersacyjną pamięcią kontekstu (ChatOpenAI, model GPT-4o-mini)
20
+ - Odpowiedzi generowane są wyłącznie na podstawie treści dokumentów PDF, wybranych stron internetowych (z pliku `urls.txt`) oraz hardkodowanych fragmentów (np. wysokości dofinansowań PFRON)
21
+ - Każda odpowiedź zawiera źródło informacji (nazwa pliku PDF, strona, sekcja lub URL)
22
+ - Obsługuje interaktywny tryb konsolowy z komendami: `stats`, `clear`, `quit`/`exit`/`q`
23
+ - Przetwarza dokumenty PDF z zachowaniem struktury (chunkowanie sekcji, nagłówków, stron)
24
+ - Pobiera i przetwarza treści z wybranych stron internetowych (BeautifulSoup, requests)
25
+ - Loguje przebieg działania i błędy (logging)
26
+
27
+ ## Źródła wiedzy
28
+ - Pliki PDF z katalogu `pdfs/`
29
+ - Adresy URL z pliku `urls.txt`
30
+ - Hardkodowane fragmenty (np. wysokość dofinansowań PFRON)
31
+
32
+ ## Wymagania
33
+ - Python 3.10+
34
+ - Klucz API OpenAI (zmienna środowiskowa `OPENAI_API_KEY`)
35
+ - Zainstalowane pakiety: `langchain`, `langchain_openai`, `langchain_community`, `fitz` (PyMuPDF), `requests`, `beautifulsoup4`
36
+
37
+ ## Uruchomienie
38
+ 1. Ustaw zmienną środowiskową `OPENAI_API_KEY` z własnym kluczem OpenAI
39
+ 2. Umieść pliki PDF w katalogu `pdfs/`
40
+ 3. (Opcjonalnie) Dodaj adresy URL do pliku `urls.txt` (jeden w linii)
41
+ 4. Zainstaluj wymagane pakiety:
42
+ ```bash
43
+ pip install -r requirements.txt
44
+ ```
45
+ 5. Uruchom program:
46
+ ```bash
47
+ python hr_assistant.py
48
+ ```
49
+
50
+ ## Tryb interaktywny
51
+ Program uruchamia się w trybie konsolowym. Dostępne komendy:
52
+ - `stats` — wyświetla statystyki bazy wiedzy
53
+ - `clear` — czyści pamięć konwersacji
54
+ - `quit`/`exit`/`q` — kończy program
55
+
56
+ ## Dodatkowe informacje
57
+ - Odpowiedzi generowane są wyłącznie na podstawie załadowanych dokumentów i stron.
58
+ - Każda odpowiedź zawiera źródło (nazwa pliku PDF, strona, sekcja lub URL).
59
+ - Baza wiedzy jest ładowana do pamięci przy starcie programu i nie jest aktualizowana w trakcie działania.
60
+ - Logi działania i błędów zapisywane są na konsoli.
61
+
62
+ ## Autor
63
+ Jacek (2024-2025)
__pycache__/hr_assistant.cpython-312.pyc ADDED
Binary file (31.3 kB). View file
 
bibliografia.csv CHANGED
@@ -1,12 +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
 
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_finalna2014.pdf
2
+ "LISTA KONTROLNA DO OCENY ŚRODOWISKA PRACY POD KĄTEM DOSTOSOWANIA DO POTRZEB OSÓB NIEPEŁNOSPRAWNYCH; PFRON, CIOP PIB; Warszawa 2014";ListaKontrolna2014.pdf
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_wytyczne2014.pdf
4
+ "Kotowska L.; Prawo pracy. Pracownik niepełnosprawny; Państwowa Inspekcja Pracy; wydanie 2/2024, stan prawny marzec 2024";Wydawnictwo PIP - Niepelnosprawny pracownik.pdf
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-barier.pdf
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_-_podumowanie_przeglądu_procedur_zatrudnieniowych_Procedury_bez_barier.pdf
7
+ "Kompendium wiedzy na temat zatrudnienia osób ze szczególnymi potrzebami; Kancelaria Prezesa rady Ministrów";PBB_HR_Podręcznik_Kompendium_wiedzy_na_temat_zatrudnienia_osób_ze_szczególnymi_potrzebami.pdf
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";podrecznik-online.pdf
9
+ "Gawska A., Poradnik dla pracodawców, o tym jak tworzyć dostępne miejsce pracy. Fundacja Aktywizacja, Warszawa 2024";Fundacja-Aktywizacja_Poradnik-dla-pracodawcow-o-tym-jak-tworzyc-dostepne-miejsce-pracy.pdf
10
+ "Gruszczyńska A., Gruntowski M,. 5 kroków do zatrudnienia Osoby z niepełnosprawnością w procesie rekrutacji. Fundacja Aktywizacja, Warszawa 2024";5-krokow-do-zatrudnienia-online.pdf
11
+ "Gruszczyńska A., Gruntowski M., Osoba z niepełnosprawnością w Twojej firmie, Fundacja Aktywizacja, Warszawa 2024";Niezbednik-pracodawcy-online.pdf
12
+ "Gawska A. Pracodawca włączający jak skutecznie zatrudniać osoby z niepełnosprawnościami?, Warszawa 2025";artykuł ze strony koREKtora
chatbot.py CHANGED
@@ -1,5 +1,6 @@
1
  import gradio as gr
2
  import os
 
3
 
4
  # --- Próba importu HRAssistant ---
5
  try:
@@ -8,6 +9,24 @@ try:
8
  except ModuleNotFoundError as e:
9
  hr_import_error = str(e)
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  # --- Inicjalizacja asystenta ---
12
  def initialize_assistant():
13
  if hr_import_error:
@@ -59,26 +78,31 @@ def respond_to_query(message, history):
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})
 
1
  import gradio as gr
2
  import os
3
+ import csv
4
 
5
  # --- Próba importu HRAssistant ---
6
  try:
 
9
  except ModuleNotFoundError as e:
10
  hr_import_error = str(e)
11
 
12
+ # --- Wczytywanie bibliografii ---
13
+ def load_bibliography(file_path="bibliografia.csv"):
14
+ bibliography = {}
15
+ try:
16
+ with open(file_path, mode='r', encoding='utf-8') as csvfile:
17
+ reader = csv.reader(csvfile, delimiter=';')
18
+ for row in reader:
19
+ if len(row) == 2:
20
+ # Klucz to nazwa pliku bez rozszerzenia, wartość to pełny opis
21
+ bibliography[row[1].strip()] = row[0].strip()
22
+ except FileNotFoundError:
23
+ print(f"Plik {file_path} nie został znaleziony.")
24
+ except Exception as e:
25
+ print(f"Błąd podczas wczytywania pliku {file_path}: {e}")
26
+ return bibliography
27
+
28
+ bibliography_data = load_bibliography()
29
+
30
  # --- Inicjalizacja asystenta ---
31
  def initialize_assistant():
32
  if hr_import_error:
 
78
  'pages': set()
79
  }
80
 
81
+ if 'page' in source_meta and source_meta['page'] is not None:
 
82
  grouped_sources[source_key]['pages'].add(source_meta['page'])
83
 
84
  for key, data in grouped_sources.items():
85
  if data['type'] == 'pdf':
86
+ file_stem = data['meta'].get('file_stem', os.path.splitext(os.path.basename(key))[0])
87
+ display_name = bibliography_data.get(file_stem, os.path.basename(key))
88
+
89
  pages = sorted(list(data['pages']))
90
+ pages_str = ""
91
+ if pages:
92
+ if len(pages) == 1:
93
+ pages_str = f"str. {pages[0]}"
94
+ else:
95
+ pages_str = "str. " + ", ".join(map(str, pages))
96
+
97
+ answer += f"\n- {display_name} ({pages_str})" if pages_str else f"\n- {display_name}"
98
+
99
  elif data['type'] == 'url':
100
  title = data['meta'].get('title', key)
101
  url = key
102
  date = data['meta'].get('added_date', '')
103
  date_str = f" (dodano: {date})" if date else ""
104
  answer += f"\n- [{title}]({url}){date_str}"
105
+
106
 
107
  history.append({"role": "user", "content": message})
108
  history.append({"role": "assistant", "content": answer})
hr_assistant.py CHANGED
@@ -1,6 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
  Asystent HR dla pracodawców zatrudniających osoby z niepełnosprawnościami.
3
- Wykorzystuje dokumenty PDF jako bazę wiedzy z wektorową bazą danych w pamięci.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  """
5
 
6
  import os
@@ -8,6 +81,7 @@ import logging
8
  from typing import List, Dict, Any, Optional, Tuple
9
  from pathlib import Path
10
  import re
 
11
 
12
  # LangChain imports (aktualne na 2024-06)
13
  from langchain_text_splitters import RecursiveCharacterTextSplitter
@@ -253,22 +327,17 @@ class HRAssistant:
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,7 +348,7 @@ class HRAssistant:
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},
@@ -362,7 +431,7 @@ class HRAssistant:
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(
@@ -459,184 +528,80 @@ class HRAssistant:
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}")
642
  return {
@@ -646,39 +611,7 @@ class HRAssistant:
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
  """
@@ -702,17 +635,35 @@ Wysokość miesięcznego dofinansowania nie może przekroczyć 90% faktycznie po
702
 
703
  def print_unique_sources(sources: list):
704
  """
705
- Wypisuje unikalne źródła na podstawie filename, page, section.
 
706
  """
707
  unique_sources = []
708
  seen = set()
709
  for source in sources:
710
- key = (source['filename'], source['page'], source['section'])
711
  if key not in seen:
712
  seen.add(key)
713
  unique_sources.append(source)
714
  for i, source in enumerate(unique_sources, 1):
715
- print(f"{i}. {source['filename']} (str. {source['page']}) - {source['section']}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
716
 
717
  def handle_command(command: str, assistant: HRAssistant) -> bool:
718
  """
@@ -773,11 +724,17 @@ def main():
773
  if cmd_result is True:
774
  continue
775
 
 
776
  # Uzyskaj odpowiedź
777
  response = assistant.ask(question)
778
  print(f"\n📝 Odpowiedź:")
779
  print(response["answer"])
780
 
 
 
 
 
 
781
  if "error" in response:
782
  print(f"\n⚠️ Błąd: {response['error']}")
783
 
@@ -796,94 +753,15 @@ if __name__ == "__main__":
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
 
 
1
+ # --- Funkcje pomocnicze do obsługi źródeł bibliograficznych ---
2
+ import os
3
+ import csv
4
+ def load_bibliography(file_path="bibliografia.csv"):
5
+ """
6
+ Wczytuje dane bibliograficzne z pliku CSV i zwraca słownik: {file_stem: opis}
7
+ """
8
+ bibliography = {}
9
+ if os.path.exists(file_path):
10
+ with open(file_path, mode='r', encoding='utf-8') as csvfile:
11
+ reader = csv.reader(csvfile, delimiter=';')
12
+ for row in reader:
13
+ if len(row) == 2:
14
+ # Usuwanie cudzysłowów z obu kolumn
15
+ key = row[1].strip().replace('"', '')
16
+ value = row[0].strip().replace('"', '')
17
+ bibliography[key] = value
18
+ return bibliography
19
+
20
+ bibliography_data = load_bibliography()
21
+
22
+ def print_unique_sources(sources: list):
23
+ """
24
+ Wypisuje unikalne źródła na podstawie filename, page, section, zamieniając nazwę pliku na opis bibliograficzny jeśli to możliwe.
25
+ Jeśli źródło to URL lub hardcoded, wypisuje tytuł lub URL.
26
+ """
27
+ unique_sources = []
28
+ seen = set()
29
+ for source in sources:
30
+ key = (source.get('filename'), source.get('page'), source.get('section'))
31
+ if key not in seen:
32
+ seen.add(key)
33
+ unique_sources.append(source)
34
+ for i, source in enumerate(unique_sources, 1):
35
+ opis = None
36
+ filename = source.get('filename')
37
+ if filename:
38
+ file_stem = os.path.splitext(filename)[0]
39
+ # Szukaj opisu w bibliografii wg różnych wariantów
40
+ opis = bibliography_data.get(filename)
41
+ if not opis:
42
+ opis = bibliography_data.get(file_stem + '.pdf')
43
+ if not opis:
44
+ opis = bibliography_data.get(file_stem)
45
+ if not opis:
46
+ opis = filename # fallback: sama nazwa pliku
47
+ else:
48
+ # Jeśli nie ma filename, spróbuj użyć tytułu lub źródła (np. URL)
49
+ opis = source.get('title') or source.get('source') or "nieznane źródło"
50
+ page = source.get('page', '')
51
+ section = source.get('section', '')
52
+ print(f"{i}. {opis} (str. {page}) - {section}")
53
  """
54
  Asystent HR dla pracodawców zatrudniających osoby z niepełnosprawnościami.
55
+
56
+ Funkcjonalności:
57
+ - Wykorzystuje dokumenty PDF jako bazę wiedzy, przetwarza je na wektorową bazę danych (FAISS) w pamięci.
58
+ - Pozwala na zadawanie pytań w języku polskim z konwersacyjną pamięcią kontekstu (ChatOpenAI, model GPT-4o-mini).
59
+ - Odpowiedzi generowane są wyłącznie na podstawie treści dokumentów PDF, wybranych stron internetowych (z pliku urls.txt) oraz hardkodowanych fragmentów (np. wysokości dofinansowań PFRON).
60
+ - Każda odpowiedź zawiera źródło informacji (nazwa pliku PDF, strona, sekcja lub URL).
61
+ - Obsługuje interaktywny tryb konsolowy z komendami: stats, clear, quit/exit/q.
62
+ - Przetwarza dokumenty PDF z zachowaniem struktury (chunkowanie sekcji, nagłówków, stron).
63
+ - Pobiera i przetwarza treści z wybranych stron internetowych (BeautifulSoup, requests).
64
+ - Loguje przebieg działania i błędy (logging).
65
+
66
+ Źródła wiedzy:
67
+ - Pliki PDF z katalogu "pdfs/"
68
+ - Adresy URL z pliku "urls.txt"
69
+ - Hardkodowane fragmenty (np. wysokość dofinansowań PFRON)
70
+
71
+ Wymagania:
72
+ - Python 3.10+
73
+ - Klucz API OpenAI (zmienna środowiskowa OPENAI_API_KEY)
74
+ - Zainstalowane pakiety: langchain, langchain_openai, langchain_community, fitz (PyMuPDF), requests, beautifulsoup4
75
+
76
+ Autor: Jacek (2024-2025)
77
  """
78
 
79
  import os
 
81
  from typing import List, Dict, Any, Optional, Tuple
82
  from pathlib import Path
83
  import re
84
+ import csv
85
 
86
  # LangChain imports (aktualne na 2024-06)
87
  from langchain_text_splitters import RecursiveCharacterTextSplitter
 
327
 
328
  prompt_template = (
329
  "Jesteś ekspertem HR specjalizującym się w zatrudnianiu osób z niepełnosprawnościami w Polsce.\n"
330
+ "Twoja wiedza opiera się na dostarczonych dokumentach, które mogą zawierać oficjalne poradniki i treści ze stron internetowych.\n\n"
331
+ "Kontekst z dokumentów:\n{context}\n\n"
332
  "Historia rozmowy:\n{chat_history}\n\n"
333
  "Pytanie: {question}\n\n"
334
  "Instrukcje:\n"
335
+ "1. Odpowiadaj w języku polskim.\n"
336
+ "2. Bazuj wyłącznie na informacjach z dostarczonego kontekstu.\n"
337
+ "3. Jeśli nie masz informacji w kontekście, powiedz to wprost.\n"
338
+ "4. Podawaj konkretne i praktyczne porady.\n"
339
+ "5. Bądź pomocny i profesjonalny.\n"
340
+ "6. Zawsze podawaj źródło informacji (URL lub nazwa dokumentu PDF), jeśli jest dostępne w metadanych kontekstu.\n\n"
 
 
 
 
 
341
  "Odpowiedź:"
342
  )
343
  custom_prompt = PromptTemplate(
 
348
  llm=self.llm,
349
  retriever=self.vectorstore.as_retriever(
350
  search_type="similarity",
351
+ search_kwargs={"k": 8}
352
  ),
353
  memory=self.memory,
354
  combine_docs_chain_kwargs={"prompt": custom_prompt},
 
431
 
432
  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.
433
 
434
+ 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, zwanym dalej "pracodawcą wykonującym działalność gospodarczą", 75% tych kosztów.
435
  """
436
 
437
  hardcoded_doc = Document(
 
528
 
529
  return url_documents
530
 
531
+ def _extract_url_content(self, soup: BeautifulSoup) -> str:
532
  """
533
+ Wyodrębnia główną treść tekstową ze strony internetowej (obiektu BeautifulSoup).
534
+ Próbuje znaleźć kontenery z główną treścią, a jeśli to się nie uda, pobiera cały tekst.
535
  """
536
+ main_content = []
537
 
538
+ # Priorytetowe selektory dla stron PFRON i podobnych
539
+ priority_selectors = [
540
+ '.frame.default',
541
+ '.csc-default',
542
+ '#c101710',
543
+ '.content-main',
544
+ 'article',
545
+ 'main',
546
+ '.content',
547
+ '.main-content',
548
+ '.post-content',
549
+ '#content',
550
+ '#main'
551
  ]
552
 
553
+ found_content = False
554
+ for selector in priority_selectors:
555
+ elements = soup.select(selector)
556
+ if elements:
557
+ for element in elements:
558
+ main_content.append(element.get_text(separator='\n', strip=True))
559
+ found_content = True
560
+ break # Znaleziono treść, więc przerwij pętlę
 
 
 
 
 
 
 
561
 
562
+ # Jeśli nie znaleziono specyficznych kontenerów, pobierz cały tekst z body
563
+ if not found_content:
564
+ if soup.body:
565
+ main_content.append(soup.body.get_text(separator='\n', strip=True))
566
+
567
+ return "\n\n".join(main_content)
568
+
569
+ def ask(self, question: str) -> Dict[str, Any]:
570
+ """
571
+ Zadaje pytanie asystentowi.
572
+ """
573
+ logger.info(f"Otrzymano pytanie: {question}")
574
+
575
  try:
576
  response = self.qa_chain.invoke({
577
  "question": question,
578
  "chat_history": self.memory.chat_memory.messages
579
  })
580
+
581
  answer = response["answer"]
582
+
583
+ # Przygotuj odpowiedź z odpowiednimi źródłami
584
  result = {
585
  "answer": answer,
586
  "sources": [],
587
+ "confidence": "medium" # Domyślna pewność, można dostosować
588
  }
589
+
590
+ if "source_documents" in response:
591
+ for doc in response["source_documents"]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
592
  source_info = {
593
+ "filename": doc.metadata.get("filename", ""),
594
+ "page": doc.metadata.get("page", ""),
595
+ "section": doc.metadata.get("section", ""),
596
  "title": doc.metadata.get("title", ""),
597
  "source": doc.metadata.get("source", ""),
598
  "added_date": doc.metadata.get("added_date", ""),
599
  "snippet": doc.page_content[:200] + "..." if len(doc.page_content) > 200 else doc.page_content
600
  }
601
+ result["sources"].append(source_info)
602
+
603
+ return result
604
+
 
 
 
 
 
 
 
 
 
 
 
 
 
605
  except Exception as e:
606
  logger.error(f"Błąd podczas przetwarzania pytania: {e}")
607
  return {
 
611
  "error": str(e)
612
  }
613
 
614
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
615
 
616
  def get_stats(self) -> Dict[str, Any]:
617
  """
 
635
 
636
  def print_unique_sources(sources: list):
637
  """
638
+ Wypisuje unikalne źródła na podstawie filename, page, section, zamieniając nazwę pliku na opis bibliograficzny jeśli to możliwe.
639
+ Jeśli źródło to URL lub hardcoded, wypisuje tytuł lub URL.
640
  """
641
  unique_sources = []
642
  seen = set()
643
  for source in sources:
644
+ key = (source.get('filename'), source.get('page'), source.get('section'))
645
  if key not in seen:
646
  seen.add(key)
647
  unique_sources.append(source)
648
  for i, source in enumerate(unique_sources, 1):
649
+ opis = None
650
+ filename = source.get('filename')
651
+ if filename:
652
+ file_stem = os.path.splitext(filename)[0]
653
+ # Szukaj opisu w bibliografii wg różnych wariantów
654
+ opis = bibliography_data.get(filename)
655
+ if not opis:
656
+ opis = bibliography_data.get(file_stem + '.pdf')
657
+ if not opis:
658
+ opis = bibliography_data.get(file_stem)
659
+ if not opis:
660
+ opis = filename # fallback: sama nazwa pliku
661
+ else:
662
+ # Jeśli nie ma filename, spróbuj użyć tytułu lub źródła (np. URL)
663
+ opis = source.get('title') or source.get('source') or "nieznane źródło"
664
+ page = source.get('page', '')
665
+ section = source.get('section', '')
666
+ print(f"{i}. {opis} (str. {page}) - {section}")
667
 
668
  def handle_command(command: str, assistant: HRAssistant) -> bool:
669
  """
 
724
  if cmd_result is True:
725
  continue
726
 
727
+
728
  # Uzyskaj odpowiedź
729
  response = assistant.ask(question)
730
  print(f"\n📝 Odpowiedź:")
731
  print(response["answer"])
732
 
733
+ # Wyświetl unikalne źródła, jeśli są dostępne
734
+ if response.get("sources"):
735
+ print("\nŹródła:")
736
+ print_unique_sources(response["sources"])
737
+
738
  if "error" in response:
739
  print(f"\n⚠️ Błąd: {response['error']}")
740
 
 
753
  # Usunięto metodę reload_knowledge_base, gdyż baza wiedzy jest teraz tylko w pamięci i nie jest aktualizowana
754
 
755
 
756
+ # Wczytywanie danych bibliograficznych z pliku CSV
757
+ bibliography_data = {}
758
+ file_path = 'bibliography.csv'
759
+ if os.path.exists(file_path):
760
+ with open(file_path, mode='r', encoding='utf-8') as csvfile:
761
+ reader = csv.reader(csvfile, delimiter=';')
762
+ for row in reader:
763
+ if len(row) == 2:
764
+ bibliography_data[row[1].strip()] = row[0].strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
765
 
766
+ print(bibliography_data)
 
767
 
requirements.txt CHANGED
@@ -1,14 +1,12 @@
 
 
1
  gradio==5.24.0
2
- pandas
3
- pydantic==2.10.6
4
- python-docx
5
  langchain
6
  langchain-community
7
  langchain-openai
8
  openai
9
- docx2txt
10
- pypdf
11
- streamlit
12
  PyMuPDF
13
- sentence-transformers
14
- faiss-cpu
 
 
1
+ beautifulsoup4
2
+ faiss-cpu
3
  gradio==5.24.0
 
 
 
4
  langchain
5
  langchain-community
6
  langchain-openai
7
  openai
8
+ pydantic==2.10.6
 
 
9
  PyMuPDF
10
+ pypdf
11
+ requests
12
+ sentence-transformers