Amdreas commited on
Commit
39a82c9
·
verified ·
1 Parent(s): b5a28f6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +85 -150
app.py CHANGED
@@ -1,28 +1,23 @@
1
- import os, io, re, json, traceback
2
- # --- vypni Gradio analytiku / telemetriu, nech nepadá API schéma ---
3
  os.environ["GRADIO_ANALYTICS_ENABLED"] = "False"
4
  os.environ["HF_HUB_DISABLE_TELEMETRY"] = "1"
5
 
6
  import gradio as gr
7
  from pdfminer.high_level import extract_text
8
  from sentence_transformers import SentenceTransformer
9
- from qdrant_client import QdrantClient
10
- from qdrant_client.http import models as qmodels
11
- import requests
12
 
13
  print("[gradio] version:", gr.__version__, flush=True)
14
 
15
- # ==== SEM VLOŽ SVOJE HODNOTY (alebo nechaj prázdne a použi Secrets) ====
16
- AUTO_QDRANT_URL = "https://1101644d-bb1d-4aad-adf1-ee86ad1de2dc.europe-west3-0.gcp.cloud.qdrant.io:6333"
17
- AUTO_QDRANT_API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOiJtIn0.vWc7ei94LhQZF9pIFYRyFZS9r9T5rFZ16Wv7V3lhhzo"
18
- # ======================================================================
19
 
20
- EMBED_MODEL_ID = "intfloat/multilingual-e5-small" # 384-dim
 
 
 
21
  _embedder = None
22
- _client = None
23
- _collection = "docs"
24
 
25
- # ----------------- helpers -----------------
26
  def get_embedder():
27
  global _embedder
28
  if _embedder is None:
@@ -30,179 +25,119 @@ def get_embedder():
30
  _embedder = SentenceTransformer(EMBED_MODEL_ID)
31
  return _embedder
32
 
33
- def chunk_text(text, size=1500, overlap=200):
34
- text = re.sub(r"\s+", " ", text)
 
 
 
35
  chunks, i = [], 0
36
  step = max(1, size - overlap)
37
- while i < len(text):
 
38
  seg = text[i:i+size]
39
  if seg.strip():
40
  chunks.append(seg)
41
  i += step
42
  return chunks
43
 
44
- def connect_qdrant(url, api_key, collection):
45
- """Create client and ensure collection exists; return (status_text, error_md)."""
46
- global _client, _collection
47
- url = (url or "").strip()
48
- api_key = (api_key or "").strip()
49
- _collection = (collection or "docs").strip()
50
-
51
- if not url or not api_key:
52
- return "❌ Zadaj QDRANT_URL a QDRANT_API_KEY.", ""
53
-
54
- try:
55
- print(f"[qdrant] connecting to {url} coll={_collection}", flush=True)
56
- _client = QdrantClient(url=url, api_key=api_key)
57
- # ping / ensure collection
58
- try:
59
- _client.get_collection(_collection)
60
- print("[qdrant] collection exists", flush=True)
61
- except Exception:
62
- print("[qdrant] creating collection", flush=True)
63
- _client.recreate_collection(
64
- collection_name=_collection,
65
- vectors_config=qmodels.VectorParams(size=384, distance=qmodels.Distance.COSINE),
66
- )
67
- return f"✅ Connected to Qdrant: {_collection}", ""
68
- except Exception as e:
69
- tb = traceback.format_exc()
70
- print("[qdrant] connect error:\n", tb, flush=True)
71
- return "❌ Connection error", f"```\n{e}\n```"
72
-
73
  def embed_passages(texts):
74
  emb = get_embedder()
75
- return emb.encode([f"passage: {t}" for t in texts], normalize_embeddings=True).tolist()
 
76
 
77
- def embed_query(text):
78
  emb = get_embedder()
