Amdreas commited on
Commit
03192e0
·
verified ·
1 Parent(s): ab54841

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +68 -60
app.py CHANGED
@@ -1,5 +1,5 @@
1
- # app.py
2
- import os, io, re, json
3
  import gradio as gr
4
  from pdfminer.high_level import extract_text
5
  from sentence_transformers import SentenceTransformer
@@ -7,42 +7,55 @@ from qdrant_client import QdrantClient
7
  from qdrant_client.http import models as qmodels
8
  import requests
9
 
10
- EMBED_MODEL_ID = "intfloat/multilingual-e5-small"
11
  _embedder = None
12
  _client = None
13
- _collection = None
14
 
15
  def get_embedder():
16
  global _embedder
17
  if _embedder is None:
 
18
  _embedder = SentenceTransformer(EMBED_MODEL_ID)
19
  return _embedder
20
 
21
- def connect_qdrant(url: str, api_key: str, collection: str):
22
- global _client, _collection
23
- _client = QdrantClient(url=url, api_key=api_key) if url else None
24
- _collection = collection
25
- if _client:
26
- try:
27
- _client.get_collection(collection)
28
- except Exception:
29
- _client.recreate_collection(
30
- collection_name=collection,
31
- vectors_config=qmodels.VectorParams(size=384, distance=qmodels.Distance.COSINE)
32
- )
33
- return True
34
-
35
  def chunk_text(text: str, size: int = 1500, overlap: int = 200):
36
  text = re.sub(r"\s+", " ", text)
37
  chunks, i = [], 0
38
  step = max(1, size - overlap)
39
  while i < len(text):
40
- chunk = text[i:i+size]
41
- if chunk.strip():
42
- chunks.append(chunk)
43
  i += step
44
  return chunks
45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  def embed_passages(texts):
47
  emb = get_embedder()
48
  return emb.encode([f"passage: {t}" for t in texts], normalize_embeddings=True).tolist()
@@ -53,25 +66,28 @@ def embed_query(text: str):
53
 
54
  def ingest_pdfs(files, topic: str, chunk_chars: int, overlap: int):
55
  if not _client:
56
- return "❌ Najprv nastav QDRANT_URL a QDRANT_API_KEY vľavo.", None
57
- total_chunks = 0
58
  for f in files or []:
59
  data = f.read()
60
  text = extract_text(io.BytesIO(data))
61
  chunks = chunk_text(text, chunk_chars, overlap)
62
  vecs = embed_passages(chunks)
63
- points = []
64
- for c, v in zip(chunks, vecs):
65
- points.append(qmodels.PointStruct(vector=v, payload={"text": c, "topic": topic}))
 
66
  _client.upsert(collection_name=_collection, points=points)
67
- total_chunks += len(chunks)
68
- return f"✅ Uložené chunky: {total_chunks}", None
69
 
70
  def search_local(query: str, topk: int, topic: str):
71
  if not _client:
72
  return []
73
  qvec = embed_query(query)
74
- qfilter = qmodels.Filter(must=[qmodels.FieldCondition(key="topic", match=qmodels.MatchValue(value=topic))]) if topic else None
 
 
75
  res = _client.search(collection_name=_collection, query_vector=qvec, limit=topk, query_filter=qfilter)
76
  return [{"score": r.score, "text": r.payload.get("text",""), "payload": r.payload} for r in res]
77
 
@@ -80,10 +96,7 @@ def web_search(query: str, n: int = 3, api_key: str | None = None):
80
  if not key:
81
  return []
82
  r = requests.post("https://api.tavily.com/search", json={
83
- "api_key": key,
84
- "query": query,
85
- "search_depth": "basic",
86
- "max_results": n
87
  }, timeout=30)
88
  data = r.json()
89
  out = []
@@ -93,58 +106,53 @@ def web_search(query: str, n: int = 3, api_key: str | None = None):
93
  return out
94
 
95
  def answer_from_context(question: str, local_hits, web_snips):
96
- parts = ["**Otázka:** " + question, "\n**Odpoveď (kompilované zo zdrojov):**"]
97
  if local_hits:
98
- parts.append("- Z lokálnych PDF vyplýva:")
99
  for i,h in enumerate(local_hits):
100
- snippet = h['text'][:400].strip().replace('\n',' ')
101
- parts.append(f" [{i+1}] {snippet} … (score {h['score']:.2f})")
102
  if web_snips:
103
- parts.append("- Z webu:")
104
  for j,w in enumerate(web_snips):
105
- snippet = w['text'][:300].strip().replace('\n',' ')
106
- parts.append(f" [W{j+1}] {snippet} … (zdroj: {w['url']})")
107
- parts.append("\n**Poznámka:** Toto je súhrn citovaných pasáží. Pre presnejšiu odpoveď pridaj viac PDF alebo zapni lepší model neskôr.")
108
- return "\n".join(parts)
109
-
110
- def on_connect(url, key, collection):
111
- try:
112
- connect_qdrant(url, key, collection)
113
- return f"✅ Pripojené k Qdrant: {url}", ""
114
- except Exception as e:
115
- return "❌ Chyba pripojenia", str(e)
116
 
