hadadrjt commited on
Commit
94e67a7
·
1 Parent(s): 8f64cac

ai: Switch to production code.

Browse files
Files changed (3) hide show
  1. README.md +1 -1
  2. ai +1 -1
  3. jarvis.py +225 -59
README.md CHANGED
@@ -3,7 +3,7 @@ title: JARVIS AI
3
  colorFrom: yellow
4
  colorTo: purple
5
  sdk: gradio
6
- sdk_version: 5.27.0
7
  app_file: jarvis.py
8
  pinned: true
9
  short_description: Inspired by Iron Man movies.
 
3
  colorFrom: yellow
4
  colorTo: purple
5
  sdk: gradio
6
+ sdk_version: 5.27.1
7
  app_file: jarvis.py
8
  pinned: true
9
  short_description: Inspired by Iron Man movies.
ai CHANGED
@@ -3,12 +3,12 @@
3
  # SPDX-FileCopyrightText: Hadad <[email protected]>
4
  # SPDX-License-Identifier: Apache-2.0
5
  #
 
6
  import sys
7
 
8
  from gradio_client import Client
9
  from rich.console import Console
10
  from rich.markdown import Markdown
11
- from rich.panel import Panel
12
 
13
  console = Console()
14
  jarvis = Client("hadadrjt/ai")
 
3
  # SPDX-FileCopyrightText: Hadad <[email protected]>
4
  # SPDX-License-Identifier: Apache-2.0
5
  #
6
+
7
  import sys
8
 
9
  from gradio_client import Client
10
  from rich.console import Console
11
  from rich.markdown import Markdown
 
12
 
13
  console = Console()
14
  jarvis = Client("hadadrjt/ai")
jarvis.py CHANGED
@@ -4,81 +4,126 @@
4
  #
5
 
6
  import asyncio
7
- import codecs
8
- import docx
9
  import gradio as gr
10
  import httpx
11
  import json
12
  import os
13
- import pandas as pd
14
- import pdfplumber
15
- import pytesseract
16
  import random
17
  import requests
18
  import threading
19
  import uuid
20
- import zipfile
21
  import io
22
 
23
- from PIL import Image
24
  from pathlib import Path
25
- from pptx import Presentation
26
- from openpyxl import load_workbook
27
 
28
- os.system("apt-get update -q -y && apt-get install -q -y tesseract-ocr tesseract-ocr-eng tesseract-ocr-ind libleptonica-dev libtesseract-dev")
 
 
29
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  JARVIS_INIT = json.loads(os.getenv("HELLO", "[]"))
31
 
 
32
  DEEP_SEARCH_PROVIDER_HOST = os.getenv("DEEP_SEARCH_PROVIDER_HOST")
33
  DEEP_SEARCH_PROVIDER_KEY = os.getenv('DEEP_SEARCH_PROVIDER_KEY')
34
  DEEP_SEARCH_INSTRUCTIONS = os.getenv("DEEP_SEARCH_INSTRUCTIONS")
35
 
 
36
  INTERNAL_AI_GET_SERVER = os.getenv("INTERNAL_AI_GET_SERVER")
37
  INTERNAL_AI_INSTRUCTIONS = os.getenv("INTERNAL_TRAINING_DATA")
38
 
 
39
  SYSTEM_PROMPT_MAPPING = json.loads(os.getenv("SYSTEM_PROMPT_MAPPING", "{}"))
40
  SYSTEM_PROMPT_DEFAULT = os.getenv("DEFAULT_SYSTEM")
41
 
 
42
  LINUX_SERVER_HOSTS = [h for h in json.loads(os.getenv("LINUX_SERVER_HOST", "[]")) if h]
43
 
 
44
  LINUX_SERVER_PROVIDER_KEYS = [k for k in json.loads(os.getenv("LINUX_SERVER_PROVIDER_KEY", "[]")) if k]
45
  LINUX_SERVER_PROVIDER_KEYS_MARKED = set()
46
  LINUX_SERVER_PROVIDER_KEYS_ATTEMPTS = {}
47
 
48
- LINUX_SERVER_ERRORS = set(map(int, os.getenv("LINUX_SERVER_ERROR", "").split(",")))
 
49
 
 
50
  AI_TYPES = {f"AI_TYPE_{i}": os.getenv(f"AI_TYPE_{i}") for i in range(1, 10)}
51
-
52
  RESPONSES = {f"RESPONSE_{i}": os.getenv(f"RESPONSE_{i}") for i in range(1, 11)}
53
 
 
54
  MODEL_MAPPING = json.loads(os.getenv("MODEL_MAPPING", "{}"))