79
- return emb.encode([f"query: {text}"], normalize_embeddings=True)[0].tolist()
80
-
81
- def ingest_pdfs(files, topic, chunk_chars, overlap):
82
- if not _client:
83
- return "❌ Najprv pripoj Qdrant (auto-connect prebieha pri štarte)."
84
- total = 0
85
- for f in files or []:
86
- data = f.read()
87
- text = extract_text(io.BytesIO(data))
88
- chunks = chunk_text(text, chunk_chars, overlap)
89
- vecs = embed_passages(chunks)
90
- points = [
91
- qmodels.PointStruct(vector=v, payload={"text": c, "topic": topic})
92
- for c, v in zip(chunks, vecs)
93
- ]
94
- _client.upsert(collection_name=_collection, points=points)
95
- total += len(chunks)
96
- return f"✅ Index hotový. Uložených chunkov: {total}"
97
-
98
- def search_local(query, topk, topic):
99
- if not _client:
100
- return []
101
- qvec = embed_query(query)
102
- qfilter = qmodels.Filter(must=[qmodels.FieldCondition(
103
- key="topic", match=qmodels.MatchValue(value=topic)
104
- )]) if topic else None
105
- res = _client.search(collection_name=_collection, query_vector=qvec, limit=int(topk), query_filter=qfilter)
106
- return [{"score": r.score, "text": r.payload.get("text",""), "payload": r.payload} for r in res]
107
-
108
- def web_search(query, n=3, api_key=None):
109
- key = api_key or os.getenv("TAVILY_API_KEY", "")
110
- if not key:
111
- return []
112
- r = requests.post("https://api.tavily.com/search", json={
113
- "api_key": key, "query": query, "search_depth": "basic", "max_results": int(n)
114
- }, timeout=30)
115
- try:
116
- data = r.json()
117
- except Exception:
118
  return []
119
- out = []
120
- for item in data.get("results", [])[:int(n)]:
121
- text = (item.get("title","") + "\n" + item.get("content",""))[:1500]
122
- out.append({"url": item.get("url"), "text": text})
123
- return out
124
 
125
- def answer_from_context(question, local_hits, web_snips):
126
  lines = [f"**Otázka:** {question}", "\n**Odpoveď (zo zdrojov):**"]
127
- if local_hits:
128
- lines.append("- PDF:")
129
- for i,h in enumerate(local_hits):
130
- snippet = h['text'][:400].strip().replace("\n"," ")
131
- lines.append(f" [{i+1}] {snippet} … (score {h['score']:.2f})")
132
- if web_snips:
133
- lines.append("- Web:")
134
- for j,w in enumerate(web_snips):
135
- snippet = w['text'][:300].strip().replace("\n"," ")
136
- lines.append(f" [W{j+1}] {snippet} … ({w['url']})")
137
- if not local_hits and not web_snips:
138
- lines.append("_Nenašli sa relevantné pasáže – skús spresniť otázku alebo pridať PDF._")
139
  return "\n".join(lines)
140
 
141
- # ----------------- UI -----------------
142
- with gr.Blocks(title="EDM & Fanuc – Hybrid RAG (PDF + Web)") as demo:
143
- gr.Markdown("# 🔧 EDM & Fanuc – Hybrid RAG (PDF + Web)")
144
 
145
  with gr.Row():
146
- # LEFT
147
  with gr.Column(scale=1):
148
- gr.Markdown("### Qdrant nastavenie")
149
- q_url = gr.Textbox(label="QDRANT_URL", value=AUTO_QDRANT_URL or os.getenv("QDRANT_URL",""), lines=2)
150
- q_key = gr.Textbox(label="QDRANT_API_KEY", value=AUTO_QDRANT_API_KEY or os.getenv("QDRANT_API_KEY",""), type="password")
151
- collection_tb = gr.Textbox(label="Collection", value="docs")
152
- connect_btn = gr.Button("🔌 Connect (manuálne)")
153
- status_md = gr.Markdown()
154
- err_md = gr.Markdown()
155
-
156
- def _connect(u,k,c):
157
- s,e = connect_qdrant(u,k,c)
158
- return s, e
159
- connect_btn.click(_connect, inputs=[q_url,q_key,collection_tb], outputs=[status_md, err_md])
160
-
161
- gr.Markdown("### Ingest PDF")
162
- topic_dd = gr.Dropdown(["EDM","FANUC","general"], value="EDM", label="Topic")
163
  chunk_chars = gr.Slider(800, 2500, value=1500, step=100, label="Chunk size")
164
  overlap = gr.Slider(0, 400, value=200, step=20, label="Overlap")
165
  upload = gr.File(file_count="multiple", file_types=[".pdf"], label="📁 PDF súbory")
166
- ingest_btn = gr.Button("📥 Indexovať do Qdrant")
 
167
  ingest_out = gr.Markdown()
168
 
169
  def _ingest(files, topic, size, ov):
170
- return ingest_pdfs(files, topic, size, ov)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  ingest_btn.click(_ingest, inputs=[upload, topic_dd, chunk_chars, overlap], outputs=ingest_out)
 
172
 
173
- # RIGHT
174
  with gr.Column(scale=2):
175
- gr.Markdown("### Chat (PDF + voliteľne web)")
176
  question = gr.Textbox(label="Otázka", placeholder="Napr. G41 vs G42 na Fanuc…", lines=3)