117
  with gr.Blocks(title="EDM & Fanuc – Hybrid RAG (PDF + Web)") as demo:
118
  gr.Markdown("# 🔧 EDM & Fanuc – Hybrid RAG (PDF + Web)")
119
 
120
  with gr.Row():
 
121
  with gr.Column(scale=1):
122
  gr.Markdown("### Qdrant nastavenie")
123
- q_url = gr.Textbox(label="QDRANT_URL", value=os.getenv("QDRANT_URL", ""))
124
- q_key = gr.Textbox(label="QDRANT_API_KEY", value=os.getenv("QDRANT_API_KEY", ""), type="password")
125
  collection_tb = gr.Textbox(label="Collection", value="docs")
126
  connect_btn = gr.Button("🔌 Connect")
127
- status = gr.Textbox(label="Status")
128
- err = gr.Textbox(label="Error")
129
 
130
  def _connect(u,k,c):
131
- s,e = on_connect(u,k,c)
132
- return s,e
133
- connect_btn.click(_connect, inputs=[q_url,q_key,collection_tb], outputs=[status, err])
134
 
135
  gr.Markdown("### Ingest PDF")
136
  topic_dd = gr.Dropdown(["EDM","FANUC","general"], value="EDM", label="Topic")
137
  chunk_chars = gr.Slider(800, 2500, value=1500, step=100, label="Chunk size")
138
  overlap = gr.Slider(0, 400, value=200, step=20, label="Overlap")
139
- upload = gr.File(file_count="multiple", file_types=[".pdf"], label="PDF súbory")
140
  ingest_btn = gr.Button("📥 Indexovať do Qdrant")
141
- ingest_out = gr.Textbox(label="Výstup")
142
 
143
  def _ingest(files, topic, size, ov):
144
- msg,_ = ingest_pdfs(files, topic, size, ov)
145
- return msg
146
  ingest_btn.click(_ingest, inputs=[upload, topic_dd, chunk_chars, overlap], outputs=ingest_out)
147
 
 
148
  with gr.Column(scale=2):
149
  gr.Markdown("### Chat (PDF + voliteľne web)")
150
  question = gr.Textbox(label="Otázka", placeholder="Napr. G41 vs G42 na Fanuc…", lines=3)
 
1
+ # app.py (Gradio)
2
+ import os, io, re, json, traceback
3
  import gradio as gr
4
  from pdfminer.high_level import extract_text
5
  from sentence_transformers import SentenceTransformer
 
7
  from qdrant_client.http import models as qmodels
8
  import requests
9
 
10
+ EMBED_MODEL_ID = "intfloat/multilingual-e5-small" # 384-dim, multilingual
11
  _embedder = None
12
  _client = None
13
+ _collection = "docs"
14
 
15
  def get_embedder():
16
  global _embedder
17
  if _embedder is None:
18
+ print("[embedder] loading:", EMBED_MODEL_ID, flush=True)
19
  _embedder = SentenceTransformer(EMBED_MODEL_ID)
20
  return _embedder
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  def chunk_text(text: str, size: int = 1500, overlap: int = 200):
23
  text = re.sub(r"\s+", " ", text)
24
  chunks, i = [], 0
25
  step = max(1, size - overlap)
26
  while i < len(text):
27
+ seg = text[i:i+size]
28
+ if seg.strip():
29
+ chunks.append(seg)
30
  i += step
31
  return chunks
32
 
33
+ def connect_qdrant(url: str, api_key: str, collection: str):
34
+ """Create client and ensure collection exists; return human message."""
35
+ global _client, _collection
36
+ if not url or not api_key:
37
+ return "❌ Zadaj QDRANT_URL a QDRANT_API_KEY.", ""
38
+
39
+ _collection = collection or "docs"
40
+ try:
41
+ print(f"[qdrant] connecting to {url} coll={_collection}", flush=True)
42
+ _client = QdrantClient(url=url, api_key=api_key)
43
+ # ping: ensure collection exists and is usable
44
+ try:
45
+ _client.get_collection(_collection)
46
+ print("[qdrant] collection exists", flush=True)
47
+ except Exception:
48
+ print("[qdrant] creating collection", flush=True)
49
+ _client.recreate_collection(
50
+ collection_name=_collection,
51
+ vectors_config=qmodels.VectorParams(size=384, distance=qmodels.Distance.COSINE),
52
+ )
53
+ return f"✅ Connected to Qdrant: {_collection}", ""
54
+ except Exception as e:
55
+ tb = traceback.format_exc()
56
+ print("[qdrant] connect error:", tb, flush=True)
57
+ return "❌ Connection error", f"```\n{e}\n```"
58
+
59
  def embed_passages(texts):
60
  emb = get_embedder()
61
  return emb.encode([f"passage: {t}" for t in texts], normalize_embeddings=True).tolist()
 