55
  MODEL_CONFIG = json.loads(os.getenv("MODEL_CONFIG", "{}"))
56
  MODEL_CHOICES = list(MODEL_MAPPING.values())
57
 
 
58
  DEFAULT_CONFIG = json.loads(os.getenv("DEFAULT_CONFIG", "{}"))
59
  DEFAULT_MODEL_KEY = list(MODEL_MAPPING.keys())[0] if MODEL_MAPPING else None
60
 
 
61
  META_TAGS = os.getenv("META_TAGS")
62
 
 
63
  ALLOWED_EXTENSIONS = json.loads(os.getenv("ALLOWED_EXTENSIONS", "[]"))
64
 
 
 
 
 
65
  class SessionWithID(requests.Session):
66
- def __init__(sess):
 
 
 
 
67
  super().__init__()
68
- sess.session_id = str(uuid.uuid4())
69
- sess.stop_event = asyncio.Event()
70
- sess.cancel_token = {"cancelled": False}
71
 
72
  def create_session():
 
 
 
 
73
  return SessionWithID()
74
 
75
  def ensure_stop_event(sess):
 
 
 
 
76
  if not hasattr(sess, "stop_event"):
77
  sess.stop_event = asyncio.Event()
78
  if not hasattr(sess, "cancel_token"):
79
  sess.cancel_token = {"cancelled": False}
80
 
81
  def marked_item(item, marked, attempts):
 
 
 
 
 
82
  marked.add(item)
83
  attempts[item] = attempts.get(item, 0) + 1
84
  if attempts[item] >= 3:
@@ -88,15 +133,30 @@ def marked_item(item, marked, attempts):
88
  threading.Timer(300, remove).start()
89
 
90
  def get_model_key(display):
 
 
 
 
91
  return next((k for k, v in MODEL_MAPPING.items() if v == display), DEFAULT_MODEL_KEY)
92
 
 
 
 
 
93
  def extract_pdf_content(fp):
 
 
 
 
 
94
  content = ""
95
  try:
96
  with pdfplumber.open(fp) as pdf:
97
  for page in pdf.pages:
 
98
  text = page.extract_text() or ""
99
  content += text + "\n"
 
100
  if page.images:
101
  img_obj = page.to_image(resolution=300)
102
  for img in page.images:
@@ -105,6 +165,7 @@ def extract_pdf_content(fp):
105
  ocr_text = pytesseract.image_to_string(cropped)
106
  if ocr_text.strip():
107
  content += ocr_text + "\n"
 
108
  tables = page.extract_tables()
109
  for table in tables:
110
  for row in table:
@@ -112,19 +173,26 @@ def extract_pdf_content(fp):
112
  if cells:
113
  content += "\t".join(cells) + "\n"
114
  except Exception as e:
115
- content += f"{fp}: {e}"
116
  return content.strip()
117
 
118
  def extract_docx_content(fp):
 
 
 
 
119
  content = ""
120
  try:
121
  doc = docx.Document(fp)
 
122
  for para in doc.paragraphs:
123
  content += para.text + "\n"
 
124
  for table in doc.tables:
125
  for row in table.rows:
126
  cells = [cell.text for cell in row.cells]
127
  content += "\t".join(cells) + "\n"
 
128
  with zipfile.ZipFile(fp) as z:
129
  for file in z.namelist():
130
  if file.startswith("word/media/"):
@@ -134,51 +202,66 @@ def extract_docx_content(fp):
134
  ocr_text = pytesseract.image_to_string(img)
135
  if ocr_text.strip():
136
  content += ocr_text + "\n"
137
- except:
 
138
  pass
139
  except Exception as e:
140
- content += f"{fp}: {e}"
141
  return content.strip()
142
 
143
  def extract_excel_content(fp):
 
 
 
 
 
144
  content = ""
145
  try:
 
146
  sheets = pd.read_excel(fp, sheet_name=None)
147
  for name, df in sheets.items():
148
  content += f"Sheet: {name}\n"
149
  content += df.to_csv(index=False) + "\n"
 
150
  wb = load_workbook(fp, data_only=True)
151
  if wb._images:
152
  for image in wb._images:
153
- img = image.ref
154
- if isinstance(img, bytes):
155
- try:
156
- pil_img = Image.open(io.BytesIO(img))
157
- ocr_text = pytesseract.image_to_string(pil_img)
158
- if ocr_text.strip():
159
- content += ocr_text + "\n"
160
- except:
161
- pass
162
  except Exception as e:
163
- content += f"{fp}: {e}"
164
  return content.strip()
