jaczad commited on
Commit
fa696d9
·
1 Parent(s): 6960e35

Linki już działają

Browse files
Files changed (2) hide show
  1. app.py +163 -59
  2. scrap.py +175 -0
app.py CHANGED
@@ -1,64 +1,168 @@
1
  import gradio as gr
2
- from huggingface_hub import InferenceClient
3
-
4
- """
5
- For more information on `huggingface_hub` Inference API support, please check the docs: https://huggingface.co/docs/huggingface_hub/v0.22.2/en/guides/inference
6
- """
7
- client = InferenceClient("HuggingFaceH4/zephyr-7b-beta")
8
-
9
-
10
- def respond(
11
- message,
12
- history: list[tuple[str, str]],
13
- system_message,
14
- max_tokens,
15
- temperature,
16
- top_p,
17
- ):
18
- messages = [{"role": "system", "content": system_message}]
19
-
20
- for val in history:
21
- if val[0]:
22
- messages.append({"role": "user", "content": val[0]})
23
- if val[1]:
24
- messages.append({"role": "assistant", "content": val[1]})
25
-
26
- messages.append({"role": "user", "content": message})
27
-
28
- response = ""
29
-
30
- for message in client.chat_completion(
31
- messages,
32
- max_tokens=max_tokens,
33
- stream=True,
34
- temperature=temperature,
35
- top_p=top_p,
36
- ):
37
- token = message.choices[0].delta.content
38
-
39
- response += token
40
- yield response
41
-
42
-
43
- """
44
- For information on how to customize the ChatInterface, peruse the gradio docs: https://www.gradio.app/docs/chatinterface
45
- """
46
- demo = gr.ChatInterface(
47
- respond,
48
- additional_inputs=[
49
- gr.Textbox(value="You are a friendly Chatbot.", label="System message"),
50
- gr.Slider(minimum=1, maximum=2048, value=512, step=1, label="Max new tokens"),
51
- gr.Slider(minimum=0.1, maximum=4.0, value=0.7, step=0.1, label="Temperature"),
52
- gr.Slider(
53
- minimum=0.1,
54
- maximum=1.0,
55
- value=0.95,
56
- step=0.05,
57
- label="Top-p (nucleus sampling)",
58
- ),
59
- ],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  )
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  if __name__ == "__main__":
64
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
+ import uuid
3
+ from langchain_chroma import Chroma
4
+ from langchain_openai import OpenAIEmbeddings, ChatOpenAI
5
+ from langchain.chains import create_history_aware_retriever, create_retrieval_chain
6
+ from langchain.chains.combine_documents import create_stuff_documents_chain
7
+ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
8
+ from langchain_core.chat_history import BaseChatMessageHistory
9
+ from langchain_community.chat_message_histories import ChatMessageHistory
10
+ from langchain_core.runnables.history import RunnableWithMessageHistory
11
+
12
+ # --- 1. Inicjalizacja modeli i retrievera ---
13
+ llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.1)
14
+ embedder = OpenAIEmbeddings(model="text-embedding-3-small")
15
+ baza = Chroma(
16
+ collection_name="szuflada",
17
+ embedding_function=embedder,
18
+ persist_directory="./szuflada"
19
+ )
20
+ # Możliwe typy wyszukiwania w retrieverze Chroma:
21
+ # - "similarity" (domyślne, bez progu)
22
+ # - "mmr" (Maximal Marginal Relevance)
23
+ # - "similarity_score_threshold" (z progiem score_threshold)
24
+
25
+ # Przykład zmiany na standardowe wyszukiwanie podobieństw:
26
+ retriever = baza.as_retriever(
27
+ search_type="similarity", # <- zmień na "similarity" lub "mmr" jeśli chcesz
28
+ search_kwargs={"k": 5}
29
+ )
30
+
31
+ # --- 2. Tworzenie łańcucha RAG z historią ---
32
+ contextualize_q_system_prompt = (
33
+ "Biorąc pod uwagę historię czatu i ostatnie pytanie użytkownika, "
34
+ "które może odnosić się do kontekstu w historii czatu, "
35
+ "sformułuj samodzielne pytanie, które można zrozumieć bez historii czatu. "
36
+ "NIE odpowiadaj na pytanie, po prostu przeformułuj je, jeśli to konieczne, "
37
+ "a w przeciwnym razie zwróć je w niezmienionej formie."
38
+ )
39
+ contextualize_q_prompt = ChatPromptTemplate.from_messages(
40
+ [
41
+ ("system", contextualize_q_system_prompt),
42
+ MessagesPlaceholder("chat_history"),
43
+ ("human", "{input}"),
44
+ ]
45
+ )
46
+ history_aware_retriever = create_history_aware_retriever(
47
+ llm, retriever, contextualize_q_prompt
48
+ )
49
+
50
+ qa_system_prompt = (
51
+ "Jesteś asystentem do zadawania pytań i odpowiedzi na temat treści ze strony mojaszuflada.pl. "
52
+ "Użyj poniższych fragmentów odzyskanego kontekstu, aby odpowiedzieć na pytanie. "
53
+ "Odpowiadaj zawsze w języku polskim. "
54
+ "Jeśli nie znasz odpowiedzi, po prostu powiedz, że nie wiesz. "
55
+ "Zachowaj zwięzłość odpowiedzi, ale bądź pomocny i przyjazny."
56
+ "\n\n{context}"
57
+ )
58
+ qa_prompt = ChatPromptTemplate.from_messages(
59
+ [
60
+ ("system", qa_system_prompt),
61
+ MessagesPlaceholder("chat_history"),
62
+ ("human", "{input}"),
63
+ ]
64
+ )
65
+ question_answer_chain = create_stuff_documents_chain(llm, qa_prompt)
66
+
67
+ rag_chain = create_retrieval_chain(history_aware_retriever, question_answer_chain)
68
+
69
+ store = {}
70
+
71
+ def get_session_history(session_id: str) -> BaseChatMessageHistory:
72
+ if session_id not in store:
73
+ store[session_id] = ChatMessageHistory()
74
+ return store[session_id]
75
+
76
+ conversational_rag_chain = RunnableWithMessageHistory(
77
+ rag_chain,
78
+ get_session_history,
79
+ input_messages_key="input",
80
+ history_messages_key="chat_history",
81
+ output_messages_key="answer",
82
  )
