hadadrjt commited on
Commit
acbfb1d
·
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 +229 -35
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
@@ -25,60 +25,97 @@ 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 +125,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 +157,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 +165,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 +194,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 +261,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,13 +282,27 @@ 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)
215
  return
@@ -227,30 +320,46 @@ 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 +382,28 @@ 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,30 +414,58 @@ 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 = ""
319
  content_started = False
320
  ignore_reasoning = False
 
321
  async for typ, chunk in chat_with_model_async(history, inp, model_display, sess, custom_prompt, deep_search):
322
  if sess.stop_event.is_set() or sess.cancel_token["cancelled"]:
323
  break
@@ -331,19 +479,22 @@ 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
338
  await queue.put(("append", responses))
339
  await queue.put(None)
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"]
@@ -354,21 +505,33 @@ async def respond_async(multi, history, model_display, sess, custom_prompt, deep
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 +539,55 @@ 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")
 
25
  from pptx import Presentation
26
  from openpyxl import load_workbook
27
 
28
+ # Install OCR tools and dependencies
29
  os.system("apt-get update -q -y && apt-get install -q -y tesseract-ocr tesseract-ocr-eng tesseract-ocr-ind libleptonica-dev libtesseract-dev")
30
 
31
+ # ============================
32
+ # Environments
33
+ # ============================
34
+
35
+ # Initial welcome messages
36
  JARVIS_INIT = json.loads(os.getenv("HELLO", "[]"))
37
 
38
+ # Deep Search
39
  DEEP_SEARCH_PROVIDER_HOST = os.getenv("DEEP_SEARCH_PROVIDER_HOST")
40
  DEEP_SEARCH_PROVIDER_KEY = os.getenv('DEEP_SEARCH_PROVIDER_KEY')
41
  DEEP_SEARCH_INSTRUCTIONS = os.getenv("DEEP_SEARCH_INSTRUCTIONS")
42
 
43
+ # AI servers, keys and instructions
44
  INTERNAL_AI_GET_SERVER = os.getenv("INTERNAL_AI_GET_SERVER")
45
  INTERNAL_AI_INSTRUCTIONS = os.getenv("INTERNAL_TRAINING_DATA")
46
 
47
+ # System instructions mapping for various models and default instructions
48
  SYSTEM_PROMPT_MAPPING = json.loads(os.getenv("SYSTEM_PROMPT_MAPPING", "{}"))
49
  SYSTEM_PROMPT_DEFAULT = os.getenv("DEFAULT_SYSTEM")
50
 
51
+ # List of available servers and keys
52
  LINUX_SERVER_HOSTS = [h for h in json.loads(os.getenv("LINUX_SERVER_HOST", "[]")) if h]
 
53
  LINUX_SERVER_PROVIDER_KEYS = [k for k in json.loads(os.getenv("LINUX_SERVER_PROVIDER_KEY", "[]")) if k]
54
+
55
+ # Sets and dicts to track problematic keys and their retry attempts
56
  LINUX_SERVER_PROVIDER_KEYS_MARKED = set()
57
  LINUX_SERVER_PROVIDER_KEYS_ATTEMPTS = {}
58
 
59
+ # HTTP error codes considered as server errors for marking keys
60
+ LINUX_SERVER_ERRORS = set(map(int, filter(None, os.getenv("LINUX_SERVER_ERROR", "").split(","))))
61
 
62
+ # UI labels and responses from environment variables for easy localization/customization
63
  AI_TYPES = {f"AI_TYPE_{i}": os.getenv(f"AI_TYPE_{i}") for i in range(1, 10)}
 
64
  RESPONSES = {f"RESPONSE_{i}": os.getenv(f"RESPONSE_{i}") for i in range(1, 11)}
65
 
66
+ # Model mapping and configuration loaded from environment variables
67
  MODEL_MAPPING = json.loads(os.getenv("MODEL_MAPPING", "{}"))
68
  MODEL_CONFIG = json.loads(os.getenv("MODEL_CONFIG", "{}"))
69
  MODEL_CHOICES = list(MODEL_MAPPING.values())
70
 
71
+ # Default model config and key for fallback
72
  DEFAULT_CONFIG = json.loads(os.getenv("DEFAULT_CONFIG", "{}"))
73
  DEFAULT_MODEL_KEY = list(MODEL_MAPPING.keys())[0] if MODEL_MAPPING else None
74
 
75
+ # Meta tags for HTML head (SEO, etc.)
76
  META_TAGS = os.getenv("META_TAGS")
77
 
78
+ # Allowed file extensions for upload (e.g., .pdf, .docx, etc.)
79
  ALLOWED_EXTENSIONS = json.loads(os.getenv("ALLOWED_EXTENSIONS", "[]"))
80
 
81
+ # ============================
82
+ # Session Management
83
+ # ============================
84
+
85
  class SessionWithID(requests.Session):
86
+ """
87
+ Custom session object that holds a unique session ID and async control flags.
88
+ Used to track individual user sessions and allow cancellation of ongoing requests.
89
+ """
90
+ def __init__(self):
91
  super().__init__()
92
+ self.session_id = str(uuid.uuid4()) # Unique ID per session
93
+ self.stop_event = asyncio.Event() # Async event to signal stop requests
94
+ self.cancel_token = {"cancelled": False} # Flag to indicate cancellation
95
 
96
  def create_session():
97
+ """
98
+ Create and return a new SessionWithID object.
99
+ Called when a new user session starts or chat is reset.
100
+ """
101
  return SessionWithID()
102
 
103
  def ensure_stop_event(sess):
104
+ """
105
+ Ensure that the session object has stop_event and cancel_token attributes.
106
+ Useful when restoring or reusing sessions.
107
+ """
108
  if not hasattr(sess, "stop_event"):
109
  sess.stop_event = asyncio.Event()
110
  if not hasattr(sess, "cancel_token"):
111
  sess.cancel_token = {"cancelled": False}
112
 
113
  def marked_item(item, marked, attempts):
114
+ """
115
+ Mark a provider key or host as temporarily problematic after repeated failures.
116
+ Automatically unmark after 5 minutes to retry.
117
+ This helps avoid repeatedly using failing providers.
118
+ """
119
  marked.add(item)
120
  attempts[item] = attempts.get(item, 0) + 1
121
  if attempts[item] >= 3:
 
125
  threading.Timer(300, remove).start()
126
 
127
  def get_model_key(display):
128
+ """
129
+ Get the internal model key (identifier) from the display name.
130
+ Returns default model key if not found.
131
+ """
132
  return next((k for k, v in MODEL_MAPPING.items() if v == display), DEFAULT_MODEL_KEY)
133
 
134
+ # ============================
135
+ # File Content Extraction Utilities
136
+ # ============================
137
+
138
  def extract_pdf_content(fp):
139
+ """
140
+ Extract text content from PDF file.
141
+ Includes OCR on embedded images to capture text within images.
142
+ Also extracts tables as tab-separated text.
143
+ """
144
  content = ""
145
  try:
146
  with pdfplumber.open(fp) as pdf:
147
  for page in pdf.pages:
148
+ # Extract text from page
149
  text = page.extract_text() or ""
150
  content += text + "\n"
151
+ # OCR on images if any
152
  if page.images:
153
  img_obj = page.to_image(resolution=300)
154
  for img in page.images:
 
157
  ocr_text = pytesseract.image_to_string(cropped)
158
  if ocr_text.strip():
159
  content += ocr_text + "\n"
160
+ # Extract tables as TSV
161
  tables = page.extract_tables()
162
  for table in tables:
163
  for row in table:
 
165
  if cells:
166
  content += "\t".join(cells) + "\n"
167
  except Exception as e:
168
+ content += f"\n[Error reading PDF {fp}: {e}]"
169
  return content.strip()
170
 
171
  def extract_docx_content(fp):
172
+ """
173
+ Extract text from Microsoft Word files.
174
+ Also performs OCR on embedded images inside the Microsoft Word archive.
175
+ """
176
  content = ""
177
  try:
178
  doc = docx.Document(fp)
179
+ # Extract paragraphs
180
  for para in doc.paragraphs:
181
  content += para.text + "\n"
182
+ # Extract tables
183
  for table in doc.tables:
184
  for row in table.rows:
185
  cells = [cell.text for cell in row.cells]
186
  content += "\t".join(cells) + "\n"
187
+ # OCR on embedded images inside Microsoft Word
188
  with zipfile.ZipFile(fp) as z:
189
  for file in z.namelist():
190
  if file.startswith("word/media/"):
 
194
  ocr_text = pytesseract.image_to_string(img)
195
  if ocr_text.strip():
196
  content += ocr_text + "\n"
197
+ except Exception:
198
+ # Ignore images that can't be processed
199
  pass
200
  except Exception as e:
201
+ content += f"\n[Error reading Microsoft Word {fp}: {e}]"
202
  return content.strip()
203
 
204
  def extract_excel_content(fp):
205
+ """
206
+ Extract content from Microsoft Excel files.
207
+ Converts sheets to CSV text.
208
+ Attempts OCR on embedded images if present.
209
+ """
210
  content = ""
211
  try:
212
+ # Extract all sheets as CSV text
213
  sheets = pd.read_excel(fp, sheet_name=None)
214
  for name, df in sheets.items():
215
  content += f"Sheet: {name}\n"
216
  content += df.to_csv(index=False) + "\n"
217
+ # Load workbook to access images
218
  wb = load_workbook(fp, data_only=True)
219
  if wb._images:
220
  for image in wb._images:
221
+ try:
222
+ pil_img = Image.open(io.BytesIO(image._data()))
223
+ ocr_text = pytesseract.image_to_string(pil_img)
224
+ if ocr_text.strip():
225
+ content += ocr_text + "\n"
226
+ except Exception:
227
+ # Ignore images that can't be processed
228
+ pass
 
229
  except Exception as e:
230
+ content += f"\n[Error reading Microsoft Excel {fp}: {e}]"
231
  return content.strip()
232
 
233
  def extract_pptx_content(fp):
234
+ """
235
+ Extract text content from Microsoft PowerPoint presentation slides.
236
+ Includes text from shapes and tables.
237
+ Performs OCR on embedded images.
238
+ """
239
  content = ""
240
  try:
241
  prs = Presentation(fp)
242
  for slide in prs.slides:
243
  for shape in slide.shapes:
244
+ # Extract text from shapes
245
  if hasattr(shape, "text") and shape.text:
246
  content += shape.text + "\n"
247
+ # OCR on images inside shapes
248
  if shape.shape_type == 13 and hasattr(shape, "image") and shape.image:
249
  try:
250
  img = Image.open(io.BytesIO(shape.image.blob))
251
  ocr_text = pytesseract.image_to_string(img)
252
  if ocr_text.strip():
253
  content += ocr_text + "\n"
254
+ except Exception:
255
  pass
256
+ # Extract tables
257
  for shape in slide.shapes:
258
  if shape.has_table:
259
  table = shape.table
 
261
  cells = [cell.text for cell in row.cells]
262
  content += "\t".join(cells) + "\n"
263
  except Exception as e:
264
+ content += f"\n[Error reading Microsoft PowerPoint {fp}: {e}]"
265
  return content.strip()
266
 
267
  def extract_file_content(fp):
268
+ """
269
+ Determine file type by extension and extract text content accordingly.
270
+ For unknown types, attempts to read as plain text.
271
+ """
272
  ext = Path(fp).suffix.lower()
273
  if ext == ".pdf":
274
  return extract_pdf_content(fp)
 
282
  try:
283
  return Path(fp).read_text(encoding="utf-8").strip()
284
  except Exception as e:
285
+ return f"\n[Error reading file {fp}: {e}]"
286
+
287
+ # ============================
288
+ # Server Communication
289
+ # ============================
290
 
291
  async def fetch_response_stream_async(host, key, model, msgs, cfg, sid, stop_event, cancel_token):
292
+ """
293
+ Async generator that streams AI responses from a backend server.
294
+ Implements retry logic and marks failing keys to avoid repeated failures.
295
+ Streams reasoning and content separately for richer UI updates.
296
+ """
297
+ for timeout in [5, 10]:
298
  try:
299
+ async with httpx.AsyncClient(timeout=timeout) as client:
300
+ async with client.stream(
301
+ "POST",
302
+ host,
303
+ json={**{"model": model, "messages": msgs, "session_id": sid, "stream": True}, **cfg},
304
+ headers={"Authorization": f"Bearer {key}"}
305
+ ) as response:
306
  if response.status_code in LINUX_SERVER_ERRORS:
307
  marked_item(key, LINUX_SERVER_PROVIDER_KEYS_MARKED, LINUX_SERVER_PROVIDER_KEYS_ATTEMPTS)
308
  return
 
320
  if isinstance(j, dict) and j.get("choices"):
321
  for ch in j["choices"]:
322
  delta = ch.get("delta", {})
323
+ # Stream reasoning text separately for UI
324
  if "reasoning" in delta and delta["reasoning"]:
325
  decoded = delta["reasoning"].encode('utf-8').decode('unicode_escape')
326
  yield ("reasoning", decoded)
327
+ # Stream main content text
328
  if "content" in delta and delta["content"]:
329
  yield ("content", delta["content"])
330
+ except Exception:
331
+ # Ignore malformed JSON or unexpected data
332
  continue
333
+ except Exception:
334
+ # Network or other errors, try next timeout or mark key
335
  continue
336
  marked_item(key, LINUX_SERVER_PROVIDER_KEYS_MARKED, LINUX_SERVER_PROVIDER_KEYS_ATTEMPTS)
337
  return
338
 
339
  async def chat_with_model_async(history, user_input, model_display, sess, custom_prompt, deep_search):
340
+ """
341
+ Core async function to interact with AI model.
342
+ Prepares message history, system instructions, and optionally integrates deep search results.
343
+ Tries multiple backend hosts and keys with fallback.
344
+ Yields streamed responses for UI updates.
345
+ """
346
  ensure_stop_event(sess)
347
  sess.stop_event.clear()
348
  sess.cancel_token["cancelled"] = False
349
+
350
  if not LINUX_SERVER_PROVIDER_KEYS or not LINUX_SERVER_HOSTS:
351
+ yield ("content", RESPONSES["RESPONSE_3"]) # No providers available
352
  return
353
+
354
  if not hasattr(sess, "session_id") or not sess.session_id:
355
  sess.session_id = str(uuid.uuid4())
356
+
357
  model_key = get_model_key(model_display)
358
  cfg = MODEL_CONFIG.get(model_key, DEFAULT_CONFIG)
359
+
360
  msgs = []
361
+
362
+ # If deep search enabled and using primary model, prepend deep search instructions and results
363
  if deep_search and model_display == MODEL_CHOICES[0]:
364
  msgs.append({"role": "system", "content": DEEP_SEARCH_INSTRUCTIONS})
365
  try:
 
382
  r = await client.post(DEEP_SEARCH_PROVIDER_HOST, headers={"Authorization": f"Bearer {DEEP_SEARCH_PROVIDER_KEY}"}, json=payload)
383
  sr_json = r.json()
384
  msgs.append({"role": "system", "content": json.dumps(sr_json)})
385
+ except Exception:
386
+ # Fail silently if deep search fails
387
  pass
388
  msgs.append({"role": "system", "content": INTERNAL_AI_INSTRUCTIONS})
389
  elif model_display == MODEL_CHOICES[0]:
390
+ # For primary model without deep search, use internal instructions
391
  msgs.append({"role": "system", "content": INTERNAL_AI_INSTRUCTIONS})
392
  else:
393
+ # For other models, use default instructions
394
  msgs.append({"role": "system", "content": custom_prompt or SYSTEM_PROMPT_MAPPING.get(model_key, SYSTEM_PROMPT_DEFAULT)})
395
+
396
+ # Append conversation history alternating user and assistant messages
397
+ msgs.extend([{"role": "user", "content": u} for u, _ in history])
398
+ msgs.extend([{"role": "assistant", "content": a} for _, a in history if a])
399
+ # Append current user input
400
  msgs.append({"role": "user", "content": user_input})
401
+
402
+ # Shuffle provider hosts and keys for load balancing and fallback
403
  candidates = [(h, k) for h in LINUX_SERVER_HOSTS for k in LINUX_SERVER_PROVIDER_KEYS]
404
  random.shuffle(candidates)
405
+
406
+ # Try each host-key pair until a successful response is received
407
  for h, k in candidates:
408
  stream_gen = fetch_response_stream_async(h, k, model_key, msgs, cfg, sess.session_id, sess.stop_event, sess.cancel_token)
409
  got_responses = False
 
414
  yield chunk
415
  if got_responses:
416
  return
417
+
418
+ # If no response from any provider, yield fallback message
419
  yield ("content", RESPONSES["RESPONSE_2"])
420
 
421
+ # ============================
422
+ # Gradio Interaction Handlers
423
+ # ============================
424
+
425
  async def respond_async(multi, history, model_display, sess, custom_prompt, deep_search):
426
+ """
427
+ Main async handler for user input submission.
428
+ Supports text + file uploads (multi-modal input).
429
+ Extracts file content and appends to user input.
430
+ Streams AI responses back to UI, updating chat history live.
431
+ Allows stopping response generation gracefully.
432
+ """
433
  ensure_stop_event(sess)
434
  sess.stop_event.clear()
435
  sess.cancel_token["cancelled"] = False
436
+
437
+ # Extract text and files from multimodal input
438
  msg_input = {"text": multi.get("text", "").strip(), "files": multi.get("files", [])}
439
+
440
+ # If no input, reset UI state and return
441
  if not msg_input["text"] and not msg_input["files"]:
442
  yield history, gr.update(value="", interactive=True, submit_btn=True, stop_btn=False), sess
443
  return
444
+
445
+ # Initialize input with extracted file contents
446
  inp = ""
447
  for f in msg_input["files"]:
448
+ # Support dict or direct file path
449
  fp = f.get("data", f.get("name", "")) if isinstance(f, dict) else f
450
  inp += f"{Path(fp).name}\n\n{extract_file_content(fp)}\n\n"
451
+
452
+ # Append user text input if any
453
  if msg_input["text"]:
454
  inp += msg_input["text"]
455
+
456
+ # Append user input to chat history with placeholder response
457
  history.append([inp, RESPONSES["RESPONSE_8"]])
458
  yield history, gr.update(interactive=False, submit_btn=False, stop_btn=True), sess
459
+
460
  queue = asyncio.Queue()
461
+
462
+ # Background async task to fetch streamed AI responses
463
  async def background():
464
  reasoning = ""
465
  responses = ""
466
  content_started = False
467
  ignore_reasoning = False
468
+
469
  async for typ, chunk in chat_with_model_async(history, inp, model_display, sess, custom_prompt, deep_search):
470
  if sess.stop_event.is_set() or sess.cancel_token["cancelled"]:
471
  break
 
479
  content_started = True
480
  ignore_reasoning = True
481
  responses = chunk
482
+ await queue.put(("reasoning", "")) # Clear reasoning on content start
483
  await queue.put(("replace", responses))
484
  else:
485
  responses += chunk
486
  await queue.put(("append", responses))
487
  await queue.put(None)
488
  return responses
489
+
490
  bg_task = asyncio.create_task(background())
491
  stop_task = asyncio.create_task(sess.stop_event.wait())
492
+
493
  try:
494
  while True:
495
  done, _ = await asyncio.wait({stop_task, asyncio.create_task(queue.get())}, return_when=asyncio.FIRST_COMPLETED)
496
  if stop_task in done:
497
+ # User requested stop, cancel background task and update UI
498
  sess.cancel_token["cancelled"] = True
499
  bg_task.cancel()
500
  history[-1][1] = RESPONSES["RESPONSE_1"]
 
505
  if result is None:
506
  raise StopAsyncIteration
507
  action, text = result
508
+ # Update last message content in history with streamed text
509
  history[-1][1] = text
510
  yield history, gr.update(interactive=False, submit_btn=False, stop_btn=True), sess
511
  except StopAsyncIteration:
512
  pass
513
  finally:
514
  stop_task.cancel()
515
+
516
+ # Await full response to ensure completion
517
  full_response = await bg_task
518
  yield history, gr.update(value="", interactive=True, submit_btn=True, stop_btn=False), sess
519
 
520
  def change_model(new):
521
+ """
522
+ Handler to change selected AI model.
523
+ Resets chat history and session.
524
+ Updates system instructions and deep search checkbox visibility accordingly.
525
+ """
526
  visible = new == MODEL_CHOICES[0]
527
+ default_prompt = SYSTEM_PROMPT_MAPPING.get(get_model_key(new), SYSTEM_PROMPT_DEFAULT)
528
+ return [], create_session(), new, default_prompt, False, gr.update(visible=visible)
529
 
530
  def stop_response(history, sess):
531
+ """
532
+ Handler to stop ongoing AI response generation.
533
+ Sets cancellation flags and updates last message to cancellation notice.
534
+ """
535
  ensure_stop_event(sess)
536
  sess.stop_event.set()
537
  sess.cancel_token["cancelled"] = True
 
539
  history[-1][1] = RESPONSES["RESPONSE_1"]
540
  return history, None, create_session()
541
 
542
+ # ============================
543
+ # Gradio UI Setup
544
+ # ============================
545
+
546
  with gr.Blocks(fill_height=True, fill_width=True, title=AI_TYPES["AI_TYPE_4"], head=META_TAGS) as jarvis:
547
+ # States to keep chat history, user session, selected model and custom instructions
548
  user_history = gr.State([])
549
  user_session = gr.State(create_session())
550
  selected_model = gr.State(MODEL_CHOICES[0] if MODEL_CHOICES else "")
551
  J_A_R_V_I_S = gr.State("")
552
+
553
+ # Chatbot UI component with initial welcome messages loaded from env variable
554
  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)
