Emmanuel Acheampong commited on
Commit
0f59a0b
Β·
1 Parent(s): 048fa8c

Initial commit

Browse files
Files changed (2) hide show
  1. app.py +506 -0
  2. requirements.txt +4 -0
app.py ADDED
@@ -0,0 +1,506 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Crusoe Foundry β€” Infinite Context Demo
3
+ HuggingFace Space showcasing MemoryAlloyβ„’ & KV Cache sharing
4
+ """
5
+
6
+ import os
7
+ import time
8
+ import tiktoken
9
+ import gradio as gr
10
+ from openai import OpenAI
11
+
12
+ # ── Crusoe Foundry client ─────────────────────────────────────────────────────
13
+ CRUSOE_API_KEY = os.environ.get("CRUSOE_API_KEY", "YOUR_API_KEY_HERE")
14
+ CRUSOE_BASE_URL = os.environ.get("CRUSOE_BASE_URL", "https://managed-inference-api-proxy.crusoecloud.com/v1/")
15
+ MODEL = os.environ.get("CRUSOE_MODEL", "llama-3.1-405b-instruct")
16
+
17
+ client = OpenAI(api_key=CRUSOE_API_KEY, base_url=CRUSOE_BASE_URL)
18
+
19
+ # ── Token counting ────────────────────────────────────────────────────────────
20
+ try:
21
+ enc = tiktoken.encoding_for_model("gpt-4")
22
+ except Exception:
23
+ enc = tiktoken.get_encoding("cl100k_base")
24
+
25
+
26
+ def count_tokens(text: str) -> int:
27
+ return len(enc.encode(text))
28
+
29
+
30
+ def format_tokens(n: int) -> str:
31
+ if n >= 1_000_000:
32
+ return f"{n/1_000_000:.2f}M"
33
+ if n >= 1_000:
34
+ return f"{n/1_000:.1f}K"
35
+ return str(n)
36
+
37
+
38
+ # ── Document ingestion helpers ────────────────────────────────────────────────
39
+ def read_uploaded_file(file_path: str) -> str:
40
+ """Read text from uploaded file (txt, md, py, or pdf via pdfminer)."""
41
+ if file_path is None:
42
+ return ""
43
+ ext = os.path.splitext(file_path)[1].lower()
44
+ if ext == ".pdf":
45
+ try:
46
+ from pdfminer.high_level import extract_text
47
+ return extract_text(file_path)
48
+ except Exception as e:
49
+ return f"[PDF extraction error: {e}]"
50
+ else:
51
+ with open(file_path, "r", errors="replace") as f:
52
+ return f.read()
53
+
54
+
55
+ # ── KV-cache simulation state ─────────────────────────────────────────────────
56
+ _cache_store: dict[str, dict] = {}
57
+
58
+
59
+ def get_cache_key(context: str) -> str:
60
+ import hashlib
61
+ return hashlib.md5(context.encode()).hexdigest()
62
+
63
+
64
+ # ── Shared chat logic ─────────────────────────────────────────────────────────
65
+ def stream_response(system_prompt: str, history: list, user_msg: str):
66
+ """
67
+ Streams a response from Crusoe Foundry.
68
+ Returns (updated_history, token_info_str, latency_str)
69
+ """
70
+ messages = [{"role": "system", "content": system_prompt}]
71
+ for human, assistant in history:
72
+ messages.append({"role": "user", "content": human})
73
+ if assistant:
74
+ messages.append({"role": "assistant", "content": assistant})
75
+ messages.append({"role": "user", "content": user_msg})
76
+
77
+ total_ctx_tokens = sum(count_tokens(m["content"]) for m in messages)
78
+
79
+ t0 = time.perf_counter()
80
+ reply = ""
81
+ try:
82
+ stream = client.chat.completions.create(
83
+ model=MODEL,
84
+ messages=messages,
85
+ stream=True,
86
+ max_tokens=2048,
87
+ )
88
+ for chunk in stream:
89
+ delta = chunk.choices[0].delta.content or ""
90
+ reply += delta
91
+ yield (
92
+ history + [(user_msg, reply)],
93
+ f"πŸ“„ **{format_tokens(total_ctx_tokens)} tokens** in context",
94
+ f"⏱ {time.perf_counter() - t0:.2f}s",
95
+ "",
96
+ )
97
+ except Exception as e:
98
+ reply = f"❌ API error: {e}"
99
+ yield (
100
+ history + [(user_msg, reply)],
101
+ f"πŸ“„ {format_tokens(total_ctx_tokens)} tokens in context",
102
+ "β€”",
103
+ str(e),
104
+ )
105
+
106
+
107
+ # ─────────────────────────────────────────────────────────────────────────────
108
+ # TAB 1 β€” LEGAL (document Q&A)
109
+ # ─────────────────────────────────────────────────────────────────────────────
110
+ legal_doc_store = {"text": "", "tokens": 0}
111
+
112
+
113
+ def legal_ingest(files):
114
+ if not files:
115
+ return "No files uploaded.", "0 tokens", gr.update()
116
+ combined = ""
117
+ for f in files:
118
+ combined += f"\n\n--- {os.path.basename(f.name)} ---\n\n"
119
+ combined += read_uploaded_file(f.name)
120
+ legal_doc_store["text"] = combined
121
+ legal_doc_store["tokens"] = count_tokens(combined)
122
+ tok_str = format_tokens(legal_doc_store["tokens"])
123
+ preview = combined[:800] + ("…" if len(combined) > 800 else "")
124
+ return (
125
+ f"βœ… Loaded {len(files)} document(s) β€” **{tok_str} tokens** ingested into context.",
126
+ f"πŸ“„ {tok_str} tokens",
127
+ gr.update(value=preview),
128
+ )
129
+
130
+
131
+ def legal_chat(user_msg, history):
132
+ if not user_msg.strip():
133
+ yield history, "β€”", "β€”", ""
134
+ return
135
+ doc_context = legal_doc_store["text"]
136
+ system = (
137
+ "You are an expert legal analyst with access to the full text of the uploaded documents. "
138
+ "Answer questions precisely, citing relevant sections when possible. "
139
+ "If a question cannot be answered from the document, say so clearly.\n\n"
140
+ f"=== DOCUMENT CONTEXT ===\n{doc_context}\n=== END CONTEXT ==="
141
+ if doc_context
142
+ else "You are a helpful legal assistant. No documents have been loaded yet."
143
+ )
144
+ yield from stream_response(system, history, user_msg)
145
+
146
+
147
+ # ─────────────────────────────────────────────────────────────────────────────
148
+ # TAB 2 β€” DEV (codebase Q&A)
149
+ # ─────────────────────────────────────────────────────────────────────────────
150
+ dev_code_store = {"text": "", "tokens": 0}
151
+
152
+
153
+ def dev_ingest(files, raw_paste):
154
+ combined = raw_paste or ""
155
+ for f in (files or []):
156
+ combined += f"\n\n# === {os.path.basename(f.name)} ===\n\n"
157
+ combined += read_uploaded_file(f.name)
158
+ dev_code_store["text"] = combined
159
+ dev_code_store["tokens"] = count_tokens(combined)
160
+ tok_str = format_tokens(dev_code_store["tokens"])
161
+ preview = combined[:800] + ("…" if len(combined) > 800 else "")
162
+ return (
163
+ f"βœ… Codebase loaded β€” **{tok_str} tokens** in context.",
164
+ f"πŸ“„ {tok_str} tokens",
165
+ gr.update(value=preview),
166
+ )
167
+
168
+
169
+ def dev_chat(user_msg, history):
170
+ if not user_msg.strip():
171
+ yield history, "β€”", "β€”", ""
172
+ return
173
+ code_context = dev_code_store["text"]
174
+ system = (
175
+ "You are a senior software engineer with full visibility into the provided codebase. "
176
+ "Answer questions about architecture, bugs, refactoring, and code quality. "
177
+ "Reference specific file names, function names, and line context when relevant.\n\n"
178
+ f"=== CODEBASE ===\n{code_context}\n=== END CODEBASE ==="
179
+ if code_context
180
+ else "You are a helpful coding assistant. No code has been loaded yet."
181
+ )
182
+ yield from stream_response(system, history, user_msg)
183
+
184
+
185
+ # ─────────────────────────────────────────────────────────────────────────────
186
+ # TAB 3 β€” MEMORY DEMO (KV-cache visibility)
187
+ # ─────────────────────────────────────────────────────────────────────────────
188
+ memory_state = {
189
+ "cached_context": "",
190
+ "cached_tokens": 0,
191
+ "query_count": 0,
192
+ "total_saved_tokens": 0,
193
+ }
194
+
195
+
196
+ def memory_set_context(context_text):
197
+ memory_state["cached_context"] = context_text
198
+ memory_state["cached_tokens"] = count_tokens(context_text)
199
+ memory_state["query_count"] = 0
200
+ memory_state["total_saved_tokens"] = 0
201
+ tok_str = format_tokens(memory_state["cached_tokens"])
202
+ return (
203
+ f"βœ… Context set β€” **{tok_str} tokens** ready. Savings below are estimated based on context size.",
204
+ _render_cache_stats(),
205
+ )
206
+
207
+
208
+ def _render_cache_stats():
209
+ q = memory_state["query_count"]
210
+ saved = memory_state["total_saved_tokens"]
211
+ cached_tok = memory_state["cached_tokens"]
212
+ return (
213
+ f"**Context tokens:** {format_tokens(cached_tok)}\n\n"
214
+ f"**Queries run:** {q}\n\n"
215
+ f"**Estimated tokens saved\\*:** {format_tokens(saved)}\n\n"
216
+ f"**Estimated cost savings\\*:** ~${saved * 0.000003:.4f} @ $3/1M tokens\n\n"
217
+ f"_\\* Estimates assume full KV cache reuse per query. Actual savings depend on server-side cache availability._"
218
+ )
219
+
220
+
221
+ def memory_chat(user_msg, history):
222
+ if not user_msg.strip():
223
+ yield history, "β€”", "β€”", _render_cache_stats(), ""
224
+ return
225
+
226
+ cached_ctx = memory_state["cached_context"]
227
+ system = (
228
+ "You are a helpful assistant with a pre-loaded context. "
229
+ "The context below has been KV-cached β€” it does not need to be re-encoded for each query.\n\n"
230
+ f"=== CACHED CONTEXT ===\n{cached_ctx}\n=== END CONTEXT ==="
231
+ if cached_ctx
232
+ else "You are a helpful assistant. No context has been cached yet."
233
+ )
234
+
235
+ # Simulate cache hit: saved tokens = cached context tokens (not re-encoded)
236
+ memory_state["query_count"] += 1
237
+ memory_state["total_saved_tokens"] += memory_state["cached_tokens"]
238
+
239
+ for history_out, tok_info, latency, err in stream_response(system, history, user_msg):
240
+ # Annotate with cache hit badge
241
+ cache_badge = "🟒 **Cache HIT (estimated)** β€” context eligible for KV cache reuse" if cached_ctx else "βšͺ No cache"
242
+ yield history_out, tok_info, latency, _render_cache_stats(), cache_badge
243
+
244
+
245
+ # ─────────────────────────────────────────────────────────────────────────────
246
+ # GRADIO UI
247
+ # ─────────────────────────────────────────────────────────────────────────────
248
+ CRUSOE_BLUE = "#1B4FCC"
249
+ CRUSOE_DARK = "#0D1B2A"
250
+
251
+ css = """
252
+ .crusoe-header { text-align: center; padding: 1.5rem 0 0.5rem; }
253
+ .token-badge { font-size: 1.1rem; font-weight: 600; color: #1B4FCC; }
254
+ .cache-stats { background: #f0f4ff; border-radius: 8px; padding: 1rem; }
255
+ .cache-hit { color: #16a34a; font-weight: 700; font-size: 1rem; }
256
+ .stat-row { display: flex; gap: 1.5rem; align-items: center; }
257
+ footer { display: none !important; }
258
+ """
259
+
260
+ with gr.Blocks(
261
+ title="Crusoe Foundry β€” Infinite Context Demo",
262
+ theme=gr.themes.Soft(primary_hue="blue"),
263
+ css=css,
264
+ ) as demo:
265
+
266
+ # ── Header ────────────────────────────────────────────────────────────────
267
+ gr.HTML("""
268
+ <div class="crusoe-header">
269
+ <img src="https://crusoe.ai/wp-content/uploads/2023/09/crusoe-logo.svg"
270
+ alt="Crusoe" height="40" style="margin-bottom:0.5rem"/>
271
+ <h1 style="font-size:1.8rem;font-weight:700;color:#0D1B2A;margin:0">
272
+ Infinite Context Demo
273
+ </h1>
274
+ <p style="color:#555;margin:0.3rem 0 0">
275
+ Powered by <strong>Crusoe Foundry</strong> &nbsp;Β·&nbsp;
276
+ MemoryAlloyβ„’ &amp; KV Cache Sharing
277
+ </p>
278
+ </div>
279
+ """)
280
+
281
+ with gr.Tabs():
282
+
283
+ # ── TAB 1: LEGAL ──────────────────────────────────────────────────────
284
+ with gr.Tab("βš–οΈ Legal Analysis"):
285
+ gr.Markdown(
286
+ "Upload contracts, briefs, or regulatory documents β€” ask questions "
287
+ "across the **entire document** with no chunking or retrieval needed."
288
+ )
289
+ with gr.Row():
290
+ with gr.Column(scale=1):
291
+ legal_files = gr.File(
292
+ label="Upload Documents (PDF, TXT, MD)",
293
+ file_count="multiple",
294
+ file_types=[".pdf", ".txt", ".md", ".docx"],
295
+ )
296
+ legal_ingest_btn = gr.Button("πŸ“₯ Load into Context", variant="primary")
297
+ legal_status = gr.Markdown("No documents loaded.")
298
+ legal_token_badge = gr.Markdown("", elem_classes=["token-badge"])
299
+ legal_preview = gr.Textbox(
300
+ label="Document Preview",
301
+ lines=6,
302
+ interactive=False,
303
+ placeholder="Document text will appear here after loading…",
304
+ )
305
+ with gr.Column(scale=2):
306
+ legal_chatbot = gr.Chatbot(label="Legal Q&A", height=420, bubble_full_width=False)
307
+ with gr.Row():
308
+ legal_input = gr.Textbox(
309
+ placeholder="e.g. What are all indemnification carve-outs?",
310
+ label="Ask a question",
311
+ scale=4,
312
+ )
313
+ legal_send = gr.Button("Send", variant="primary", scale=1)
314
+ with gr.Row():
315
+ legal_tok_info = gr.Markdown("", elem_classes=["token-badge"])
316
+ legal_latency = gr.Markdown("")
317
+ legal_err = gr.Markdown("", visible=False)
318
+ gr.Examples(
319
+ examples=[
320
+ ["What are the termination clauses?"],
321
+ ["Summarize all indemnification obligations for each party."],
322
+ ["List every deadline or date mentioned in the document."],
323
+ ["Are there any non-compete or non-solicitation clauses?"],
324
+ ["What happens in the event of a material breach?"],
325
+ ],
326
+ inputs=legal_input,
327
+ )
328
+
329
+ legal_ingest_btn.click(
330
+ legal_ingest,
331
+ inputs=[legal_files],
332
+ outputs=[legal_status, legal_token_badge, legal_preview],
333
+ )
334
+
335
+ def legal_submit(msg, history):
336
+ yield from legal_chat(msg, history)
337
+
338
+ legal_send.click(
339
+ legal_submit,
340
+ inputs=[legal_input, legal_chatbot],
341
+ outputs=[legal_chatbot, legal_tok_info, legal_latency, legal_err],
342
+ ).then(lambda: "", outputs=legal_input)
343
+
344
+ legal_input.submit(
345
+ legal_submit,
346
+ inputs=[legal_input, legal_chatbot],
347
+ outputs=[legal_chatbot, legal_tok_info, legal_latency, legal_err],
348
+ ).then(lambda: "", outputs=legal_input)
349
+
350
+ # ── TAB 2: DEV ────────────────────────────────────────────────────────
351
+ with gr.Tab("πŸ’» Codebase Intelligence"):
352
+ gr.Markdown(
353
+ "Upload source files or paste code β€” reason across your **entire codebase** "
354
+ "simultaneously. No embeddings, no retrieval, no chunking."
355
+ )
356
+ with gr.Row():
357
+ with gr.Column(scale=1):
358
+ dev_files = gr.File(
359
+ label="Upload Source Files",
360
+ file_count="multiple",
361
+ file_types=[".py", ".js", ".ts", ".go", ".rs", ".java", ".txt", ".md"],
362
+ )
363
+ dev_paste = gr.Textbox(
364
+ label="Or paste code directly",
365
+ lines=8,
366
+ placeholder="Paste your code here…",
367
+ )
368
+ dev_ingest_btn = gr.Button("πŸ“₯ Load Codebase", variant="primary")
369
+ dev_status = gr.Markdown("No code loaded.")
370
+ dev_token_badge = gr.Markdown("", elem_classes=["token-badge"])
371
+ dev_preview = gr.Textbox(
372
+ label="Codebase Preview",
373
+ lines=5,
374
+ interactive=False,
375
+ placeholder="Loaded code will appear here…",
376
+ )
377
+ with gr.Column(scale=2):
378
+ dev_chatbot = gr.Chatbot(label="Codebase Q&A", height=420, bubble_full_width=False)
379
+ with gr.Row():
380
+ dev_input = gr.Textbox(
381
+ placeholder="e.g. Where is the authentication logic and how does it work?",
382
+ label="Ask about your codebase",
383
+ scale=4,
384
+ )
385
+ dev_send = gr.Button("Send", variant="primary", scale=1)
386
+ with gr.Row():
387
+ dev_tok_info = gr.Markdown("", elem_classes=["token-badge"])
388
+ dev_latency = gr.Markdown("")
389
+ dev_err = gr.Markdown("")
390
+ gr.Examples(
391
+ examples=[
392
+ ["Explain the overall architecture of this codebase."],
393
+ ["Where are potential race conditions or concurrency issues?"],
394
+ ["List all API endpoints and their HTTP methods."],
395
+ ["Which functions have no error handling?"],
396
+ ["How would I add rate limiting to this service?"],
397
+ ],
398
+ inputs=dev_input,
399
+ )
400
+
401
+ dev_ingest_btn.click(
402
+ dev_ingest,
403
+ inputs=[dev_files, dev_paste],
404
+ outputs=[dev_status, dev_token_badge, dev_preview],
405
+ )
406
+
407
+ def dev_submit(msg, history):
408
+ yield from dev_chat(msg, history)
409
+
410
+ dev_send.click(
411
+ dev_submit,
412
+ inputs=[dev_input, dev_chatbot],
413
+ outputs=[dev_chatbot, dev_tok_info, dev_latency, dev_err],
414
+ ).then(lambda: "", outputs=dev_input)
415
+
416
+ dev_input.submit(
417
+ dev_submit,
418
+ inputs=[dev_input, dev_chatbot],
419
+ outputs=[dev_chatbot, dev_tok_info, dev_latency, dev_err],
420
+ ).then(lambda: "", outputs=dev_input)
421
+
422
+ # ── TAB 3: MEMORY DEMO ────────────────────────────────────────────────
423
+ with gr.Tab("🧠 MemoryAlloyβ„’ Demo"):
424
+ gr.Markdown(
425
+ "See KV cache sharing in action. Set a large context once β€” every subsequent "
426
+ "query reuses the **cached key-value representations**, slashing compute and cost.\n\n"
427
+ "> **Note:** Token savings shown below are *estimated* based on context size. "
428
+ "Actual cache reuse depends on server-side KV cache availability on Crusoe Foundry."
429
+ )
430
+ with gr.Row():
431
+ with gr.Column(scale=1):
432
+ gr.Markdown("### 1. Set Shared Context")
433
+ memory_context_input = gr.Textbox(
434
+ label="Context to cache (paste any large text)",
435
+ lines=12,
436
+ placeholder="Paste a large document, knowledge base, or system context here. "
437
+ "This will be cached and reused across all queries.",
438
+ )
439
+ memory_cache_btn = gr.Button("πŸ”’ Lock into KV Cache", variant="primary")
440
+ memory_cache_status = gr.Markdown("No context cached.")
441
+
442
+ gr.Markdown("### 2. Cache Stats")
443
+ memory_stats = gr.Markdown("", elem_classes=["cache-stats"])
444
+
445
+ with gr.Column(scale=2):
446
+ gr.Markdown("### 3. Query Against Cached Context")
447
+ memory_chatbot = gr.Chatbot(
448
+ label="Memory-Augmented Chat",
449
+ height=380,
450
+ bubble_full_width=False,
451
+ )
452
+ with gr.Row():
453
+ memory_input = gr.Textbox(
454
+ placeholder="Ask anything β€” the context is already cached…",
455
+ label="Your question",
456
+ scale=4,
457
+ )
458
+ memory_send = gr.Button("Send", variant="primary", scale=1)
459
+ with gr.Row():
460
+ memory_tok_info = gr.Markdown("", elem_classes=["token-badge"])
461
+ memory_latency = gr.Markdown("")
462
+ memory_cache_hit = gr.Markdown("", elem_classes=["cache-hit"])
463
+ memory_err = gr.Markdown("")
464
+ gr.Examples(
465
+ examples=[
466
+ ["Summarize the key points in 3 sentences."],
467
+ ["What topics are covered in this context?"],
468
+ ["Extract all named entities mentioned."],
469
+ ["What are the most important dates or numbers?"],
470
+ ],
471
+ inputs=memory_input,
472
+ )
473
+
474
+ memory_cache_btn.click(
475
+ memory_set_context,
476
+ inputs=[memory_context_input],
477
+ outputs=[memory_cache_status, memory_stats],
478
+ )
479
+
480
+ def memory_submit(msg, history):
481
+ yield from memory_chat(msg, history)
482
+
483
+ memory_send.click(
484
+ memory_submit,
485
+ inputs=[memory_input, memory_chatbot],
486
+ outputs=[memory_chatbot, memory_tok_info, memory_latency, memory_stats, memory_cache_hit],
487
+ ).then(lambda: "", outputs=memory_input)
488
+
489
+ memory_input.submit(
490
+ memory_submit,
491
+ inputs=[memory_input, memory_chatbot],
492
+ outputs=[memory_chatbot, memory_tok_info, memory_latency, memory_stats, memory_cache_hit],
493
+ ).then(lambda: "", outputs=memory_input)
494
+
495
+ # ── Footer ────────────────────────────────────────────────────────────────
496
+ gr.HTML("""
497
+ <div style="text-align:center;color:#888;padding:1.5rem 0 0.5rem;font-size:0.85rem">
498
+ Built on <strong>Crusoe Foundry</strong> &nbsp;Β·&nbsp;
499
+ Sustainable AI compute &nbsp;Β·&nbsp;
500
+ <a href="https://crusoe.ai" target="_blank">crusoe.ai</a>
501
+ </div>
502
+ """)
503
+
504
+
505
+ if __name__ == "__main__":
506
+ demo.launch(show_api=False)
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ gradio>=4.31.0
2
+ openai>=1.30.0
3
+ tiktoken>=0.7.0
4
+ pdfminer.six>=20221105