ShubhamMhaske commited on
Commit
b5921ff
Β·
verified Β·
1 Parent(s): bdf53fa

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +58 -545
app.py CHANGED
@@ -6,39 +6,9 @@ import gradio as gr
6
  from google import genai
7
  from google.genai import types
8
 
9
- # ──────────────────────────────────────────────────────────────────────────────
10
- # Config
11
- # ──────────────────────────────────────────────────────────────────────────────
12
  DEFAULT_MODEL = "gemini-2.5-flash"
13
- SAMPLES_DIR_DEFAULT = "samples"
14
-
15
- SAMPLE_FILES = [
16
- {"path": "Pride_and_Prejudice.txt", "title": "Pride and Prejudice", "author": "Jane Austen", "year": 1813},
17
- {"path": "Adventures_of_Sherlock_Holmes.txt", "title": "The Adventures of Sherlock Holmes", "author": "Arthur Conan Doyle", "year": 1892},
18
- {"path": "Alices_Adventures_in_Wonderland.txt", "title": "Alice's Adventures in Wonderland", "author": "Lewis Carroll", "year": 1865},
19
- {"path": "Moby_Dick.txt", "title": "Moby-Dick", "author": "Herman Melville", "year": 1851},
20
- ]
21
-
22
- # ──────────────────────────────────────────────────────────────────────────────
23
- # Helpers
24
- # ──────────────────────────────────────────────────────────────────────────────
25
- def _file_exists(path: Path) -> bool:
26
- try:
27
- return path.exists() and path.is_file() and path.stat().st_size > 0
28
- except Exception:
29
- return False
30
-
31
- def _size_human(n: int) -> str:
32
- if n < 1024:
33
- return f"{n} B"
34
- kb = n / 1024
35
- if kb < 1024:
36
- return f"{kb:.1f} KB"
37
- mb = kb / 1024
38
- return f"{mb:.2f} MB"
39
 
40
  def _require_client(client_obj):
41
- """Strict. never fallback to env. always require user-pasted key."""
42
  if client_obj is None:
43
  raise RuntimeError("Set your Gemini API key first.")
44
  return client_obj
@@ -53,204 +23,72 @@ def _progress_html(pct: float, text: str) -> str:
53
  </div>