165
 
166
  def extract_pptx_content(fp):
 
 
 
 
 
167
  content = ""
168
  try:
169
  prs = Presentation(fp)
170
  for slide in prs.slides:
171
  for shape in slide.shapes:
 
172
  if hasattr(shape, "text") and shape.text:
173
  content += shape.text + "\n"
 
174
  if shape.shape_type == 13 and hasattr(shape, "image") and shape.image:
175
  try:
176
  img = Image.open(io.BytesIO(shape.image.blob))
177
  ocr_text = pytesseract.image_to_string(img)
178
  if ocr_text.strip():
179
  content += ocr_text + "\n"
180
- except:
181
  pass
 
182
  for shape in slide.shapes:
183
  if shape.has_table:
184
  table = shape.table
@@ -186,10 +269,14 @@ def extract_pptx_content(fp):
186
  cells = [cell.text for cell in row.cells]
187
  content += "\t".join(cells) + "\n"
188
  except Exception as e:
189
- content += f"{fp}: {e}"
190
  return content.strip()
191
 
192
  def extract_file_content(fp):
 
 
 
 
193
  ext = Path(fp).suffix.lower()
194
  if ext == ".pdf":
195
  return extract_pdf_content(fp)
@@ -203,12 +290,21 @@ def extract_file_content(fp):
203
  try:
204
  return Path(fp).read_text(encoding="utf-8").strip()
205
  except Exception as e:
206
- return f"{fp}: {e}"
 
 
 
 
207
 
208
  async def fetch_response_stream_async(host, key, model, msgs, cfg, sid, stop_event, cancel_token):
209
- for t in [5, 10]:
 
 
 
 
 
210
  try:
211
- async with httpx.AsyncClient(timeout=t) as client:
212
  async with client.stream("POST", host, json={**{"model": model, "messages": msgs, "session_id": sid, "stream": True}, **cfg}, headers={"Authorization": f"Bearer {key}"}) as response:
213
  if response.status_code in LINUX_SERVER_ERRORS:
214
  marked_item(key, LINUX_SERVER_PROVIDER_KEYS_MARKED, LINUX_SERVER_PROVIDER_KEYS_ATTEMPTS)