66
 
67
  def ingest_pdfs(files, topic: str, chunk_chars: int, overlap: int):
68
  if not _client:
69
+ return "❌ Najprv klikni 🔌 Connect (Qdrant URL + API key)."
70
+ total = 0
71
  for f in files or []:
72
  data = f.read()
73
  text = extract_text(io.BytesIO(data))
74
  chunks = chunk_text(text, chunk_chars, overlap)
75
  vecs = embed_passages(chunks)
76
+ points = [
77
+ qmodels.PointStruct(vector=v, payload={"text": c, "topic": topic})
78
+ for c, v in zip(chunks, vecs)
79
+ ]
80
  _client.upsert(collection_name=_collection, points=points)
81
+ total += len(chunks)
82
+ return f"✅ Index hotový. Uložených chunkov: {total}"
83
 
84
  def search_local(query: str, topk: int, topic: str):
85
  if not _client:
86
  return []
87
  qvec = embed_query(query)
88
+ qfilter = qmodels.Filter(must=[qmodels.FieldCondition(
89
+ key="topic", match=qmodels.MatchValue(value=topic)
90
+ )]) if topic else None
91
  res = _client.search(collection_name=_collection, query_vector=qvec, limit=topk, query_filter=qfilter)
92
  return [{"score": r.score, "text": r.payload.get("text",""), "payload": r.payload} for r in res]
93
 
 
96
  if not key:
97
  return []
98
  r = requests.post("https://api.tavily.com/search", json={
99
+ "api_key": key, "query": query, "search_depth": "basic", "max_results": n
 
 
 
100
  }, timeout=30)
101
  data = r.json()
102
  out = []
 
106
  return out
107
 
108
  def answer_from_context(question: str, local_hits, web_snips):
109
+ lines = [f"**Otázka:** {question}", "\n**Odpoveď (zo zdrojov):**"]
110
  if local_hits:
111
+ lines.append("- PDF:")
112
  for i,h in enumerate(local_hits):
113
+ snippet = h['text'][:400].strip().replace("\n"," ")
114
+ lines.append(f" [{i+1}] {snippet} … (score {h['score']:.2f})")
115
  if web_snips:
116
+ lines.append("- Web:")
117
  for j,w in enumerate(web_snips):
118
+ snippet = w['text'][:300].strip().replace("\n"," ")
119
+ lines.append(f" [W{j+1}] {snippet} … ({w['url']})")
120
+ if not local_hits and not web_snips:
121
+ lines.append("_Nenašli sa relevantné pasáže – skús spresniť otázku alebo pridať PDF._")
122
+ return "\n".join(lines)
 
 
 
 
 
 
123
 
124
  with gr.Blocks(title="EDM & Fanuc – Hybrid RAG (PDF + Web)") as demo:
125
  gr.Markdown("# 🔧 EDM & Fanuc – Hybrid RAG (PDF + Web)")
126
 
127
  with gr.Row():
128
+ # LEFT
129
  with gr.Column(scale=1):
130
  gr.Markdown("### Qdrant nastavenie")
131
+ q_url = gr.Textbox(label="QDRANT_URL", value=os.getenv("QDRANT_URL",""))
132
+ q_key = gr.Textbox(label="QDRANT_API_KEY", value=os.getenv("QDRANT_API_KEY",""), type="password")
133
  collection_tb = gr.Textbox(label="Collection", value="docs")
134
  connect_btn = gr.Button("🔌 Connect")
135
+ status_md = gr.Markdown()
136
+ err_md = gr.Markdown()
137
 
138
  def _connect(u,k,c):
139
+ s,e = connect_qdrant(u,k,c)
140
+ return s, e
141
+ connect_btn.click(_connect, inputs=[q_url,q_key,collection_tb], outputs=[status_md, err_md])
142
 
143
  gr.Markdown("### Ingest PDF")
144
  topic_dd = gr.Dropdown(["EDM","FANUC","general"], value="EDM", label="Topic")
145
  chunk_chars = gr.Slider(800, 2500, value=1500, step=100, label="Chunk size")
146
  overlap = gr.Slider(0, 400, value=200, step=20, label="Overlap")
147
+ upload = gr.File(file_count="multiple", file_types=[".pdf"], label="📁 PDF súbory")
148
  ingest_btn = gr.Button("📥 Indexovať do Qdrant")
149
+ ingest_out = gr.Markdown()
150
 
151
  def _ingest(files, topic, size, ov):
152
+ return ingest_pdfs(files, topic, size, ov)
 
153
  ingest_btn.click(_ingest, inputs=[upload, topic_dd, chunk_chars, overlap], outputs=ingest_out)
154
 
155
+ # RIGHT
156
  with gr.Column(scale=2):
157
  gr.Markdown("### Chat (PDF + voliteľne web)")
158
  question = gr.Textbox(label="Otázka", placeholder="Napr. G41 vs G42 na Fanuc…", lines=3)