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.
@@ -0,0 +1,272 @@
1
+ """
2
+ Telegram channel implementation for LollmsBot.
3
+
4
+ Uses shared Agent for all business logic.
5
+ """
6
+
7
+ import logging
8
+ from typing import Any, Awaitable, Callable, List, Optional, Set
9
+
10
+ try:
11
+ from telegram import Update
12
+ from telegram.ext import (
13
+ Application,
14
+ ApplicationBuilder,
15
+ CommandHandler,
16
+ ContextTypes,
17
+ MessageHandler,
18
+ filters,
19
+ )
20
+ TELEGRAM_AVAILABLE = True
21
+ except ImportError as e:
22
+ TELEGRAM_AVAILABLE = False
23
+ TELEGRAM_IMPORT_ERROR = str(e)
24
+ # Create dummy classes for type checking
25
+ class Update: pass
26
+ class Application: pass
27
+ class ApplicationBuilder: pass
28
+ class CommandHandler: pass
29
+ class ContextTypes:
30
+ DEFAULT_TYPE = Any
31
+ class MessageHandler: pass
32
+ class filters:
33
+ TEXT = None
34
+ COMMAND = None
35
+
36
+ from lollmsbot.agent import Agent, PermissionLevel
37
+
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+
42
+ class TelegramChannel:
43
+ """Telegram messaging channel using shared Agent.
44
+
45
+ All business logic is delegated to the Agent. This class handles
46
+ only Telegram-specific protocol concerns.
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ agent: Agent,
52
+ bot_token: str,
53
+ allowed_users: Optional[List[int]] = None,
54
+ blocked_users: Optional[List[int]] = None,
55
+ ):
56
+ if not TELEGRAM_AVAILABLE:
57
+ raise ImportError(
58
+ f"Telegram support requires 'python-telegram-bot'. "
59
+ f"Install with: pip install 'python-telegram-bot>=20.0' "
60
+ f"Original error: {TELEGRAM_IMPORT_ERROR}"
61
+ )
62
+
63
+ self.agent = agent
64
+ self.bot_token = bot_token
65
+ self.allowed_users: Optional[Set[int]] = set(allowed_users) if allowed_users else None
66
+ self.blocked_users: Set[int] = set(blocked_users) if blocked_users else set()
67
+ self.application: Optional[Application] = None
68
+ self._is_running = False
69
+
70
+ def _can_interact(self, user_id: int) -> tuple[bool, str]:
71
+ """Check if user can interact with bot."""
72
+ if user_id in self.blocked_users:
73
+ return False, "user blocked"
74
+
75
+ if self.allowed_users is not None:
76
+ if user_id not in self.allowed_users:
77
+ return False, "not in allowed users"
78
+
79
+ return True, ""
80
+
81
+ def _get_user_id(self, tg_user_id: int) -> str:
82
+ """Generate consistent user ID for Agent."""
83
+ return f"telegram:{tg_user_id}"
84
+
85
+ async def start(self) -> None:
86
+ """Start the Telegram bot."""
87
+ if not TELEGRAM_AVAILABLE:
88
+ raise ImportError("python-telegram-bot is not installed")
89
+
90
+ if self._is_running:
91
+ logger.warning("Telegram channel is already running")
92
+ return
93
+
94
+ try:
95
+ self.application = (
96
+ ApplicationBuilder()
97
+ .token(self.bot_token)
98
+ .build()
99
+ )
100
+
101
+ # Add handlers
102
+ self.application.add_handler(
103
+ CommandHandler("start", self._handle_start_command)
104
+ )
105
+ self.application.add_handler(
106
+ CommandHandler("help", self._handle_help_command)
107
+ )
108
+ self.application.add_handler(
109
+ MessageHandler(filters.TEXT & ~filters.COMMAND, self._handle_message)
110
+ )
111
+ self.application.add_error_handler(self._handle_error)
112
+
113
+ # Start
114
+ await self.application.initialize()
115
+ await self.application.start()
116
+ await self.application.updater.start_polling(drop_pending_updates=True)
117
+
118
+ self._is_running = True
119
+ logger.info("Telegram channel started successfully")
120
+
121
+ except Exception as exc:
122
+ logger.error(f"Failed to start Telegram channel: {exc}")
123
+ self._is_running = False
124
+ raise
125
+
126
+ async def stop(self) -> None:
127
+ """Stop the Telegram bot."""
128
+ if not self._is_running:
129
+ return
130
+
131
+ try:
132
+ if self.application:
133
+ await self.application.updater.stop()
134
+ await self.application.stop()
135
+ await self.application.shutdown()
136
+ self.application = None
137
+
138
+ self._is_running = False
139
+ logger.info("Telegram channel stopped successfully")
140
+
141
+ except Exception as exc:
142
+ logger.error(f"Error stopping Telegram channel: {exc}")
143
+ raise
144
+
145
+ async def _handle_start_command(
146
+ self,
147
+ update: Update,
148
+ context: ContextTypes.DEFAULT_TYPE,
149
+ ) -> None:
150
+ """Handle /start command."""
151
+ if not update.effective_chat or not update.message:
152
+ return
153
+
154
+ user_id = update.effective_user.id if update.effective_user else 0
155
+
156
+ # Check permissions
157
+ can_interact, reason = self._can_interact(user_id)
158
+ if not can_interact:
159
+ await update.message.reply_text("⛔ You don't have permission to use this bot.")
160
+ return
161
+
162
+ # Get or create user permissions in agent
163
+ agent_user_id = self._get_user_id(user_id)
164
+
165
+ welcome_text = (
166
+ f"👋 Hello! I'm {self.agent.name}.\n\n"
167
+ f"Send me any message and I'll respond using my AI backend.\n\n"
168
+ f"Your ID: `{user_id}`\n"
169
+ f"Use /help for more information."
170
+ )
171
+
172
+ await update.message.reply_text(welcome_text, parse_mode="Markdown")
173
+
174
+ async def _handle_help_command(
175
+ self,
176
+ update: Update,
177
+ context: ContextTypes.DEFAULT_TYPE,
178
+ ) -> None:
179
+ """Handle /help command."""
180
+ help_text = (
181
+ f"*{self.agent.name} - Available Commands*\n\n"
182
+ "/start - Start the bot\n"
183
+ "/help - Show this help message\n\n"
184
+ "Just send any message to chat with me!"
185
+ )
186
+ await update.message.reply_text(help_text, parse_mode="Markdown")
187
+
188
+ async def _handle_message(
189
+ self,
190
+ update: Update,
191
+ context: ContextTypes.DEFAULT_TYPE,
192
+ ) -> None:
193
+ """Handle incoming text messages."""
194
+ if not update.effective_chat or not update.message or not update.message.text:
195
+ return
196
+
197
+ user_id = update.effective_user.id if update.effective_user else 0
198
+ chat_id = update.effective_chat.id
199
+
200
+ # Check permissions
201
+ can_interact, reason = self._can_interact(user_id)
202
+ if not can_interact:
203
+ logger.warning(f"Message from unauthorized user {user_id}: {reason}")
204
+ await update.message.reply_text("⛔ Access denied.")
205
+ return
206
+
207
+ # Build context
208
+ agent_user_id = self._get_user_id(user_id)
209
+ context_data = {
210
+ "channel": "telegram",
211
+ "telegram_user_id": user_id,
212
+ "telegram_username": update.effective_user.username if update.effective_user else None,
213
+ "telegram_chat_id": chat_id,
214
+ "is_dm": update.effective_chat.type == "private",
215
+ "chat_type": update.effective_chat.type,
216
+ }
217
+
218
+ message_text = update.message.text
219
+
220
+ logger.info(f"Processing message from Telegram user {user_id}: {message_text[:50]}...")
221
+
222
+ try:
223
+ # Use Agent for processing
224
+ result = await self.agent.chat(
225
+ user_id=agent_user_id,
226
+ message=message_text,
227
+ context=context_data,
228
+ )
229
+
230
+ if result.get("permission_denied"):
231
+ await update.message.reply_text("⛔ You don't have permission to use this bot.")
232
+ return
233
+
234
+ if not result.get("success"):
235
+ error_msg = result.get("error", "Unknown error")
236
+ await update.message.reply_text(f"❌ Error: {error_msg[:400]}")
237
+ return
238
+
239
+ response = result.get("response", "No response")
240
+ # Telegram has 4096 char limit
241
+ if len(response) > 4000:
242
+ response = response[:4000] + "\n... (truncated)"
243
+
244
+ await update.message.reply_text(response)
245
+
246
+ except Exception as exc:
247
+ logger.error(f"Error processing Telegram message: {exc}")
248
+ try:
249
+ await update.message.reply_text("❌ An error occurred. Please try again.")
250
+ except Exception:
251
+ pass
252
+
253
+ async def _handle_error(
254
+ self,
255
+ update: Optional[Update],
256
+ context: ContextTypes.DEFAULT_TYPE,
257
+ ) -> None:
258
+ """Handle errors."""
259
+ logger.error(f"Telegram bot error: {context.error}")
260
+
261
+ if update and update.effective_message:
262
+ try:
263
+ await update.effective_message.reply_text(
264
+ "❌ An unexpected error occurred."
265
+ )
266
+ except Exception:
267
+ pass
268
+
269
+ def __repr__(self) -> str:
270
+ status = "running" if self._is_running else "stopped"
271
+ restricted = f", restricted={len(self.allowed_users)} users" if self.allowed_users else ""
272
+ return f"TelegramChannel({status}{restricted})"
lollmsbot/cli.py ADDED
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ lollmsBot CLI - Gateway + Wizard + UI
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import argparse
8
+ import sys
9
+ from typing import List
10
+
11
+ try:
12
+ from rich.console import Console
13
+ from rich.panel import Panel
14
+ from rich.table import Table
15
+ from rich.text import Text
16
+ from rich import box
17
+ console = Console()
18
+ except ImportError:
19
+ print("Install dev deps: pip install -e .[dev]")
20
+ sys.exit(1)
21
+
22
+
23
+ def print_ui_banner() -> None:
24
+ """Print beautiful UI launch banner."""
25
+ console.print()
26
+
27
+ # Create ASCII art style banner
28
+ banner = Text()
29
+ banner.append("╭─────────╮\n", style="blue")
30
+ banner.append("│ 🤖 │ ", style="blue")
31
+ banner.append("LollmsBot", style="bold cyan")
32
+ banner.append(" Web UI\n", style="bold blue")
33
+ banner.append("╰─────────╯\n", style="blue")
34
+
35
+ panel = Panel(
36
+ banner,
37
+ box=box.DOUBLE_EDGE,
38
+ border_style="bright_cyan",
39
+ title="[bold]Starting Interface[/bold]",
40
+ subtitle="[dim]Real-time AI Chat[/dim]"
41
+ )
42
+ console.print(panel)
43
+
44
+
45
+ def print_gateway_banner(host: str, port: int, ui_enabled: bool) -> None:
46
+ """Print gateway startup banner with status."""
47
+
48
+ # For display purposes, use localhost if host is 0.0.0.0 or empty
49
+ # Browsers can't connect to 0.0.0.0, they need localhost/127.0.0.1
50
+ display_host = "localhost" if host in ("0.0.0.0", "") else host
51
+
52
+ # Status indicators
53
+ status_table = Table(
54
+ show_header=False,
55
+ box=box.SIMPLE,
56
+ border_style="blue",
57
+ padding=(0, 2)
58
+ )
59
+ status_table.add_column("Service", style="cyan")
60
+ status_table.add_column("Status", style="green")
61
+ status_table.add_column("URL", style="dim")
62
+
63
+ status_table.add_row(
64
+ "🔌 Gateway API",
65
+ "✅ Active",
66
+ f"http://{display_host}:{port}"
67
+ )
68
+ status_table.add_row(
69
+ "📚 API Docs",
70
+ "✅ Available",
71
+ f"http://{display_host}:{port}/docs"
72
+ )
73
+
74
+ if ui_enabled:
75
+ status_table.add_row(
76
+ "🌐 Web UI",
77
+ "✅ Mounted",
78
+ f"http://{display_host}:{port}/ui"
79
+ )
80
+ else:
81
+ status_table.add_row(
82
+ "🌐 Web UI",
83
+ "⭕ Disabled",
84
+ "Use --ui to enable"
85
+ )
86
+
87
+ panel = Panel(
88
+ status_table,
89
+ box=box.ROUNDED,
90
+ border_style="bright_green" if ui_enabled else "yellow",
91
+ title="[bold bright_green]🚀 Gateway Starting[/bold bright_green]",
92
+ subtitle=f"[dim]LoLLMS Agentic Bot | Host: {host}[/dim]"
93
+ )
94
+ console.print()
95
+ console.print(panel)
96
+ console.print()
97
+
98
+
99
+ def main(argv: List[str] | None = None) -> None:
100
+ parser = argparse.ArgumentParser(
101
+ prog="lollmsbot",
102
+ description="Agentic LoLLMS Assistant (Clawdbot-style)",
103
+ formatter_class=argparse.RawDescriptionHelpFormatter,
104
+ epilog="""
105
+ ┌─────────────────────────────────────────────────────────────┐
106
+ │ Examples: │
107
+ │ lollmsbot wizard # Interactive setup │
108
+ │ lollmsbot gateway # Run API server │
109
+ │ lollmsbot gateway --ui # API + Web UI together │
110
+ │ lollmsbot ui # Web UI only (standalone) │
111
+ │ lollmsbot ui --port 3000 # UI on custom port │
112
+ └─────────────────────────────────────────────────────────────┘
113
+ """
114
+ )
115
+ parser.add_argument("--version", action="version", version="lollmsBot 0.1.0")
116
+
117
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
118
+
119
+ # Gateway command
120
+ gateway_parser = subparsers.add_parser(
121
+ "gateway",
122
+ help="Run API gateway server",
123
+ description="Start the main API gateway with optional channels and UI"
124
+ )
125
+ gateway_parser.add_argument("--host", type=str, default="0.0.0.0", help="Bind address (default: 0.0.0.0)")
126
+ gateway_parser.add_argument("--port", type=int, default=8800, help="Port number (default: 8800)")
127
+ gateway_parser.add_argument("--ui", action="store_true", help="Also start web UI at /ui")
128
+
129
+ # UI command (standalone)
130
+ ui_parser = subparsers.add_parser(
131
+ "ui",
132
+ help="Run web UI only (standalone mode)",
133
+ description="Start just the web interface without the full gateway"
134
+ )
135
+ ui_parser.add_argument("--host", type=str, default="127.0.0.1", help="Bind address (default: 127.0.0.1)")
136
+ ui_parser.add_argument("--port", type=int, default=8080, help="Port number (default: 8080)")
137
+ ui_parser.add_argument("--quiet", "-q", action="store_true", help="Minimal console output")
138
+
139
+ # Wizard command
140
+ wizard_parser = subparsers.add_parser(
141
+ "wizard",
142
+ help="Interactive setup wizard",
143
+ description="Configure LoLLMS connection and bot settings interactively"
144
+ )
145
+
146
+ args = parser.parse_args(argv)
147
+
148
+ try:
149
+ if args.command == "gateway":
150
+ import uvicorn
151
+ from lollmsbot.config import GatewaySettings
152
+ from lollmsbot import gateway
153
+
154
+ settings = GatewaySettings.from_env()
155
+ host = args.host or settings.host
156
+ port = args.port or settings.port
157
+
158
+ # Print startup banner
159
+ print_gateway_banner(host, port, args.ui)
160
+
161
+ # Enable UI if requested
162
+ if args.ui:
163
+ # Use localhost for UI server internally, gateway will mount it
164
+ gateway.enable_ui(host="127.0.0.1", port=8080)
165
+
166
+ # Run server
167
+ uvicorn.run(
168
+ "lollmsbot.gateway:app",
169
+ host=host,
170
+ port=port,
171
+ reload=args.host == "127.0.0.1" and not args.ui,
172
+ log_level="info",
173
+ )
174
+
175
+ elif args.command == "ui":
176
+ # Run standalone UI with full rich output
177
+ from lollmsbot.ui.app import WebUI
178
+ import uvicorn
179
+
180
+ print_ui_banner()
181
+
182
+ ui = WebUI(verbose=not args.quiet)
183
+ ui.print_server_ready(args.host, args.port)
184
+
185
+ try:
186
+ uvicorn.run(
187
+ ui.app,
188
+ host=args.host,
189
+ port=args.port,
190
+ log_level="warning" if args.quiet else "info",
191
+ )
192
+ except KeyboardInterrupt:
193
+ ui._print_shutdown_message()
194
+
195
+ elif args.command == "wizard":
196
+ from lollmsbot import wizard
197
+ wizard.run_wizard()
198
+
199
+ else:
200
+ parser.print_help()
201
+ console.print("\n[bold cyan]💡 Need help? Try: lollmsbot wizard[/]")
202
+
203
+ except KeyboardInterrupt:
204
+ console.print("\n[yellow]👋 Goodbye![/]")
205
+ sys.exit(130)
206
+ except ImportError as e:
207
+ console.print(f"[red]❌ Missing dependency: {e}[/]")
208
+ console.print("[cyan]💡 Run: pip install -e .[dev][/]")
209
+ sys.exit(1)
210
+ except Exception as e:
211
+ console.print(f"[red]💥 Error: {e}[/]")
212
+ console.print_exception(show_locals=True)
213
+ sys.exit(1)
214
+
215
+
216
+ if __name__ == "__main__":
217
+ main()
lollmsbot/config.py ADDED
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env python
2
+ from __future__ import annotations
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Dict, Any, Optional
6
+ from dataclasses import dataclass, field
7
+ from dotenv import load_dotenv
8
+ import json
9
+
10
+ load_dotenv()
11
+
12
+ console = None # Forward ref
13
+
14
+ def _get_bool(name: str, default: bool = False) -> bool:
15
+ val = os.getenv(name)
16
+ if val is None:
17
+ return default
18
+ return val.lower() in ("1", "true", "yes", "on")
19
+
20
+ @dataclass
21
+ class BotConfig:
22
+ """Bot behavior configuration settings."""
23
+ name: str = field(default="LollmsBot")
24
+ max_history: int = field(default=10)
25
+
26
+ @classmethod
27
+ def from_env(cls) -> "BotConfig":
28
+ """Load from environment variables."""
29
+ return cls(
30
+ name=os.getenv("LOLLMSBOT_NAME", "LollmsBot"),
31
+ max_history=int(os.getenv("LOLLMSBOT_MAX_HISTORY", "10")),
32
+ )
33
+
34
+ @dataclass
35
+ class LollmsSettings:
36
+ """LoLLMS connection settings."""
37
+ host_address: str = field(default="http://localhost:9600")
38
+ api_key: Optional[str] = field(default=None)
39
+ verify_ssl: bool = field(default=True)
40
+ binding_name: Optional[str] = field(default=None)
41
+ model_name: Optional[str] = field(default=None)
42
+ context_size: Optional[int] = field(default=None)
43
+
44
+ @classmethod
45
+ def from_env(cls) -> "LollmsSettings":
46
+ """Load from environment variables."""
47
+ global console
48
+ return cls(
49
+ host_address=os.getenv("LOLLMS_HOST_ADDRESS", "http://localhost:9600"),
50
+ api_key=os.getenv("LOLLMS_API_KEY"),
51
+ verify_ssl=_get_bool("LOLLMS_VERIFY_SSL", True),
52
+ binding_name=os.getenv("LOLLMS_BINDING_NAME"),
53
+ model_name=os.getenv("LOLLMS_MODEL_NAME"),
54
+ context_size=int(os.getenv("LOLLMS_CONTEXT_SIZE", "32000")) or None,
55
+ )
56
+
57
+ @classmethod
58
+ def from_wizard(cls) -> "LollmsSettings":
59
+ """Load from wizard config."""
60
+ wizard_path = Path.home() / ".lollmsbot" / "config.json"
61
+ if not wizard_path.exists():
62
+ return cls.from_env()
63
+
64
+ try:
65
+ wizard_data = json.loads(wizard_path.read_text())
66
+ lollms_data = wizard_data.get("lollms", {})
67
+ if lollms_data.get("host_address"):
68
+ console.print("[green]📡 Using wizard config![/]" if console else "Using wizard config")
69
+ return cls(
70
+ host_address=lollms_data.get("host_address", "http://localhost:9600"),
71
+ api_key=lollms_data.get("api_key"),
72
+ verify_ssl=_get_bool(str(lollms_data.get("verify_ssl", True))),
73
+ binding_name=lollms_data.get("binding_name"),
74
+ )
75
+ except:
76
+ pass
77
+ return cls.from_env()
78
+
79
+ @dataclass
80
+ class GatewaySettings:
81
+ """Gateway server settings."""
82
+ host: str = field(default="localhost")
83
+ port: int = field(default=8800)
84
+
85
+ @classmethod
86
+ def from_env(cls) -> "GatewaySettings":
87
+ return cls(
88
+ host=os.getenv("LOLLMSBOT_HOST", "localhost"),
89
+ port=int(os.getenv("LOLLMSBOT_PORT", "8800")),
90
+ )