h2ogpte 1.6.41rc5__py3-none-any.whl → 1.6.43rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. h2ogpte/__init__.py +1 -1
  2. h2ogpte/cli/__init__.py +0 -0
  3. h2ogpte/cli/commands/__init__.py +0 -0
  4. h2ogpte/cli/commands/command_handlers/__init__.py +0 -0
  5. h2ogpte/cli/commands/command_handlers/agent.py +41 -0
  6. h2ogpte/cli/commands/command_handlers/chat.py +37 -0
  7. h2ogpte/cli/commands/command_handlers/clear.py +8 -0
  8. h2ogpte/cli/commands/command_handlers/collection.py +67 -0
  9. h2ogpte/cli/commands/command_handlers/config.py +113 -0
  10. h2ogpte/cli/commands/command_handlers/disconnect.py +36 -0
  11. h2ogpte/cli/commands/command_handlers/exit.py +37 -0
  12. h2ogpte/cli/commands/command_handlers/help.py +8 -0
  13. h2ogpte/cli/commands/command_handlers/history.py +29 -0
  14. h2ogpte/cli/commands/command_handlers/rag.py +146 -0
  15. h2ogpte/cli/commands/command_handlers/research_agent.py +45 -0
  16. h2ogpte/cli/commands/command_handlers/session.py +77 -0
  17. h2ogpte/cli/commands/command_handlers/status.py +33 -0
  18. h2ogpte/cli/commands/dispatcher.py +79 -0
  19. h2ogpte/cli/core/__init__.py +0 -0
  20. h2ogpte/cli/core/app.py +105 -0
  21. h2ogpte/cli/core/config.py +199 -0
  22. h2ogpte/cli/core/encryption.py +104 -0
  23. h2ogpte/cli/core/session.py +171 -0
  24. h2ogpte/cli/integrations/__init__.py +0 -0
  25. h2ogpte/cli/integrations/agent.py +338 -0
  26. h2ogpte/cli/integrations/rag.py +442 -0
  27. h2ogpte/cli/main.py +90 -0
  28. h2ogpte/cli/ui/__init__.py +0 -0
  29. h2ogpte/cli/ui/hbot_prompt.py +435 -0
  30. h2ogpte/cli/ui/prompts.py +129 -0
  31. h2ogpte/cli/ui/status_bar.py +133 -0
  32. h2ogpte/cli/utils/__init__.py +0 -0
  33. h2ogpte/cli/utils/file_manager.py +411 -0
  34. h2ogpte/h2ogpte.py +471 -67
  35. h2ogpte/h2ogpte_async.py +482 -68
  36. h2ogpte/h2ogpte_sync_base.py +8 -1
  37. h2ogpte/rest_async/__init__.py +6 -3
  38. h2ogpte/rest_async/api/chat_api.py +29 -0
  39. h2ogpte/rest_async/api/collections_api.py +293 -0
  40. h2ogpte/rest_async/api/extractors_api.py +2874 -70
  41. h2ogpte/rest_async/api/prompt_templates_api.py +32 -32
  42. h2ogpte/rest_async/api_client.py +1 -1
  43. h2ogpte/rest_async/configuration.py +1 -1
  44. h2ogpte/rest_async/models/__init__.py +5 -2
  45. h2ogpte/rest_async/models/chat_completion.py +4 -2
  46. h2ogpte/rest_async/models/chat_completion_delta.py +5 -3
  47. h2ogpte/rest_async/models/chat_completion_request.py +1 -1
  48. h2ogpte/rest_async/models/chat_session.py +4 -2
  49. h2ogpte/rest_async/models/chat_settings.py +1 -1
  50. h2ogpte/rest_async/models/collection.py +4 -2
  51. h2ogpte/rest_async/models/collection_create_request.py +4 -2
  52. h2ogpte/rest_async/models/create_chat_session_request.py +87 -0
  53. h2ogpte/rest_async/models/extraction_request.py +1 -1
  54. h2ogpte/rest_async/models/extractor.py +4 -2
  55. h2ogpte/rest_async/models/guardrails_settings.py +8 -4
  56. h2ogpte/rest_async/models/guardrails_settings_create_request.py +1 -1
  57. h2ogpte/rest_async/models/process_document_job_request.py +1 -1
  58. h2ogpte/rest_async/models/question_request.py +1 -1
  59. h2ogpte/rest_async/models/{reset_and_share_prompt_template_request.py → reset_and_share_request.py} +6 -6
  60. h2ogpte/{rest_sync/models/reset_and_share_prompt_template_with_groups_request.py → rest_async/models/reset_and_share_with_groups_request.py} +6 -6
  61. h2ogpte/rest_async/models/summarize_request.py +1 -1
  62. h2ogpte/rest_async/models/update_collection_workspace_request.py +87 -0
  63. h2ogpte/rest_async/models/update_extractor_privacy_request.py +87 -0
  64. h2ogpte/rest_sync/__init__.py +6 -3
  65. h2ogpte/rest_sync/api/chat_api.py +29 -0
  66. h2ogpte/rest_sync/api/collections_api.py +293 -0
  67. h2ogpte/rest_sync/api/extractors_api.py +2874 -70
  68. h2ogpte/rest_sync/api/prompt_templates_api.py +32 -32
  69. h2ogpte/rest_sync/api_client.py +1 -1
  70. h2ogpte/rest_sync/configuration.py +1 -1
  71. h2ogpte/rest_sync/models/__init__.py +5 -2
  72. h2ogpte/rest_sync/models/chat_completion.py +4 -2
  73. h2ogpte/rest_sync/models/chat_completion_delta.py +5 -3
  74. h2ogpte/rest_sync/models/chat_completion_request.py +1 -1
  75. h2ogpte/rest_sync/models/chat_session.py +4 -2
  76. h2ogpte/rest_sync/models/chat_settings.py +1 -1
  77. h2ogpte/rest_sync/models/collection.py +4 -2
  78. h2ogpte/rest_sync/models/collection_create_request.py +4 -2
  79. h2ogpte/rest_sync/models/create_chat_session_request.py +87 -0
  80. h2ogpte/rest_sync/models/extraction_request.py +1 -1
  81. h2ogpte/rest_sync/models/extractor.py +4 -2
  82. h2ogpte/rest_sync/models/guardrails_settings.py +8 -4
  83. h2ogpte/rest_sync/models/guardrails_settings_create_request.py +1 -1
  84. h2ogpte/rest_sync/models/process_document_job_request.py +1 -1
  85. h2ogpte/rest_sync/models/question_request.py +1 -1
  86. h2ogpte/rest_sync/models/{reset_and_share_prompt_template_request.py → reset_and_share_request.py} +6 -6
  87. h2ogpte/{rest_async/models/reset_and_share_prompt_template_with_groups_request.py → rest_sync/models/reset_and_share_with_groups_request.py} +6 -6
  88. h2ogpte/rest_sync/models/summarize_request.py +1 -1
  89. h2ogpte/rest_sync/models/update_collection_workspace_request.py +87 -0
  90. h2ogpte/rest_sync/models/update_extractor_privacy_request.py +87 -0
  91. h2ogpte/session.py +3 -2
  92. h2ogpte/session_async.py +22 -6
  93. h2ogpte/types.py +6 -0
  94. {h2ogpte-1.6.41rc5.dist-info → h2ogpte-1.6.43rc1.dist-info}/METADATA +5 -1
  95. {h2ogpte-1.6.41rc5.dist-info → h2ogpte-1.6.43rc1.dist-info}/RECORD +98 -59
  96. h2ogpte-1.6.43rc1.dist-info/entry_points.txt +2 -0
  97. {h2ogpte-1.6.41rc5.dist-info → h2ogpte-1.6.43rc1.dist-info}/WHEEL +0 -0
  98. {h2ogpte-1.6.41rc5.dist-info → h2ogpte-1.6.43rc1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,442 @@
1
+ import asyncio
2
+ import json
3
+ from typing import List, Dict, Any, Optional
4
+ from pathlib import Path
5
+ from rich.console import Console
6
+
7
+ from ...h2ogpte import H2OGPTE
8
+ from ...h2ogpte_async import H2OGPTEAsync
9
+ from ...h2ogpte import PartialChatMessage
10
+
11
+ console = Console()
12
+
13
+
14
+ class H2OGPTEClient:
15
+ def __init__(self, address: str, api_key: str):
16
+ self.address = address
17
+ self.api_key = api_key
18
+ self.client = H2OGPTE(address=address, api_key=api_key)
19
+ self.async_client = H2OGPTEAsync(address=address, api_key=api_key)
20
+ self.username = None
21
+ self.current_collection_id = None
22
+ self.current_collection_name = None
23
+ self.current_chat_session_id = None
24
+ self.current_chat_session_name = None
25
+
26
+ async def test_connection_and_get_meta(self) -> bool:
27
+ try:
28
+ import concurrent.futures
29
+
30
+ def get_meta():
31
+ return self.client.get_meta()
32
+
33
+ loop = asyncio.get_event_loop()
34
+ with concurrent.futures.ThreadPoolExecutor() as executor:
35
+ meta = await asyncio.wait_for(
36
+ loop.run_in_executor(executor, get_meta), timeout=5.0
37
+ )
38
+ self.username = meta.username
39
+ console.print(
40
+ f"[green]✓[/green] Connected as user: [cyan]{self.username}[/cyan]"
41
+ )
42
+ return True
43
+
44
+ except asyncio.TimeoutError:
45
+ console.print(f"[red]Connection timeout (5s) - network may be slow[/red]")
46
+ return False
47
+ except Exception as e:
48
+ console.print(f"[red]Connection failed: {e}[/red]")
49
+ return False
50
+
51
+ async def create_collection(
52
+ self, name: str, description: str = ""
53
+ ) -> Optional[str]:
54
+ try:
55
+ collection_id = await self.async_client.create_collection(
56
+ name=name, description=description
57
+ )
58
+ self.current_collection_id = collection_id
59
+ self.current_collection_name = name
60
+ console.print(f"[green]✓[/green] Collection '[cyan]{name}[/cyan]' created")
61
+ return collection_id
62
+
63
+ except Exception as e:
64
+ console.print(f"[red]Error creating collection: {e}[/red]")
65
+ return None
66
+
67
+ async def create_chat_session(
68
+ self, collection_id: Optional[str] = None, session_name: str = "chat-session"
69
+ ) -> Optional[str]:
70
+ try:
71
+ chat_session_id = await self.async_client.create_chat_session(collection_id)
72
+ self.current_chat_session_id = chat_session_id
73
+ self.current_chat_session_name = session_name
74
+ console.print(
75
+ f"[green]✓[/green] Chat session '[cyan]{session_name}[/cyan]' created"
76
+ )
77
+ return chat_session_id
78
+
79
+ except Exception as e:
80
+ console.print(f"[red]Error creating chat session: {e}[/red]")
81
+ return None
82
+
83
+ async def rename_chat_session(self, session_id: str, session_name: str) -> bool:
84
+ try:
85
+ await self.async_client.rename_chat_session(session_id, session_name)
86
+ self.current_chat_session_name = session_name
87
+ console.print(
88
+ f"[green]✓[/green] Chat session renamed to '[cyan]{session_name}[/cyan]'"
89
+ )
90
+ return True
91
+
92
+ except Exception as e:
93
+ console.print(f"[red]Error renaming chat session: {e}[/red]")
94
+ return False
95
+
96
+ async def query_streaming(
97
+ self,
98
+ message: str,
99
+ timeout: int = 2400,
100
+ use_agent: bool = False,
101
+ agent_type: str = None,
102
+ ) -> Optional[Dict[str, Any]]:
103
+ if not self.current_chat_session_id:
104
+ console.print("[red]No active chat session. Create one first.[/red]")
105
+ return None
106
+
107
+ try:
108
+ import re
109
+
110
+ accumulated_content = ""
111
+ displayed_turn_count = 0
112
+ last_turn_title = None
113
+ header_shown = False
114
+ final_response_content = ""
115
+
116
+ def callback(chat_message):
117
+ nonlocal accumulated_content, displayed_turn_count, last_turn_title, header_shown, final_response_content
118
+
119
+ if not isinstance(chat_message, PartialChatMessage):
120
+ return
121
+
122
+ content = chat_message.content
123
+ if not content:
124
+ return
125
+
126
+ accumulated_content += content
127
+ final_response_content = (
128
+ accumulated_content # Keep track of full content for final response
129
+ )
130
+
131
+ if use_agent:
132
+ if not header_shown:
133
+ console.print("[blue]Agentic Analysis[/blue]")
134
+ header_shown = True
135
+
136
+ chunks = accumulated_content.split("ENDOFTURN\n")
137
+ completed_chunks = len(chunks) - 1
138
+ while displayed_turn_count < completed_chunks:
139
+ chunk = chunks[displayed_turn_count]
140
+ turn_title = None
141
+
142
+ turn_title_match = re.search(
143
+ r"<(?:stream_)?turn_title>(.*?)</(?:stream_)?turn_title>",
144
+ chunk,
145
+ re.DOTALL,
146
+ )
147
+ if turn_title_match:
148
+ turn_title = turn_title_match.group(1).strip()
149
+ else:
150
+ lines = chunk.strip().split("\n")
151
+ for line in lines:
152
+ if line.strip():
153
+ turn_title = line.strip()
154
+ break
155
+
156
+ if turn_title:
157
+ turn_title = re.sub(
158
+ r"^[\*#\s\t\n]+", "", turn_title
159
+ ).strip()
160
+ turn_title = re.sub(r"\*+$", "", turn_title).strip()
161
+
162
+ if turn_title and turn_title != last_turn_title:
163
+ console.print(f" • {turn_title}")
164
+ last_turn_title = turn_title
165
+
166
+ displayed_turn_count += 1
167
+
168
+ if len(chunks) > displayed_turn_count and chunks[-1].strip():
169
+ current_chunk = chunks[-1]
170
+
171
+ turn_title_match = re.search(
172
+ r"<(?:stream_)?turn_title>(.*?)</(?:stream_)?turn_title>",
173
+ current_chunk,
174
+ re.DOTALL,
175
+ )
176
+ if turn_title_match:
177
+ new_title = turn_title_match.group(1).strip()
178
+ else:
179
+ lines = current_chunk.strip().split("\n")
180
+ new_title = None
181
+ for line in lines:
182
+ if line.strip():
183
+ new_title = line.strip()
184
+ break
185
+
186
+ if new_title:
187
+ cleaned_title = re.sub(
188
+ r"^[\*#\s\t\n]+", "", new_title
189
+ ).strip()
190
+ cleaned_title = re.sub(r"\*+$", "", cleaned_title).strip()
191
+
192
+ if cleaned_title and cleaned_title != last_turn_title:
193
+ console.print(f" • {cleaned_title}")
194
+ last_turn_title = cleaned_title
195
+ else:
196
+ console.print(content, end="")
197
+
198
+ if use_agent:
199
+ console.print("\n", end="")
200
+ else:
201
+ console.print("\n[bold green]H2OGPTE:[/bold green] ", end="")
202
+
203
+ query_params = {"timeout": timeout, "llm": "auto", "callback": callback}
204
+
205
+ if use_agent:
206
+ query_params["llm_args"] = {
207
+ "use_agent": True,
208
+ }
209
+ if agent_type:
210
+ query_params["llm_args"]["agent_type"] = agent_type
211
+
212
+ async with self.async_client.connect(
213
+ self.current_chat_session_id
214
+ ) as session:
215
+ reply = await session.query(message, **query_params)
216
+
217
+ usage = await self.async_client.list_chat_message_meta_part(
218
+ reply.id, "usage_stats"
219
+ )
220
+ usage_dict = json.loads(usage.content)
221
+
222
+ if use_agent:
223
+ console.print("\n[blue]Final Response:[/blue]")
224
+
225
+ if hasattr(reply, "content") and reply.content:
226
+ response_content = reply.content
227
+ if response_content:
228
+ console.print(response_content)
229
+ else:
230
+ console.print("No response content available.")
231
+
232
+ console.print()
233
+ else:
234
+ console.print()
235
+
236
+ return usage_dict
237
+
238
+ except Exception as e:
239
+ console.print(f"\n[red]Query error: {e}[/red]")
240
+ return None
241
+
242
+ async def upload_files(self, file_paths: List[Path]) -> Dict[str, str]:
243
+ if not self.current_collection_id:
244
+ console.print("[red]No active collection. Create one first.[/red]")
245
+ return {}
246
+
247
+ uploaded_files = {}
248
+ upload_ids = []
249
+
250
+ try:
251
+ import concurrent.futures
252
+
253
+ for file_path in file_paths:
254
+
255
+ def upload_file():
256
+ with open(file_path, "rb") as f:
257
+ return self.client.upload(file_path.name, f)
258
+
259
+ loop = asyncio.get_event_loop()
260
+ with concurrent.futures.ThreadPoolExecutor() as executor:
261
+ upload_id = await loop.run_in_executor(executor, upload_file)
262
+ uploaded_files[str(file_path)] = upload_id
263
+ upload_ids.append(upload_id)
264
+ console.print(f"[green]✓[/green] Uploaded: {file_path.name}")
265
+
266
+ last_seen_update = None
267
+
268
+ def callback(job):
269
+ nonlocal last_seen_update
270
+ if not job:
271
+ return
272
+
273
+ current_update = job.last_update_date
274
+ if last_seen_update is not None and current_update <= last_seen_update:
275
+ return
276
+
277
+ last_seen_update = current_update
278
+ for s in job.statuses:
279
+ if not s:
280
+ continue
281
+
282
+ status_text = getattr(s, "status", None)
283
+ if status_text:
284
+ console.print(f"[blue] {status_text}...[/blue]")
285
+
286
+ def ingest_uploads():
287
+ ingest_result = self.client.ingest_uploads(
288
+ self.current_collection_id, upload_ids, callback=callback
289
+ )
290
+ console.print(f"[green]✓[/green] Ingested {len(upload_ids)} files")
291
+ return ingest_result
292
+
293
+ loop = asyncio.get_event_loop()
294
+ with concurrent.futures.ThreadPoolExecutor() as executor:
295
+ await loop.run_in_executor(executor, ingest_uploads)
296
+
297
+ except Exception as e:
298
+ console.print(f"[red]Upload error: {e}[/red]")
299
+
300
+ return uploaded_files
301
+
302
+
303
+ class RAGManager:
304
+ def __init__(self):
305
+ self.client: Optional[H2OGPTEClient] = None
306
+ self.connected = False
307
+
308
+ async def auto_reconnect(self, settings) -> bool:
309
+ if not settings.rag.endpoint:
310
+ return False
311
+
312
+ api_key = settings.get_rag_api_key()
313
+ if not api_key:
314
+ return False
315
+
316
+ console.print(f"[blue]Reconnecting to {settings.rag.endpoint}...[/blue]")
317
+ return await self.connect_and_get_user(settings.rag.endpoint, api_key)
318
+
319
+ async def connect_and_get_user(self, address: str, api_key: str) -> bool:
320
+ console.print(f"[blue]Connecting to H2OGPTE at {address}...[/blue]")
321
+
322
+ try:
323
+ self.client = H2OGPTEClient(address, api_key)
324
+
325
+ if await self.client.test_connection_and_get_meta():
326
+ self.connected = True
327
+ console.print("[green]✓[/green] Successfully connected to H2OGPTE")
328
+ return True
329
+ else:
330
+ self.connected = False
331
+ console.print("[red]✗[/red] Failed to connect to H2OGPTE")
332
+ return False
333
+
334
+ except ImportError as e:
335
+ console.print(f"[red]✗[/red] {e}")
336
+ return False
337
+ except Exception as e:
338
+ console.print(f"[red]✗[/red] Unexpected error: {e}")
339
+ self.connected = False
340
+ return False
341
+
342
+ async def switch_to_collection(self, collection_name: str) -> bool:
343
+ if not self.connected or not self.client:
344
+ console.print("[red]Not connected to H2OGPTE. Use /register first.[/red]")
345
+ return False
346
+
347
+ collection_id = await self.client.create_collection(
348
+ name=collection_name, description=f"CLI-Collection: {collection_name}"
349
+ )
350
+
351
+ if collection_id:
352
+ self.client.current_chat_session_id = None
353
+ self.client.current_chat_session_name = None
354
+ console.print("[green]✓[/green] Collection switched successfully")
355
+ return True
356
+ else:
357
+ console.print("[red]✗[/red] Failed to switch to collection")
358
+ return False
359
+
360
+ async def get_username(self) -> Optional[str]:
361
+ if self.client:
362
+ return self.client.username
363
+ return None
364
+
365
+ async def get_collection_name(self) -> Optional[str]:
366
+ if self.client:
367
+ return self.client.current_collection_name
368
+ return None
369
+
370
+ async def get_chat_session_name(self) -> Optional[str]:
371
+ if self.client:
372
+ return self.client.current_chat_session_name
373
+ return None
374
+
375
+ async def create_chat_session(self, session_name: str) -> bool:
376
+ if not self.connected or not self.client:
377
+ console.print("[red]Not connected to H2OGPTE. Use /register first.[/red]")
378
+ return False
379
+
380
+ session_id = await self.client.create_chat_session(
381
+ collection_id=self.client.current_collection_id, session_name=session_name
382
+ )
383
+ return session_id is not None
384
+
385
+ async def create_chat_session_with_name(self, session_name: str) -> Optional[str]:
386
+ if not self.connected or not self.client:
387
+ console.print("[red]Not connected to H2OGPTE. Use /register first.[/red]")
388
+ return None
389
+
390
+ session_id = await self.client.create_chat_session(
391
+ collection_id=self.client.current_collection_id, session_name=session_name
392
+ )
393
+
394
+ if session_id and session_name != "chat-session":
395
+ await self.client.rename_chat_session(session_id, session_name)
396
+
397
+ return session_id
398
+
399
+ async def send_message(
400
+ self, message: str, use_agent: bool = False, agent_type: str = None
401
+ ) -> Optional[Dict[str, Any]]:
402
+ if not self.connected or not self.client:
403
+ console.print("[red]Not connected to H2OGPTE. Use /register first.[/red]")
404
+ return None
405
+
406
+ if not self.client.current_collection_id:
407
+ console.print("[blue]Creating default collection...[/blue]")
408
+ collection_id = await self.client.create_collection(
409
+ name="CLI-Collection",
410
+ description="CLI automatically created collection",
411
+ )
412
+ if not collection_id:
413
+ console.print("[red]Failed to create collection[/red]")
414
+ return None
415
+
416
+ if not self.client.current_chat_session_id:
417
+ import datetime
418
+
419
+ session_name = f"chat-{datetime.datetime.now().strftime('%Y%m%d-%H%M%S')}"
420
+ console.print(f"[blue]Creating chat session '{session_name}'...[/blue]")
421
+ session_id = await self.client.create_chat_session(
422
+ collection_id=self.client.current_collection_id,
423
+ session_name=session_name,
424
+ )
425
+ if session_id:
426
+ await self.client.rename_chat_session(session_id, session_name)
427
+
428
+ return await self.client.query_streaming(
429
+ message, use_agent=use_agent, agent_type=agent_type
430
+ )
431
+
432
+ async def upload_files(self, paths: List[Path]) -> Dict[str, str]:
433
+ if not self.connected or not self.client:
434
+ console.print("[red]Not connected to H2OGPTE. Use /register first.[/red]")
435
+ return {}
436
+
437
+ console.print(f"[blue]Uploading {len(paths)} file(s)...[/blue]")
438
+ return await self.client.upload_files(paths)
439
+
440
+ async def close(self):
441
+ self.connected = False
442
+ self.client = None
h2ogpte/cli/main.py ADDED
@@ -0,0 +1,90 @@
1
+ import asyncio
2
+ import sys
3
+ from rich.traceback import install
4
+ from rich.console import Console
5
+
6
+ from .core.app import initialize_app
7
+ from .commands.dispatcher import dispatch_command
8
+
9
+ install(show_locals=True)
10
+
11
+
12
+ async def main_loop():
13
+ try:
14
+ app = initialize_app()
15
+ app.settings.ensure_directories()
16
+ app.ui.show_welcome()
17
+ await app.try_auto_reconnect()
18
+
19
+ exit_attempts = 0
20
+ while True:
21
+ try:
22
+ command = app.ui.prompt.get_input("❯ ")
23
+
24
+ if not command.strip():
25
+ continue
26
+
27
+ if command in ["/exit", "/quit"]:
28
+ exit_attempts += 1
29
+ if exit_attempts > 1 and not sys.stdin.isatty():
30
+ app.console.print(
31
+ "[cyan]Non-interactive mode: Exiting without confirmation[/cyan]"
32
+ )
33
+ break
34
+
35
+ should_continue = await dispatch_command(command)
36
+ if not should_continue:
37
+ break
38
+
39
+ if command not in ["/exit", "/quit"]:
40
+ exit_attempts = 0
41
+
42
+ except KeyboardInterrupt:
43
+ app.console.print(
44
+ "\n[yellow]⚠ Interrupted. Use /exit to quit or continue typing.[/yellow]"
45
+ )
46
+ continue
47
+ except EOFError:
48
+ app.console.print("\n[cyan]Goodbye![/cyan]")
49
+ break
50
+ except Exception as e:
51
+ app.console.print(f"[red]Unexpected error: {e}[/red]")
52
+ app.console.print("[dim]Type /exit to quit or try again[/dim]")
53
+ continue
54
+
55
+ try:
56
+ await app.cleanup()
57
+ except Exception as e:
58
+ app.console.print(
59
+ f"[yellow]Warning: Could not clean up properly: {e}[/yellow]"
60
+ )
61
+
62
+ except Exception as e:
63
+ Console().print(f"[red]Critical error in main loop: {e}[/red]")
64
+
65
+
66
+ def main():
67
+ try:
68
+ try:
69
+ loop = asyncio.get_running_loop()
70
+ Console().print(
71
+ "[yellow]Running in existing event loop, creating task...[/yellow]"
72
+ )
73
+ task = loop.create_task(main_loop())
74
+ loop.run_until_complete(task)
75
+ except RuntimeError:
76
+ asyncio.run(main_loop())
77
+ except KeyboardInterrupt:
78
+ Console().print("\n[yellow]Interrupted[/yellow]")
79
+ sys.exit(0)
80
+ except Exception as e:
81
+ console = Console()
82
+ console.print(f"[red]Fatal error: {e}[/red]")
83
+ import traceback
84
+
85
+ console.print(f"[dim]{traceback.format_exc()}[/dim]")
86
+ sys.exit(1)
87
+
88
+
89
+ if __name__ == "__main__":
90
+ main()
File without changes