Beexly commited on
Commit
f446e90
·
verified ·
1 Parent(s): 6b8a01c

Upload folder using huggingface_hub

Browse files
Files changed (4) hide show
  1. README.md +5 -0
  2. __pycache__/app.cpython-311.pyc +0 -0
  3. app.py +108 -19
  4. requirements.txt +2 -0
README.md CHANGED
@@ -37,6 +37,11 @@ or prediction engine — this is Garrett's own tooling, unrelated to GSE.**
37
  - Streaming responses
38
  - Copy button on every reply (hover a message)
39
  - Chat history capped at the last 12 turns to bound context growth
 
 
 
 
 
40
 
41
  ## Swapping the model
42
 
 
37
  - Streaming responses
38
  - Copy button on every reply (hover a message)
39
  - Chat history capped at the last 12 turns to bound context growth
40
+ - **Documents tab**: upload PDF/DOCX/PPTX/HTML/TXT/MD, parsed locally via
41
+ `docling` (MIT, no external API), chunked, and indexed with BM25 keyword
42
+ search (`rank_bm25`, pure Python, no embedding model, no extra GPU
43
+ weight). Check "Search my documents" in the Chat tab to have the model
44
+ retrieve and cite relevant excerpts instead of answering blind.
45
 
46
  ## Swapping the model
47
 
__pycache__/app.cpython-311.pyc ADDED
Binary file (12.1 kB). View file
 
app.py CHANGED
@@ -9,9 +9,13 @@ from transformers import (
9
  BitsAndBytesConfig,
10
  TextIteratorStreamer,
11
  )
 
 
12
 
13
  MODEL_ID = "Qwen/Qwen2.5-72B-Instruct"
14
  MAX_HISTORY_TURNS = 12
 
 
15
 