54
  """
55
 
56
- # ──────────────────────────────────────────────────────────────────────────────
57
- # Core flows
58
- # ──────────────────────────────────────────────────────────────────────────────
59
  def ui_set_api_key(api_key: str):
60
- """Initialize Gemini client. API key is mandatory in Spaces."""
61
  api_key = (api_key or "").strip()
62
  if not api_key:
63
- return None, "❌ API key required. paste your Gemini key and click Set key."
64
  try:
65
  client = genai.Client(api_key=api_key)
66
- return client, "βœ… API key set for this session."
67
  except Exception as e:
68
- return None, f"❌ Failed to initialize client. {e}"
69
-
70
- def create_store_with_samples(client_state, samples_dir: str, store_display_name: str,
71
- progress=gr.Progress(track_tqdm=True)):
72
- """
73
- Generator. Creates a NEW store and imports whatever classics exist in samples_dir.
74
- Yields: store_state(str), store_status(md), progress_html(html), create_button(update)
75
- """
76
- if client_state is None:
77
- msg = "❌ Set your API key first in the section above."
78
- yield "", msg, _progress_html(0, "Waiting for API key"), gr.update(interactive=True)
79
- return
80
-
81
- client = _require_client(client_state)
82
- display_name = (store_display_name or "").strip() or "file-search-samples"
83
- store = client.file_search_stores.create(config={"display_name": display_name})
84
- store_name = store.name
85
-
86
- base = Path(samples_dir or SAMPLES_DIR_DEFAULT)
87
- present = [spec for spec in SAMPLE_FILES if _file_exists(base / spec["path"])]
88
- total = max(len(present), 1)
89
-
90
- logs = []
91
- header = f"**Creating store:** `{store_name}` Β· display name **{display_name}**\n\n**Folder:** `{base.resolve()}`\n"
92
- status_md = header
93
-
94
- # Disable button immediately and show initial progress
95
- yield (store_name, status_md, _progress_html(1, "Preparing indexing"),
96
- gr.update(interactive=False, value="Creating…"))
97
-
98
- spinner = ["β ‹","β ™","β Ή","β Έ","β Ό","β ΄","β ¦","β §","β ‡","⠏"]
99
- done_count = 0
100
-
101
- for spec in SAMPLE_FILES:
102
- p = base / spec["path"]
103
- if not _file_exists(p):
104
- logs.append(f"β€’ Missing: {p}")
105
- status_md = header + "\n".join(logs)
106
- pct = (done_count / total) * 100
107
- yield (store_name, status_md, _progress_html(pct, "Checking files"), gr.update(interactive=False))
108
- continue
109
-
110
- # Upload
111
- progress(((done_count + 0.05) / total), desc=f"Uploading {p.name}")
112
- logs.append(f"β€’ Uploading: {p.name}")
113
- status_md = header + "\n".join(logs)
114
- pct = (done_count / total) * 100 + 5
115
- yield (store_name, status_md, _progress_html(pct, f"Uploading {p.name}"), gr.update(interactive=False))
116
-
117
- uploaded = client.files.upload(file=str(p), config={"display_name": spec["title"]})
118
-
119
- # Import + metadata
120
- meta = [
121
- types.CustomMetadata(key="title", string_value=spec["title"]),
122
- types.CustomMetadata(key="author", string_value=spec["author"]),
123
- types.CustomMetadata(key="year", numeric_value=spec["year"]),
124
- types.CustomMetadata(key="local_path", string_value=str(p)),
125
- ]
126
- import_cfg = types.ImportFileConfig(custom_metadata=meta)
127
- op = client.file_search_stores.import_file(
128
- file_search_store_name=store_name,
129
- file_name=uploaded.name,
130
- config=import_cfg,
131
- )
132
-
133
- # Poll with visible progress
134
- tick = 0
135
- while not op.done:
136
- time.sleep(0.5)
137
- tick += 1
138
- step_pct = min(95, 5 + tick * 3) # 5β†’95 for this file
139
- overall = (done_count / total) * 100 + (step_pct / total)
140
- progress(min(0.95, 0.05 + 0.03 * tick), desc=f"Indexing {p.name} {spinner[tick % len(spinner)]}")
141
- yield (store_name, status_md, _progress_html(overall, f"Indexing {p.name} {spinner[tick % len(spinner)]}"),
142
- gr.update(interactive=False))
143
- op = client.operations.get(op)
144
 
145
- done_count += 1
146
- logs.append(f"β€’ Indexed: {p.name} (author={spec['author']}, year={spec['year']})")
147
- status_md = header + "\n".join(logs)
148
- pct = (done_count / total) * 100
149
- yield (store_name, status_md, _progress_html(pct, f"Indexed {p.name}"), gr.update(interactive=False))
150
-
151
- final_header = "βœ… Store created and files indexed." if present else "⚠️ Store created. no classics found to index."
152
- status_md = f"{final_header}\n\n{status_md}"
153
- yield (store_name, status_md, _progress_html(100, "Finished"),
154
- gr.update(interactive=False, value="Store created"))
155
-
156
- def make_empty_store(client_state, display_name: str):
157
- if client_state is None:
158
- return "", "❌ Set your API key first."
159
- client = _require_client(client_state)
160
- dn = (display_name or "").strip() or "file-search-uploads"
161
- store = client.file_search_stores.create(config={"display_name": dn})
162
- return store.name, f"βœ… Created empty store for uploads. `{store.name}`"
163
-
164
- def set_existing_store(client_state, name: str):
165
- if client_state is None:
166
- return "", "❌ Set your API key first."
167
- client = _require_client(client_state)
168
- name = (name or "").strip()
169
- if not name:
170
- return "", "⚠️ Paste a store resource like `fileSearchStores/...`"
171
- try:
172
- store = client.file_search_stores.get(name=name)
173
- return store.name, f"βœ… Using existing store: `{store.name}`"
174
- except Exception as e:
175
- return "", f"❌ Could not get store. {e}"
176
-
177
- def upload_and_index(client_state, store_name: str, file_obj, display_file_name: str,
178
- max_tokens: int, overlap_tokens: int,
179
  progress=gr.Progress(track_tqdm=True)):
180
- """
181
- Generator. Upload local file to selected store and index it.
182
- Yields: op_summary(code), upload_status(md), upload_progress(html)
183
- """
184
  if client_state is None:
185
- yield gr.update(value=""), "❌ Set your API key first.", _progress_html(0, "Waiting for API key")
186
- return
187
- client = _require_client(client_state)
188
-
189
- if not store_name:
190
- yield gr.update(value=""), "⚠️ Create or select a store first.", _progress_html(0, "Waiting")
191
  return
192
  if file_obj is None:
193
- yield gr.update(value=""), "⚠️ Choose a file to upload.", _progress_html(0, "Waiting")
194
  return
195
 
196
- # Chunking config
197
- chunk_cfg = None
198
- if max_tokens or overlap_tokens:
199
- chunk_cfg = types.ChunkingConfig(
200
- white_space_config=types.WhiteSpaceConfig(
201
- max_tokens_per_chunk=int(max_tokens or 200),
202
- max_overlap_tokens=int(overlap_tokens or 20),
203
- )
204
- )
205
 
206
- cfg = None
207
- if display_file_name or chunk_cfg:
208
- cfg = types.UploadToFileSearchStoreConfig(
209
- display_name=(display_file_name.strip() or None) if display_file_name else None,
210
- chunking_config=chunk_cfg,
211
- )
212
 
213
- # Upload + index with progress
214
  fname = Path(file_obj.name).name
 
215
  progress(0.05, desc=f"Uploading {fname}")
216
- yield gr.update(value=""), f"Uploading **{fname}** …", _progress_html(5, f"Uploading {fname}")
 
 
217
 
218
- op = client.file_search_stores.upload_to_file_search_store(
219
- file=file_obj.name,
220
  file_search_store_name=store_name,
221
- config=cfg,
 
222
  )
223
-
224
  tick = 0
225
  spinner = ["β ‹","β ™","β Ή","β Έ","β Ό","β ΄","β ¦","β §","β ‡","⠏"]
226
  while not op.done:
227
  time.sleep(0.5)
228
  tick += 1
229
- pct = min(95, 5 + tick * 3)
230
- progress(min(0.95, 0.05 + 0.03 * tick), desc=f"Indexing {fname} {spinner[tick % len(spinner)]}")
231
- yield gr.update(value=""), f"Indexing **{fname}** …", _progress_html(pct, f"Indexing {fname} {spinner[tick % len(spinner)]}")
 
 
232
  op = client.operations.get(op)
233
 
234
- progress(1.0, desc="Done")
235
- op_summary = getattr(op, "response", None)
236
- op_text = json.dumps(op_summary, indent=2, default=str) if op_summary else "Indexed."
237
- yield gr.update(value=op_text), f"βœ… File indexed into `{store_name}`", _progress_html(100, "Finished")
238
 
239
- def ask(client_state, store_name: str, history_msgs, question: str, model_id: str, metadata_filter: str):
240
  if client_state is None:
241
- return history_msgs, "", "❌ Set your API key first."
242
- client = _require_client(client_state)
243
-
244
  if not store_name:
245
- return history_msgs, "", "⚠️ Pick or create a store first."
 
 
246
  q = (question or "").strip()
247
  if not q:
248
- return history_msgs, "", "⚠️ Type a question."
249
 
250
  tool = types.Tool(
251
  file_search=types.FileSearch(
252
- file_search_store_names=[store_name],
253
- metadata_filter=(metadata_filter.strip() or None),
254
  )
255
  )
256
  resp = client.models.generate_content(
@@ -258,383 +96,58 @@ def ask(client_state, store_name: str, history_msgs, question: str, model_id: st
258
  contents=q,
259
  config=types.GenerateContentConfig(tools=[tool]),
260
  )
261
-
262
- answer = resp.text or "No answer text."
263
  history = list(history_msgs or [])
264
  history.append({"role": "user", "content": q})
265
  history.append({"role": "assistant", "content": answer})
266
 
267
- grounding = "No grounding_metadata returned."
268
- try:
269
- gm = resp.candidates[0].grounding_metadata if resp.candidates else None
270
- if hasattr(gm, "model_dump"):
271
- grounding = json.dumps(gm.model_dump(), indent=2, default=str)
272
- elif isinstance(gm, dict):
273
- grounding = json.dumps(gm, indent=2, default=str)
274
- elif gm is not None:
275
- grounding = str(gm)
276
- except Exception as e:
277
- grounding = f"(Could not parse grounding metadata: {e})"
278
-
279
- return history, grounding, "βœ… Done."
280
-
281
- def list_stores(client_state):
282
- if client_state is None:
283
- return "❌ Set your API key first."
284
- client = _require_client(client_state)
285
- items = []
286
- for s in client.file_search_stores.list():
287
- items.append(f"{s.name} | display_name={getattr(s, 'display_name', '')}")
288
- return "\n".join(items) or "(none)"
289
-
290
- def delete_store(client_state, store_name: str):
291
- if client_state is None:
292
- return "❌ Set your API key first."
293
- client = _require_client(client_state)
294
- if not store_name:
295
- return "⚠️ Enter a store name to delete."
296
- try:
297
- client.file_search_stores.delete(name=store_name, config={"force": True})
298
- return f"πŸ—‘οΈ Deleted: `{store_name}`"
299
- except Exception as e:
300
- return f"❌ Delete failed. {e}"
301
-
302
- # ──────────────────────────────────────────────────────────────────────────────
303
- # Local samples UI
304
- # ──────────────────────────────────────────────────────────────────────────────
305
- def render_samples_panel(samples_dir: str):
306
- base = Path(samples_dir or SAMPLES_DIR_DEFAULT)
307
-
308
- files = []
309
- if base.exists() and base.is_dir():
310
- for p in sorted(base.iterdir(), key=lambda x: x.name.lower()):
311
- if p.is_file():
312
- size = _size_human(p.stat().st_size)
313
- ext = p.suffix[1:].upper() if p.suffix else "FILE"
314
- files.append(f"""
315
- <div class="file-card">
316
- <div class="file-name">{p.name}</div>
317
- <div class="file-meta"><span class="pill">{ext}</span> <span class="muted">{size}</span></div>
318
- <div class="file-path">{p}</div>
319
- </div>
320
- """)
321
- else:
322
- files.append("<div class='muted'>Folder not found. Create it and add files.</div>")
323
-
324
- tiles = []
325
- for spec in SAMPLE_FILES:
326
- p = base / spec["path"]
327
- ok = _file_exists(p)
328
- badge = '<span class="badge-ok">FOUND</span>' if ok else '<span class="badge-miss">MISSING</span>'
329
- tiles.append(f"""
330
- <div class="tile">
331
- <div class="tile-hd">{spec['title']}</div>
332
- <div class="tile-sub">{spec['author']} Β· {spec['year']}</div>
333
- <div class="tile-path">{p}</div>
334
- <div class="tile-badge">{badge}</div>
335
- </div>
336
- """)
337
-
338
- html = f"""
339
- <div class="gallery">
340
- <div class="gallery-col">
341
- <div class="section-hd">All files in <code>{base.resolve()}</code></div>
342
- <div class="file-grid">{''.join(files) if files else "<div class='muted'>No files</div>"}</div>
343
- </div>
344
- <div class="gallery-col">
345
- <div class="section-hd">Classics this demo can auto-index</div>
346
- <div class="tiles-wrap">{''.join(tiles)}</div>
347
- <div class="muted" style="margin-top:6px;">Tip. missing tiles mean you still need to save those .txt files into the folder above.</div>
348
- </div>
349
- </div>
350
- """
351
- return html
352
 
353
- # ──────────────────────────────────────────────────────────────────────────────
354
- # UI (palette + theme)
355
- # ──────────────────────────────────────────────────────────────────────────────
356
  custom_css = """