83
 
84
+ # --- 3. Funkcje pomocnicze dla Gradio ---
85
+ def format_sources(source_docs):
86
+ if not source_docs:
87
+ return "_Brak źródeł do wyświetlenia._"
88
+
89
+ sources = []
90
+ for doc in source_docs:
91
+ metadata = doc.metadata
92
+ title = metadata.get("title", "Brak tytułu")
93
+ source_url = metadata.get("source", "Brak URL")
94
+
95
+ pub_date_raw = metadata.get("published_time")
96
+ if pub_date_raw:
97
+ pub_date = pub_date_raw.split("T")[0]
98
+ sources.append(f"- [{title}]({source_url}) ({pub_date})")
99
+ else:
100
+ sources.append(f"- [{title}]({source_url})")
101
+ # Dodaj informację o liczbie chunków (debug)
102
+ sources.append(f"\n_Znaleziono chunków: {len(source_docs)}_")
103
+ return "\n".join(sources)
104
+
105
+ # --- 4. Budowa interfejsu Gradio ---
106
+ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue"), title="Szuflada Chatbot") as demo:
107
+ session_id = gr.State(lambda: str(uuid.uuid4()))
108
+
109
+ gr.Markdown(
110
+ """# Czat z Szufladą\n### Zadaj pytanie na temat treści ze strony [mojaszuflada.pl](https://mojaszuflada.pl)
111
+ """
112
+ )
113
 
114
+ chatbot = gr.Chatbot(
115
+ label="Rozmowa",
116
+ height=500,
117
+ )
118
+
119
+ with gr.Row():
120
+ msg = gr.Textbox(
121
+ show_label=False,
122
+ placeholder="Wpisz swoje pytanie...",
123
+ container=False,
124
+ scale=7,
125
+ )
126
+ submit_btn = gr.Button("Wyślij", variant="primary", scale=1)
127
+
128
+ def respond(message, chat_history, sess_id):
129
+ response = conversational_rag_chain.invoke(
130
+ {"input": message},
131
+ config={"configurable": {"session_id": sess_id}},
132
+ )
133
+ # Dodaj logowanie score dla debugowania
134
+ context_docs = response.get("context", [])
135
+ for i, doc in enumerate(context_docs):
136
+ score = doc.metadata.get("score", "brak score")
137
+ print(f"Chunk {i+1}: score={score}, title={doc.metadata.get('title')}")
138
+ sources_md = format_sources(context_docs)
139
+ answer_with_sources = response["answer"] + "\n\n**Źródła:**\n" + sources_md
140
+ chat_history.append((message, answer_with_sources))
141
+ return chat_history
142
+
143
+ submit_btn.click(
144
+ respond,
145
+ [msg, chatbot, session_id],
146
+ [chatbot]
147
+ ).then(lambda: gr.update(value=""), None, [msg], queue=False)
148
+
149
+ msg.submit(
150
+ respond,
151
+ [msg, chatbot, session_id],
152
+ [chatbot]
153
+ ).then(lambda: gr.update(value=""), None, [msg], queue=False)
154
+
155
+ # --- 5. Uruchomienie aplikacji ---
156
  if __name__ == "__main__":
