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/__init__.py +1 -0
- lollmsbot/agent.py +1682 -0
- lollmsbot/channels/__init__.py +22 -0
- lollmsbot/channels/discord.py +408 -0
- lollmsbot/channels/http_api.py +449 -0
- lollmsbot/channels/telegram.py +272 -0
- lollmsbot/cli.py +217 -0
- lollmsbot/config.py +90 -0
- lollmsbot/gateway.py +606 -0
- lollmsbot/guardian.py +692 -0
- lollmsbot/heartbeat.py +826 -0
- lollmsbot/lollms_client.py +37 -0
- lollmsbot/skills.py +1483 -0
- lollmsbot/soul.py +482 -0
- lollmsbot/storage/__init__.py +245 -0
- lollmsbot/storage/sqlite_store.py +332 -0
- lollmsbot/tools/__init__.py +151 -0
- lollmsbot/tools/calendar.py +717 -0
- lollmsbot/tools/filesystem.py +663 -0
- lollmsbot/tools/http.py +498 -0
- lollmsbot/tools/shell.py +519 -0
- lollmsbot/ui/__init__.py +11 -0
- lollmsbot/ui/__main__.py +121 -0
- lollmsbot/ui/app.py +1122 -0
- lollmsbot/ui/routes.py +39 -0
- lollmsbot/wizard.py +1493 -0
- lollmsbot-0.0.1.dist-info/METADATA +25 -0
- lollmsbot-0.0.1.dist-info/RECORD +32 -0
- lollmsbot-0.0.1.dist-info/WHEEL +5 -0
- lollmsbot-0.0.1.dist-info/entry_points.txt +2 -0
- lollmsbot-0.0.1.dist-info/licenses/LICENSE +201 -0
- lollmsbot-0.0.1.dist-info/top_level.txt +1 -0
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)
|