16
  SYSTEM_PROMPTS = {
17
  "General": "You are a helpful assistant.",
@@ -43,16 +47,82 @@ model = AutoModelForCausalLM.from_pretrained(
43
  attn_implementation="sdpa",
44
  )
45
 
46
-
47
- def _duration(message, history, system_prompt_name, max_new_tokens, temperature):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  return min(120, 40 + int(max_new_tokens) // 10)
49
 
50
 
51
  @spaces.GPU(duration=_duration)
52
- def generate(message, history, system_prompt_name, max_new_tokens, temperature):
53
  history = history[-(MAX_HISTORY_TURNS * 2):]
54
  system_prompt = SYSTEM_PROMPTS.get(system_prompt_name, SYSTEM_PROMPTS["General"])
55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  messages = [{"role": "system", "content": system_prompt}]
57
  for turn in history:
58
  messages.append({"role": turn["role"], "content": turn["content"]})
@@ -90,23 +160,42 @@ def generate(message, history, system_prompt_name, max_new_tokens, temperature):
90
 
91
  chatbot = gr.Chatbot(type="messages", show_copy_button=True, label="Chat")
92
 
93
- demo = gr.ChatInterface(
94
- fn=generate,
95
- type="messages",
96
- chatbot=chatbot,
97
- additional_inputs=[
98
- gr.Dropdown(
99
- list(SYSTEM_PROMPTS.keys()), value="General", label="System prompt"
100
- ),
101
- gr.Slider(64, 1024, value=512, step=64, label="Max new tokens"),
102
- gr.Slider(0.0, 1.5, value=0.7, step=0.1, label="Temperature"),
103
- ],
104
- title="Beexly Studio Chat (ZeroGPU)",
105
- description=(
106
  f"Personal free-GPU chat — `{MODEL_ID}` (4-bit), streaming, on ZeroGPU. "
107
- "Hover any reply for a copy button. Not part of any Galaxy Sports Edge product."
108
- ),
109
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
  if __name__ == "__main__":
112
  demo.launch()
 
9
  BitsAndBytesConfig,
10
  TextIteratorStreamer,
11
  )
12
+ from docling.document_converter import DocumentConverter
13
+ from rank_bm25 import BM25Okapi
14
 
15
  MODEL_ID = "Qwen/Qwen2.5-72B-Instruct"
16
  MAX_HISTORY_TURNS = 12
17
+ CHUNK_CHARS = 800
18
+ TOP_K = 4
19
 
20
  SYSTEM_PROMPTS = {
21
  "General": "You are a helpful assistant.",
 
47
  attn_implementation="sdpa",
48
  )
49
 
50
+ _docling_converter = None
51
+ CHUNKS = [] # list of {"source": filename, "text": chunk_text}
52
+ BM25_INDEX = None
53
+
54
+
55
+ def _docling():
56
+ global _docling_converter
57
+ if _docling_converter is None:
58
+ _docling_converter = DocumentConverter()
59
+ return _docling_converter
60
+
61
+
62
+ def _chunk_markdown(md_text, source, max_chars=CHUNK_CHARS):
63
+ paragraphs = [p.strip() for p in md_text.split("\n\n") if p.strip()]
64
+ chunks, current = [], ""
65
+ for p in paragraphs:
66
+ if len(current) + len(p) + 1 <= max_chars:
67
+ current = f"{current}\n{p}".strip()
68
+ else:
69
+ if current:
70
+ chunks.append(current)
71
+ current = p
72
+ if current:
73
+ chunks.append(current)
74
+ return [{"source": source, "text": c} for c in chunks]
75
+
76
+
77
+ def index_documents(files):
78
+ global CHUNKS, BM25_INDEX
79
+ if not files:
80
+ return "No files given.", ""
81
+ new_chunks = []
82
+ for f in files:
83
+ result = _docling().convert(f.name if hasattr(f, "name") else f)
84
+ md = result.document.export_to_markdown()
85
+ source = (f.name if hasattr(f, "name") else str(f)).split("/")[-1]
86
+ new_chunks.extend(_chunk_markdown(md, source))
87
+ CHUNKS = new_chunks # replace — one working set at a time, kept simple
88
+ tokenized = [c["text"].lower().split() for c in CHUNKS]
89
+ BM25_INDEX = BM25Okapi(tokenized) if tokenized else None
90
+ sources = sorted({c["source"] for c in CHUNKS})
91
+ status = f"Indexed {len(sources)} document(s), {len(CHUNKS)} chunks."
92
+ listing = "\n".join(f"- {s}" for s in sources)
93
+ return status, listing
94
+
95
+
96
+ def _retrieve(query, k=TOP_K):
97
+ if BM25_INDEX is None or not CHUNKS:
98
+ return []
99
+ scores = BM25_INDEX.get_scores(query.lower().split())
100
+ ranked = sorted(range(len(CHUNKS)), key=lambda i: scores[i], reverse=True)
101
+ return [CHUNKS[i] for i in ranked[:k] if scores[i] > 0]
102
+
103
+
104
+ def _duration(message, history, system_prompt_name, use_docs, max_new_tokens, temperature):
105
  return min(120, 40 + int(max_new_tokens) // 10)
106
 
107
 
108
  @spaces.GPU(duration=_duration)
109
+ def generate(message, history, system_prompt_name, use_docs, max_new_tokens, temperature):
110
  history = history[-(MAX_HISTORY_TURNS * 2):]
111
  system_prompt = SYSTEM_PROMPTS.get(system_prompt_name, SYSTEM_PROMPTS["General"])
112
 
113
+ retrieved = _retrieve(message) if use_docs else []
114
+ if retrieved:
115
+ context_block = "\n\n".join(
116
+ f"[Source: {c['source']}]\n{c['text']}" for c in retrieved
117
+ )
118
+ system_prompt = (
119
+ f"{system_prompt}\n\nUse the following excerpts from the user's own "
120
+ "uploaded documents if relevant to their question. Cite the source "
121
+ "filename in brackets when you use one. If the excerpts don't answer "
122
+ "the question, say so and answer from general knowledge instead.\n\n"
123
+ f"{context_block}"
124
+ )
125
+
126
  messages = [{"role": "system", "content": system_prompt}]
127
  for turn in history:
128
  messages.append({"role": turn["role"], "content": turn["content"]})
 
160
 
161
  chatbot = gr.Chatbot(type="messages", show_copy_button=True, label="Chat")
162
 
163
+ with gr.Blocks(title="Beexly Studio Chat (ZeroGPU)") as demo:
164
+ gr.Markdown(
165
+ "# Beexly Studio Chat (ZeroGPU)\n"
 
 
 
 
 
 
 
 
 
 
166
  f"Personal free-GPU chat — `{MODEL_ID}` (4-bit), streaming, on ZeroGPU. "
167
+ "Not part of any Galaxy Sports Edge product."
168
+ )
169
+ with gr.Tab("Chat"):
170
+ chat_iface = gr.ChatInterface(
171
+ fn=generate,
172
+ type="messages",
173
+ chatbot=chatbot,
174
+ additional_inputs=[
175
+ gr.Dropdown(
176
+ list(SYSTEM_PROMPTS.keys()), value="General", label="System prompt"
177
+ ),
178
+ gr.Checkbox(
179
+ label="Search my documents (Documents tab)", value=False
180
+ ),
181
+ gr.Slider(64, 1024, value=512, step=64, label="Max new tokens"),
182
+ gr.Slider(0.0, 1.5, value=0.7, step=0.1, label="Temperature"),
183
+ ],
184
+ )
185
+ with gr.Tab("Documents"):
186
+ gr.Markdown(
187
+ "Upload documents (PDF/DOCX/PPTX/HTML/TXT/MD), parsed locally via "
188
+ "`docling` (MIT, no external API) and retrieved with BM25 keyword "
189
+ "search — no embedding model, no extra GPU weight. Check "
190
+ "'Search my documents' in the Chat tab to use them."
191
+ )
192
+ docs_files = gr.File(label="Documents", file_count="multiple")
193
+ docs_btn = gr.Button("Index documents")
194
+ docs_status = gr.Textbox(label="Status", interactive=False)
195
+ docs_list = gr.Textbox(label="Indexed sources", interactive=False)
196
+ docs_btn.click(
197
+ index_documents, inputs=docs_files, outputs=[docs_status, docs_list]
198
+ )
199
 
200
  if __name__ == "__main__":
201
  demo.launch()
requirements.txt CHANGED
@@ -4,3 +4,5 @@ transformers>=4.46.0
4
  accelerate>=1.1.0
5
  torch>=2.4.0
6
  bitsandbytes>=0.44.0
 
 
 
4
  accelerate>=1.1.0
5
  torch>=2.4.0
6
  bitsandbytes>=0.44.0
7
+ docling
8
+ rank_bm25