ZENLLC commited on
Commit
f1a89df
·
verified ·
1 Parent(s): 00011fa

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +39 -33
app.py CHANGED
@@ -1,14 +1,14 @@
1
  import os
2
  import math
3
- import requests
4
- from typing import List, Dict, Any, Tuple
5
 
 
6
  import gradio as gr
7
  from openai import OpenAI
8
 
9
  # -------------------- CONFIG --------------------
10
 
11
- CHAT_MODEL = "gpt-5.1" # Change here if your OpenAI model ID differs
12
  EMBED_MODEL = "text-embedding-3-large"
13
 
14
  DEFAULT_SYSTEM_PROMPT = """You are a Retrieval-Augmented Generation (RAG) assistant.
@@ -17,7 +17,7 @@ Rules:
17
  - Answer ONLY using the provided knowledge base context and system instructions.
18
  - If the answer is not clearly supported by the context, say "I don’t know based on the current knowledge base."
19
  - Do not invent sources, statistics, or facts that are not present in the context.
20
- - When applicable, cite which source you used (e.g., "According to the uploaded PDF" or "Based on zenai.world").
21
  - Be clear, concise, and structured.
22
  """
23
 
@@ -29,15 +29,18 @@ PRESET_CONFIGS = {
29
  },
30
  "ZEN Sites Deep QA (zenai.world + AI Arena)": {
31
  "system": DEFAULT_SYSTEM_PROMPT
32
- + "\n\nYou specialize in answering questions about ZEN AI’s mission, programs, and AI Arena.",
33
  "urls": "https://zenai.world\nhttps://us.zenai.biz",
34
- "text": "ZEN AI builds the first global AI × Web3 literacy movement with youth, homeschool, and professional tracks.",
 
 
 
35
  },
36
  "Policy Explainer (external PDFs / links)": {
37
  "system": DEFAULT_SYSTEM_PROMPT
38
- + "\n\nYou act as a neutral policy explainer. Summarize clearly, highlight key risks and opportunities.",
39
  "urls": "",
40
- "text": "This preset is for uploading AI policy PDFs, legal texts, and reports.",
41
  },
42
  "Research Notebook / Personal RAG Sandbox": {
43
  "system": DEFAULT_SYSTEM_PROMPT
@@ -47,8 +50,8 @@ PRESET_CONFIGS = {
47
  },
48
  }
49
 
 
50
 
51
- # -------------------- HELPER FUNCTIONS --------------------
52
 
53
  def chunk_text(text: str, max_chars: int = 2000, overlap: int = 200) -> List[str]:
54
  """Simple character-based chunking with overlap."""
@@ -74,25 +77,27 @@ def cosine_similarity(a: List[float], b: List[float]) -> float:
74
  return 0.0
75
  dot = sum(x * y for x, y in zip(a, b))
76
  norm_a = math.sqrt(sum(x * x for x in a))
77
- norm_b = math.sqrt(sum(y * y for y in b))
78
  if norm_a == 0 or norm_b == 0:
79
  return 0.0
80
  return dot / (norm_a * norm_b)
81
 
82
 
 
 
 
83
  def fetch_url_text(url: str) -> str:
84
- """Fetch text from a URL in a very lightweight way."""
85
  try:
86
- resp = requests.get(url, timeout=10)
87
  resp.raise_for_status()
88
- # crude HTML stripping: keep text only
89
  text = resp.text
90
- # Remove basic tags
 
91
  for tag in ["<script", "<style"]:
92
  if tag in text:
93
- # Truncate at first occurrence of script/style to avoid junk
94
  text = text.split(tag)[0]
95
- # Replace angle brackets
96
  text = text.replace("<", " ").replace(">", " ")
97
  return text
98
  except Exception as e:
@@ -108,13 +113,14 @@ def read_file_text(path: str) -> str:
108
  if any(path_lower.endswith(ext) for ext in [".txt", ".md", ".csv", ".json"]):
109
  with open(path, "r", encoding="utf-8", errors="ignore") as f:
110
  return f.read()
111
- # If you want to support PDFs or DOCX, you can add optional parsing here,
112
- # but we avoid extra dependencies to keep the app robust.
113
  return f"[Unsupported file type for RAG content: {os.path.basename(path)}]"
114
  except Exception as e:
115
  return f"[Error reading file {os.path.basename(path)}: {e}]"
