hadadrjt commited on
Commit
f99ad65
·
1 Parent(s): 5175777

ai: Restructured repo for production.

Browse files
.gitattributes DELETED
@@ -1,35 +0,0 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -4,7 +4,7 @@ colorFrom: yellow
4
  colorTo: purple
5
  sdk: gradio
6
  sdk_version: 5.29.0
7
- app_file: jarvis.py
8
  pinned: true
9
  short_description: Inspired by Iron Man movies.
10
  models:
 
4
  colorTo: purple
5
  sdk: gradio
6
  sdk_version: 5.29.0
7
+ app_file: app.py
8
  pinned: true
9
  short_description: Inspired by Iron Man movies.
10
  models:
app.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+
6
+ from src.main.gradio import launch_ui
7
+
8
+ # J.A.R.V.I.S.
9
+ if __name__ == "__main__":
10
+ launch_ui()
ai → assets/bin/ai RENAMED
File without changes
API.md → docs/API.md RENAMED
@@ -10,7 +10,7 @@ pip install gradio_client rich --upgrade
10
  #### DOWNLOAD JARVIS SCRIPT
11
  ```bash
12
  # Terminal script.
13
- wget https://huggingface.co/spaces/hadadrjt/ai/raw/main/ai
14
 
15
  # Set permission.
16
  chmod a+x ai
 
10
  #### DOWNLOAD JARVIS SCRIPT
11
  ```bash
12
  # Terminal script.
13
+ wget https://huggingface.co/spaces/hadadrjt/ai/raw/main/assets/bin/ai
14
 
15
  # Set permission.
16
  chmod a+x ai