357
- /* ---------- Palette ---------- */
358
- :root {
359
- --primary: #6C5CE7; --secondary: #00D2FF; --accent: #FF8A65;
360
- --surface: #FFFFFF; --bg: #F6F7FB; --ink: #1B1E2B; --muted: #9AA0B7;
361
- --success: #22C55E; --danger: #EF4444; --code: #0f172a;
362
- }
363
- html[data-theme="dark"] {
364
- --primary: #7A70FF; --secondary: #00BFEA; --accent: #FF9E80;
365
- --surface: #151728; --bg: #0E0F16; --ink: #E9ECFF; --muted: #AAB0D6;
366
- --success: #23D497; --danger: #FF6B6B; --code: #0B0F1C;
367
- }
368
- /* ---------- Layout ---------- */
369
- body { background: var(--bg) !important; }
370
- .gradio-container { max-width: 1120px !important; margin: auto !important; }
371
- /* ---------- Hero ---------- */
372
- .hero {
373
- border-radius: 18px;
374
- padding: 22px 26px;
375
- background: #3F51B5;
376
- box-shadow: 0 12px 36px rgba(0,0,0,.14);
377
- margin: 8px 0 16px 0;
378
- }
379
- .hero h1 { color: var(--surface); font-size: 30px; margin: 0 0 8px; }
380
- .hero p { color: color-mix(in oklab, var(--surface) 78%, transparent); margin: 0; }
381
- /* ---------- Cards & panels ---------- */
382
- .gr-box, .gr-panel {
383
- background: var(--surface) !important;
384
- border: 1px solid color-mix(in oklab, var(--ink) 10%, transparent) !important;
385
- border-radius: 14px !important;
386
- box-shadow: 0 6px 16px rgba(0,0,0,.06);
387
- }
388
- /* ---------- Inputs ---------- */
389
- input, textarea, select, .gr-textbox, .gr-number, .gr-dropdown, .gr-file, .gr-code {
390
- background: color-mix(in oklab, var(--surface) 96%, transparent) !important;
391
- color: var(--ink) !important;
392
- border: 1px solid color-mix(in oklab, var(--ink) 12%, transparent) !important;
393
- border-radius: 10px !important;
394
- }
395
- /* ---------- Buttons ---------- */
396
- button, .gr-button { border-radius: 10px !important; border: 1px solid color-mix(in oklab, var(--ink) 10%, transparent) !important; }
397
- button.primary { background: linear-gradient(135deg, var(--primary), var(--secondary)) !important; color: white !important; }
398
- button.secondary { background: color-mix(in oklab, var(--surface) 90%, transparent) !important; color: var(--ink) !important; }
399
- /* ---------- Accordion (Spaces uses <details>) ---------- */
400
- .gradio-container details {
401
- margin: 10px 0;
402
- background: transparent !important;
403
- border: 0 !important;
404
- }
405
- .gradio-container details > summary {
406
- list-style: none;
407
- background: color-mix(in oklab, var(--surface) 98%, transparent);
408
- border: 1px solid color-mix(in oklab, var(--ink) 10%, transparent);
409
- border-radius: 12px;
410
- padding: 10px 14px;
411
- font-weight: 600;
412
- color: var(--ink);
413
- cursor: pointer;
414
- }
415
- .gradio-container details > summary::-webkit-details-marker { display: none; }
416
- .gradio-container details[open] > summary {
417
- border-bottom-left-radius: 0; border-bottom-right-radius: 0;
418
- }
419
- .gradio-container details > div {
420
- background: var(--surface);
421
- border: 1px solid color-mix(in oklab, var(--ink) 10%, transparent);
422
- border-top: 0;
423
- border-bottom-left-radius: 12px; border-bottom-right-radius: 12px;
424
- padding: 12px 14px;
425
- box-shadow: 0 6px 16px rgba(0,0,0,.06);
426
- }
427
- /* ---------- Chatbot ---------- */
428
- .gr-chatbot {
429
- background: color-mix(in oklab, var(--surface) 96%, transparent) !important;
430
- border: 1px solid color-mix(in oklab, var(--ink) 10%, transparent) !important;
431
- border-radius: 14px !important;
432
- }
433
- .gr-chatbot .message {
434
- border-radius: 14px; padding: 12px 14px; font-size: 17px; line-height: 1.5;
435
- }
436
- .gr-chatbot .message.user {
437
- background: color-mix(in oklab, var(--secondary) 12%, transparent) !important;
438
- border: 1px solid color-mix(in oklab, var(--secondary) 20%, transparent);
439
- }
440
- .gr-chatbot .message.bot {
441
- background: color-mix(in oklab, var(--primary) 12%, transparent) !important;
442
- border: 1px solid color-mix(in oklab, var(--primary) 20%, transparent);
443
- }
444
- /* ---------- Chips ---------- */
445
- .chip { padding:8px 12px; border-radius:999px; background: color-mix(in oklab, var(--primary) 12%, var(--surface));
446
- color: var(--ink); border:1px solid color-mix(in oklab, var(--primary) 20%, transparent); user-select:none; }
447
- .chip:hover { filter:brightness(1.04); }
448
- /* ---------- Sample gallery ---------- */
449
- .section-hd { font-weight: 700; margin-bottom: 8px; color: var(--ink); }
450
- .gallery { display:grid; grid-template-columns: 1.3fr .9fr; gap: 14px; }
451
- @media (max-width: 980px) { .gallery { grid-template-columns: 1fr; } }
452
- .file-grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 10px; }
453
- .file-card { background: color-mix(in oklab, var(--surface) 96%, transparent); border:1px solid color-mix(in oklab, var(--ink) 10%, transparent);
454
- border-radius: 12px; padding: 10px 12px; box-shadow:0 6px 16px rgba(0,0,0,.05); }
455
- .file-name { font-weight: 700; margin-bottom: 4px; color: var(--ink); }
456
- .file-meta { font-size: 13px; color: var(--muted); display:flex; gap:8px; align-items:center; }
457
- .file-path { color: var(--muted); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; word-break: break-all; margin-top: 4px; }
458
- .pill { padding: 2px 8px; border-radius: 999px; background: color-mix(in oklab, var(--secondary) 14%, transparent); color: var(--ink);
459
- border:1px solid color-mix(in oklab, var(--secondary) 20%, transparent); }
460
- .tiles-wrap { display:grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap:12px; }
461
- .tile { background: color-mix(in oklab, var(--surface) 96%, transparent); border:1px solid color-mix(in oklab, var(--ink) 10%, transparent);
462
- border-radius:12px; padding:12px; box-shadow:0 6px 16px rgba(0,0,0,.06); display:flex; flex-direction:column; gap:6px; }
463
- .tile-hd { font-weight:700; color:var(--ink); }
464
- .tile-sub { color: var(--muted); font-size: 14px; }
465
- .tile-path { color: var(--muted); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; word-break: break-all; }
466
- .badge-ok, .badge-miss { font-size: 11.5px; padding: 3px 8px; border-radius: 999px; color: white; width: fit-content; }
467
- .badge-ok { background: var(--success); }
468
- .badge-miss { background: var(--danger); }
469
- /* ---------- Code blocks ---------- */
470
- .gr-code { background: var(--code) !important; color: #d9e1ff !important; border: 1px solid #1f2a44 !important; }
471
- /* ---------- Progress card ---------- */
472
- .progress-card {
473
- background: color-mix(in oklab, var(--surface) 96%, transparent);
474
- border: 1px solid color-mix(in oklab, var(--ink) 10%, transparent);
475
- border-radius: 12px;
476
- padding: 10px 12px;
477
- box-shadow: 0 6px 16px rgba(0,0,0,.05);
478
- }
479
- .progress-head { font-weight: 600; color: var(--ink); margin-bottom: 6px; }
480
- .pbar { height: 12px; width: 100%; background: #e8ebff; border-radius: 999px; overflow: hidden; }
481
- html[data-theme="dark"] .pbar { background: #2a2f57; }
482
- .pbar-fill { height: 100%; background: linear-gradient(90deg, var(--primary), var(--secondary));
483
- border-radius: 999px; transition: width .25s ease; }
484
- .pbar-foot { font-size: 12px; color: var(--muted); margin-top: 6px; }
485
  """
486
 
487
- # Pre-render samples gallery
488
- _initial_gallery_html = render_samples_panel(SAMPLES_DIR_DEFAULT)
489
-
490
- with gr.Blocks(
491
- title="Gemini File Search – Gradio Demo (Spaces)",
492
- theme=gr.themes.Soft(primary_hue="indigo", neutral_hue="slate"),
493
- ) as demo:
494
  gr.HTML(f"<style>{custom_css}</style>")
495
 
496
  client_state = gr.State(value=None)
497
- store_state = gr.State(value="")
498
  chat_state = gr.State(value=[])
499
 
500
- # API key
501
- with gr.Accordion("API key", open=True):
502
- gr.Markdown("_This Space **requires** your Gemini API key for each session. keys are **not** stored server-side._")
503
- with gr.Row():
504
- api_tb = gr.Textbox(label="Gemini API key", placeholder="Paste your API key…", type="password")
505
- api_btn = gr.Button("Set key", elem_classes=["primary"])
506
  api_status = gr.Markdown()
507
 
508
- # Build from samples
509
- with gr.Group():
510
- gr.Markdown("### Build a store from your `samples/` folder")
511
- with gr.Row():
512
- samples_dir_tb = gr.Textbox(label="Samples folder path", value=SAMPLES_DIR_DEFAULT, scale=3)
513
- store_display_name = gr.Textbox(label="Store display name", value="file-search-samples", scale=2)
514
- create_from_samples_btn = gr.Button("Create store with these files", elem_classes=["primary"], scale=1)
515
- samples_preview = gr.HTML(value=_initial_gallery_html)
516
- samples_progress_html = gr.HTML(value="")
517
- store_status = gr.Markdown()
518
- store_name_out = gr.Textbox(label="Active store name", interactive=False)
519
-
520
- # Existing store
521
- with gr.Accordion("Use existing store (paste resource name)", open=False):
522
- gr.Markdown("_Paste a resource like `fileSearchStores/...` to switch the active store._")
523
- with gr.Row():
524
- use_existing_input = gr.Textbox(label="Existing store resource", placeholder="fileSearchStores/…", scale=4)
525
- use_existing_btn = gr.Button("Use store", elem_classes=["secondary"], scale=1)
526
- existing_status = gr.Markdown()
527
-
528
- # Upload own files
529
- with gr.Accordion("Upload & index your own files", open=False):
530
- gr.Markdown("_Create or select a store. then upload any local file here._")
531
- with gr.Row():
532
- upload_store_dn = gr.Textbox(label="Or create empty store for uploads (display name)", value="file-search-uploads", scale=3)
533
- create_empty_btn = gr.Button("Create empty store", elem_classes=["secondary"], scale=1)
534
- with gr.Row():
535
- file_uploader = gr.File(label="Choose a file (txt, pdf, docx, etc.)")
536
- disp_file_name = gr.Textbox(label="Display file name (for citations)", placeholder="my-notes.txt")
537
- with gr.Row():
538
- max_tokens = gr.Number(label="max_tokens_per_chunk", value=200, precision=0)
539
- overlap_tokens = gr.Number(label="max_overlap_tokens", value=20, precision=0)
540
  upload_btn = gr.Button("Upload & Index", elem_classes=["primary"])
541
- upload_progress_html = gr.HTML(value="")
542
- op_summary = gr.Code(label="Operation summary")
543
- upload_status = gr.Markdown()
544
 
545
  gr.Markdown("---")
546
- # Q&A
547
- gr.Markdown("### Ask grounded questions")
548
- with gr.Row():
549
- model = gr.Dropdown(label="Model", value=DEFAULT_MODEL, choices=["gemini-2.5-flash", "gemini-2.5-pro"], scale=1)
550
- metadata_filter = gr.Textbox(label="Optional metadata_filter (AIP-160)", placeholder='author="Jane Austen" AND year=1813', scale=2)
551
- chatbot = gr.Chatbot(label="Grounded Q&A", height=520, type="messages")
552
-
553
- # Example chips
554
- gr.Markdown("Try some examples:")
555
- with gr.Row():
556
- chip1 = gr.Button("Who is Mr. Darcy. what role does he play", elem_classes=["chip"])
557
- chip2 = gr.Button("What is the opening line of Moby-Dick", elem_classes=["chip"])
558
- chip3 = gr.Button("Summarize the tea party scene in Alice", elem_classes=["chip"])
559
- chip4 = gr.Button("Give 3 clues Holmes uses in a story", elem_classes=["chip"])
560
- chip5 = gr.Button("What is Pemberley. why is it important", elem_classes=["chip"])
561
- chip6 = gr.Button("Who is Captain Ahab. what drives him", elem_classes=["chip"])
562
-
563
- with gr.Row():
564
- question_tb = gr.Textbox(placeholder="Type your question…", show_label=False, scale=5)
565
- ask_btn = gr.Button("Ask", elem_classes=["primary"], scale=1)
566
- clear_btn = gr.Button("Clear", elem_classes=["secondary"], scale=1)
567
- with gr.Accordion("grounding_metadata (raw)", open=False):
568
- grounding_md = gr.Code(label="grounding_metadata")
569
- note = gr.Markdown()
570
-
571
- # Manage stores (optional tools)
572
- with gr.Accordion("Manage stores", open=False):
573
- list_btn = gr.Button("List my stores", elem_classes=["secondary"])
574
- list_out = gr.Code()
575
- with gr.Row():
576
- del_name = gr.Textbox(label="Store name to delete")
577
- del_btn = gr.Button("Delete store", elem_classes=["secondary"])
578
- del_status = gr.Markdown()
579
 
580
  # Wiring
581
- api_btn.click(ui_set_api_key, [api_tb], [client_state, api_status], show_progress=True)
582
-
583
- samples_dir_tb.change(render_samples_panel, [samples_dir_tb], [samples_preview], show_progress=False)
584
-
585
- create_from_samples_btn.click(
586
- create_store_with_samples,
587
- [client_state, samples_dir_tb, store_display_name],
588
- [store_state, store_status, samples_progress_html, create_from_samples_btn],
589
- show_progress=True,
590
- ).then(lambda s: s, [store_state], [store_name_out], show_progress=False)
591
-
592
- use_existing_btn.click(
593
- set_existing_store,
594
- [client_state, use_existing_input],
595
- [store_state, existing_status],
596
- show_progress=True,
597
- ).then(lambda s: s, [store_state], [store_name_out], show_progress=False)
598
-
599
- create_empty_btn.click(
600
- make_empty_store,
601
- [client_state, upload_store_dn],
602
- [store_state, upload_status],
603
- show_progress=True,
604
- ).then(lambda s: s, [store_state], [store_name_out], show_progress=False)
605
 
606
  upload_btn.click(
607
  upload_and_index,
608
- [client_state, store_state, file_uploader, disp_file_name, max_tokens, overlap_tokens],
609
- [op_summary, upload_status, upload_progress_html],
610
- show_progress=True,
611
  )
612
 
613
- def _ask_and_update(client_state, store_name, chat_messages, q, model, mfilter):
614
- history, grounding, msg = ask(client_state, store_name, chat_messages, q, model, mfilter)
615
- return history, grounding, msg, ""
616
-
617
  ask_btn.click(
618
- _ask_and_update,
619
- [client_state, store_state, chat_state, question_tb, model, metadata_filter],
620
- [chatbot, grounding_md, note, question_tb],
621
- show_progress=True,
622
- ).then(lambda h: h, [chatbot], [chat_state], show_progress=False)
623
-
624
- clear_btn.click(lambda: ([], "", ""), None, [chatbot, grounding_md, note], show_progress=False)\
625
- .then(lambda: [], None, [chat_state], show_progress=False)
626
-
627
- # Chips β†’ prefill the question textbox
628
- chip1.click(lambda: "Who is Mr. Darcy. what role does he play in the story", None, [question_tb], show_progress=False)
629
- chip2.click(lambda: "What is the opening line of Moby-Dick", None, [question_tb], show_progress=False)
630
- chip3.click(lambda: "Summarize the tea party scene and why it is absurd", None, [question_tb], show_progress=False)
631
- chip4.click(lambda: "Give 3 clues Holmes uses in any one Sherlock Holmes story", None, [question_tb], show_progress=False)
632
- chip5.click(lambda: "What is Pemberley. why is it important in Pride and Prejudice", None, [question_tb], show_progress=False)
633
- chip6.click(lambda: "Who is Captain Ahab. what drives his obsession", None, [question_tb], show_progress=False)
634
-
635
- # Tools
636
- list_btn.click(list_stores, [client_state], [list_out], show_progress=True)
637
- del_btn.click(delete_store, [client_state, del_name], [del_status], show_progress=True)
638
 
639
  if __name__ == "__main__":
640
- demo.queue().launch()
 
 
 
6
  from google import genai
7
  from google.genai import types
8
 
 
 
 
9
  DEFAULT_MODEL = "gemini-2.5-flash"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  def _require_client(client_obj):
 
12
  if client_obj is None:
13
  raise RuntimeError("Set your Gemini API key first.")
14
  return client_obj
 
23
  </div>
24
  """
25
 
 
 
 
26
  def ui_set_api_key(api_key: str):
 
27
  api_key = (api_key or "").strip()
28
  if not api_key:
29
+ return None, "❌ API key required."
30
  try:
31
  client = genai.Client(api_key=api_key)
32
+ return client, "βœ… API key set."
33
  except Exception as e:
34
+ return None, f"❌ Failed to set API key: {e}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
+ def upload_and_index(client_state, file_obj,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  progress=gr.Progress(track_tqdm=True)):
 
 
 
 
38
  if client_state is None:
39
+ yield None, "❌ Set API key first.", _progress_html(0, "Waiting for API key")
 
 
 
 
 
40
  return
41
  if file_obj is None:
42
+ yield None, "⚠️ Please upload a file.", _progress_html(0, "Waiting")
43
  return
44
 
45
+ client = _require_client(client_state)
 
 
 
 
 
 
 
 
46
 
47
+ # Create a new store automatically
48
+ store = client.file_search_stores.create(config={"display_name": "my-upload-store"})
49
+ store_name = store.name
 
 
 
50
 
 
51
  fname = Path(file_obj.name).name
52
+
53
  progress(0.05, desc=f"Uploading {fname}")
54
+ yield None, f"Uploading **{fname}** …", _progress_html(5, f"Uploading {fname}")
55
+
56
+ uploaded = client.files.upload(file=str(file_obj.name), config={"display_name": fname})
57
 
58
+ import_cfg = types.ImportFileConfig(custom_metadata=[])
59
+ op = client.file_search_stores.import_file(
60
  file_search_store_name=store_name,
61
+ file_name=uploaded.name,
62
+ config=import_cfg,
63
  )
 
64
  tick = 0
65
  spinner = ["β ‹","β ™","β Ή","β Έ","β Ό","β ΄","β ¦","β §","β ‡","⠏"]
66
  while not op.done:
67
  time.sleep(0.5)
68
  tick += 1
69
+ step_pct = min(95, 5 + tick * 3)
70
+ overall = step_pct
71
+ progress(min(0.95, 0.05 + 0.03 * tick),
72
+ desc=f"Indexing {fname} {spinner[tick % len(spinner)]}")
73
+ yield store_name, f"Indexing **{fname}** …", _progress_html(overall, f"Indexing {fname}")
74
  op = client.operations.get(op)
75
 
76
+ yield store_name, f"βœ… File indexed into store `{store_name}`", _progress_html(100, "Done")
 
 
 
77
 
78
+ def ask(client_state, store_name: str, history_msgs, question: str, model_id: str):
79
  if client_state is None:
80
+ return history_msgs, "❌ Set API key first."
 
 
81
  if not store_name:
82
+ return history_msgs, "⚠️ Upload & index a file first."
83
+
84
+ client = _require_client(client_state)
85
  q = (question or "").strip()
86
  if not q:
87
+ return history_msgs, "⚠️ Please type a question."
88
 
89
  tool = types.Tool(
90
  file_search=types.FileSearch(
91
+ file_search_store_names=[store_name]
 
92
  )
93
  )
94
  resp = client.models.generate_content(
 
96
  contents=q,
97
  config=types.GenerateContentConfig(tools=[tool]),
98
  )
99
+ answer = resp.text or "No answer."
 
100
  history = list(history_msgs or [])
101
  history.append({"role": "user", "content": q})
102
  history.append({"role": "assistant", "content": answer})
103
 
104
+ return history, answer
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
 
 
 
106
  custom_css = """
107
+ /* Add your CSS here, or leave empty */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  """
109
 
110
+ with gr.Blocks(title="Gemini File Search – Upload & Chat Demo") as demo:
 
 
 
 
 
 
111
  gr.HTML(f"<style>{custom_css}</style>")
112
 
113
  client_state = gr.State(value=None)
114
+ store_state = gr.State(value="") # will hold store name after upload
115
  chat_state = gr.State(value=[])
116
 
117
+ with gr.Accordion("API Key (required)", open=True):
118
+ api_tb = gr.Textbox(label="Gemini API key", placeholder="Paste your API key…", type="password")
119
+ api_btn = gr.Button("Set API Key")
 
 
 
120
  api_status = gr.Markdown()
121
 
122
+ with gr.Row():
123
+ file_uploader = gr.File(label="Upload file to index")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  upload_btn = gr.Button("Upload & Index", elem_classes=["primary"])
125
+ upload_status = gr.Markdown()
126
+ # store_state will be set after upload
 
127
 
128
  gr.Markdown("---")
129
+ gr.Markdown("### Ask questions about the uploaded file")
130
+ question_tb = gr.Textbox(placeholder="Type your question…", show_label=False)
131
+ ask_btn = gr.Button("Ask")
132
+ chatbot = gr.Chatbot(label="Chat", height=400, type="messages")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
  # Wiring
135
+ api_btn.click(ui_set_api_key, [api_tb], [client_state, api_status])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
 
137
  upload_btn.click(
138
  upload_and_index,
139
+ [client_state, file_uploader],
140
+ [store_state, upload_status, upload_btn],
141
+ show_progress=True
142
  )
143
 
 
 
 
 
144
  ask_btn.click(
145
+ ask,
146
+ [client_state, store_state, chat_state, question_tb, gr.Dropdown(value=DEFAULT_MODEL, choices=[DEFAULT_MODEL])],
147
+ [chatbot, chat_state],
148
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
 
150
  if __name__ == "__main__":
151
+ demo.launch(
152
+ theme=gr.themes.Soft(primary_hue="indigo", neutral_hue="slate")
153
+ )