177
- topic_chat = gr.Dropdown(["EDM","FANUC","general"], value="EDM", label="Topic filter")
178
- topk = gr.Slider(1, 8, value=5, step=1, label="Top-K")
179
- minscore = gr.Slider(0, 1, value=0.35, step=0.01, label="Min lokálne score pre vypnutie webu")
180
- use_web = gr.Checkbox(value=True, label="Povoliť web doplnenie (Tavily)")
181
- tavily_key = gr.Textbox(label="TAVILY_API_KEY (nepovinné)", value=os.getenv("TAVILY_API_KEY",""), type="password")
182
  ask_btn = gr.Button("💬 Spýtať sa")
183
  answer_md = gr.Markdown()
184
 
185
- def _ask(q, topic, k, thr, web, tkey):
186
- local_hits = search_local(q, int(k), topic)
187
- good = any(h.get("score",0) >= float(thr) for h in local_hits)
188
- web_snips = web_search(q, n=3, api_key=tkey) if (web and not good) else []
189
- return answer_from_context(q, local_hits, web_snips)
190
-
191
- ask_btn.click(_ask, inputs=[question, topic_chat, topk, minscore, use_web, tavily_key], outputs=answer_md)
192
-
193
- # --- AUTO CONNECT po nahratí UI (ak sú vyplnené hodnoty) ---
194
- def _auto_connect():
195
- url = AUTO_QDRANT_URL or os.getenv("QDRANT_URL","")
196
- key = AUTO_QDRANT_API_KEY or os.getenv("QDRANT_API_KEY","")
197
- if url and key:
198
- s, e = connect_qdrant(url, key, "docs")
199
- return s + ("\n\n" + e if e else "")
200
- return "ℹ️ Zadaj QDRANT_URL a QDRANT_API_KEY (alebo vyplň AUTO_… priamo v kóde)."
201
 
202
- demo.load(_auto_connect, inputs=None, outputs=status_md)
203
 
204
  if __name__ == "__main__":