CREDITS → docs/CREDITS RENAMED
File without changes
LICENSE → docs/LICENSE RENAMED
File without changes
NOTICE → docs/NOTICE RENAMED
File without changes
jarvis.py DELETED
@@ -1,573 +0,0 @@
1
- #
2
- # SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
3
- # SPDX-License-Identifier: Apache-2.0
4
- #
5
-
6
- import asyncio
7
- import codecs # Reasoning
8
- import docx # Microsoft Word
9
- import gradio as gr
10
- import httpx
11
- import json
12
- import os
13
- import pandas as pd # Microsoft Excel
14
- import pdfplumber # PDF
15
- import pytesseract # OCR
16
- import random
17
- import requests
18
- import threading
19
- import uuid
20
- import zipfile # Microsoft Word
21
- import io
22
-
23
- from PIL import Image # OCR
24
- from pathlib import Path
25
- from pptx import Presentation # Microsoft PowerPoint
26
- from openpyxl import load_workbook # Microsoft Excel
27
-
28
- # ============================
29
- # System Setup
30
- # ============================
31
-
32
- # Install Tesseract OCR and dependencies for text extraction from images.
33
- os.system("apt-get update -q -y && \
34
- apt-get install -q -y tesseract-ocr \
35
- tesseract-ocr-eng tesseract-ocr-ind \
36
- libleptonica-dev libtesseract-dev"
37
- )
38
-
39
- # ============================
40
- # HF Secrets Setup
41
- # ============================
42
-
43
- # Initial welcome messages
44
- JARVIS_INIT = json.loads(os.getenv("HELLO", "[]"))
45
-
46
- # Deep Search
47
- DEEP_SEARCH_PROVIDER_HOST = os.getenv("DEEP_SEARCH_PROVIDER_HOST")
48
- DEEP_SEARCH_PROVIDER_KEY = os.getenv('DEEP_SEARCH_PROVIDER_KEY')
49
- DEEP_SEARCH_INSTRUCTIONS = os.getenv("DEEP_SEARCH_INSTRUCTIONS")
50
-
51
- # Servers and instructions
52
- INTERNAL_AI_GET_SERVER = os.getenv("INTERNAL_AI_GET_SERVER")
53
- INTERNAL_AI_INSTRUCTIONS = os.getenv("INTERNAL_TRAINING_DATA")
54
-
55
- # System instructions mapping
56
- SYSTEM_PROMPT_MAPPING = json.loads(os.getenv("SYSTEM_PROMPT_MAPPING", "{}"))
57
- SYSTEM_PROMPT_DEFAULT = os.getenv("DEFAULT_SYSTEM")
58
-
59
- # List of available servers
60
- LINUX_SERVER_HOSTS = [h for h in json.loads(os.getenv("LINUX_SERVER_HOST", "[]")) if h]
61
-
62
- # List of available keys
63
- LINUX_SERVER_PROVIDER_KEYS = [k for k in json.loads(os.getenv("LINUX_SERVER_PROVIDER_KEY", "[]")) if k]
64
- LINUX_SERVER_PROVIDER_KEYS_MARKED = set()
65
- LINUX_SERVER_PROVIDER_KEYS_ATTEMPTS = {}
66
-
67
- # Server errors codes
68
- LINUX_SERVER_ERRORS = set(map(int, filter(None, os.getenv("LINUX_SERVER_ERROR", "").split(","))))
69
-
70
- # Personal UI
71
- AI_TYPES = {f"AI_TYPE_{i}": os.getenv(f"AI_TYPE_{i}") for i in range(1, 10)}
72
- RESPONSES = {f"RESPONSE_{i}": os.getenv(f"RESPONSE_{i}") for i in range(1, 11)}
73
-
74
- # Model mapping
75
- MODEL_MAPPING = json.loads(os.getenv("MODEL_MAPPING", "{}"))
76
- MODEL_CONFIG = json.loads(os.getenv("MODEL_CONFIG", "{}"))
77
- MODEL_CHOICES = list(MODEL_MAPPING.values())
78
-
79
- # Default model config and key for fallback
80
- DEFAULT_CONFIG = json.loads(os.getenv("DEFAULT_CONFIG", "{}"))
81
- DEFAULT_MODEL_KEY = list(MODEL_MAPPING.keys())[0] if MODEL_MAPPING else None
82
-
83
- # HTML <head> codes (SEO, etc.)
84
- META_TAGS = os.getenv("META_TAGS")
85
-
86
- # Allowed file extensions
87
- ALLOWED_EXTENSIONS = json.loads(os.getenv("ALLOWED_EXTENSIONS", "[]"))
88
-
89
- # ============================
90
- # Session Management
91
- # ============================
92
-
93
- class SessionWithID(requests.Session):
94
- """
95
- Custom session object that holds a unique session ID and async control flags.
96
- Used to track individual user sessions and allow cancellation of ongoing requests.
97
- """
98
- def __init__(self):
99
- super().__init__()
100
- self.session_id = str(uuid.uuid4()) # Unique ID per session
101
- self.stop_event = asyncio.Event() # Async event to signal stop requests
102
- self.cancel_token = {"cancelled": False} # Flag to indicate cancellation
103
-
104
- def create_session():
105
- """
106
- Create and return a new SessionWithID object.
107
- Called when a new user session starts or chat is reset.
108
- """
109
- return SessionWithID()
110
-
111
- def ensure_stop_event(sess):
112
- """
113
- Ensure that the session object has stop_event and cancel_token attributes.
114
- Useful when restoring or reusing sessions.
115
- """
116
- if not hasattr(sess, "stop_event"):
117
- sess.stop_event = asyncio.Event()
118
- if not hasattr(sess, "cancel_token"):
119
- sess.cancel_token = {"cancelled": False}
120
-
121
- def marked_item(item, marked, attempts):
122
- """
123
- Mark a provider key or host as temporarily problematic after repeated failures.
124
- Automatically unmark after 5 minutes to retry.
125
- This helps avoid repeatedly using failing providers.
126
- """
127
- marked.add(item)
128
- attempts[item] = attempts.get(item, 0) + 1
129
- if attempts[item] >= 3:
130
- def remove():
131
- marked.discard(item)
132
- attempts.pop(item, None)
133
- threading.Timer(300, remove).start()
134
-
135
- def get_model_key(display):
136
- """
137
- Get the internal model key (identifier) from the display name.
138
- Returns default model key if not found.
139
- """
140
- return next((k for k, v in MODEL_MAPPING.items() if v == display), DEFAULT_MODEL_KEY)
141
-
142
- # ============================
143
- # File Content Extraction Utilities
144
- # ============================
145
-
146
- def extract_pdf_content(fp):
147
- """
148
- Extract text content from PDF file.
149
- Includes OCR on embedded images to capture text within images.
150
- Also extracts tables as tab-separated text.
151
- """
152
- content = ""
153
- try:
154
- with pdfplumber.open(fp) as pdf:
155
- for page in pdf.pages:
156
- # Extract text from page
157
- text = page.extract_text() or ""
158
- content += text + "\n"
159
- # OCR on images if any
160
- if page.images:
161
- img_obj = page.to_image(resolution=300)
162
- for img in page.images:
163
- bbox = (img["x0"], img["top"], img["x1"], img["bottom"])
164
- cropped = img_obj.original.crop(bbox)
165
- ocr_text = pytesseract.image_to_string(cropped)
166
- if ocr_text.strip():
167
- content += ocr_text + "\n"
168
- # Extract tables as TSV
169
- tables = page.extract_tables()
170
- for table in tables:
171
- for row in table:
172
- cells = [str(cell) for cell in row if cell is not None]
173
- if cells:
174
- content += "\t".join(cells) + "\n"
175
- except Exception as e:
176
- content += f"\n[Error reading PDF {fp}: {e}]"
177
- return content.strip()
178
-
179
- def extract_docx_content(fp):
180
- """
181
- Extract text from Microsoft Word files.
182
- Also performs OCR on embedded images inside the Microsoft Word archive.
183
- """
184
- content = ""
185
- try:
186
- doc = docx.Document(fp)
187
- # Extract paragraphs
188
- for para in doc.paragraphs:
189
- content += para.text + "\n"
190
- # Extract tables
191
- for table in doc.tables:
192
- for row in table.rows:
193
- cells = [cell.text for cell in row.cells]
194
- content += "\t".join(cells) + "\n"
195
- # OCR on embedded images inside Microsoft Word
196
- with zipfile.ZipFile(fp) as z:
197
- for file in z.namelist():
198
- if file.startswith("word/media/"):
199
- data = z.read(file)
200
- try:
201
- img = Image.open(io.BytesIO(data))
202
- ocr_text = pytesseract.image_to_string(img)
203
- if ocr_text.strip():
204
- content += ocr_text + "\n"
205
- except Exception:
206
- # Ignore images that can't be processed
207
- pass
208
- except Exception as e:
209
- content += f"\n[Error reading Microsoft Word {fp}: {e}]"
210
- return content.strip()
211
-
212
- def extract_excel_content(fp):
213
- """
214
- Extract content from Microsoft Excel files.
215
- Converts sheets to CSV text.
216
- Attempts OCR on embedded images if present.
217
- """
218
- content = ""
219
- try:
220
- # Extract all sheets as CSV text
221
- sheets = pd.read_excel(fp, sheet_name=None)
222
- for name, df in sheets.items():
223
- content += f"Sheet: {name}\n"
224
- content += df.to_csv(index=False) + "\n"
225
- # Load workbook to access images
226
- wb = load_workbook(fp, data_only=True)
227
- if wb._images:
228
- for image in wb._images:
229
- try:
230
- pil_img = Image.open(io.BytesIO(image._data()))
231
- ocr_text = pytesseract.image_to_string(pil_img)
232
- if ocr_text.strip():
233
- content += ocr_text + "\n"
234
- except Exception:
235
- # Ignore images that can't be processed
236
- pass
237
- except Exception as e:
238
- content += f"\n[Error reading Microsoft Excel {fp}: {e}]"
239
- return content.strip()
240
-
241
- def extract_pptx_content(fp):
242
- """
243
- Extract text content from Microsoft PowerPoint presentation slides.
244
- Includes text from shapes and tables.
245
- Performs OCR on embedded images.
246
- """
247
- content = ""
248
- try:
249
- prs = Presentation(fp)
250
- for slide in prs.slides:
251
- for shape in slide.shapes:
252
- # Extract text from shapes
253
- if hasattr(shape, "text") and shape.text:
254
- content += shape.text + "\n"
255
- # OCR on images inside shapes
256
- if shape.shape_type == 13 and hasattr(shape, "image") and shape.image:
257
- try:
258
- img = Image.open(io.BytesIO(shape.image.blob))
259
- ocr_text = pytesseract.image_to_string(img)
260
- if ocr_text.strip():
261
- content += ocr_text + "\n"
262
- except Exception:
263
- pass
264
- # Extract tables
265
- for shape in slide.shapes:
266
- if shape.has_table:
267
- table = shape.table
268
- for row in table.rows:
269
- cells = [cell.text for cell in row.cells]
270
- content += "\t".join(cells) + "\n"
271
- except Exception as e:
272
- content += f"\n[Error reading Microsoft PowerPoint {fp}: {e}]"
273
- return content.strip()
274
-
275
- def extract_file_content(fp):
276
- """
277
- Determine file type by extension and extract text content accordingly.
278
- For unknown types, attempts to read as plain text.
279
- """
280
- ext = Path(fp).suffix.lower()
281
- if ext == ".pdf":
282
- return extract_pdf_content(fp)
283
- elif ext in [".doc", ".docx"]:
284
- return extract_docx_content(fp)
285
- elif ext in [".xlsx", ".xls"]:
286
- return extract_excel_content(fp)
287
- elif ext in [".ppt", ".pptx"]:
288
- return extract_pptx_content(fp)
289
- else:
290
- try:
291
- return Path(fp).read_text(encoding="utf-8").strip()
292
- except Exception as e:
293
- return f"\n[Error reading file {fp}: {e}]"
294
-
295
- # ============================
296
- # AI Server Communication
297
- # ============================
298
-
299
- async def fetch_response_stream_async(host, key, model, msgs, cfg, sid, stop_event, cancel_token):
300
- """
301
- Async generator that streams AI responses from a backend server.
302
- Implements retry logic and marks failing keys to avoid repeated failures.
303
- Streams reasoning and content separately for richer UI updates.
304
- """
305
- for timeout in [5, 10]:
306
- try:
307
- async with httpx.AsyncClient(timeout=timeout) as client:
308
- async with client.stream("POST", host, json={**{"model": model, "messages": msgs, "session_id": sid, "stream": True}, **cfg}, headers={"Authorization": f"Bearer {key}"}) as response:
309
- if response.status_code in LINUX_SERVER_ERRORS:
310
- marked_item(key, LINUX_SERVER_PROVIDER_KEYS_MARKED, LINUX_SERVER_PROVIDER_KEYS_ATTEMPTS)
311
- return
312
- async for line in response.aiter_lines():
313
- if stop_event.is_set() or cancel_token["cancelled"]:
314
- return
315
- if not line:
316
- continue
317
- if line.startswith("data: "):
318
- data = line[6:]
319
- if data.strip() == RESPONSES["RESPONSE_10"]:
320
- return
321
- try:
322
- j = json.loads(data)
323
- if isinstance(j, dict) and j.get("choices"):
324
- for ch in j["choices"]:
325
- delta = ch.get("delta", {})
326
- # Stream reasoning text separately for UI
327
- if "reasoning" in delta and delta["reasoning"]:
328
- decoded = delta["reasoning"].encode('utf-8').decode('unicode_escape')
329
- yield ("reasoning", decoded)
330
- # Stream main content text
331
- if "content" in delta and delta["content"]:
332
- yield ("content", delta["content"])
333
- except Exception:
334
- # Ignore malformed JSON or unexpected data
335
- continue
336
- except Exception:
337
- # Network or other errors, try next timeout or mark key
338
- continue
339
- marked_item(key, LINUX_SERVER_PROVIDER_KEYS_MARKED, LINUX_SERVER_PROVIDER_KEYS_ATTEMPTS)
340
- return
341
-
342
- async def chat_with_model_async(history, user_input, model_display, sess, custom_prompt, deep_search):
343
- """
344
- Core async function to interact with AI model.
345
- Prepares message history, system instructions, and optionally integrates deep search results.
346
- Tries multiple backend hosts and keys with fallback.
347
- Yields streamed responses for UI updates.
348
- """
349
- ensure_stop_event(sess)
350
- sess.stop_event.clear()
351
- sess.cancel_token["cancelled"] = False
352
- if not LINUX_SERVER_PROVIDER_KEYS or not LINUX_SERVER_HOSTS:
353
- yield ("content", RESPONSES["RESPONSE_3"]) # No providers available
354
- return
355
- if not hasattr(sess, "session_id") or not sess.session_id:
356
- sess.session_id = str(uuid.uuid4())
357
- model_key = get_model_key(model_display)
358
- cfg = MODEL_CONFIG.get(model_key, DEFAULT_CONFIG)
359
- msgs = []
360
- # If deep search enabled and using primary model, prepend deep search instructions and results
361
- if deep_search and model_display == MODEL_CHOICES[0]:
362
- msgs.append({"role": "system", "content": DEEP_SEARCH_INSTRUCTIONS})
363
- try:
364
- async with httpx.AsyncClient() as client:
365
- payload = {
366
- "query": user_input,
367
- "topic": "general",
368
- "search_depth": "basic",
369
- "chunks_per_source": 5,
370
- "max_results": 5,
371
- "time_range": None,
372
- "days": 7,
373
- "include_answer": True,
374
- "include_raw_content": False,
375
- "include_images": False,
376
- "include_image_descriptions": False,
377
- "include_domains": [],
378
- "exclude_domains": []
379
- }
380
- r = await client.post(DEEP_SEARCH_PROVIDER_HOST, headers={"Authorization": f"Bearer {DEEP_SEARCH_PROVIDER_KEY}"}, json=payload)
381
- sr_json = r.json()
382
- msgs.append({"role": "system", "content": json.dumps(sr_json)})
383
- except Exception:
384
- # Fail silently if deep search fails
385
- pass
386
- msgs.append({"role": "system", "content": INTERNAL_AI_INSTRUCTIONS})
387
- elif model_display == MODEL_CHOICES[0]:
388
- # For primary model without deep search, use internal instructions
389
- msgs.append({"role": "system", "content": INTERNAL_AI_INSTRUCTIONS})
390
- else:
391
- # For other models, use default instructions
392
- msgs.append({"role": "system", "content": custom_prompt or SYSTEM_PROMPT_MAPPING.get(model_key, SYSTEM_PROMPT_DEFAULT)})
393
- # Append conversation history alternating user and assistant messages
394
- msgs.extend([{"role": "user", "content": u} for u, _ in history])
395
- msgs.extend([{"role": "assistant", "content": a} for _, a in history if a])
396
- # Append current user input
397
- msgs.append({"role": "user", "content": user_input})
398
- # Shuffle provider hosts and keys for load balancing and fallback
399
- candidates = [(h, k) for h in LINUX_SERVER_HOSTS for k in LINUX_SERVER_PROVIDER_KEYS]
400
- random.shuffle(candidates)
401
- # Try each host-key pair until a successful response is received
402
- for h, k in candidates:
403
- stream_gen = fetch_response_stream_async(h, k, model_key, msgs, cfg, sess.session_id, sess.stop_event, sess.cancel_token)
404
- got_responses = False
405
- async for chunk in stream_gen:
406
- if sess.stop_event.is_set() or sess.cancel_token["cancelled"]:
407
- return
408
- got_responses = True
409
- yield chunk
410
- if got_responses:
411
- return
412
- # If no response from any provider, yield fallback message
413
- yield ("content", RESPONSES["RESPONSE_2"])
414
-
415
- # ============================
416
- # Gradio Interaction Handlers
417
- # ============================
418
-
419
- async def respond_async(multi, history, model_display, sess, custom_prompt, deep_search):
420
- """
421
- Main async handler for user input submission.
422
- Supports text + file uploads (multi-modal input).
423
- Extracts file content and appends to user input.
424
- Streams AI responses back to UI, updating chat history live.
425
- Allows stopping response generation gracefully.
426
- """
427
- ensure_stop_event(sess)
428
- sess.stop_event.clear()
429
- sess.cancel_token["cancelled"] = False
430
- # Extract text and files from multimodal input
431
- msg_input = {"text": multi.get("text", "").strip(), "files": multi.get("files", [])}
432
- # If no input, reset UI state and return
433
- if not msg_input["text"] and not msg_input["files"]:
434
- yield history, gr.update(value="", interactive=True, submit_btn=True, stop_btn=False), sess
435
- return
436
- # Initialize input with extracted file contents
437
- inp = ""
438
- for f in msg_input["files"]:
439
- # Support dict or direct file path
440
- fp = f.get("data", f.get("name", "")) if isinstance(f, dict) else f
441
- inp += f"{Path(fp).name}\n\n{extract_file_content(fp)}\n\n"
442
- # Append user text input if any
443
- if msg_input["text"]:
444
- inp += msg_input["text"]
445
- # Append user input to chat history with placeholder response
446
- history.append([inp, RESPONSES["RESPONSE_8"]])
447
- yield history, gr.update(interactive=False, submit_btn=False, stop_btn=True), sess
448
- queue = asyncio.Queue()
449
- # Background async task to fetch streamed AI responses
450
- async def background():
451
- reasoning = ""
452
- responses = ""
453
- content_started = False
454
- ignore_reasoning = False
455
- async for typ, chunk in chat_with_model_async(history, inp, model_display, sess, custom_prompt, deep_search):
456
- if sess.stop_event.is_set() or sess.cancel_token["cancelled"]:
457
- break
458
- if typ == "reasoning":
459
- if ignore_reasoning:
460
- continue
461
- reasoning += chunk
462
- await queue.put(("reasoning", reasoning))
463
- elif typ == "content":
464
- if not content_started:
465
- content_started = True
466
- ignore_reasoning = True
467
- responses = chunk
468
- await queue.put(("reasoning", "")) # Clear reasoning on content start
469
- await queue.put(("replace", responses))
470
- else:
471
- responses += chunk
472
- await queue.put(("append", responses))
473
- await queue.put(None)
474
- return responses
475
- bg_task = asyncio.create_task(background())
476
- stop_task = asyncio.create_task(sess.stop_event.wait())
477
- pending_tasks = {bg_task, stop_task}
478
- try:
479
- while True:
480
- queue_task = asyncio.create_task(queue.get())
481
- pending_tasks.add(queue_task)
482
- done, _ = await asyncio.wait({stop_task, queue_task}, return_when=asyncio.FIRST_COMPLETED)
483
- for task in done:
484
- pending_tasks.discard(task)
485
- if task is stop_task:
486
- # User requested stop, cancel background task and update UI
487
- sess.cancel_token["cancelled"] = True
488
- bg_task.cancel()
489
- try:
490
- await bg_task
491
- except asyncio.CancelledError:
492
- pass
493
- history[-1][1] = RESPONSES["RESPONSE_1"]
494
- yield history, gr.update(value="", interactive=True, submit_btn=True, stop_btn=False), sess
495
- return
496
- result = task.result()
497
- if result is None:
498
- raise StopAsyncIteration
499
- action, text = result
500
- # Update last message content in history with streamed text
501
- history[-1][1] = text
502
- yield history, gr.update(interactive=False, submit_btn=False, stop_btn=True), sess
503
- except StopAsyncIteration:
504
- pass
505
- finally:
506
- for task in pending_tasks:
507
- task.cancel()
508
- await asyncio.gather(*pending_tasks, return_exceptions=True)
509
- yield history, gr.update(value="", interactive=True, submit_btn=True, stop_btn=False), sess
510
-
511
- def toggle_deep_search(deep_search_value, history, sess, prompt, model):
512
- """
513
- Toggle deep search checkbox. Keeps chat intact for production compatibility.
514
- """
515
- return history, sess, prompt, model, gr.update(value=deep_search_value)
516
-
517
- def change_model(new):
518
- """
519
- Handler to change selected AI model.
520
- Resets chat history and session.
521
- Updates system instructions and deep search checkbox visibility accordingly.
522
- Deep search is only available for default model.
523
- """
524
- visible = new == MODEL_CHOICES[0]
525
- default_prompt = SYSTEM_PROMPT_MAPPING.get(get_model_key(new), SYSTEM_PROMPT_DEFAULT)
526
- # On model change, clear chat, create new session, reset deep search, update visibility
527
- return [], create_session(), new, default_prompt, False, gr.update(visible=visible)
528
-
529
- def stop_response(history, sess):
530
- """
531
- Handler to stop ongoing AI response generation.
532
- Sets cancellation flags and updates last message to cancellation notice.
533
- """
534
- ensure_stop_event(sess)
535
- sess.stop_event.set()
536
- sess.cancel_token["cancelled"] = True
537
- if history:
538
- history[-1][1] = RESPONSES["RESPONSE_1"]
539
- return history, None, create_session()
540
-
541
- # ============================
542
- # Gradio UI Setup
543
- # ============================
544
-
545
- with gr.Blocks(fill_height=True, fill_width=True, title=AI_TYPES["AI_TYPE_4"], head=META_TAGS) as jarvis:
546
- user_history = gr.State([])
547
- user_session = gr.State(create_session())
548
- selected_model = gr.State(MODEL_CHOICES[0] if MODEL_CHOICES else "")
549
- J_A_R_V_I_S = gr.State("")
550
- # Chatbot UI
551
- chatbot = gr.Chatbot(label=AI_TYPES["AI_TYPE_1"], show_copy_button=True, scale=1, elem_id=AI_TYPES["AI_TYPE_2"], examples=JARVIS_INIT)
552
- # Deep search
553
- deep_search = gr.Checkbox(label=AI_TYPES["AI_TYPE_8"], value=False, info=AI_TYPES["AI_TYPE_9"], visible=True)
554
- deep_search.change(fn=toggle_deep_search, inputs=[deep_search, user_history, user_session, J_A_R_V_I_S, selected_model], outputs=[chatbot, user_session, J_A_R_V_I_S, selected_model, deep_search])
555
- # User's input
556
- msg = gr.MultimodalTextbox(show_label=False, placeholder=RESPONSES["RESPONSE_5"], interactive=True, file_count="single", file_types=ALLOWED_EXTENSIONS)
557
- # Sidebar to select AI models
558
- with gr.Sidebar(open=False): model_radio = gr.Radio(show_label=False, choices=MODEL_CHOICES, value=MODEL_CHOICES[0])
559
- # Models change
560
- model_radio.change(fn=change_model, inputs=[model_radio], outputs=[user_history, user_session, selected_model, J_A_R_V_I_S, deep_search, deep_search])
561
- # Initial welcome messages
562
- def on_example_select(evt: gr.SelectData): return evt.value
563
- chatbot.example_select(fn=on_example_select, inputs=[], outputs=[msg]).then(fn=respond_async, inputs=[msg, user_history, selected_model, user_session, J_A_R_V_I_S, deep_search], outputs=[chatbot, msg, user_session])
564
- # Clear chat
565
- def clear_chat(history, sess, prompt, model): return [], create_session(), prompt, model
566
- chatbot.clear(fn=clear_chat, inputs=[user_history, user_session, J_A_R_V_I_S, selected_model], outputs=[chatbot, user_session, J_A_R_V_I_S, selected_model])
567
- # Submit message
568
- msg.submit(fn=respond_async, inputs=[msg, user_history, selected_model, user_session, J_A_R_V_I_S, deep_search], outputs=[chatbot, msg, user_session], api_name=INTERNAL_AI_GET_SERVER)
569
- # Stop message
570
- msg.stop(fn=stop_response, inputs=[user_history, user_session], outputs=[chatbot, msg, user_session])
571
-
572
- # Launch
573
- jarvis.queue(default_concurrency_limit=2).launch(max_file_size="1mb")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/__init__.py ADDED
File without changes
src/config.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+
6
+ import os
7
+ import json
8
+
9
+ # Initial welcome messages
10
+ JARVIS_INIT = json.loads(os.getenv("HELLO", "[]"))
11
+
12
+ # Deep Search
13
+ DEEP_SEARCH_PROVIDER_HOST = os.getenv("DEEP_SEARCH_PROVIDER_HOST")
14
+ DEEP_SEARCH_PROVIDER_KEY = os.getenv('DEEP_SEARCH_PROVIDER_KEY')
15
+ DEEP_SEARCH_INSTRUCTIONS = os.getenv("DEEP_SEARCH_INSTRUCTIONS")
16
+
17
+ # Servers and instructions
18
+ INTERNAL_AI_GET_SERVER = os.getenv("INTERNAL_AI_GET_SERVER")
19
+ INTERNAL_AI_INSTRUCTIONS = os.getenv("INTERNAL_TRAINING_DATA")
20
+
21
+ # System instructions mapping
22
+ SYSTEM_PROMPT_MAPPING = json.loads(os.getenv("SYSTEM_PROMPT_MAPPING", "{}"))
23
+ SYSTEM_PROMPT_DEFAULT = os.getenv("DEFAULT_SYSTEM")
24
+
25
+ # List of available servers
26
+ LINUX_SERVER_HOSTS = [h for h in json.loads(os.getenv("LINUX_SERVER_HOST", "[]")) if h]
27
+
28
+ # List of available keys
29
+ LINUX_SERVER_PROVIDER_KEYS = [k for k in json.loads(os.getenv("LINUX_SERVER_PROVIDER_KEY", "[]")) if k]
30
+ LINUX_SERVER_PROVIDER_KEYS_MARKED = set()
31
+ LINUX_SERVER_PROVIDER_KEYS_ATTEMPTS = {}
32
+
33
+ # Server errors codes
34
+ LINUX_SERVER_ERRORS = set(map(int, filter(None, os.getenv("LINUX_SERVER_ERROR", "").split(","))))
35
+
36
+ # Human friendly AI setup
37
+ AI_TYPES = {f"AI_TYPE_{i}": os.getenv(f"AI_TYPE_{i}") for i in range(1, 10)}
38
+ RESPONSES = {f"RESPONSE_{i}": os.getenv(f"RESPONSE_{i}") for i in range(1, 11)}
39
+
40
+ # Model mapping
41
+ MODEL_MAPPING = json.loads(os.getenv("MODEL_MAPPING", "{}"))
42
+ MODEL_CONFIG = json.loads(os.getenv("MODEL_CONFIG", "{}"))
43
+ MODEL_CHOICES = list(MODEL_MAPPING.values())
44
+
45
+ # Default model config and key for fallback
46
+ DEFAULT_CONFIG = json.loads(os.getenv("DEFAULT_CONFIG", "{}"))
47
+ DEFAULT_MODEL_KEY = list(MODEL_MAPPING.keys())[0] if MODEL_MAPPING else None
48
+
49
+ # HTML <head> codes (SEO, etc.)
50
+ META_TAGS = os.getenv("META_TAGS")
51
+
52
+ # Allowed file extensions
53
+ ALLOWED_EXTENSIONS = json.loads(os.getenv("ALLOWED_EXTENSIONS", "[]"))
src/cores/__init__.py ADDED
File without changes
src/cores/client.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+
6
+ import asyncio
7
+ import httpx
8
+ import json
9
+ import random
10
+ import uuid
11
+
12
+ from src.config import *
13
+ from src.cores.server import fetch_response_stream_async
14
+ from src.cores.session import ensure_stop_event, get_model_key
15
+
16
+ async def chat_with_model_async(history, user_input, model_display, sess, custom_prompt, deep_search):
17
+ """
18
+ Core async function to interact with AI model.
19
+ Prepares message history, system instructions, and optionally integrates deep search results.
20
+ Tries multiple backend hosts and keys with fallback.
21
+ Yields streamed responses for UI updates.
22
+ """
23
+ ensure_stop_event(sess)
24
+ sess.stop_event.clear()
25
+ sess.cancel_token["cancelled"] = False
26
+ if not LINUX_SERVER_PROVIDER_KEYS or not LINUX_SERVER_HOSTS:
27
+ yield ("content", RESPONSES["RESPONSE_3"]) # No providers available
28
+ return
29
+ if not hasattr(sess, "session_id") or not sess.session_id:
30
+ sess.session_id = str(uuid.uuid4())
31
+ model_key = get_model_key(model_display, MODEL_MAPPING, DEFAULT_MODEL_KEY)
32
+ cfg = MODEL_CONFIG.get(model_key, DEFAULT_CONFIG)
33
+ msgs = []
34
+ # If deep search enabled and using primary model, prepend deep search instructions and results
35
+ if deep_search and model_display == MODEL_CHOICES[0]:
36
+ msgs.append({"role": "system", "content": DEEP_SEARCH_INSTRUCTIONS})
37
+ try:
38
+ async with httpx.AsyncClient() as client:
39
+ payload = {
40
+ "query": user_input,
41
+ "topic": "general",
42
+ "search_depth": "basic",
43
+ "chunks_per_source": 5,
44
+ "max_results": 5,
45
+ "time_range": None,
46
+ "days": 7,
47
+ "include_answer": True,
48
+ "include_raw_content": False,
49
+ "include_images": False,
50
+ "include_image_descriptions": False,
51
+ "include_domains": [],
52
+ "exclude_domains": []
53
+ }
54
+ r = await client.post(DEEP_SEARCH_PROVIDER_HOST, headers={"Authorization": f"Bearer {DEEP_SEARCH_PROVIDER_KEY}"}, json=payload)
55
+ sr_json = r.json()
56
+ msgs.append({"role": "system", "content": json.dumps(sr_json)})
57
+ except Exception:
58
+ # Fail silently if deep search fails
59
+ pass
60
+ msgs.append({"role": "system", "content": INTERNAL_AI_INSTRUCTIONS})
61
+ elif model_display == MODEL_CHOICES[0]:
62
+ # For primary model without deep search, use internal instructions
63
+ msgs.append({"role": "system", "content": INTERNAL_AI_INSTRUCTIONS})
64
+ else:
65
+ # For other models, use default instructions
66
+ msgs.append({"role": "system", "content": custom_prompt or SYSTEM_PROMPT_MAPPING.get(model_key, SYSTEM_PROMPT_DEFAULT)})
67
+ # Append conversation history alternating user and assistant messages
68
+ msgs.extend([{"role": "user", "content": u} for u, _ in history])
69
+ msgs.extend([{"role": "assistant", "content": a} for _, a in history if a])
70
+ # Append current user input
71
+ msgs.append({"role": "user", "content": user_input})
72
+ # Shuffle provider hosts and keys for load balancing and fallback
73
+ candidates = [(h, k) for h in LINUX_SERVER_HOSTS for k in LINUX_SERVER_PROVIDER_KEYS]
74
+ random.shuffle(candidates)
75
+ # Try each host-key pair until a successful response is received
76
+ for h, k in candidates:
77
+ stream_gen = fetch_response_stream_async(h, k, model_key, msgs, cfg, sess.session_id, sess.stop_event, sess.cancel_token)
78
+ got_responses = False
79
+ async for chunk in stream_gen:
80
+ if sess.stop_event.is_set() or sess.cancel_token["cancelled"]:
81
+ return
82
+ got_responses = True
83
+ yield chunk
84
+ if got_responses:
85
+ return
86
+ # If no response from any provider, yield fallback message
87
+ yield ("content", RESPONSES["RESPONSE_2"])
src/cores/server.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+
6
+ import codecs # Reasoning
7
+ import httpx
8
+ import json
9
+
10
+ from src.cores.session import marked_item
11
+ from src.config import LINUX_SERVER_ERRORS, LINUX_SERVER_PROVIDER_KEYS_MARKED, LINUX_SERVER_PROVIDER_KEYS_ATTEMPTS, RESPONSES
12
+
13
+ async def fetch_response_stream_async(host, key, model, msgs, cfg, sid, stop_event, cancel_token):
14
+ """
15
+ Async generator that streams AI responses from a backend server.
16
+ Implements retry logic and marks failing keys to avoid repeated failures.
17
+ Streams reasoning and content separately for richer UI updates.
18
+ """
19
+ for timeout in [5, 10]:
20
+ try:
21
+ async with httpx.AsyncClient(timeout=timeout) as client:
22
+ async with client.stream(
23
+ "POST",
24
+ host,
25
+ json={**{"model": model, "messages": msgs, "session_id": sid, "stream": True}, **cfg},
26
+ headers={"Authorization": f"Bearer {key}"}
27
+ ) as response:
28
+ if response.status_code in LINUX_SERVER_ERRORS:
29
+ marked_item(key, LINUX_SERVER_PROVIDER_KEYS_MARKED, LINUX_SERVER_PROVIDER_KEYS_ATTEMPTS)
30
+ return
31
+ async for line in response.aiter_lines():
32
+ if stop_event.is_set() or cancel_token["cancelled"]:
33
+ return
34
+ if not line:
35
+ continue
36
+ if line.startswith("data: "):
37
+ data = line[6:]
38
+ if data.strip() == RESPONSES["RESPONSE_10"]:
39
+ return
40
+ try:
41
+ j = json.loads(data)
42
+ if isinstance(j, dict) and j.get("choices"):
43
+ for ch in j["choices"]:
44
+ delta = ch.get("delta", {})
45
+ # Stream reasoning text separately for UI
46
+ if "reasoning" in delta and delta["reasoning"]:
47
+ decoded = delta["reasoning"].encode('utf-8').decode('unicode_escape')
48
+ yield ("reasoning", decoded)
49
+ # Stream main content text
50
+ if "content" in delta and delta["content"]:
51
+ yield ("content", delta["content"])
52
+ except Exception:
53
+ # Ignore malformed JSON or unexpected data
54
+ continue
55
+ except Exception:
56
+ # Network or other errors, try next timeout or mark key
57
+ continue
58
+ marked_item(key, LINUX_SERVER_PROVIDER_KEYS_MARKED, LINUX_SERVER_PROVIDER_KEYS_ATTEMPTS)
59
+ return
src/cores/session.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+
6
+ import asyncio
7
+ import requests
8
+ import uuid
9
+ import threading
10
+
11
+ from src.config import LINUX_SERVER_PROVIDER_KEYS_MARKED, LINUX_SERVER_PROVIDER_KEYS_ATTEMPTS
12
+
13
+ class SessionWithID(requests.Session):
14
+ """
15
+ Custom session object that holds a unique session ID and async control flags.
16
+ Used to track individual user sessions and allow cancellation of ongoing requests.
17
+ """
18
+ def __init__(self):
19
+ super().__init__()
20
+ self.session_id = str(uuid.uuid4()) # Unique ID per session
21
+ self.stop_event = asyncio.Event() # Async event to signal stop requests
22
+ self.cancel_token = {"cancelled": False} # Flag to indicate cancellation
23
+
24
+ def create_session():
25
+ """
26
+ Create and return a new SessionWithID object.
27
+ Called when a new user session starts or chat is reset.
28
+ """
29
+ return SessionWithID()
30
+
31
+ def ensure_stop_event(sess):
32
+ """
33
+ Ensure that the session object has stop_event and cancel_token attributes.
34
+ Useful when restoring or reusing sessions.
35
+ """
36
+ if not hasattr(sess, "stop_event"):
37
+ sess.stop_event = asyncio.Event()
38
+ if not hasattr(sess, "cancel_token"):
39
+ sess.cancel_token = {"cancelled": False}
40
+
41
+ def marked_item(item, marked, attempts):
42
+ """
43
+ Mark a provider key or host as temporarily problematic after repeated failures.
44
+ Automatically unmark after 5 minutes to retry.
45
+ This helps avoid repeatedly using failing providers.
46
+ """
47
+ marked.add(item)
48
+ attempts[item] = attempts.get(item, 0) + 1
49
+ if attempts[item] >= 3:
50
+ def remove():
51
+ marked.discard(item)
52
+ attempts.pop(item, None)
53
+ threading.Timer(300, remove).start()
54
+
55
+ def get_model_key(display, MODEL_MAPPING, DEFAULT_MODEL_KEY):
56
+ """
57
+ Get the internal model key (identifier) from the display name.
58
+ Returns default model key if not found.
59
+ """
60
+ return next((k for k, v in MODEL_MAPPING.items() if v == display), DEFAULT_MODEL_KEY)
src/main/__init__.py ADDED
File without changes
src/main/file_extractors.py ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+
6
+ import pdfplumber # PDF
7
+ import pytesseract # OCR
8
+ import docx # Microsoft Word
9
+ import zipfile # Microsoft Word
10
+ import io
11
+ import pandas as pd # Microsoft Excel
12
+ import warnings
13
+ import re
14
+
15
+ from openpyxl import load_workbook # Microsoft Excel
16
+ from pptx import Presentation # Microsoft PowerPoint
17
+ from PIL import Image, ImageEnhance, ImageFilter # OCR
18
+ from pathlib import Path
19
+
20
+ def clean_text(text):
21
+ """Clean and normalize extracted outputs."""
22
+ # Remove non-printable and special characters except common punctuation
23
+ text = re.sub(r'[^a-zA-Z0-9\s.,?!():;\'"-]', '', text)
24
+ # Remove isolated single letters (likely OCR noise)
25
+ text = re.sub(r'\b[a-zA-Z]\b', '', text)
26
+ # Normalize whitespace and remove empty lines
27
+ lines = [line.strip() for line in text.splitlines() if line.strip()]
28
+ return "\n".join(lines)
29
+
30
+ def format_table(df, max_rows=10):
31
+ """Format pandas DataFrame as a readable table string, limited to max rows."""
32
+ if df.empty:
33
+ return ""
34
+ # Drop fully empty rows and columns to reduce NaN clutter
35
+ df_clean = df.dropna(axis=0, how='all').dropna(axis=1, how='all')
36
+ # Replace NaN with empty string to avoid 'NaN' in output
37
+ df_clean = df_clean.fillna('')
38
+ if df_clean.empty:
39
+ return ""
40
+ display_df = df_clean.head(max_rows)
41
+ table_str = display_df.to_string(index=False)
42
+ if len(df_clean) > max_rows:
43
+ table_str += f"\n... ({len(df_clean) - max_rows} more rows)"
44
+ return table_str
45
+
46
+ def preprocess_image(img):
47
+ """Preprocess image for better OCR accuracy."""
48
+ try:
49
+ img = img.convert("L") # Grayscale
50
+ enhancer = ImageEnhance.Contrast(img)
51
+ img = enhancer.enhance(2) # Increase contrast
52
+ img = img.filter(ImageFilter.MedianFilter()) # Reduce noise
53
+ # Binarize image (threshold)
54
+ img = img.point(lambda x: 0 if x < 140 else 255, '1')
55
+ return img
56
+ except Exception:
57
+ return img
58
+
59
+ def ocr_image(img):
60
+ """Perform OCR on PIL Image with preprocessing and clean result."""
61
+ try:
62
+ img = preprocess_image(img)
63
+ text = pytesseract.image_to_string(img, lang='eng', config='--psm 6')
64
+ text = clean_text(text)
65
+ return text
66
+ except Exception:
67
+ return ""
68
+
69
+ def extract_pdf_content(fp):
70
+ """
71
+ Extract text content from PDF file.
72
+ Includes OCR on embedded images to capture text within images.
73
+ Also extracts tables as tab-separated text.
74
+ """
75
+ content = ""
76
+ try:
77
+ with pdfplumber.open(fp) as pdf:
78
+ for i, page in enumerate(pdf.pages, 1):
79
+ text = page.extract_text() or ""
80
+ content += f"Page {i} Text:\n{clean_text(text)}\n\n"
81
+ # OCR on images if any
82
+ if page.images:
83
+ img_obj = page.to_image(resolution=300)
84
+ for img in page.images:
85
+ bbox = (img["x0"], img["top"], img["x1"], img["bottom"])
86
+ cropped = img_obj.original.crop(bbox)
87
+ ocr_text = ocr_image(cropped)
88
+ if ocr_text:
89
+ content += f"[OCR Text from image on page {i}]:\n{ocr_text}\n\n"
90
+ # Extract tables as TSV
91
+ tables = page.extract_tables()
92
+ for idx, table in enumerate(tables, 1):
93
+ if table:
94
+ df = pd.DataFrame(table[1:], columns=table[0])
95
+ content += f"Table {idx} on page {i}:\n{format_table(df)}\n\n"
96
+ except Exception as e:
97
+ content += f"\n[Error reading PDF {fp}: {e}]"
98
+ return content.strip()
99
+
100
+ def extract_docx_content(fp):
101
+ """
102
+ Extract text from Microsoft Word files.
103
+ Also performs OCR on embedded images inside the Microsoft Word archive.
104
+ """
105
+ content = ""
106
+ try:
107
+ doc = docx.Document(fp)
108
+ paragraphs = [para.text.strip() for para in doc.paragraphs if para.text.strip()]
109
+ if paragraphs:
110
+ content += "Paragraphs:\n" + "\n".join(paragraphs) + "\n\n"
111
+ # Extract tables
112
+ tables = []
113
+ for table in doc.tables:
114
+ rows = []
115
+ for row in table.rows:
116
+ cells = [cell.text.strip() for cell in row.cells]
117
+ rows.append(cells)
118
+ if rows:
119
+ df = pd.DataFrame(rows[1:], columns=rows[0])
120
+ tables.append(df)
121
+ for i, df in enumerate(tables, 1):
122
+ content += f"Table {i}:\n{format_table(df)}\n\n"
123
+ # OCR on embedded images inside Microsoft Word
124
+ with zipfile.ZipFile(fp) as z:
125
+ for file in z.namelist():
126
+ if file.startswith("word/media/"):
127
+ data = z.read(file)
128
+ try:
129
+ img = Image.open(io.BytesIO(data))
130
+ ocr_text = ocr_image(img)
131
+ if ocr_text:
132
+ content += f"[OCR Text from embedded image]:\n{ocr_text}\n\n"
133
+ except Exception:
134
+ pass
135
+ except Exception as e:
136
+ content += f"\n[Error reading Microsoft Word {fp}: {e}]"
137
+ return content.strip()
138
+
139
+ def extract_excel_content(fp):
140
+ """
141
+ Extract content from Microsoft Excel files.
142
+ Converts sheets to readable tables and replaces NaN values.
143
+ Does NOT attempt to extract images to avoid errors.
144
+ """
145
+ content = ""
146
+ try:
147
+ with warnings.catch_warnings():
148
+ warnings.simplefilter("ignore") # Suppress openpyxl warnings
149
+ # Explicitly specify the engine to avoid potential issues
150
+ sheets = pd.read_excel(fp, sheet_name=None, engine='openpyxl')
151
+ for sheet_name, df in sheets.items():
152
+ content += f"Sheet: {sheet_name}\n"
153
+ content += format_table(df) + "\n\n"
154
+ except Exception as e:
155
+ content += f"\n[Error reading Microsoft Excel {fp}: {e}]"
156
+ return content.strip()
157
+
158
+ def extract_pptx_content(fp):
159
+ """
160
+ Extract text content from Microsoft PowerPoint presentation slides.
161
+ Includes text from shapes and tables.
162
+ Performs OCR on embedded images.
163
+ """
164
+ content = ""
165
+ try:
166
+ prs = Presentation(fp)
167
+ for i, slide in enumerate(prs.slides, 1):
168
+ slide_texts = []
169
+ for shape in slide.shapes:
170
+ if hasattr(shape, "text") and shape.text.strip():
171
+ slide_texts.append(shape.text.strip())
172
+ if shape.shape_type == 13 and hasattr(shape, "image") and shape.image:
173
+ try:
174
+ img = Image.open(io.BytesIO(shape.image.blob))
175
+ ocr_text = ocr_image(img)
176
+ if ocr_text:
177
+ slide_texts.append(f"[OCR Text from image]:\n{ocr_text}")
178
+ except Exception:
179
+ pass
180
+ if slide_texts:
181
+ content += f"Slide {i} Text:\n" + "\n".join(slide_texts) + "\n\n"
182
+ else:
183
+ content += f"Slide {i} Text:\nNo text found on this slide.\n\n"
184
+ # Extract tables
185
+ for shape in slide.shapes:
186
+ if shape.has_table:
187
+ rows = []
188
+ table = shape.table
189
+ for row in table.rows:
190
+ cells = [cell.text.strip() for cell in row.cells]
191
+ rows.append(cells)
192
+ if rows:
193
+ df = pd.DataFrame(rows[1:], columns=rows[0])
194
+ content += f"Table on slide {i}:\n{format_table(df)}\n\n"
195
+ except Exception as e:
196
+ content += f"\n[Error reading Microsoft PowerPoint {fp}: {e}]"
197
+ return content.strip()
198
+
199
+ def extract_file_content(fp):
200
+ """
201
+ Determine file type by extension and extract text content accordingly.
202
+ For unknown types, attempts to read as plain text.
203
+ """
204
+ ext = Path(fp).suffix.lower()
205
+ if ext == ".pdf":
206
+ return extract_pdf_content(fp)
207
+ elif ext in [".doc", ".docx"]:
208
+ return extract_docx_content(fp)
209
+ elif ext in [".xlsx", ".xls"]:
210
+ return extract_excel_content(fp)
211
+ elif ext in [".ppt", ".pptx"]:
212
+ return extract_pptx_content(fp)
213
+ else:
214
+ try:
215
+ text = Path(fp).read_text(encoding="utf-8")
216
+ return clean_text(text)
217
+ except Exception as e:
218
+ return f"\n[Error reading file {fp}: {e}]"
src/main/gradio.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+
6
+ import gradio as gr
7
+ import asyncio
8
+
9
+ from pathlib import Path
10
+ from src.config import *
11
+ from src.cores.session import create_session, ensure_stop_event, get_model_key
12
+ from src.main.file_extractors import extract_file_content
13
+ from src.cores.client import chat_with_model_async
14
+
15
+ async def respond_async(multi, history, model_display, sess, custom_prompt, deep_search):
16
+ """
17
+ Main async handler for user input submission.
18
+ Supports text + file uploads (multi-modal input).
19
+ Extracts file content and appends to user input.
20
+ Streams AI responses back to UI, updating chat history live.
21
+ Allows stopping response generation gracefully.
22
+ """
23
+ ensure_stop_event(sess)
24
+ sess.stop_event.clear()
25
+ sess.cancel_token["cancelled"] = False
26
+ # Extract text and files from multimodal input
27
+ msg_input = {"text": multi.get("text", "").strip(), "files": multi.get("files", [])}
28
+ # If no input, reset UI state and return
29
+ if not msg_input["text"] and not msg_input["files"]:
30
+ yield history, gr.update(value="", interactive=True, submit_btn=True, stop_btn=False), sess
31
+ return
32
+ # Initialize input with extracted file contents
33
+ inp = ""
34
+ for f in msg_input["files"]:
35
+ # Support dict or direct file path
36
+ fp = f.get("data", f.get("name", "")) if isinstance(f, dict) else f
37
+ inp += f"{Path(fp).name}\n\n{extract_file_content(fp)}\n\n"
38
+ # Append user text input if any
39
+ if msg_input["text"]:
40
+ inp += msg_input["text"]
41
+ # Append user input to chat history with placeholder response
42
+ history.append([inp, RESPONSES["RESPONSE_8"]])
43
+ yield history, gr.update(interactive=False, submit_btn=False, stop_btn=True), sess
44
+ queue = asyncio.Queue()
45
+ # Background async task to fetch streamed AI responses
46
+ async def background():
47
+ reasoning = ""
48
+ responses = ""
49
+ content_started = False
50
+ ignore_reasoning = False
51
+ async for typ, chunk in chat_with_model_async(history, inp, model_display, sess, custom_prompt, deep_search):
52
+ if sess.stop_event.is_set() or sess.cancel_token["cancelled"]:
53
+ break
54
+ if typ == "reasoning":
55
+ if ignore_reasoning:
56
+ continue
57
+ reasoning += chunk
58
+ await queue.put(("reasoning", reasoning))
59
+ elif typ == "content":
60
+ if not content_started:
61
+ content_started = True
62
+ ignore_reasoning = True
63
+ responses = chunk
64
+ await queue.put(("reasoning", "")) # Clear reasoning on content start
65
+ await queue.put(("replace", responses))
66
+ else:
67
+ responses += chunk
68
+ await queue.put(("append", responses))
69
+ await queue.put(None)
70
+ return responses
71
+ bg_task = asyncio.create_task(background())
72
+ stop_task = asyncio.create_task(sess.stop_event.wait())
73
+ pending_tasks = {bg_task, stop_task}
74
+ try:
75
+ while True:
76
+ queue_task = asyncio.create_task(queue.get())
77
+ pending_tasks.add(queue_task)
78
+ done, _ = await asyncio.wait({stop_task, queue_task}, return_when=asyncio.FIRST_COMPLETED)
79
+ for task in done:
80
+ pending_tasks.discard(task)
81
+ if task is stop_task:
82
+ # User requested stop, cancel background task and update UI
83
+ sess.cancel_token["cancelled"] = True
84
+ bg_task.cancel()
85
+ try:
86
+ await bg_task
87
+ except asyncio.CancelledError:
88
+ pass
89
+ history[-1][1] = RESPONSES["RESPONSE_1"]
90
+ yield history, gr.update(value="", interactive=True, submit_btn=True, stop_btn=False), sess
91
+ return
92
+ result = task.result()
93
+ if result is None:
94
+ raise StopAsyncIteration
95
+ action, text = result
96
+ # Update last message content in history with streamed text
97
+ history[-1][1] = text
98
+ yield history, gr.update(interactive=False, submit_btn=False, stop_btn=True), sess
99
+ except StopAsyncIteration:
100
+ pass
101
+ finally:
102
+ for task in pending_tasks:
103
+ task.cancel()
104
+ await asyncio.gather(*pending_tasks, return_exceptions=True)
105
+ yield history, gr.update(value="", interactive=True, submit_btn=True, stop_btn=False), sess
106
+
107
+ def toggle_deep_search(deep_search_value, history, sess, prompt, model):
108
+ """
109
+ Toggle deep search checkbox. Keeps chat intact for production compatibility.
110
+ """
111
+ return history, sess, prompt, model, gr.update(value=deep_search_value)
112
+
113
+ def change_model(new):
114
+ """
115
+ Handler to change selected AI model.
116
+ Resets chat history and session.
117
+ Updates system instructions and deep search checkbox visibility accordingly.
118
+ Deep search is only available for default model.
119
+ """
120
+ visible = new == MODEL_CHOICES[0]
121
+ default_prompt = SYSTEM_PROMPT_MAPPING.get(get_model_key(new, MODEL_MAPPING, DEFAULT_MODEL_KEY), SYSTEM_PROMPT_DEFAULT)
122
+ # On model change, clear chat, create new session, reset deep search, update visibility
123
+ return [], create_session(), new, default_prompt, False, gr.update(visible=visible)
124
+
125
+ def stop_response(history, sess):
126
+ """
127
+ Handler to stop ongoing AI response generation.
128
+ Sets cancellation flags and updates last message to cancellation notice.
129
+ """
130
+ ensure_stop_event(sess)
131
+ sess.stop_event.set()
132
+ sess.cancel_token["cancelled"] = True
133
+ if history:
134
+ history[-1][1] = RESPONSES["RESPONSE_1"]
135
+ return history, None, create_session()
136
+
137
+ def launch_ui():
138
+ # ============================
139
+ # System Setup
140
+ # ============================
141
+
142
+ # Install Tesseract OCR and dependencies for text extraction from images.
143
+ import os
144
+ os.system("apt-get update -q -y && \
145
+ apt-get install -q -y tesseract-ocr \
146
+ tesseract-ocr-eng tesseract-ocr-ind \
147
+ libleptonica-dev libtesseract-dev"
148
+ )
149
+
150
+ with gr.Blocks(fill_height=True, fill_width=True, title=AI_TYPES["AI_TYPE_4"], head=META_TAGS) as jarvis:
151
+ user_history = gr.State([])
152
+ user_session = gr.State(create_session())
153
+ selected_model = gr.State(MODEL_CHOICES[0] if MODEL_CHOICES else "")
154
+ J_A_R_V_I_S = gr.State("")
155
+ # Chatbot UI
156
+ with gr.Column(): chatbot = gr.Chatbot(label=AI_TYPES["AI_TYPE_1"], show_copy_button=True, scale=1, elem_id=AI_TYPES["AI_TYPE_2"], examples=JARVIS_INIT)
157
+ # Deep search
158
+ deep_search = gr.Checkbox(label=AI_TYPES["AI_TYPE_8"], value=False, info=AI_TYPES["AI_TYPE_9"], visible=True)
159
+ deep_search.change(fn=toggle_deep_search, inputs=[deep_search, user_history, user_session, J_A_R_V_I_S, selected_model], outputs=[chatbot, user_session, J_A_R_V_I_S, selected_model, deep_search])
160
+ # User's input
161
+ msg = gr.MultimodalTextbox(show_label=False, placeholder=RESPONSES["RESPONSE_5"], interactive=True, file_count="single", file_types=ALLOWED_EXTENSIONS)
162
+ # Sidebar to select AI models
163
+ with gr.Sidebar(open=False): model_radio = gr.Radio(show_label=False, choices=MODEL_CHOICES, value=MODEL_CHOICES[0])
164
+ # Models change
165
+ model_radio.change(fn=change_model, inputs=[model_radio], outputs=[user_history, user_session, selected_model, J_A_R_V_I_S, deep_search, deep_search])
166
+ # Initial welcome messages
167
+ def on_example_select(evt: gr.SelectData): return evt.value
168
+ chatbot.example_select(fn=on_example_select, inputs=[], outputs=[msg]).then(fn=respond_async, inputs=[msg, user_history, selected_model, user_session, J_A_R_V_I_S, deep_search], outputs=[chatbot, msg, user_session])
169
+ # Clear chat
170
+ def clear_chat(history, sess, prompt, model): return [], create_session(), prompt, model
171
+ chatbot.clear(fn=clear_chat, inputs=[user_history, user_session, J_A_R_V_I_S, selected_model], outputs=[chatbot, user_session, J_A_R_V_I_S, selected_model])
172
+ # Submit message
173
+ msg.submit(fn=respond_async, inputs=[msg, user_history, selected_model, user_session, J_A_R_V_I_S, deep_search], outputs=[chatbot, msg, user_session], api_name=INTERNAL_AI_GET_SERVER)
174
+ # Stop message
175
+ msg.stop(fn=stop_response, inputs=[user_history, user_session], outputs=[chatbot, msg, user_session])
176
+ # Launch
177
+ jarvis.queue(default_concurrency_limit=2).launch(max_file_size="1mb")