555
+
556
+ # Checkbox to enable/disable deep search feature
557
  deep_search = gr.Checkbox(label=AI_TYPES["AI_TYPE_8"], value=False, info=AI_TYPES["AI_TYPE_9"], visible=True)
558
+
559
+ # Multimodal Textbox (support text input + file upload. Limited to single file, restricted file types)
560
  msg = gr.MultimodalTextbox(show_label=False, placeholder=RESPONSES["RESPONSE_5"], interactive=True, file_count="single", file_types=ALLOWED_EXTENSIONS)
561
+
562
+ # Sidebar with radio buttons to select AI model
563
  with gr.Sidebar(open=False):
564
  model_radio = gr.Radio(show_label=False, choices=MODEL_CHOICES, value=MODEL_CHOICES[0])
565
+
566
+ # When model changes, reset chat and update prompt/deep search checkbox visibility
567
  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])
568
+
569
+ # When initial welcome messages selected from chatbot, populate input box and trigger response
570
  def on_example_select(evt: gr.SelectData):
571
  return evt.value
572
+
573
+ chatbot.example_select(fn=on_example_select, inputs=[], outputs=[msg]).then(
574
+ fn=respond_async,
575
+ inputs=[msg, user_history, selected_model, user_session, J_A_R_V_I_S, deep_search],
576
+ outputs=[chatbot, msg, user_session]
577
+ )
578
+
579
+ # Clear chat resets history, session and states
580
  def clear_chat(history, sess, prompt, model):
581
  return [], create_session(), prompt, model, []
582
+
583
  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])
584
  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])
585
+
586
+ # Submit message triggers async AI response generation
587
  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)
588
+
589
+ # Stop button cancels ongoing response generation
590
  msg.stop(fn=stop_response, inputs=[user_history, user_session], outputs=[chatbot, msg, user_session])
591
+
592
+ # Launch
593
  jarvis.queue(default_concurrency_limit=2).launch(max_file_size="1mb")