@@ -227,30 +323,41 @@ async def fetch_response_stream_async(host, key, model, msgs, cfg, sid, stop_eve
227
  if isinstance(j, dict) and j.get("choices"):
228
  for ch in j["choices"]:
229
  delta = ch.get("delta", {})
 
230
  if "reasoning" in delta and delta["reasoning"]:
231
  decoded = delta["reasoning"].encode('utf-8').decode('unicode_escape')
232
  yield ("reasoning", decoded)
 
233
  if "content" in delta and delta["content"]:
234
  yield ("content", delta["content"])
235
- except:
 
236
  continue
237
- except:
 
238
  continue
239
  marked_item(key, LINUX_SERVER_PROVIDER_KEYS_MARKED, LINUX_SERVER_PROVIDER_KEYS_ATTEMPTS)
240
  return
241
 
242
  async def chat_with_model_async(history, user_input, model_display, sess, custom_prompt, deep_search):
 
 
 
 
 
 
243
  ensure_stop_event(sess)
244
  sess.stop_event.clear()
245
  sess.cancel_token["cancelled"] = False
246
  if not LINUX_SERVER_PROVIDER_KEYS or not LINUX_SERVER_HOSTS:
247
- yield ("content", RESPONSES["RESPONSE_3"])
248
  return
249
  if not hasattr(sess, "session_id") or not sess.session_id:
250
  sess.session_id = str(uuid.uuid4())
251
  model_key = get_model_key(model_display)
252
  cfg = MODEL_CONFIG.get(model_key, DEFAULT_CONFIG)
253
  msgs = []
 
254
  if deep_search and model_display == MODEL_CHOICES[0]:
255
  msgs.append({"role": "system", "content": DEEP_SEARCH_INSTRUCTIONS})
256
  try:
@@ -273,17 +380,25 @@ async def chat_with_model_async(history, user_input, model_display, sess, custom
273
  r = await client.post(DEEP_SEARCH_PROVIDER_HOST, headers={"Authorization": f"Bearer {DEEP_SEARCH_PROVIDER_KEY}"}, json=payload)
274
  sr_json = r.json()
275
  msgs.append({"role": "system", "content": json.dumps(sr_json)})
276
- except:
 
277
  pass
278
  msgs.append({"role": "system", "content": INTERNAL_AI_INSTRUCTIONS})
279
  elif model_display == MODEL_CHOICES[0]:
 
280
  msgs.append({"role": "system", "content": INTERNAL_AI_INSTRUCTIONS})
281
  else:
 
282
  msgs.append({"role": "system", "content": custom_prompt or SYSTEM_PROMPT_MAPPING.get(model_key, SYSTEM_PROMPT_DEFAULT)})
283
- msgs.extend([{"role": "user", "content": u} for u, _ in history] + [{"role": "assistant", "content": a} for _, a in history if a])
 
 
 
284
  msgs.append({"role": "user", "content": user_input})
 
285
  candidates = [(h, k) for h in LINUX_SERVER_HOSTS for k in LINUX_SERVER_PROVIDER_KEYS]
286
  random.shuffle(candidates)
 
287
  for h, k in candidates:
288
  stream_gen = fetch_response_stream_async(h, k, model_key, msgs, cfg, sess.session_id, sess.stop_event, sess.cancel_token)
289
  got_responses = False
@@ -294,25 +409,44 @@ async def chat_with_model_async(history, user_input, model_display, sess, custom
294
  yield chunk
295
  if got_responses:
296
  return
 
297
  yield ("content", RESPONSES["RESPONSE_2"])
298
 
 
 
 
 
299
  async def respond_async(multi, history, model_display, sess, custom_prompt, deep_search):
 
 
 
 
 
 
 
300
  ensure_stop_event(sess)
301
  sess.stop_event.clear()
302
  sess.cancel_token["cancelled"] = False
 
303
  msg_input = {"text": multi.get("text", "").strip(), "files": multi.get("files", [])}
 
304
  if not msg_input["text"] and not msg_input["files"]:
305
  yield history, gr.update(value="", interactive=True, submit_btn=True, stop_btn=False), sess
306
  return
 
307
  inp = ""
308
  for f in msg_input["files"]:
 
309
  fp = f.get("data", f.get("name", "")) if isinstance(f, dict) else f
310
  inp += f"{Path(fp).name}\n\n{extract_file_content(fp)}\n\n"
 
311
  if msg_input["text"]:
312
  inp += msg_input["text"]
 
313
  history.append([inp, RESPONSES["RESPONSE_8"]])
314
  yield history, gr.update(interactive=False, submit_btn=False, stop_btn=True), sess
315
  queue = asyncio.Queue()
 
316
  async def background():
317
  reasoning = ""
318
  responses = ""
@@ -331,7 +465,7 @@ async def respond_async(multi, history, model_display, sess, custom_prompt, deep
331
  content_started = True
332
  ignore_reasoning = True
333
  responses = chunk
334
- await queue.put(("reasoning", ""))
335
  await queue.put(("replace", responses))
336
  else:
337
  responses += chunk
@@ -340,35 +474,55 @@ async def respond_async(multi, history, model_display, sess, custom_prompt, deep
340
  return responses
341
  bg_task = asyncio.create_task(background())
342
  stop_task = asyncio.create_task(sess.stop_event.wait())
 
343
  try:
344
  while True:
345
- done, _ = await asyncio.wait({stop_task, asyncio.create_task(queue.get())}, return_when=asyncio.FIRST_COMPLETED)
346
- if stop_task in done:
347
- sess.cancel_token["cancelled"] = True
348
- bg_task.cancel()
349
- history[-1][1] = RESPONSES["RESPONSE_1"]
350
- yield history, gr.update(value="", interactive=True, submit_btn=True, stop_btn=False), sess
351
- return
352
- for d in done:
353
- result = d.result()
 
 
 
 
 
 
 
 
354
  if result is None:
355
  raise StopAsyncIteration
356
  action, text = result
 
357
  history[-1][1] = text
358
  yield history, gr.update(interactive=False, submit_btn=False, stop_btn=True), sess
359
  except StopAsyncIteration:
360
  pass
361
  finally:
362
- stop_task.cancel()
363
- full_response = await bg_task
 
364
  yield history, gr.update(value="", interactive=True, submit_btn=True, stop_btn=False), sess
365
 
366
  def change_model(new):
 
 
 
 
 
367
  visible = new == MODEL_CHOICES[0]
368
- default = SYSTEM_PROMPT_MAPPING.get(get_model_key(new), SYSTEM_PROMPT_DEFAULT)
369
- return [], create_session(), new, default, False, gr.update(visible=visible)
370
 
371
  def stop_response(history, sess):
 
 
 
 
372
  ensure_stop_event(sess)
373
  sess.stop_event.set()
374
  sess.cancel_token["cancelled"] = True
@@ -376,24 +530,36 @@ def stop_response(history, sess):
376
  history[-1][1] = RESPONSES["RESPONSE_1"]
377
  return history, None, create_session()
378
 
 
 
 
 
379
  with gr.Blocks(fill_height=True, fill_width=True, title=AI_TYPES["AI_TYPE_4"], head=META_TAGS) as jarvis:
380
  user_history = gr.State([])
381
  user_session = gr.State(create_session())
382
  selected_model = gr.State(MODEL_CHOICES[0] if MODEL_CHOICES else "")
383
  J_A_R_V_I_S = gr.State("")
 
384
  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)
 
385
  deep_search = gr.Checkbox(label=AI_TYPES["AI_TYPE_8"], value=False, info=AI_TYPES["AI_TYPE_9"], visible=True)
 
386
  msg = gr.MultimodalTextbox(show_label=False, placeholder=RESPONSES["RESPONSE_5"], interactive=True, file_count="single", file_types=ALLOWED_EXTENSIONS)
387
- with gr.Sidebar(open=False):
388
- model_radio = gr.Radio(show_label=False, choices=MODEL_CHOICES, value=MODEL_CHOICES[0])
 
389
  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])
390
- def on_example_select(evt: gr.SelectData):
391
- return evt.value
392
  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])