205
- # show_api=False => obíde bug s generovaním OpenAPI / “No API found”
206
  demo.launch(
207
  server_name="0.0.0.0",
208
  server_port=int(os.environ.get("PORT", 7860)),
 
1
+ import os, io, re, traceback
 
2
  os.environ["GRADIO_ANALYTICS_ENABLED"] = "False"
3
  os.environ["HF_HUB_DISABLE_TELEMETRY"] = "1"
4
 
5
  import gradio as gr
6
  from pdfminer.high_level import extract_text
7
  from sentence_transformers import SentenceTransformer
8
+ import numpy as np
 
 
9
 
10
  print("[gradio] version:", gr.__version__, flush=True)
11
 
12
+ EMBED_MODEL_ID = "intfloat/multilingual-e5-small" # 384-dim, multilingual
 
 
 
13
 
14
+ # -------- in-memory store --------
15
+ library_chunks = [] # list[str]
16
+ library_meta = [] # list[{source, idx}]
17
+ library_embeddings = None # np.ndarray [N, D] normalized
18
  _embedder = None
 
 
19
 
20
+ # -------- helpers --------
21
  def get_embedder():
22
  global _embedder
23
  if _embedder is None:
 
25
  _embedder = SentenceTransformer(EMBED_MODEL_ID)
26
  return _embedder
27
 
28
+ def clean_text(t: str) -> str:
29
+ return re.sub(r"\s+", " ", t).strip()
30
+
31
+ def chunk_text(text: str, size: int = 1500, overlap: int = 200):
32
+ text = clean_text(text)
33
  chunks, i = [], 0
34
  step = max(1, size - overlap)
35
+ L = len(text)
36
+ while i < L:
37
  seg = text[i:i+size]
38
  if seg.strip():
39
  chunks.append(seg)
40
  i += step
41
  return chunks
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  def embed_passages(texts):
44
  emb = get_embedder()
45
+ vecs = emb.encode([f"passage: {t}" for t in texts], normalize_embeddings=True)
46
+ return np.asarray(vecs, dtype=np.float32)
47
 
48
+ def embed_query(q):
49
  emb = get_embedder()
50
+ vec = emb.encode([f"query: {q}"], normalize_embeddings=True)[0]
51
+ return np.asarray(vec, dtype=np.float32)
52
+
53
+ def add_to_library(chunks, source_name):
54
+ global library_chunks, library_meta, library_embeddings
55
+ vecs = embed_passages(chunks)
56
+ if library_embeddings is None:
57
+ library_embeddings = vecs
58
+ else:
59
+ library_embeddings = np.vstack([library_embeddings, vecs])
60
+ start = len(library_chunks)
61
+ library_chunks.extend(chunks)
62
+ library_meta.extend({"source": source_name, "idx": i} for i in range(start, start+len(chunks)))
63
+ return len(chunks)
64
+
65
+ def reset_library():
66
+ global library_chunks, library_meta, library_embeddings
67
+ library_chunks = []
68
+ library_meta = []
69
+ library_embeddings = None
70
+
71
+ def top_k(query, k=5):
72
+ if library_embeddings is None or len(library_chunks) == 0:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  return []
74
+ qv = embed_query(query)
75
+ # cosine since embeddings are normalized -> dot product
76
+ scores = library_embeddings @ qv
77
+ idx = np.argsort(-scores)[:k]
78
+ return [(float(scores[i]), library_chunks[i], library_meta[i]) for i in idx]
79
 
80
+ def build_answer(question, hits):
81
  lines = [f"**Otázka:** {question}", "\n**Odpoveď (zo zdrojov):**"]
82
+ if not hits:
83
+ lines.append("_Nenašli sa relevantné pasáže. Nahraj PDF alebo skús inú formuláciu._")
84
+ for i,(s,txt,meta) in enumerate(hits, start=1):
85
+ snippet = clean_text(txt)[:400]
86
+ src = meta.get("source", "PDF")
87
+ lines.append(f"[{i}] {snippet} … (score {s:.2f}, zdroj: {src})")
 
 
 
 
 
 
88
  return "\n".join(lines)
89
 
90
+ # -------- UI --------
91
+ with gr.Blocks(title="EDM & Fanuc – PDF RAG (bez Qdrant)") as demo:
92
+ gr.Markdown("# 🔧 EDM & Fanuc – PDF RAG (bez Qdrant)")
93
 
94
  with gr.Row():
 
95
  with gr.Column(scale=1):
96
+ gr.Markdown("### 📥 Ingest PDF (textové, nie skeny)")
97
+ topic_dd = gr.Dropdown(["EDM","FANUC","general"], value="EDM", label="Topic (len štítok)")
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  chunk_chars = gr.Slider(800, 2500, value=1500, step=100, label="Chunk size")
99
  overlap = gr.Slider(0, 400, value=200, step=20, label="Overlap")
100
  upload = gr.File(file_count="multiple", file_types=[".pdf"], label="📁 PDF súbory")
101
+ ingest_btn = gr.Button("📥 Pridať do knižnice")
102
+ reset_btn = gr.Button("🗑️ Vyčistiť knižnicu")
103
  ingest_out = gr.Markdown()
104
 
105
  def _ingest(files, topic, size, ov):
106
+ if not files:
107
+ return "⚠️ Nahraj aspoň jedno PDF."
108
+ total_chunks = 0
109
+ for f in files:
110
+ name = getattr(f, "name", "document.pdf")
111
+ try:
112
+ text = extract_text(io.BytesIO(f.read()))
113
+ except Exception as e:
114
+ return f"❌ Chyba pri čítaní {name}: {e}"
115
+ chunks = chunk_text(text, size, ov)
116
+ added = add_to_library(chunks, f"{topic}:{name}")
117
+ total_chunks += added
118
+ return f"✅ Pridané chunky: {total_chunks}. Aktuálna knižnica: {len(library_chunks)} pasáží."
119
+
120
+ def _reset():
121
+ reset_library()
122
+ return "✅ Knižnica vyčistená."
123
+
124
  ingest_btn.click(_ingest, inputs=[upload, topic_dd, chunk_chars, overlap], outputs=ingest_out)
125
+ reset_btn.click(_reset, outputs=ingest_out)
126
 
 
127
  with gr.Column(scale=2):
128
+ gr.Markdown("### 💬 Chat z lokálnej knižnice")
129
  question = gr.Textbox(label="Otázka", placeholder="Napr. G41 vs G42 na Fanuc…", lines=3)
130
+ topk = gr.Slider(1, 10, value=5, step=1, label="Top-K pasáží")
 
 
 
 
131
  ask_btn = gr.Button("💬 Spýtať sa")
132
  answer_md = gr.Markdown()
133
 
134
+ def _ask(q, k):
135
+ hits = top_k(q, int(k))
136
+ return build_answer(q, hits)
 
 
 
 
 
 
 
 
 
 
 
 
 
137
 
138
+ ask_btn.click(_ask, inputs=[question, topk], outputs=answer_md)
139
 
140
  if __name__ == "__main__":
 
141
  demo.launch(
142
  server_name="0.0.0.0",
143
  server_port=int(os.environ.get("PORT", 7860)),