youclaw 4.6.0__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.
- youclaw/__init__.py +24 -0
- youclaw/bot.py +185 -0
- youclaw/cli.py +469 -0
- youclaw/commands.py +151 -0
- youclaw/config.py +170 -0
- youclaw/core_skills.py +210 -0
- youclaw/dashboard.py +1347 -0
- youclaw/discord_handler.py +187 -0
- youclaw/env_manager.py +61 -0
- youclaw/main.py +273 -0
- youclaw/memory_manager.py +440 -0
- youclaw/ollama_client.py +486 -0
- youclaw/personality_manager.py +42 -0
- youclaw/scheduler_manager.py +226 -0
- youclaw/search_client.py +66 -0
- youclaw/skills_manager.py +127 -0
- youclaw/telegram_handler.py +181 -0
- youclaw/vector_manager.py +94 -0
- youclaw-4.6.0.dist-info/LICENSE +21 -0
- youclaw-4.6.0.dist-info/METADATA +128 -0
- youclaw-4.6.0.dist-info/RECORD +24 -0
- youclaw-4.6.0.dist-info/WHEEL +5 -0
- youclaw-4.6.0.dist-info/entry_points.txt +2 -0
- youclaw-4.6.0.dist-info/top_level.txt +1 -0
youclaw/commands.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""
|
|
2
|
+
YouClaw Command Handler
|
|
3
|
+
Handles bot commands across all platforms.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Optional, Dict, Any
|
|
8
|
+
from .ollama_client import ollama_client
|
|
9
|
+
from .memory_manager import memory_manager
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CommandHandler:
|
|
15
|
+
"""Handles bot commands"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, prefix: str = "!"):
|
|
18
|
+
self.prefix = prefix
|
|
19
|
+
self.commands = {
|
|
20
|
+
"help": self.cmd_help,
|
|
21
|
+
"reset": self.cmd_reset,
|
|
22
|
+
"model": self.cmd_model,
|
|
23
|
+
"stats": self.cmd_stats,
|
|
24
|
+
"models": self.cmd_models,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
def is_command(self, message: str) -> bool:
|
|
28
|
+
"""Check if message is a command"""
|
|
29
|
+
return message.strip().startswith(self.prefix) or message.strip().startswith("/")
|
|
30
|
+
|
|
31
|
+
def parse_command(self, message: str) -> tuple[str, list[str]]:
|
|
32
|
+
"""Parse command and arguments"""
|
|
33
|
+
# Remove prefix
|
|
34
|
+
if message.startswith(self.prefix):
|
|
35
|
+
message = message[len(self.prefix):]
|
|
36
|
+
elif message.startswith("/"):
|
|
37
|
+
message = message[1:]
|
|
38
|
+
|
|
39
|
+
parts = message.strip().split()
|
|
40
|
+
command = parts[0].lower() if parts else ""
|
|
41
|
+
args = parts[1:] if len(parts) > 1 else []
|
|
42
|
+
|
|
43
|
+
return command, args
|
|
44
|
+
|
|
45
|
+
async def handle_command(
|
|
46
|
+
self,
|
|
47
|
+
platform: str,
|
|
48
|
+
user_id: str,
|
|
49
|
+
message: str,
|
|
50
|
+
**kwargs
|
|
51
|
+
) -> Optional[str]:
|
|
52
|
+
"""
|
|
53
|
+
Handle a command and return response.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
platform: Platform name (discord, telegram)
|
|
57
|
+
user_id: User identifier
|
|
58
|
+
message: Command message
|
|
59
|
+
**kwargs: Additional platform-specific data
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Response string or None if not a command
|
|
63
|
+
"""
|
|
64
|
+
if not self.is_command(message):
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
command, args = self.parse_command(message)
|
|
68
|
+
|
|
69
|
+
if command in self.commands:
|
|
70
|
+
try:
|
|
71
|
+
return await self.commands[command](platform, user_id, args, **kwargs)
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.error(f"Error executing command {command}: {e}")
|
|
74
|
+
return f"❌ Error executing command: {str(e)}"
|
|
75
|
+
else:
|
|
76
|
+
return f"❓ Unknown command: `{command}`. Type `{self.prefix}help` for available commands."
|
|
77
|
+
|
|
78
|
+
async def cmd_help(self, platform: str, user_id: str, args: list, **kwargs) -> str:
|
|
79
|
+
"""Show help message"""
|
|
80
|
+
return """🦞 **YouClaw - Your Personal AI Assistant**
|
|
81
|
+
|
|
82
|
+
**Available Commands:**
|
|
83
|
+
• `!help` or `/help` - Show this help message
|
|
84
|
+
• `!reset` or `/reset` - Clear conversation history
|
|
85
|
+
• `!model [name]` or `/model [name]` - Show or switch AI model
|
|
86
|
+
• `!models` or `/models` - List available models
|
|
87
|
+
• `!stats` or `/stats` - Show bot statistics
|
|
88
|
+
|
|
89
|
+
**How to use:**
|
|
90
|
+
Just talk to me naturally! I'll remember our conversation and provide intelligent responses using my local AI brain (Ollama).
|
|
91
|
+
|
|
92
|
+
I work across Discord and Telegram, and I'll remember you on both platforms!"""
|
|
93
|
+
|
|
94
|
+
async def cmd_reset(self, platform: str, user_id: str, args: list, **kwargs) -> str:
|
|
95
|
+
"""Reset conversation history"""
|
|
96
|
+
channel_id = kwargs.get("channel_id")
|
|
97
|
+
await memory_manager.clear_conversation(platform, user_id, channel_id)
|
|
98
|
+
return "🔄 Conversation history cleared! Starting fresh."
|
|
99
|
+
|
|
100
|
+
async def cmd_model(self, platform: str, user_id: str, args: list, **kwargs) -> str:
|
|
101
|
+
"""Show or switch model"""
|
|
102
|
+
if not args:
|
|
103
|
+
# Show current model
|
|
104
|
+
return f"🤖 Current model: `{ollama_client.model}`\n\nUse `!model <name>` to switch models."
|
|
105
|
+
|
|
106
|
+
# Switch model
|
|
107
|
+
model_name = args[0]
|
|
108
|
+
success = await ollama_client.switch_model(model_name)
|
|
109
|
+
|
|
110
|
+
if success:
|
|
111
|
+
return f"✅ Switched to model: `{model_name}`"
|
|
112
|
+
else:
|
|
113
|
+
models = await ollama_client.get_available_models()
|
|
114
|
+
models_list = "\n• ".join(models) if models else "None"
|
|
115
|
+
return f"❌ Model `{model_name}` not found.\n\n**Available models:**\n• {models_list}"
|
|
116
|
+
|
|
117
|
+
async def cmd_models(self, platform: str, user_id: str, args: list, **kwargs) -> str:
|
|
118
|
+
"""List available models"""
|
|
119
|
+
models = await ollama_client.get_available_models()
|
|
120
|
+
|
|
121
|
+
if not models:
|
|
122
|
+
return "❌ No models found. Make sure Ollama is running and has models installed."
|
|
123
|
+
|
|
124
|
+
current = ollama_client.model
|
|
125
|
+
models_list = "\n".join([
|
|
126
|
+
f"• `{m}` {'← current' if m == current else ''}"
|
|
127
|
+
for m in models
|
|
128
|
+
])
|
|
129
|
+
|
|
130
|
+
return f"🤖 **Available Models:**\n{models_list}\n\nUse `!model <name>` to switch."
|
|
131
|
+
|
|
132
|
+
async def cmd_stats(self, platform: str, user_id: str, args: list, **kwargs) -> str:
|
|
133
|
+
"""Show bot statistics"""
|
|
134
|
+
stats = await memory_manager.get_stats()
|
|
135
|
+
health = await ollama_client.check_health()
|
|
136
|
+
|
|
137
|
+
return f"""📊 **YouClaw Statistics**
|
|
138
|
+
|
|
139
|
+
**Memory:**
|
|
140
|
+
• Total messages: {stats['total_messages']}
|
|
141
|
+
• Unique users: {stats['unique_users']}
|
|
142
|
+
• Database: `{stats['database_path']}`
|
|
143
|
+
|
|
144
|
+
**AI Engine:**
|
|
145
|
+
• Model: `{ollama_client.model}`
|
|
146
|
+
• Status: {'🟢 Online' if health else '🔴 Offline'}
|
|
147
|
+
• Host: `{ollama_client.host}`"""
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# Global command handler instance
|
|
151
|
+
command_handler = CommandHandler()
|
youclaw/config.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""
|
|
2
|
+
YouClaw Configuration Management
|
|
3
|
+
Centralized configuration with environment variable loading and validation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from dotenv import load_dotenv
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
# Load environment variables from .env file
|
|
15
|
+
load_dotenv()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class OllamaConfig:
|
|
20
|
+
"""Ollama service configuration"""
|
|
21
|
+
host: str = os.getenv("OLLAMA_HOST", "http://localhost:11434")
|
|
22
|
+
model: str = os.getenv("OLLAMA_MODEL", "qwen2.5:1.5b-instruct")
|
|
23
|
+
temperature: float = float(os.getenv("OLLAMA_TEMPERATURE", "0.7"))
|
|
24
|
+
max_tokens: int = int(os.getenv("OLLAMA_MAX_TOKENS", "2048"))
|
|
25
|
+
timeout: int = int(os.getenv("OLLAMA_TIMEOUT", "60"))
|
|
26
|
+
|
|
27
|
+
def __post_init__(self):
|
|
28
|
+
"""Validate configuration"""
|
|
29
|
+
if not self.host:
|
|
30
|
+
raise ValueError("OLLAMA_HOST must be set")
|
|
31
|
+
if not self.model:
|
|
32
|
+
raise ValueError("OLLAMA_MODEL must be set")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class DiscordConfig:
|
|
37
|
+
"""Discord bot configuration"""
|
|
38
|
+
token: Optional[str] = os.getenv("DISCORD_BOT_TOKEN")
|
|
39
|
+
enabled: bool = os.getenv("ENABLE_DISCORD", "true").lower() == "true"
|
|
40
|
+
|
|
41
|
+
def __post_init__(self):
|
|
42
|
+
"""Validate configuration"""
|
|
43
|
+
if self.enabled and not self.token:
|
|
44
|
+
raise ValueError("DISCORD_BOT_TOKEN must be set when Discord is enabled")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class TelegramConfig:
|
|
49
|
+
"""Telegram bot configuration"""
|
|
50
|
+
token: Optional[str] = os.getenv("TELEGRAM_BOT_TOKEN")
|
|
51
|
+
enabled: bool = os.getenv("ENABLE_TELEGRAM", "true").lower() == "true"
|
|
52
|
+
|
|
53
|
+
def __post_init__(self):
|
|
54
|
+
"""Validate configuration"""
|
|
55
|
+
if self.enabled and not self.token:
|
|
56
|
+
raise ValueError("TELEGRAM_BOT_TOKEN must be set when Telegram is enabled")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class BotConfig:
|
|
61
|
+
"""General bot configuration"""
|
|
62
|
+
prefix: str = os.getenv("BOT_PREFIX", "!")
|
|
63
|
+
max_context_messages: int = int(os.getenv("MAX_CONTEXT_MESSAGES", "20"))
|
|
64
|
+
database_path: str = os.getenv("DATABASE_PATH", "./data/bot.db")
|
|
65
|
+
log_level: str = os.getenv("LOG_LEVEL", "INFO")
|
|
66
|
+
search_url: str = os.getenv("SEARCH_ENGINE_URL", "http://57.128.250.34:8080/search")
|
|
67
|
+
admin_user_identity: str = os.getenv("ADMIN_USER_IDENTITY", "telegram:default") # format platform:id
|
|
68
|
+
|
|
69
|
+
def __post_init__(self):
|
|
70
|
+
"""Validate configuration"""
|
|
71
|
+
if self.max_context_messages < 1:
|
|
72
|
+
raise ValueError("MAX_CONTEXT_MESSAGES must be at least 1")
|
|
73
|
+
|
|
74
|
+
# Create data directory if it doesn't exist
|
|
75
|
+
os.makedirs(os.path.dirname(self.database_path), exist_ok=True)
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class EmailConfig:
|
|
79
|
+
"""Email service configuration"""
|
|
80
|
+
imap_host: str = os.getenv("EMAIL_IMAP_HOST", "")
|
|
81
|
+
imap_port: int = int(os.getenv("EMAIL_IMAP_PORT", "993"))
|
|
82
|
+
smtp_host: str = os.getenv("EMAIL_SMTP_HOST", "")
|
|
83
|
+
smtp_port: int = int(os.getenv("EMAIL_SMTP_PORT", "587"))
|
|
84
|
+
user: str = os.getenv("EMAIL_USER", "")
|
|
85
|
+
password: str = os.getenv("EMAIL_PASSWORD", "")
|
|
86
|
+
enabled: bool = os.getenv("ENABLE_EMAIL", "false").lower() == "true"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class Config:
|
|
90
|
+
"""Main configuration class"""
|
|
91
|
+
|
|
92
|
+
def __init__(self):
|
|
93
|
+
self.bot = BotConfig()
|
|
94
|
+
self.ollama = OllamaConfig()
|
|
95
|
+
self.discord = DiscordConfig()
|
|
96
|
+
self.telegram = TelegramConfig()
|
|
97
|
+
self.email = EmailConfig()
|
|
98
|
+
self.search_url = os.getenv("SEARCH_ENGINE_URL", "http://57.128.250.34:8080/search")
|
|
99
|
+
|
|
100
|
+
# Refresh from database if possible
|
|
101
|
+
try:
|
|
102
|
+
# This is a bit tricky during first init, but refresh_from_db handles it
|
|
103
|
+
pass
|
|
104
|
+
except:
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
async def refresh_from_db(self):
|
|
108
|
+
"""Refresh dynamic settings from database"""
|
|
109
|
+
try:
|
|
110
|
+
from .memory_manager import memory_manager
|
|
111
|
+
|
|
112
|
+
# Override tokens if present in DB
|
|
113
|
+
dt = await memory_manager.get_global_setting("discord_token")
|
|
114
|
+
if dt and dt.strip(): self.discord.token = dt
|
|
115
|
+
|
|
116
|
+
tt = await memory_manager.get_global_setting("telegram_token")
|
|
117
|
+
if tt and tt.strip(): self.telegram.token = tt
|
|
118
|
+
|
|
119
|
+
# Override status
|
|
120
|
+
de_val = await memory_manager.get_global_setting("discord_enabled")
|
|
121
|
+
if de_val:
|
|
122
|
+
self.discord.enabled = de_val.lower() == "true"
|
|
123
|
+
|
|
124
|
+
te_val = await memory_manager.get_global_setting("telegram_enabled")
|
|
125
|
+
if te_val:
|
|
126
|
+
self.telegram.enabled = te_val.lower() == "true"
|
|
127
|
+
|
|
128
|
+
st = await memory_manager.get_global_setting("search_url")
|
|
129
|
+
if st and st.strip(): self.search_url = st
|
|
130
|
+
|
|
131
|
+
# Email Settings
|
|
132
|
+
eh = await memory_manager.get_global_setting("email_imap_host")
|
|
133
|
+
if eh: self.email.imap_host = eh
|
|
134
|
+
ep = await memory_manager.get_global_setting("email_imap_port")
|
|
135
|
+
if ep: self.email.imap_port = int(ep)
|
|
136
|
+
sh = await memory_manager.get_global_setting("email_smtp_host")
|
|
137
|
+
if sh: self.email.smtp_host = sh
|
|
138
|
+
sp = await memory_manager.get_global_setting("email_smtp_port")
|
|
139
|
+
if sp: self.email.smtp_port = int(sp)
|
|
140
|
+
eu = await memory_manager.get_global_setting("email_user")
|
|
141
|
+
if eu: self.email.user = eu
|
|
142
|
+
epw = await memory_manager.get_global_setting("email_password")
|
|
143
|
+
if epw: self.email.password = epw
|
|
144
|
+
ee = await memory_manager.get_global_setting("email_enabled")
|
|
145
|
+
if ee: self.email.enabled = ee.lower() == "true"
|
|
146
|
+
|
|
147
|
+
# Model Persistence
|
|
148
|
+
am = await memory_manager.get_global_setting("active_model")
|
|
149
|
+
if am and am.strip():
|
|
150
|
+
self.ollama.model = am
|
|
151
|
+
# Note: ollama_client properties are tied to this config instance
|
|
152
|
+
|
|
153
|
+
logger.info(f"Config Refreshed: Discord={self.discord.enabled}, Telegram={self.telegram.enabled}, Search={self.search_url}")
|
|
154
|
+
except Exception as e:
|
|
155
|
+
logger.error(f"Error refreshing config from DB: {e}")
|
|
156
|
+
|
|
157
|
+
def __repr__(self):
|
|
158
|
+
return (
|
|
159
|
+
f"Config(\n"
|
|
160
|
+
f" Ollama: {self.ollama.host} (model: {self.ollama.model})\n"
|
|
161
|
+
f" Discord: {'enabled' if self.discord.enabled else 'disabled'}\n"
|
|
162
|
+
f" Telegram: {'enabled' if self.telegram.enabled else 'disabled'}\n"
|
|
163
|
+
f" Prefix: {self.bot.prefix}\n"
|
|
164
|
+
f" Database: {self.bot.database_path}\n"
|
|
165
|
+
f")"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# Global config instance
|
|
170
|
+
config = Config()
|
youclaw/core_skills.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""
|
|
2
|
+
YouClaw Core Skills
|
|
3
|
+
A collection of built-in tools for the bot.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import logging
|
|
9
|
+
import imaplib
|
|
10
|
+
import smtplib
|
|
11
|
+
import email
|
|
12
|
+
from email.message import EmailMessage
|
|
13
|
+
from datetime import datetime, timedelta
|
|
14
|
+
from .config import config
|
|
15
|
+
from .skills_manager import skill_manager
|
|
16
|
+
from .scheduler_manager import scheduler_manager
|
|
17
|
+
from .search_client import search_client
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
@skill_manager.skill(name="list_emails", description="Check for unread emails in your inbox.")
|
|
22
|
+
def list_emails(limit: int = 5) -> str:
|
|
23
|
+
"""Connects to the IMAP server and retrieves a summary of the latest unread emails."""
|
|
24
|
+
if not config.email.enabled:
|
|
25
|
+
return "Email protocol is currently deactivated. Please enable it in the Control Center."
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
# Connect to IMAP
|
|
29
|
+
mail = imaplib.IMAP4_SSL(config.email.imap_host, config.email.imap_port)
|
|
30
|
+
mail.login(config.email.user, config.email.password)
|
|
31
|
+
mail.select("inbox")
|
|
32
|
+
|
|
33
|
+
# Search for unread emails
|
|
34
|
+
status, messages = mail.search(None, 'UNSEEN')
|
|
35
|
+
if status != 'OK':
|
|
36
|
+
return "Failed to search neural streams for messages."
|
|
37
|
+
|
|
38
|
+
email_ids = messages[0].split()
|
|
39
|
+
if not email_ids:
|
|
40
|
+
return "No unread messages found in your neural inbox."
|
|
41
|
+
|
|
42
|
+
results = []
|
|
43
|
+
# Get the latest 'limit' emails
|
|
44
|
+
for e_id in reversed(email_ids[-limit:]):
|
|
45
|
+
status, msg_data = mail.fetch(e_id, '(RFC822)')
|
|
46
|
+
for response_part in msg_data:
|
|
47
|
+
if isinstance(response_part, tuple):
|
|
48
|
+
msg = email.message_from_bytes(response_part[1])
|
|
49
|
+
subject = msg["subject"]
|
|
50
|
+
sender = msg["from"]
|
|
51
|
+
results.append(f"FROM: {sender}\nSUBJECT: {subject}")
|
|
52
|
+
|
|
53
|
+
mail.logout()
|
|
54
|
+
summary = "\n\n".join(results)
|
|
55
|
+
return f"Found {len(email_ids)} unread messages. Here are the latest {len(results)}:\n\n{summary}"
|
|
56
|
+
|
|
57
|
+
except Exception as e:
|
|
58
|
+
logger.error(f"IMAP Error: {e}")
|
|
59
|
+
return f"Protocol Fault during IMAP handshake: {str(e)}"
|
|
60
|
+
|
|
61
|
+
@skill_manager.skill(name="send_email", description="Send an email to a specific recipient.")
|
|
62
|
+
def send_email(to_address: str, subject: str, body: str) -> str:
|
|
63
|
+
"""Connects to the SMTP server and transmits a new email message."""
|
|
64
|
+
if not config.email.enabled:
|
|
65
|
+
return "Email protocol is currently deactivated. Please enable it in the Control Center."
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
msg = EmailMessage()
|
|
69
|
+
msg.set_content(body)
|
|
70
|
+
msg['Subject'] = subject
|
|
71
|
+
msg['From'] = config.email.user
|
|
72
|
+
msg['To'] = to_address
|
|
73
|
+
|
|
74
|
+
# Connect to SMTP
|
|
75
|
+
with smtplib.SMTP(config.email.smtp_host, config.email.smtp_port) as server:
|
|
76
|
+
server.starttls()
|
|
77
|
+
server.login(config.email.user, config.email.password)
|
|
78
|
+
server.send_message(msg)
|
|
79
|
+
|
|
80
|
+
return f"Message successfully transmitted to {to_address}."
|
|
81
|
+
except Exception as e:
|
|
82
|
+
logger.error(f"SMTP Error: {e}")
|
|
83
|
+
return f"Protocol Fault during SMTP transmission: {str(e)}"
|
|
84
|
+
|
|
85
|
+
# Removed as per user request (small model limitations)
|
|
86
|
+
# @skill_manager.skill(name="web_search", description="Search the internet for real-time information, news, or specific facts.")
|
|
87
|
+
# async def web_search(query: str) -> str:
|
|
88
|
+
# """Useful for answering questions about current events or finding information not in training data."""
|
|
89
|
+
# return await search_client.search(query)
|
|
90
|
+
|
|
91
|
+
@skill_manager.skill(name="read_file", description="Read the contents of a file on the server.", admin_only=True)
|
|
92
|
+
def read_file(file_path: str) -> str:
|
|
93
|
+
"""Reads a file and returns its content. Use this to examine logs, configs, or data files."""
|
|
94
|
+
try:
|
|
95
|
+
if not os.path.exists(file_path):
|
|
96
|
+
return f"Error: File '{file_path}' does not exist."
|
|
97
|
+
|
|
98
|
+
# Limit file size for safety (1MB)
|
|
99
|
+
if os.path.getsize(file_path) > 1024 * 1024:
|
|
100
|
+
return "Error: File is too large to read (max 1MB)."
|
|
101
|
+
|
|
102
|
+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
103
|
+
return f.read()
|
|
104
|
+
except Exception as e:
|
|
105
|
+
return f"Error reading file: {str(e)}"
|
|
106
|
+
|
|
107
|
+
@skill_manager.skill(name="shell_command", description="DANGEROUS: Execute a bash command on the server. Use with extreme caution.", admin_only=True)
|
|
108
|
+
def shell_command(command: str) -> str:
|
|
109
|
+
"""Executes a system command and returns the output (stdout and stderr)."""
|
|
110
|
+
try:
|
|
111
|
+
logger.warning(f"Executing shell command: {command}")
|
|
112
|
+
result = subprocess.run(
|
|
113
|
+
command,
|
|
114
|
+
shell=True,
|
|
115
|
+
capture_output=True,
|
|
116
|
+
text=True,
|
|
117
|
+
timeout=10
|
|
118
|
+
)
|
|
119
|
+
output = result.stdout
|
|
120
|
+
if result.stderr:
|
|
121
|
+
output += f"\nErrors:\n{result.stderr}"
|
|
122
|
+
return output or "Command executed successfully (no output)."
|
|
123
|
+
except subprocess.TimeoutExpired:
|
|
124
|
+
return "Error: Command timed out after 10 seconds."
|
|
125
|
+
except Exception as e:
|
|
126
|
+
return f"Error executing command: {str(e)}"
|
|
127
|
+
|
|
128
|
+
@skill_manager.skill(name="schedule_reminder", description="Schedule a reminder for the user at a specific time.")
|
|
129
|
+
async def schedule_reminder(message: str, minutes_from_now: int, platform: str, user_id: str) -> str:
|
|
130
|
+
"""Useful for setting timers or reminders. The bot will message the user proactively."""
|
|
131
|
+
run_date = datetime.now() + timedelta(minutes=minutes_from_now)
|
|
132
|
+
await scheduler_manager.add_notification_job(
|
|
133
|
+
platform=platform,
|
|
134
|
+
user_id=user_id,
|
|
135
|
+
message=f"⏰ REMINDER: {message}",
|
|
136
|
+
run_date=run_date
|
|
137
|
+
)
|
|
138
|
+
return f"I've scheduled your reminder for '{message}' in {minutes_from_now} minutes."
|
|
139
|
+
|
|
140
|
+
@skill_manager.skill(name="store_secret", description="Securely store a personal secret (like an API key).")
|
|
141
|
+
async def store_secret(key: str, value: str, platform: str, user_id: str) -> str:
|
|
142
|
+
"""Useful for when the user provides a token or key that they want the bot to remember for future actions.
|
|
143
|
+
The secret is stored ONLY for this specific user on this platform."""
|
|
144
|
+
from .memory_manager import memory_manager
|
|
145
|
+
await memory_manager.set_user_secret(platform, user_id, key, value)
|
|
146
|
+
return f"I've securely stored your '{key}' secret."
|
|
147
|
+
@skill_manager.skill(name="run_python_code", description="Execute arbitrary Python code on the server.", admin_only=True)
|
|
148
|
+
def run_python_code(code: str) -> str:
|
|
149
|
+
"""Executes Python code and returns the result of the last expression or printed output."""
|
|
150
|
+
try:
|
|
151
|
+
# Create a temporary script to run
|
|
152
|
+
with open('temp_script.py', 'w') as f:
|
|
153
|
+
f.write(code)
|
|
154
|
+
|
|
155
|
+
result = subprocess.run(
|
|
156
|
+
['python3', 'temp_script.py'],
|
|
157
|
+
capture_output=True,
|
|
158
|
+
text=True,
|
|
159
|
+
timeout=15
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
os.remove('temp_script.py')
|
|
163
|
+
|
|
164
|
+
output = result.stdout
|
|
165
|
+
if result.stderr:
|
|
166
|
+
output += f"\nErrors:\n{result.stderr}"
|
|
167
|
+
return output or "Code executed successfully (no output)."
|
|
168
|
+
except Exception as e:
|
|
169
|
+
return f"Error executing Python code: {str(e)}"
|
|
170
|
+
@skill_manager.skill(name="synthesize_new_skill", description="Permanently save code as a NEW bot skill.", admin_only=True)
|
|
171
|
+
def synthesize_new_skill(skill_name: str, description: str, code: str) -> str:
|
|
172
|
+
"""Save the code to dynamic_skills/ folder so it can be used in future conversations."""
|
|
173
|
+
try:
|
|
174
|
+
# Sanitize skill name
|
|
175
|
+
import re
|
|
176
|
+
safe_name = re.sub(r'[^a-zA-Z0-9_]', '', skill_name).lower()
|
|
177
|
+
if not safe_name:
|
|
178
|
+
return "Error: Invalid skill name."
|
|
179
|
+
|
|
180
|
+
file_path = os.path.join('dynamic_skills', f"{safe_name}.py")
|
|
181
|
+
|
|
182
|
+
# Format as a proper skill module
|
|
183
|
+
module_content = (
|
|
184
|
+
'from skills_manager import skill_manager\n\n'
|
|
185
|
+
f'@skill_manager.skill(name="{safe_name}", description="{description}")\n'
|
|
186
|
+
f'def {safe_name}(**kwargs):\n'
|
|
187
|
+
f' """{description}"""\n'
|
|
188
|
+
' # Synthesized Code:\n'
|
|
189
|
+
' ' + code.replace('\n', '\n ') + '\n'
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
with open(file_path, 'w') as f:
|
|
193
|
+
f.write(module_content)
|
|
194
|
+
|
|
195
|
+
return f"Successfully synthesized new skill: {safe_name}. I can now use it in future tasks!"
|
|
196
|
+
except Exception as e:
|
|
197
|
+
return f"Error synthesizing skill: {str(e)}"
|
|
198
|
+
|
|
199
|
+
@skill_manager.skill(name="watch_url", description="Set up a watchdog to monitor a URL for changes or status. The bot will alert you if the status changes.")
|
|
200
|
+
async def watch_url(url: str, interval_minutes: int, platform: str, user_id: str) -> str:
|
|
201
|
+
"""Useful for monitoring websites, servers, or APIs. Example: 'Watch https://google.com every 5 minutes'."""
|
|
202
|
+
job_id = f"watch_{user_id}_{hash(url)}"
|
|
203
|
+
await scheduler_manager.add_watcher_job(
|
|
204
|
+
platform=platform,
|
|
205
|
+
user_id=user_id,
|
|
206
|
+
target_url=url,
|
|
207
|
+
interval_minutes=interval_minutes,
|
|
208
|
+
job_id=job_id
|
|
209
|
+
)
|
|
210
|
+
return f"I've set up a watchdog for {url}. I'll check it every {interval_minutes} minutes and alert you if I see issues."
|