lollmsbot 0.0.1__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.
lollmsbot/gateway.py ADDED
@@ -0,0 +1,606 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ lollmsBot Gateway - Central Agent Architecture with File Delivery
4
+ """
5
+ import asyncio
6
+ import json
7
+ import os
8
+ import secrets
9
+ import hashlib
10
+ import hmac
11
+ from typing import Any, Dict, List, Optional, Set
12
+ from contextlib import asynccontextmanager
13
+ from pathlib import Path
14
+
15
+ from fastapi import FastAPI, HTTPException, Request, status, Depends
16
+ from fastapi.middleware.cors import CORSMiddleware
17
+ from fastapi.responses import RedirectResponse, JSONResponse
18
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
19
+ from pydantic import BaseModel
20
+ from rich.console import Console
21
+
22
+ from lollmsbot.config import BotConfig, LollmsSettings
23
+ from lollmsbot.agent import Agent, PermissionLevel
24
+ # Import tools for registration
25
+ from lollmsbot.tools.filesystem import FilesystemTool
26
+ from lollmsbot.tools.http import HttpTool
27
+ from lollmsbot.tools.calendar import CalendarTool
28
+ from lollmsbot.tools.shell import ShellTool
29
+
30
+ console = Console()
31
+ app = FastAPI(title="lollmsBot API")
32
+
33
+ # UI instance (optional)
34
+ _ui_instance: Optional[Any] = None
35
+ _ui_enabled: bool = False
36
+
37
+ # HTTP API channel (optional)
38
+ _http_api: Optional[Any] = None
39
+
40
+ # ========== SHARED AGENT INSTANCE ==========
41
+ _agent: Optional[Agent] = None
42
+
43
+ def get_agent() -> Agent:
44
+ """Get or create the shared Agent instance with tools registered."""
45
+ global _agent
46
+ if _agent is None:
47
+ config = BotConfig.from_env()
48
+ _agent = Agent(
49
+ config=config,
50
+ name="LollmsBot",
51
+ default_permissions=PermissionLevel.BASIC,
52
+ )
53
+
54
+ # Register default tools - THIS IS KEY FOR FILE GENERATION!
55
+ async def register_tools():
56
+ try:
57
+ await _agent.register_tool(FilesystemTool())
58
+ console.print("[green] • FilesystemTool registered[/]")
59
+ except Exception as e:
60
+ console.print(f"[yellow] • FilesystemTool failed: {e}[/]")
61
+
62
+ try:
63
+ await _agent.register_tool(HttpTool())
64
+ console.print("[green] • HttpTool registered[/]")
65
+ except Exception as e:
66
+ console.print(f"[yellow] • HttpTool failed: {e}[/]")
67
+
68
+ try:
69
+ await _agent.register_tool(CalendarTool())
70
+ console.print("[green] • CalendarTool registered[/]")
71
+ except Exception as e:
72
+ console.print(f"[yellow] • CalendarTool failed: {e}[/]")
73
+
74
+ # Shell tool - more dangerous, only register if explicitly enabled
75
+ if os.getenv("LOLLMSBOT_ENABLE_SHELL", "").lower() in ("true", "1", "yes"):
76
+ try:
77
+ await _agent.register_tool(ShellTool())
78
+ console.print("[green] • ShellTool registered[/]")
79
+ except Exception as e:
80
+ console.print(f"[yellow] • ShellTool failed: {e}[/]")
81
+ else:
82
+ console.print("[dim] • ShellTool disabled (set LOLLMSBOT_ENABLE_SHELL=true to enable)[/]")
83
+
84
+ # Run tool registration synchronously during init
85
+ try:
86
+ loop = asyncio.get_event_loop()
87
+ if loop.is_running():
88
+ asyncio.create_task(register_tools())
89
+ else:
90
+ loop.run_until_complete(register_tools())
91
+ except RuntimeError:
92
+ pass
93
+
94
+ console.print(f"[green]✅ Agent initialized: {_agent}[/]")
95
+ return _agent
96
+
97
+ # ========== SHARED LOLLMS CLIENT ==========
98
+ _lollms_client: Optional[Any] = None
99
+
100
+ def get_lollms_client():
101
+ """Get or create shared LoLLMS client."""
102
+ global _lollms_client
103
+ if _lollms_client is None:
104
+ try:
105
+ from lollmsbot.lollms_client import build_lollms_client
106
+ _lollms_client = build_lollms_client()
107
+ console.print("[green]✅ LoLLMS client initialized[/]")
108
+ except Exception as e:
109
+ console.print(f"[yellow]⚠️ LoLLMS client unavailable: {e}[/]")
110
+ _lollms_client = None
111
+ return _lollms_client
112
+
113
+ # ========== CONFIGURATION ==========
114
+
115
+ def _load_wizard_config() -> Dict[str, Any]:
116
+ """Load config from wizard's config.json if it exists."""
117
+ wizard_path = Path.home() / ".lollmsbot" / "config.json"
118
+ if wizard_path.exists():
119
+ try:
120
+ return json.loads(wizard_path.read_text())
121
+ except (json.JSONDecodeError, IOError):
122
+ pass
123
+ return {}
124
+
125
+ _WIZARD_CONFIG = _load_wizard_config()
126
+
127
+ def _get_config(service: str, key: str, env_name: str, default: Any = None) -> Any:
128
+ """Get config value: wizard config > env var > default."""
129
+ if service in _WIZARD_CONFIG and key in _WIZARD_CONFIG[service]:
130
+ return _WIZARD_CONFIG[service][key]
131
+ return os.getenv(env_name, default)
132
+
133
+ # Security settings
134
+ DEFAULT_HOST = "127.0.0.1"
135
+ HOST = _get_config("lollmsbot", "host", "LOLLMSBOT_HOST", DEFAULT_HOST)
136
+ PORT = int(_get_config("lollmsbot", "port", "LOLLMSBOT_PORT", "8800"))
137
+ API_KEY = _get_config("lollmsbot", "api_key", "LOLLMSBOT_API_KEY", None)
138
+
139
+ if HOST not in ("127.0.0.1", "localhost", "::1") and not API_KEY:
140
+ API_KEY = secrets.token_urlsafe(32)
141
+ console.print(f"[bold yellow]⚠️ Auto-generated API key: {API_KEY}[/]")
142
+
143
+ _security = HTTPBearer(auto_error=False)
144
+
145
+ # Channel tokens
146
+ DISCORD_TOKEN = _get_config("discord", "bot_token", "DISCORD_BOT_TOKEN", None)
147
+ DISCORD_ALLOWED_USERS = _get_config("discord", "allowed_users", "DISCORD_ALLOWED_USERS", None)
148
+ DISCORD_ALLOWED_GUILDS = _get_config("discord", "allowed_guilds", "DISCORD_ALLOWED_GUILDS", None)
149
+ DISCORD_BLOCKED_USERS = _get_config("discord", "blocked_users", "DISCORD_BLOCKED_USERS", None)
150
+ DISCORD_REQUIRE_MENTION_GUILD = _get_config("discord", "require_mention_guild", "DISCORD_REQUIRE_MENTION_GUILD", "true")
151
+ DISCORD_REQUIRE_MENTION_DM = _get_config("discord", "require_mention_dm", "DISCORD_REQUIRE_MENTION_DM", "false")
152
+ TELEGRAM_TOKEN = _get_config("telegram", "bot_token", "TELEGRAM_BOT_TOKEN", None)
153
+
154
+ _active_channels: Dict[str, Any] = {}
155
+ _channel_tasks: List[asyncio.Task] = []
156
+
157
+ # ========== SECURITY ==========
158
+
159
+ def _verify_api_key(credentials: Optional[HTTPAuthorizationCredentials]) -> bool:
160
+ """Verify API key."""
161
+ if API_KEY is None:
162
+ return True
163
+ if credentials is None:
164
+ return False
165
+ provided = credentials.credentials.encode('utf-8')
166
+ expected = API_KEY.encode('utf-8')
167
+ return hmac.compare_digest(provided, expected)
168
+
169
+ async def require_auth(request: Request, credentials: Optional[HTTPAuthorizationCredentials] = Depends(_security)):
170
+ """Require authentication for external access."""
171
+ client_host = request.client.host if request.client else "unknown"
172
+
173
+ # Always allow localhost
174
+ if client_host in ("127.0.0.1", "::1", "localhost"):
175
+ return
176
+
177
+ if API_KEY is None:
178
+ raise HTTPException(
179
+ status_code=status.HTTP_403_FORBIDDEN,
180
+ detail="External access not permitted. Gateway is in local-only mode.",
181
+ )
182
+
183
+ if not _verify_api_key(credentials):
184
+ raise HTTPException(
185
+ status_code=status.HTTP_401_UNAUTHORIZED,
186
+ detail="Invalid or missing API key.",
187
+ headers={"WWW-Authenticate": "Bearer"},
188
+ )
189
+
190
+ # ========== MODELS ==========
191
+
192
+ class Health(BaseModel):
193
+ status: str = "ok"
194
+ url: str = f"http://{HOST}:{PORT}"
195
+
196
+ class ChatReq(BaseModel):
197
+ message: str
198
+ user_id: Optional[str] = "anonymous"
199
+
200
+ class ChatResp(BaseModel):
201
+ success: bool
202
+ response: str
203
+ error: Optional[str] = None
204
+ tools_used: List[str] = []
205
+ files_generated: int = 0
206
+ file_downloads: List[Dict[str, Any]] = []
207
+
208
+ class PermissionReq(BaseModel):
209
+ admin_user_id: str
210
+ target_user_id: str
211
+ level: str # "NONE", "BASIC", "TOOLS", "ADMIN"
212
+ allowed_tools: Optional[List[str]] = None
213
+ denied_tools: Optional[List[str]] = None
214
+
215
+ # ========== CORS ==========
216
+
217
+ _cors_origins = ["http://localhost", "http://127.0.0.1"]
218
+ if HOST not in ("127.0.0.1", "localhost", "::1"):
219
+ _cors_origins = []
220
+
221
+ app.add_middleware(
222
+ CORSMiddleware,
223
+ allow_origins=_cors_origins,
224
+ allow_credentials=True,
225
+ allow_methods=["GET", "POST"],
226
+ allow_headers=["*"],
227
+ )
228
+
229
+ # ========== ROUTES ==========
230
+
231
+ @app.get("/")
232
+ async def root():
233
+ agent = get_agent()
234
+ lollms_ok = get_lollms_client() is not None
235
+
236
+ # Check channels
237
+ channels_status = {
238
+ "discord": "enabled" if DISCORD_TOKEN else "disabled",
239
+ "telegram": "enabled" if TELEGRAM_TOKEN else "disabled",
240
+ }
241
+ if "discord" in _active_channels:
242
+ channels_status["discord"] = "active"
243
+ if "telegram" in _active_channels:
244
+ channels_status["telegram"] = "active"
245
+
246
+ return {
247
+ "api": f"http://{HOST}:{PORT}",
248
+ "docs": "/docs",
249
+ "health": "/health",
250
+ "chat": "/chat",
251
+ "agent": {
252
+ "name": agent.name,
253
+ "state": agent.state.name,
254
+ "tools": list(agent.tools.keys()),
255
+ },
256
+ "lollms": {
257
+ "connected": lollms_ok,
258
+ "host": LollmsSettings.from_env().host_address,
259
+ },
260
+ "security": {
261
+ "host": HOST,
262
+ "local_only": HOST in ("127.0.0.1", "localhost", "::1"),
263
+ "auth_required": API_KEY is not None,
264
+ },
265
+ "channels": channels_status,
266
+ "features": {
267
+ "file_delivery": True,
268
+ "web_ui": _ui_enabled,
269
+ }
270
+ }
271
+
272
+ @app.get("/health", response_model=Health)
273
+ async def health():
274
+ agent = get_agent()
275
+ lollms_client = get_lollms_client()
276
+ lollms_ok = lollms_client is not None
277
+
278
+ discord_status = "active" if "discord" in _active_channels else "disabled"
279
+ telegram_status = "active" if "telegram" in _active_channels else "disabled"
280
+
281
+ # Count pending files across channels
282
+ pending_files = 0
283
+ if _http_api:
284
+ pending_files += len(_http_api._pending_files) if hasattr(_http_api, '_pending_files') else 0
285
+
286
+ return {
287
+ "status": "ok",
288
+ "url": f"http://{HOST}:{PORT}",
289
+ "discord": discord_status,
290
+ "telegram": telegram_status,
291
+ "lollms": {
292
+ "connected": lollms_ok,
293
+ "host": LollmsSettings.from_env().host_address,
294
+ },
295
+ "agent": agent.state.name,
296
+ "tools": list(agent.tools.keys()),
297
+ "security": {
298
+ "mode": "local" if HOST in ("127.0.0.1", "localhost", "::1") else "network",
299
+ "auth_enabled": API_KEY is not None,
300
+ },
301
+ "features": {
302
+ "pending_files": pending_files,
303
+ "file_delivery_enabled": True,
304
+ }
305
+ }
306
+
307
+ @app.post("/chat", response_model=ChatResp, dependencies=[Depends(require_auth)])
308
+ async def chat(req: ChatReq):
309
+ """Process a chat message through the Agent with file delivery support."""
310
+ agent = get_agent()
311
+
312
+ result = await agent.chat(
313
+ user_id=req.user_id or "anonymous",
314
+ message=req.message,
315
+ context={"channel": "gateway_http", "source": "api"},
316
+ )
317
+
318
+ # Build file download info if files were generated
319
+ file_downloads = []
320
+ files_generated = result.get("files_to_send", [])
321
+
322
+ # If we have an HTTP API channel, it may have registered the files
323
+ if _http_api and hasattr(_http_api, '_pending_files'):
324
+ for file_info in files_generated:
325
+ file_path = file_info.get("path")
326
+ # Find matching registered file
327
+ for file_id, delivery in _http_api._pending_files.items():
328
+ if delivery.original_path == file_path:
329
+ file_downloads.append({
330
+ "filename": delivery.filename,
331
+ "download_url": f"/files/download/{file_id}",
332
+ "description": delivery.description,
333
+ "expires_in_seconds": int(_http_api._file_ttl_seconds - (time.time() - delivery.created_at)),
334
+ })
335
+ break
336
+
337
+ # Also check if files can be served directly
338
+ if not file_downloads and files_generated:
339
+ # Create direct download URLs for known output directory
340
+ for file_info in files_generated:
341
+ file_path = file_info.get("path", "")
342
+ filename = file_info.get("filename") or Path(file_path).name
343
+ # Add basic file info even without HTTP API channel
344
+ file_downloads.append({
345
+ "filename": filename,
346
+ "path": file_path,
347
+ "description": file_info.get("description", "Generated file"),
348
+ "note": "File saved to server filesystem, download via direct access if enabled",
349
+ })
350
+
351
+ return ChatResp(
352
+ success=result.get("success", False),
353
+ response=result.get("response", ""),
354
+ error=result.get("error"),
355
+ tools_used=result.get("tools_used", []),
356
+ files_generated=len(files_generated),
357
+ file_downloads=file_downloads,
358
+ )
359
+
360
+ @app.post("/admin/permission", dependencies=[Depends(require_auth)])
361
+ async def set_permission(req: PermissionReq):
362
+ """Admin endpoint to set user permissions."""
363
+ agent = get_agent()
364
+
365
+ # This would need to be implemented in the Agent class
366
+ # For now, return a placeholder
367
+ return {
368
+ "success": False,
369
+ "error": "Admin permission management not yet implemented in this version",
370
+ }
371
+
372
+ # ========== FILE DOWNLOAD ENDPOINTS ==========
373
+
374
+ @app.get("/files/download/{file_id}")
375
+ async def download_file(file_id: str):
376
+ """Download a generated file by ID (proxies to HTTP API channel if available)."""
377
+ if _http_api and hasattr(_http_api, '_pending_files'):
378
+ if file_id in _http_api._pending_files:
379
+ delivery = _http_api._pending_files[file_id]
380
+ from fastapi.responses import FileResponse
381
+ return FileResponse(
382
+ path=delivery.original_path,
383
+ filename=delivery.filename,
384
+ media_type=delivery.content_type or "application/octet-stream",
385
+ )
386
+
387
+ raise HTTPException(status_code=404, detail="File not found")
388
+
389
+ @app.get("/files/list")
390
+ async def list_files():
391
+ """List pending files for download."""
392
+ files = []
393
+ if _http_api and hasattr(_http_api, '_pending_files'):
394
+ for file_id, delivery in _http_api._pending_files.items():
395
+ files.append({
396
+ "file_id": file_id,
397
+ "filename": delivery.filename,
398
+ "description": delivery.description,
399
+ "expires_in_seconds": int(_http_api._file_ttl_seconds - (time.time() - delivery.created_at)),
400
+ })
401
+
402
+ return {"files": files, "count": len(files)}
403
+
404
+ # ========== UI ENABLE ==========
405
+
406
+ def enable_ui(host: str = "127.0.0.1", port: int = 8080) -> None:
407
+ """Enable the web UI."""
408
+ global _ui_enabled, _ui_instance
409
+
410
+ try:
411
+ from lollmsbot.ui.app import WebUI
412
+ agent = get_agent()
413
+ _ui_instance = WebUI(agent=agent, verbose=False)
414
+
415
+ # Mount UI at /ui
416
+ app.mount("/ui", _ui_instance.app, name="ui")
417
+ _ui_enabled = True
418
+
419
+ @app.get("/ui")
420
+ async def ui_redirect():
421
+ return RedirectResponse(url="/ui/")
422
+
423
+ console.print(f"[green]✅ Web UI mounted at /ui[/]")
424
+
425
+ except Exception as e:
426
+ console.print(f"[yellow]⚠️ Could not enable UI: {e}[/]")
427
+ import traceback
428
+ traceback.print_exc()
429
+
430
+ # ========== HTTP API ENABLE ==========
431
+
432
+ def enable_http_api(host: str = "0.0.0.0", port: int = 8800) -> None:
433
+ """Enable standalone HTTP API channel (for advanced file delivery)."""
434
+ global _http_api
435
+
436
+ # The main gateway already provides HTTP API, but this enables the full
437
+ # HttpApiChannel with advanced file delivery if needed separately
438
+ console.print(f"[dim]HTTP API available at main gateway endpoints[/]")
439
+
440
+ # ========== LIFESPAN ==========
441
+
442
+ @asynccontextmanager
443
+ async def lifespan(app_: FastAPI):
444
+ # Show security info
445
+ is_local = HOST in ("127.0.0.1", "localhost", "::1")
446
+
447
+ if is_local:
448
+ console.print(f"[bold green]🔒 SECURITY: Local-only mode[/]")
449
+ else:
450
+ console.print(f"[bold red]🌐 SECURITY: Public interface {HOST}[/]")
451
+ if API_KEY:
452
+ console.print(f"[bold green]🔐 API key authentication ENABLED[/]")
453
+
454
+ # Initialize shared Agent and LoLLMS client
455
+ agent = get_agent()
456
+ lollms_client = get_lollms_client()
457
+
458
+ # Ensure tools are registered
459
+ async def ensure_tools():
460
+ if len(agent.tools) == 0:
461
+ try:
462
+ await agent.register_tool(FilesystemTool())
463
+ console.print("[green] • FilesystemTool registered[/]")
464
+ except Exception as e:
465
+ if "already registered" not in str(e):
466
+ console.print(f"[yellow] • FilesystemTool: {e}[/]")
467
+
468
+ try:
469
+ await agent.register_tool(HttpTool())
470
+ console.print("[green] • HttpTool registered[/]")
471
+ except Exception as e:
472
+ if "already registered" not in str(e):
473
+ console.print(f"[yellow] • HttpTool: {e}[/]")
474
+
475
+ try:
476
+ await agent.register_tool(CalendarTool())
477
+ console.print("[green] • CalendarTool registered[/]")
478
+ except Exception as e:
479
+ if "already registered" not in str(e):
480
+ console.print(f"[yellow] • CalendarTool: {e}[/]")
481
+
482
+ await ensure_tools()
483
+
484
+ console.print(f"[green]🚀 Gateway starting on http://{HOST}:{PORT}[/]")
485
+ console.print(f"[dim] • Chat endpoint: POST /chat[/]")
486
+ console.print(f"[dim] • File downloads: GET /files/download/<file_id>[/]")
487
+
488
+ # Auto-enable UI
489
+ if os.getenv("LOLLMSBOT_ENABLE_UI", "").lower() in ("true", "1", "yes"):
490
+ enable_ui()
491
+
492
+ global _active_channels, _channel_tasks
493
+
494
+ # Discord with full agent capabilities
495
+ if DISCORD_TOKEN:
496
+ try:
497
+ from lollmsbot.channels.discord import DiscordChannel
498
+
499
+ def parse_id_list(val: Optional[str]) -> Optional[Set[int]]:
500
+ if not val:
501
+ return None
502
+ try:
503
+ return set(int(x.strip()) for x in val.split(","))
504
+ except ValueError:
505
+ return None
506
+
507
+ allowed_users = parse_id_list(DISCORD_ALLOWED_USERS)
508
+ allowed_guilds = parse_id_list(DISCORD_ALLOWED_GUILDS)
509
+ blocked_users = parse_id_list(DISCORD_BLOCKED_USERS)
510
+
511
+ require_mention_guild = DISCORD_REQUIRE_MENTION_GUILD.lower() in ("true", "1", "yes")
512
+ require_mention_dm = DISCORD_REQUIRE_MENTION_DM.lower() in ("true", "1", "yes")
513
+
514
+ channel = DiscordChannel(
515
+ agent=agent,
516
+ bot_token=DISCORD_TOKEN,
517
+ allowed_users=allowed_users,
518
+ allowed_guilds=allowed_guilds,
519
+ blocked_users=blocked_users,
520
+ require_mention_in_guild=require_mention_guild,
521
+ require_mention_in_dm=require_mention_dm,
522
+ )
523
+ _active_channels["discord"] = channel
524
+
525
+ task = asyncio.create_task(channel.start())
526
+ _channel_tasks.append(task)
527
+
528
+ async def wait_discord():
529
+ ready = await channel.wait_for_ready(timeout=15.0)
530
+ if ready:
531
+ console.print("[bold green]✅ Discord connected with FULL AGENT capabilities![/]")
532
+ console.print("[dim] File delivery enabled: Users receive generated files via DM[/]")
533
+ if allowed_users:
534
+ console.print(f"[dim] Allowed users: {len(allowed_users)}[/]")
535
+ if blocked_users:
536
+ console.print(f"[dim] Blocked users: {len(blocked_users)}[/]")
537
+ else:
538
+ console.print("[yellow]⚠️ Discord still connecting...[/]")
539
+
540
+ asyncio.create_task(wait_discord())
541
+
542
+ except Exception as e:
543
+ console.print(f"[red]❌ Discord failed: {e}[/]")
544
+ import traceback
545
+ traceback.print_exc()
546
+ else:
547
+ console.print("[dim]ℹ️ Discord disabled (no DISCORD_BOT_TOKEN)[/]")
548
+
549
+ # Telegram
550
+ if TELEGRAM_TOKEN:
551
+ try:
552
+ from lollmsbot.channels.telegram import TelegramChannel
553
+
554
+ channel = TelegramChannel(
555
+ agent=agent,
556
+ bot_token=TELEGRAM_TOKEN,
557
+ )
558
+ _active_channels["telegram"] = channel
559
+
560
+ task = asyncio.create_task(channel.start())
561
+ _channel_tasks.append(task)
562
+ console.print("[green]✅ Telegram started[/]")
563
+
564
+ except Exception as e:
565
+ console.print(f"[red]❌ Telegram failed: {e}[/]")
566
+ else:
567
+ console.print("[dim]ℹ️ Telegram disabled (no TELEGRAM_BOT_TOKEN)[/]")
568
+
569
+ # Summary
570
+ console.print(f"[bold green]📊 Active channels: {len(_active_channels)}[/]")
571
+ console.print(f"[bold green]🤖 Agent: {agent.name} ({len(agent.tools)} tools)[/]")
572
+ if lollms_client:
573
+ console.print(f"[bold green]🔗 LoLLMS: Connected ({LollmsSettings.from_env().host_address})[/]")
574
+ else:
575
+ console.print(f"[yellow]⚠️ LoLLMS: Not connected - tools will work but chat uses fallback mode[/]")
576
+
577
+ yield
578
+
579
+ # Cleanup
580
+ console.print("[yellow]🛑 Shutting down...[/]")
581
+
582
+ for name, channel in _active_channels.items():
583
+ try:
584
+ await channel.stop()
585
+ console.print(f"[dim] • {name} stopped[/]")
586
+ except Exception as e:
587
+ console.print(f"[red] • {name} error: {e}[/]")
588
+
589
+ _active_channels.clear()
590
+
591
+ for task in _channel_tasks:
592
+ if not task.done():
593
+ task.cancel()
594
+ try:
595
+ await task
596
+ except asyncio.CancelledError:
597
+ pass
598
+
599
+ _channel_tasks.clear()
600
+ console.print("[green]👋 Gateway shutdown complete[/]")
601
+
602
+ app.router.lifespan_context = lifespan
603
+
604
+ if __name__ == "__main__":
605
+ import uvicorn
606
+ uvicorn.run("lollmsbot.gateway:app", host=HOST, port=PORT)