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.
@@ -0,0 +1,187 @@
1
+ """
2
+ YouClaw Discord Handler
3
+ Handles Discord-specific message processing and bot interactions.
4
+ """
5
+
6
+ import discord
7
+ from discord.ext import commands as discord_commands
8
+ import logging
9
+ from typing import Optional
10
+ from .config import config
11
+ from .ollama_client import ollama_client
12
+ from .memory_manager import memory_manager
13
+ from .search_client import search_client
14
+ from .commands import command_handler
15
+ import asyncio
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class DiscordHandler:
21
+ """Handles Discord platform integration"""
22
+
23
+ def __init__(self):
24
+ # Set up Discord intents
25
+ intents = discord.Intents.default()
26
+ intents.message_content = True # Required to read message content
27
+ intents.messages = True
28
+ intents.guilds = True
29
+
30
+ self.bot = discord_commands.Bot(command_prefix=config.bot.prefix, intents=intents)
31
+ self.setup_events()
32
+
33
+ def setup_events(self):
34
+ """Set up Discord event handlers"""
35
+
36
+ @self.bot.event
37
+ async def on_ready():
38
+ logger.info(f"Discord bot logged in as {self.bot.user}")
39
+ await self.bot.change_presence(
40
+ activity=discord.Activity(
41
+ type=discord.ActivityType.listening,
42
+ name="your messages | !help"
43
+ )
44
+ )
45
+
46
+ @self.bot.event
47
+ async def on_message(message: discord.Message):
48
+ # Ignore messages from the bot itself
49
+ if message.author == self.bot.user:
50
+ return
51
+
52
+ # Ignore messages from other bots
53
+ if message.author.bot:
54
+ return
55
+
56
+ # Only respond to DMs or mentions
57
+ is_dm = isinstance(message.channel, discord.DMChannel)
58
+ is_mentioned = self.bot.user in message.mentions
59
+
60
+ if not (is_dm or is_mentioned):
61
+ return
62
+
63
+ # Get user info
64
+ user_id = str(message.author.id)
65
+ channel_id = str(message.channel.id) if not is_dm else None
66
+ content = message.content
67
+
68
+ # Remove mention from content
69
+ if is_mentioned:
70
+ content = content.replace(f"<@{self.bot.user.id}>", "").strip()
71
+
72
+ # Show typing indicator
73
+ async with message.channel.typing():
74
+ # Check if it's a command
75
+ command_response = await command_handler.handle_command(
76
+ platform="discord",
77
+ user_id=user_id,
78
+ message=content,
79
+ channel_id=channel_id
80
+ )
81
+
82
+ if command_response:
83
+ await self.send_message(message.channel, command_response)
84
+ return
85
+
86
+ # Get conversation history
87
+ history = await memory_manager.get_conversation_history(
88
+ platform="discord",
89
+ user_id=user_id,
90
+ channel_id=channel_id
91
+ )
92
+
93
+ # Add current message to history
94
+ await memory_manager.add_message(
95
+ platform="discord",
96
+ user_id=user_id,
97
+ role="user",
98
+ content=content,
99
+ channel_id=channel_id,
100
+ metadata={"username": str(message.author)}
101
+ )
102
+
103
+ # Build messages for LLM
104
+ messages = history + [{"role": "user", "content": content}]
105
+
106
+ # Get user profile and onboarding status
107
+ profile = await memory_manager.get_user_profile(platform="discord", user_id=user_id)
108
+
109
+ # Check global settings
110
+ search_enabled = (await memory_manager.get_global_setting("search_enabled", "true")).lower() == "true"
111
+
112
+ # Decide if we need to search
113
+ search_context = None
114
+ if search_enabled and any(word in content.lower() for word in ['search', 'find', 'who is', 'what is', 'latest', 'news']):
115
+ search_results = await search_client.search(content)
116
+ search_context = search_results
117
+
118
+ # Get AI response with autonomous tool use
119
+ logger.info(f"Starting Discord reasoning loop for user {user_id}...")
120
+
121
+ try:
122
+ # Use chat_with_tools for autonomous behavior
123
+ response = await ollama_client.chat_with_tools(
124
+ messages=messages,
125
+ user_profile=profile,
126
+ context={"user_id": user_id, "platform": "discord"}
127
+ )
128
+
129
+ if response.strip():
130
+ await self.send_message(message.channel, response)
131
+ else:
132
+ await message.channel.send("Hmm, I'm a bit speechless.")
133
+
134
+ logger.info(f"Reasoning complete for user {user_id}")
135
+
136
+ except Exception as e:
137
+ logger.error(f"Error during Discord reasoning: {e}")
138
+ error_msg = "Oops, I lost my train of thought. Can we try again?"
139
+ await message.channel.send(error_msg)
140
+ response = error_msg
141
+
142
+ # Simple heuristic to 'complete' onboarding
143
+ if not profile['onboarding_completed']:
144
+ if len(history) > 2:
145
+ await memory_manager.update_user_profile(
146
+ platform="discord",
147
+ user_id=user_id,
148
+ onboarding_completed=True
149
+ )
150
+
151
+ # Save AI response to memory
152
+ await memory_manager.add_message(
153
+ platform="discord",
154
+ user_id=user_id,
155
+ role="assistant",
156
+ content=response,
157
+ channel_id=channel_id
158
+ )
159
+
160
+ async def send_message(self, channel, content: str):
161
+ """Send a message, handling Discord's 2000 char limit"""
162
+ if len(content) <= 2000:
163
+ await channel.send(content)
164
+ else:
165
+ # Split into chunks
166
+ chunks = [content[i:i+2000] for i in range(0, len(content), 2000)]
167
+ for chunk in chunks:
168
+ await channel.send(chunk)
169
+
170
+ async def start(self):
171
+ """Start the Discord bot"""
172
+ if not config.discord.enabled:
173
+ logger.info("Discord is disabled in config")
174
+ return
175
+
176
+ logger.info("Starting Discord bot...")
177
+ await self.bot.start(config.discord.token)
178
+
179
+ async def stop(self):
180
+ """Stop the Discord bot"""
181
+ if self.bot:
182
+ await self.bot.close()
183
+ logger.info("Discord bot stopped")
184
+
185
+
186
+ # Global Discord handler instance
187
+ discord_handler = DiscordHandler()
youclaw/env_manager.py ADDED
@@ -0,0 +1,61 @@
1
+ """
2
+ YouClaw Environment Manager
3
+ Safely reads and writes to the .env file to allow live configuration updates.
4
+ """
5
+
6
+ import os
7
+ import logging
8
+ from pathlib import Path
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ class EnvManager:
13
+ """Manages reading and writing to the .env file"""
14
+
15
+ def __init__(self, env_path: str = ".env"):
16
+ self.env_path = Path(env_path)
17
+
18
+ def get_all(self) -> dict:
19
+ """Read all environment variables from the file"""
20
+ if not self.env_path.exists():
21
+ return {}
22
+
23
+ env_vars = {}
24
+ with open(self.env_path, 'r') as f:
25
+ for line in f:
26
+ line = line.strip()
27
+ if not line or line.startswith('#') or '=' not in line:
28
+ continue
29
+ key, val = line.split('=', 1)
30
+ env_vars[key.strip()] = val.strip()
31
+ return env_vars
32
+
33
+ def set_key(self, key: str, value: str):
34
+ """Update or add a key-value pair in the .env file"""
35
+ lines = []
36
+ found = False
37
+
38
+ if self.env_path.exists():
39
+ with open(self.env_path, 'r') as f:
40
+ lines = f.readlines()
41
+
42
+ for i, line in enumerate(lines):
43
+ line_strip = line.strip()
44
+ if line_strip.startswith(f"{key}="):
45
+ lines[i] = f"{key}={value}\n"
46
+ found = True
47
+ break
48
+
49
+ if not found:
50
+ # Add with a newline if file is not empty and doesn't end with one
51
+ if lines and not lines[-1].endswith('\n'):
52
+ lines[-1] += '\n'
53
+ lines.append(f"{key}={value}\n")
54
+
55
+ with open(self.env_path, 'w') as f:
56
+ f.writelines(lines)
57
+
58
+ logger.info(f"Updated .env: {key}=***")
59
+
60
+ # Global instance
61
+ env_manager = EnvManager()
youclaw/main.py ADDED
@@ -0,0 +1,273 @@
1
+ """
2
+ YouClaw CLI - Command Line Interface
3
+ Provides commands for installation, health checks, and management.
4
+ """
5
+
6
+ import sys
7
+ import subprocess
8
+ import os
9
+ import asyncio
10
+ import argparse
11
+ from pathlib import Path
12
+
13
+ # Add parent directory to path for imports
14
+ sys.path.insert(0, str(Path(__file__).parent))
15
+
16
+
17
+ class YouClawCLI:
18
+ """YouClaw command line interface"""
19
+
20
+ def __init__(self):
21
+ self.project_dir = Path(__file__).parent
22
+
23
+ def cmd_install(self, args):
24
+ """Run installation script"""
25
+ print("šŸ¦ž Running YouClaw installation...")
26
+ install_script = self.project_dir / "install.sh"
27
+
28
+ if not install_script.exists():
29
+ print("āŒ install.sh not found!")
30
+ return 1
31
+
32
+ result = subprocess.run(["bash", str(install_script)])
33
+ return result.returncode
34
+
35
+ def cmd_check(self, args):
36
+ """Run health checks"""
37
+ print("šŸ¦ž YouClaw Health Check")
38
+ print("=" * 50)
39
+
40
+ # Check Python version
41
+ print("\nšŸ“¦ Python Version:")
42
+ python_version = sys.version.split()[0]
43
+ print(f" {python_version}", end="")
44
+ if sys.version_info >= (3, 10):
45
+ print(" āœ…")
46
+ else:
47
+ print(" āŒ (Need 3.10+)")
48
+
49
+ # Check Ollama
50
+ print("\nšŸ¤– Ollama:")
51
+ try:
52
+ result = subprocess.run(
53
+ ["curl", "-s", "http://localhost:11434/api/tags"],
54
+ capture_output=True,
55
+ timeout=5
56
+ )
57
+ if result.returncode == 0:
58
+ print(" Connected āœ…")
59
+ # Parse models
60
+ import json
61
+ try:
62
+ data = json.loads(result.stdout)
63
+ models = [m["name"] for m in data.get("models", [])]
64
+ print(f" Models: {', '.join(models) if models else 'None'}")
65
+ except:
66
+ pass
67
+ else:
68
+ print(" Not running āŒ")
69
+ except Exception as e:
70
+ print(f" Error: {e} āŒ")
71
+
72
+ # Check virtual environment
73
+ print("\nšŸ Virtual Environment:")
74
+ venv_path = self.project_dir / "venv"
75
+ if venv_path.exists():
76
+ print(f" {venv_path} āœ…")
77
+ else:
78
+ print(" Not found āŒ")
79
+
80
+ # Check .env file
81
+ print("\nāš™ļø Configuration:")
82
+ env_path = self.project_dir / ".env"
83
+ if env_path.exists():
84
+ print(" .env file exists āœ…")
85
+ # Check for tokens
86
+ with open(env_path) as f:
87
+ content = f.read()
88
+ has_discord = "DISCORD_BOT_TOKEN=" in content and "your_discord" not in content
89
+ has_telegram = "TELEGRAM_BOT_TOKEN=" in content and "your_telegram" not in content
90
+
91
+ if has_discord:
92
+ print(" Discord token configured āœ…")
93
+ else:
94
+ print(" Discord token not set āš ļø")
95
+
96
+ if has_telegram:
97
+ print(" Telegram token configured āœ…")
98
+ else:
99
+ print(" Telegram token not set āš ļø")
100
+ else:
101
+ print(" .env file missing āŒ")
102
+
103
+ # Check database
104
+ print("\nšŸ’¾ Database:")
105
+ db_path = self.project_dir / "data" / "bot.db"
106
+ if db_path.exists():
107
+ size = db_path.stat().st_size
108
+ print(f" {db_path} ({size} bytes) āœ…")
109
+ else:
110
+ print(" Not created yet (will be created on first run)")
111
+
112
+ # Check systemd service
113
+ print("\nšŸ”§ Systemd Service:")
114
+ try:
115
+ result = subprocess.run(
116
+ ["systemctl", "--user", "is-active", "youclaw"],
117
+ capture_output=True,
118
+ text=True
119
+ )
120
+ status = result.stdout.strip()
121
+ if status == "active":
122
+ print(" Running āœ…")
123
+ elif status == "inactive":
124
+ print(" Stopped āš ļø")
125
+ else:
126
+ print(f" Status: {status}")
127
+ except Exception as e:
128
+ print(f" Not installed āš ļø")
129
+
130
+ print("\n" + "=" * 50)
131
+ return 0
132
+
133
+ def cmd_status(self, args):
134
+ """Show service status"""
135
+ print("šŸ¦ž YouClaw Status\n")
136
+ result = subprocess.run(
137
+ ["systemctl", "--user", "status", "youclaw"],
138
+ capture_output=False
139
+ )
140
+ return result.returncode
141
+
142
+ def cmd_logs(self, args):
143
+ """View logs"""
144
+ print("šŸ¦ž YouClaw Logs (Ctrl+C to exit)\n")
145
+
146
+ cmd = ["journalctl", "--user", "-u", "youclaw"]
147
+
148
+ if args.follow:
149
+ cmd.append("-f")
150
+
151
+ if args.lines:
152
+ cmd.extend(["-n", str(args.lines)])
153
+
154
+ result = subprocess.run(cmd)
155
+ return result.returncode
156
+
157
+ def cmd_start(self, args):
158
+ """Start YouClaw service"""
159
+ print("šŸ¦ž Starting YouClaw...")
160
+ result = subprocess.run(["systemctl", "--user", "start", "youclaw"])
161
+ if result.returncode == 0:
162
+ print("āœ… YouClaw started")
163
+ return result.returncode
164
+
165
+ def cmd_stop(self, args):
166
+ """Stop YouClaw service"""
167
+ print("šŸ¦ž Stopping YouClaw...")
168
+ result = subprocess.run(["systemctl", "--user", "stop", "youclaw"])
169
+ if result.returncode == 0:
170
+ print("āœ… YouClaw stopped")
171
+ return result.returncode
172
+
173
+ def cmd_restart(self, args):
174
+ """Restart YouClaw service"""
175
+ print("šŸ¦ž Restarting YouClaw...")
176
+ result = subprocess.run(["systemctl", "--user", "restart", "youclaw"])
177
+ if result.returncode == 0:
178
+ print("āœ… YouClaw restarted")
179
+ return result.returncode
180
+
181
+ def cmd_dashboard(self, args):
182
+ """Start web dashboard"""
183
+ print("šŸ¦ž Starting YouClaw Dashboard...")
184
+ print(" Dashboard will be available at http://localhost:8080")
185
+ print(" Press Ctrl+C to stop\n")
186
+
187
+ # Import and run dashboard
188
+ try:
189
+ from dashboard import run_dashboard
190
+ asyncio.run(run_dashboard(port=args.port))
191
+ except ImportError as e:
192
+ print(f"āŒ Dashboard module not found. Make sure dashboard.py exists. Error: {e}")
193
+ return 1
194
+ except KeyboardInterrupt:
195
+ print("\nšŸ‘‹ Dashboard stopped")
196
+ return 0
197
+
198
+ def run(self):
199
+ """Main CLI entry point"""
200
+ parser = argparse.ArgumentParser(
201
+ description="YouClaw - Your Personal AI Assistant",
202
+ formatter_class=argparse.RawDescriptionHelpFormatter,
203
+ epilog="""
204
+ Examples:
205
+ youclaw install # Run installation
206
+ youclaw check # Health check
207
+ youclaw start # Start service
208
+ youclaw logs -f # Follow logs
209
+ youclaw dashboard # Start web dashboard
210
+ """
211
+ )
212
+
213
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
214
+
215
+ # Install command
216
+ subparsers.add_parser("install", help="Run installation script")
217
+
218
+ # Check command
219
+ subparsers.add_parser("check", help="Run health checks")
220
+
221
+ # Status command
222
+ subparsers.add_parser("status", help="Show service status")
223
+
224
+ # Logs command
225
+ logs_parser = subparsers.add_parser("logs", help="View logs")
226
+ logs_parser.add_argument("-f", "--follow", action="store_true", help="Follow log output")
227
+ logs_parser.add_argument("-n", "--lines", type=int, default=50, help="Number of lines to show")
228
+
229
+ # Start command
230
+ subparsers.add_parser("start", help="Start YouClaw service")
231
+
232
+ # Stop command
233
+ subparsers.add_parser("stop", help="Stop YouClaw service")
234
+
235
+ # Restart command
236
+ subparsers.add_parser("restart", help="Restart YouClaw service")
237
+
238
+ # Dashboard command
239
+ dashboard_parser = subparsers.add_parser("dashboard", help="Start web dashboard")
240
+ dashboard_parser.add_argument("-p", "--port", type=int, default=8080, help="Dashboard port")
241
+
242
+ args = parser.parse_args()
243
+
244
+ if not args.command:
245
+ parser.print_help()
246
+ return 0
247
+
248
+ # Execute command
249
+ command_map = {
250
+ "install": self.cmd_install,
251
+ "check": self.cmd_check,
252
+ "status": self.cmd_status,
253
+ "logs": self.cmd_logs,
254
+ "start": self.cmd_start,
255
+ "stop": self.cmd_stop,
256
+ "restart": self.cmd_restart,
257
+ "dashboard": self.cmd_dashboard,
258
+ }
259
+
260
+ if args.command in command_map:
261
+ return command_map[args.command](args)
262
+ else:
263
+ print(f"Unknown command: {args.command}")
264
+ return 1
265
+
266
+
267
+ def main():
268
+ cli = YouClawCLI()
269
+ sys.exit(cli.run())
270
+
271
+
272
+ if __name__ == "__main__":
273
+ main()