Amdreas commited on
Commit
bcf2d46
·
verified ·
1 Parent(s): 448937a

Update app.py

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