116
 
117
 
 
 
 
118
  def build_embeddings(
119
  api_key: str,
120
  docs: List[Dict[str, Any]],
@@ -124,13 +130,14 @@ def build_embeddings(
124
  return [], "⚠️ No documents to index."
125
 
126
  client = OpenAI(api_key=api_key)
127
- kb_chunks = []
128
  total_chunks = 0
129
 
130
  for d in docs:
131
  source = d.get("source", "unknown")
132
  text = d.get("text", "")
133
  chunks = chunk_text(text, max_chars=2000, overlap=200)
 
134
  for idx, ch in enumerate(chunks):
135
  try:
136
  emb_resp = client.embeddings.create(
@@ -148,7 +155,6 @@ def build_embeddings(
148
  )
149
  total_chunks += 1
150
  except Exception as e:
151
- # Keep going even if one embedding fails
152
  kb_chunks.append(
153
  {
154
  "id": f"{source}_{idx}_error",
@@ -183,7 +189,7 @@ def retrieve_context(
183
  except Exception as e:
184
  return "", f"⚠️ Error creating query embedding: {e}"
185
 
186
- scored = []
187
  for d in kb:
188
  emb = d.get("embedding") or []
189
  if not emb:
@@ -215,6 +221,7 @@ def retrieve_context(
215
 
216
  # -------------------- GRADIO CALLBACKS --------------------
217
 
 
218
  def save_api_key(api_key: str):
219
  api_key = (api_key or "").strip()
220
  if not api_key:
@@ -233,13 +240,13 @@ def build_knowledge_base(
233
  api_key: str,
234
  urls_text: str,
235
  raw_text: str,
236
- file_paths: List[str] | None,
237
  ):
238
  api_key = (api_key or "").strip()
239
  if not api_key:
240
  return "❌ Please save your OpenAI API key first.", []
241
 
242
- docs = []
243
 
244
  # URLs
245
  urls = [u.strip() for u in (urls_text or "").splitlines() if u.strip()]
@@ -252,9 +259,7 @@ def build_knowledge_base(
252
  docs.append({"source": "Raw Text Block", "text": raw_text})
253
 
254
  # Files
255
- if file_paths is not None:
256
- if isinstance(file_paths, str):
257
- file_paths = [file_paths]
258
  for p in file_paths:
259
  if not p:
260
  continue
@@ -295,7 +300,8 @@ def chat_with_rag(
295
  client = OpenAI(api_key=api_key)
296
 
297
  # Assemble messages for OpenAI
298
- messages = []
 
299
  combined_system = (
300
  DEFAULT_SYSTEM_PROMPT.strip()
301
  + "\n\n---\n\nUser System Instructions:\n"
@@ -312,7 +318,7 @@ def chat_with_rag(
312
  )
313
  messages.append({"role": "system", "content": context_block})
314
 
315
- # Add truncated history for conversational continuity
316
  recent_history = history[-10:] if history else []
317
  for msg in recent_history:
318
  if msg.get("role") in ("user", "assistant"):
@@ -332,7 +338,6 @@ def chat_with_rag(
332
  except Exception as e:
333
  answer = f"⚠️ OpenAI API error: {e}"
334
 
335
- # Update history for display and next turn
336
  new_history = history + [
337
  {"role": "user", "content": user_message},
338
  {"role": "assistant", "content": answer},
@@ -347,10 +352,10 @@ def clear_chat():
347
 
348
  # -------------------- UI LAYOUT --------------------
349
 
350
- with gr.Blocks(title="RAG Chatbot — GPT-5.1 + URLs / Files / Text") as demo:
351
  gr.Markdown(
352
  """
353
- # 🔍 RAG Chatbot — GPT-5.1 + URLs / Files / Text
354
 
355
  1. Enter your **OpenAI API key** and click **Save**.
356
  2. Add knowledge via **URLs**, **uploaded files**, and/or **raw text**.
@@ -417,7 +422,7 @@ with gr.Blocks(title="RAG Chatbot — GPT-5.1 + URLs / Files / Text") as demo:
417
  gr.Markdown("### 💬 RAG Chat")
418
 
419
  chatbot = gr.Chatbot(
420
- label="RAG Chatbot (GPT-5.1)",
421
  type="messages",
422
  height=450,
423
  )
@@ -457,13 +462,14 @@ with gr.Blocks(title="RAG Chatbot — GPT-5.1 + URLs / Files / Text") as demo:
457
  outputs=[kb_status_md, kb_state],
458
  )
459
 
460
- # Wiring: chat send
461
  send_btn.click(
462
  fn=chat_with_rag,
463
  inputs=[user_input, api_key_state, kb_state, system_box, chat_state],
464
  outputs=[chatbot, chat_state, debug_md],
465
  )
466
 
 
467
  user_input.submit(
468
  fn=chat_with_rag,
469
  inputs=[user_input, api_key_state, kb_state, system_box, chat_state],
 
1
  import os
2
  import math
3
+ from typing import List, Dict, Any, Tuple, Optional
 
4
 
5
+ import requests
6
  import gradio as gr
7
  from openai import OpenAI
8
 
9
  # -------------------- CONFIG --------------------
10
 
11
+ CHAT_MODEL = "gpt-5" # main chat model
12
  EMBED_MODEL = "text-embedding-3-large"
13
 
14
  DEFAULT_SYSTEM_PROMPT = """You are a Retrieval-Augmented Generation (RAG) assistant.
 
17
  - Answer ONLY using the provided knowledge base context and system instructions.
18
  - If the answer is not clearly supported by the context, say "I don’t know based on the current knowledge base."
19
  - Do not invent sources, statistics, or facts that are not present in the context.
20
+ - When applicable, cite which source you used (e.g., "According to the uploaded file" or "Based on zenai.world").
21
  - Be clear, concise, and structured.
22
  """
23
 
 
29
  },
30
  "ZEN Sites Deep QA (zenai.world + AI Arena)": {
31
  "system": DEFAULT_SYSTEM_PROMPT
32
+ + "\n\nYou specialize in answering questions about ZEN AI’s mission, programs, AI Pioneer, and ZEN AI Arena.",
33
  "urls": "https://zenai.world\nhttps://us.zenai.biz",
34
+ "text": (
35
+ "ZEN AI is building the first global AI × Web3 literacy and automation movement, "
36
+ "with youth, homeschool, and professional tracks and blockchain-verified credentials."
37
+ ),
38
  },
39
  "Policy Explainer (external PDFs / links)": {
40
  "system": DEFAULT_SYSTEM_PROMPT
41
+ + "\n\nYou act as a neutral policy explainer. Summarize clearly, highlight key risks, opportunities, and practical implications.",
42
  "urls": "",
43
+ "text": "This preset is for uploading AI policy PDFs, legal texts, and governance reports.",
44
  },
45
  "Research Notebook / Personal RAG Sandbox": {
46
  "system": DEFAULT_SYSTEM_PROMPT
 
50
  },
51
  }
52
 
53
+ # -------------------- TEXT / EMBEDDING HELPERS --------------------
54
 
 
55
 
56
  def chunk_text(text: str, max_chars: int = 2000, overlap: int = 200) -> List[str]:
57
  """Simple character-based chunking with overlap."""
 
77
  return 0.0
78
  dot = sum(x * y for x, y in zip(a, b))
79
  norm_a = math.sqrt(sum(x * x for x in a))
80
+ norm_b = math.sqrt(sum(x * x for x in b))
81
  if norm_a == 0 or norm_b == 0:
82
  return 0.0
83
  return dot / (norm_a * norm_b)
84
 
85
 
86
+ # -------------------- DATA SOURCE HELPERS --------------------
87
+
88
+
89
  def fetch_url_text(url: str) -> str:
90
+ """Fetch text from a URL in a lightweight way."""
91
  try:
92
+ resp = requests.get(url, timeout=12)
93
  resp.raise_for_status()
 
94
  text = resp.text
95
+
96
+ # crude HTML stripping: cut off at first script/style and remove angle brackets
97
  for tag in ["<script", "<style"]:
98
  if tag in text:
 
99
  text = text.split(tag)[0]
100
+
101
  text = text.replace("<", " ").replace(">", " ")
102
  return text
103
  except Exception as e:
 
113
  if any(path_lower.endswith(ext) for ext in [".txt", ".md", ".csv", ".json"]):
114
  with open(path, "r", encoding="utf-8", errors="ignore") as f:
115
  return f.read()
 
 
116
  return f"[Unsupported file type for RAG content: {os.path.basename(path)}]"
117
  except Exception as e:
118
  return f"[Error reading file {os.path.basename(path)}: {e}]"
119
 
120
 
121
+ # -------------------- EMBEDDING / KB BUILD --------------------
122
+
123
+
124
  def build_embeddings(
125
  api_key: str,
126
  docs: List[Dict[str, Any]],
 
130
  return [], "⚠️ No documents to index."
131
 
132
  client = OpenAI(api_key=api_key)
133
+ kb_chunks: List[Dict[str, Any]] = []
134
  total_chunks = 0
135
 
136
  for d in docs:
137
  source = d.get("source", "unknown")
138
  text = d.get("text", "")
139
  chunks = chunk_text(text, max_chars=2000, overlap=200)
140
+
141
  for idx, ch in enumerate(chunks):
142
  try:
143
  emb_resp = client.embeddings.create(
 
155
  )
156
  total_chunks += 1
157
  except Exception as e:
 
158
  kb_chunks.append(
159
  {
160
  "id": f"{source}_{idx}_error",
 
189
  except Exception as e:
190
  return "", f"⚠️ Error creating query embedding: {e}"
191
 
192
+ scored: List[Tuple[float, Dict[str, Any]]] = []
193
  for d in kb:
194
  emb = d.get("embedding") or []
195
  if not emb:
 
221
 
222
  # -------------------- GRADIO CALLBACKS --------------------
223
 
224
+
225
  def save_api_key(api_key: str):
226
  api_key = (api_key or "").strip()
227
  if not api_key:
 
240
  api_key: str,
241
  urls_text: str,
242
  raw_text: str,
243
+ file_paths: Optional[List[str]],
244
  ):
245
  api_key = (api_key or "").strip()
246
  if not api_key:
247
  return "❌ Please save your OpenAI API key first.", []
248
 
249
+ docs: List[Dict[str, Any]] = []
250
 
251
  # URLs
252
  urls = [u.strip() for u in (urls_text or "").splitlines() if u.strip()]
 
259
  docs.append({"source": "Raw Text Block", "text": raw_text})
260
 
261
  # Files
262
+ if file_paths:
 
 
263
  for p in file_paths:
264
  if not p:
265
  continue
 
300
  client = OpenAI(api_key=api_key)
301
 
302
  # Assemble messages for OpenAI
303
+ messages: List[Dict[str, str]] = []
304
+
305
  combined_system = (
306
  DEFAULT_SYSTEM_PROMPT.strip()
307
  + "\n\n---\n\nUser System Instructions:\n"
 
318
  )
319
  messages.append({"role": "system", "content": context_block})
320
 
321
+ # Add truncated history
322
  recent_history = history[-10:] if history else []
323
  for msg in recent_history:
324
  if msg.get("role") in ("user", "assistant"):
 
338
  except Exception as e:
339
  answer = f"⚠️ OpenAI API error: {e}"
340
 
 
341
  new_history = history + [
342
  {"role": "user", "content": user_message},
343
  {"role": "assistant", "content": answer},
 
352
 
353
  # -------------------- UI LAYOUT --------------------
354
 
355
+ with gr.Blocks(title="RAG Chatbot — GPT-5 + URLs / Files / Text") as demo:
356
  gr.Markdown(
357
  """
358
+ # 🔍 RAG Chatbot — GPT-5 + URLs / Files / Text
359
 
360
  1. Enter your **OpenAI API key** and click **Save**.
361
  2. Add knowledge via **URLs**, **uploaded files**, and/or **raw text**.
 
422
  gr.Markdown("### 💬 RAG Chat")
423
 
424
  chatbot = gr.Chatbot(
425
+ label="RAG Chatbot (GPT-5)",
426
  type="messages",
427
  height=450,
428
  )
 
462
  outputs=[kb_status_md, kb_state],
463
  )
464
 
465
+ # Wiring: chat send (button)
466
  send_btn.click(
467
  fn=chat_with_rag,
468
  inputs=[user_input, api_key_state, kb_state, system_box, chat_state],
469
  outputs=[chatbot, chat_state, debug_md],
470
  )
471
 
472
+ # Wiring: chat send (Enter key)
473
  user_input.submit(
474
  fn=chat_with_rag,
475
  inputs=[user_input, api_key_state, kb_state, system_box, chat_state],