393
- def clear_chat(history, sess, prompt, model):
394
- return [], create_session(), prompt, model, []
395
  deep_search.change(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, user_history])
396
  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, user_history])
 
397
  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)
 
398
  msg.stop(fn=stop_response, inputs=[user_history, user_session], outputs=[chatbot, msg, user_session])
 
 
399
  jarvis.queue(default_concurrency_limit=2).launch(max_file_size="1mb")
 
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:
 
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:
 
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:
 
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/"):
 
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
 
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)
 
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)
 
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:
 
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
 
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 = ""
 
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
 
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 change_model(new):
512
+ """
513
+ Handler to change selected AI model.
514
+ Resets chat history and session.
515
+ Updates system instructions and deep search checkbox visibility accordingly.
516
+ """
517
  visible = new == MODEL_CHOICES[0]
518
+ default_prompt = SYSTEM_PROMPT_MAPPING.get(get_model_key(new), SYSTEM_PROMPT_DEFAULT)
519
+ return [], create_session(), new, default_prompt, False, gr.update(visible=visible)
520
 
521
  def stop_response(history, sess):
522
+ """
523
+ Handler to stop ongoing AI response generation.
524
+ Sets cancellation flags and updates last message to cancellation notice.
525
+ """
526
  ensure_stop_event(sess)
527
  sess.stop_event.set()
528
  sess.cancel_token["cancelled"] = True
 
530
  history[-1][1] = RESPONSES["RESPONSE_1"]
531
  return history, None, create_session()
532
 
533
+ # ============================
534
+ # Gradio UI Setup
535
+ # ============================
536
+
537
  with gr.Blocks(fill_height=True, fill_width=True, title=AI_TYPES["AI_TYPE_4"], head=META_TAGS) as jarvis:
538
  user_history = gr.State([])
539
  user_session = gr.State(create_session())
540
  selected_model = gr.State(MODEL_CHOICES[0] if MODEL_CHOICES else "")
541
  J_A_R_V_I_S = gr.State("")
542
+ # Chatbot UI
543
  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)
544
+ # Deep search
545
  deep_search = gr.Checkbox(label=AI_TYPES["AI_TYPE_8"], value=False, info=AI_TYPES["AI_TYPE_9"], visible=True)
546
+ # User's input
547
  msg = gr.MultimodalTextbox(show_label=False, placeholder=RESPONSES["RESPONSE_5"], interactive=True, file_count="single", file_types=ALLOWED_EXTENSIONS)
548
+ # Sidebar to select AI models
549
+ with gr.Sidebar(open=False): model_radio = gr.Radio(show_label=False, choices=MODEL_CHOICES, value=MODEL_CHOICES[0])
550
+ # Models change
551
  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])
552
+ # Initial welcome messages
553
+ def on_example_select(evt: gr.SelectData): return evt.value
554
  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])
555
+ # Clear chat
556
+ def clear_chat(history, sess, prompt, model): return [], create_session(), prompt, model, []
557
  deep_search.change(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, user_history])
558
  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, user_history])
559
+ # Submit message
560
  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)
561
+ # Stop message
562
  msg.stop(fn=stop_response, inputs=[user_history, user_session], outputs=[chatbot, msg, user_session])
563
+
564
+ # Launch
565
  jarvis.queue(default_concurrency_limit=2).launch(max_file_size="1mb")