157
+ # --- TEST: sprawdź bezpośrednio retriever ---
158
+ test_query = "test" # <- wpisz tu frazę, która powinna być w bazie
159
+ print("\n=== TEST RETRIEVER ===")
160
+ docs = retriever.get_relevant_documents(test_query)
161
+ print(f"Znaleziono {len(docs)} dokumentów dla zapytania: '{test_query}'")
162
+ for i, doc in enumerate(docs):
163
+ score = doc.metadata.get("score", "brak score")
164
+ print(f"Chunk {i+1}: score={score}, title={doc.metadata.get('title')}, source={doc.metadata.get('source')}")
165
+ print("=== KONIEC TESTU ===\n")
166
+ # --- KONIEC TESTU ---
167
+
168
+ demo.launch(inbrowser=True)
scrap.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_community.document_loaders import SitemapLoader
2
+ from bs4 import BeautifulSoup
3
+ import re
4
+ from langchain_chroma import Chroma
5
+ from langchain_openai import OpenAIEmbeddings
6
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
7
+ from langchain_core.documents import Document
8
+ import requests
9
+ from tqdm import tqdm
10
+
11
+ def process_documents(docs: list[Document]) -> list[Document]:
12
+ """
13
+ Przetwarza listę dokumentów, wyodrębniając treść i metadane z HTML.
14
+ """
15
+ processed_docs = []
16
+ for doc in docs:
17
+ soup = BeautifulSoup(doc.page_content, "lxml")
18
+
19
+ # Wyodrębnienie głównej treści
20
+ article = soup.find("article")
21
+ if article:
22
+ content = article.get_text(separator="\n", strip=True)
23
+ else:
24
+ content = soup.get_text(separator="\n", strip=True)
25
+
26
+ # Wyodrębnienie metadanych
27
+ metadata = doc.metadata.copy() # Kopiujemy istniejące metadane (np. source)
28
+
29
+ # Title: Zgodnie z sugestią, tytuł jest pobierany tylko ze znacznika <title>
30
+ if soup.title:
31
+ title_text = soup.title.get_text(strip=True)
32
+ if title_text:
33
+ metadata["title"] = title_text
34
+
35
+ # Data publikacji
36
+ # Published time: prefer meta[property=article:published_time], then <time>, then regex search
37
+ pub_date_tag = soup.find("meta", property="article:published_time")
38
+ if pub_date_tag and pub_date_tag.get("content"):
39
+ metadata["published_time"] = pub_date_tag["content"]
40
+ else:
41
+ time_tag = soup.find("time")
42
+ if time_tag and time_tag.get("datetime"):
43
+ metadata["published_time"] = time_tag.get("datetime")
44
+ elif time_tag and time_tag.get_text(strip=True):
45
+ metadata["published_time"] = time_tag.get_text(strip=True)
46
+ else:
47
+ # Polish pages often have 'Opublikowano w dniu 8 marca 2011' as plain text
48
+ text = soup.get_text(separator="\n", strip=True)
49
+ m = re.search(r"Opublikowano(?: w dniu)?[:\s]+([0-9]{1,2}\s+\w+\s+\d{4})", text, re.IGNORECASE)
50
+ if m:
51
+ metadata["published_time"] = m.group(1)
52
+
53
+ # Kategorie
54
+ categories = [
55
+ tag["content"]
56
+ for tag in soup.find_all("meta", property="article:section")
57
+ if tag.get("content")
58
+ ]
59
+ if categories:
60
+ metadata["categories"] = ", ".join(categories)
61
+
62
+ # Słowa kluczowe (tagi)
63
+ keywords = [
64
+ tag["content"]
65
+ for tag in soup.find_all("meta", property="article:tag")
66
+ if tag.get("content")
67
+ ]
68
+ if keywords:
69
+ metadata["keywords"] = ", ".join(keywords)
70
+
71
+
72
+
73
+ processed_docs.append(Document(page_content=content, metadata=metadata))
74
+ return processed_docs
75
+
76
+
77
+ embedder=OpenAIEmbeddings(model="text-embedding-3-small", show_progress_bar=True)
78
+
79
+ baza=Chroma(collection_name="szuflada", embedding_function=embedder, persist_directory="./szuflada")
80
+
81
+ # --- DODANA SEKCJA ---
82
+ # Czyszczenie istniejącej kolekcji przed dodaniem nowych danych
83
+ # To zapewnia, że pracujemy na świeżych danych z metadanymi.
84
+ print("Czyszczenie istniejącej kolekcji w bazie danych...")
85
+ try:
86
+ baza.delete_collection()
87
+ print("Kolekcja została wyczyszczona.")
88
+ # Po usunięciu kolekcji, musimy ponownie zainicjować obiekt Chroma
89
+ baza=Chroma(collection_name="szuflada", embedding_function=embedder, persist_directory="./szuflada")
90
+ except Exception as e:
91
+ print(f"Nie można było wyczyścić kolekcji (może nie istniała): {e}")
92
+ # --- KONIEC DODANEJ SEKCJI ---
93
+
94
+
95
+
96
+ # --- Nowa logika ładowania danych ---
97
+ print("Pobieranie i parsowanie mapy strony...")
98
+ headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'}
99
+ sitemap_url = "https://mojaszuflada.pl/wp-sitemap.xml"
100
+ docs = []
101
+
102
+ try:
103
+ response = requests.get(sitemap_url, headers=headers)
104
+ response.raise_for_status()
105
+ sitemap_xml = response.text
106
+
107
+ sitemap_soup = BeautifulSoup(sitemap_xml, "xml")
108
+ urls = [loc.text for loc in sitemap_soup.find_all("loc")]
109
+
110
+ sitemap_urls = [url for url in urls if url.endswith(".xml")]
111
+ page_urls = [url for url in urls if not url.endswith(".xml")]
112
+
113
+ for sub_sitemap_url in tqdm(sitemap_urls, desc="Parsowanie pod-map"):
114
+ try:
115
+ response = requests.get(sub_sitemap_url, headers=headers)
116
+ response.raise_for_status()
117
+ sub_sitemap_xml = response.text
118
+ sub_sitemap_soup = BeautifulSoup(sub_sitemap_xml, "xml")
119
+ page_urls.extend([loc.text for loc in sub_sitemap_soup.find_all("loc")])
120
+ except requests.RequestException as e:
121
+ print(f"Pominięto pod-mapę {sub_sitemap_url}: {e}")
122
+
123
+ print(f"Znaleziono {len(page_urls)} adresów URL do przetworzenia.")
124
+
125
+ for url in tqdm(page_urls, desc="Pobieranie stron"):
126
+ try:
127
+ response = requests.get(url, headers=headers)
128
+ response.raise_for_status()
129
+ doc = Document(
130
+ page_content=response.text,
131
+ metadata={"source": url, "loc": url}
132
+ )
133
+ docs.append(doc)
134
+ except requests.RequestException as e:
135
+ print(f"Pominięto stronę {url}: {e}")
136
+
137
+ except requests.RequestException as e:
138
+ print(f"Krytyczny błąd: Nie udało się pobrać głównej mapy strony: {e}")
139
+ # docs will be empty and the script will exit gracefully later
140
+
141
+ if not docs:
142
+ print("Nie załadowano żadnych dokumentów. Zakończenie pracy.")
143
+ exit()
144
+
145
+
146
+ processed_docs = process_documents(docs)
147
+
148
+ print("\nPrzykładowe metadane przetworzonych dokumentów (pierwsze 5):")
149
+ for pd in processed_docs[:5]:
150
+ print(pd.metadata)
151
+
152
+ text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
153
+ chunks = text_splitter.split_documents(processed_docs)
154
+
155
+
156
+ batch_size = 1000
157
+ # --- WALIDACJA METADANYCH DLA CHUNKÓW ---
158
+ # Sprawdzamy, czy każdy chunk zawiera oczekiwane metadane (źródło, tytuł, data publikacji)
159
+ required_meta_keys = ["source", "title", "published_time"]
160
+ missing_counts = {k: 0 for k in required_meta_keys}
161
+ for idx, chunk in enumerate(chunks):
162
+ md = chunk.metadata or {}
163
+ for k in required_meta_keys:
164
+ if not md.get(k):
165
+ missing_counts[k] += 1
166
+
167
+ print(f"Liczba chunków: {len(chunks)}")
168
+ print("Braki metadanych (liczba chunków bez klucza/wartości):", missing_counts)
169
+ print("Przykładowe metadane dla pierwszych 5 chunków:")
170
+ for sample in chunks[:5]:
171
+ print(sample.metadata)
172
+ # --- KONIEC WALIDACJI ---
173
+
174
+ for i in range(0, len(chunks), batch_size):
175
+ baza.add_documents(documents=chunks[i:i + batch_size])