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/ollama_client.py
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
"""
|
|
2
|
+
YouClaw Ollama Client
|
|
3
|
+
Handles all interactions with the local Ollama LLM for intelligent responses.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import logging
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import List, Dict, Optional, AsyncGenerator, Any
|
|
10
|
+
import aiohttp
|
|
11
|
+
import json
|
|
12
|
+
from .config import config
|
|
13
|
+
from .skills_manager import skill_manager
|
|
14
|
+
from .personality_manager import PERSONALITIES, DEFAULT_PERSONALITY
|
|
15
|
+
from .search_client import search_client
|
|
16
|
+
from . import core_skills # Ensure skills are registered
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class OllamaClient:
|
|
22
|
+
"""Client for interacting with Ollama LLM"""
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def host(self): return config.ollama.host
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def model(self): return config.ollama.model
|
|
29
|
+
|
|
30
|
+
@model.setter
|
|
31
|
+
def model(self, value):
|
|
32
|
+
from .config import config
|
|
33
|
+
config.ollama.model = value
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def temperature(self): return config.ollama.temperature
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def max_tokens(self): return config.ollama.max_tokens
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def timeout(self): return config.ollama.timeout
|
|
43
|
+
|
|
44
|
+
def __init__(self):
|
|
45
|
+
self.session: Optional[aiohttp.ClientSession] = None
|
|
46
|
+
|
|
47
|
+
async def initialize(self):
|
|
48
|
+
"""Initialize the HTTP session"""
|
|
49
|
+
self.session = aiohttp.ClientSession(
|
|
50
|
+
timeout=aiohttp.ClientTimeout(total=self.timeout)
|
|
51
|
+
)
|
|
52
|
+
logger.info(f"Ollama client initialized: {self.host} (model: {self.model})")
|
|
53
|
+
|
|
54
|
+
async def close(self):
|
|
55
|
+
"""Close the HTTP session"""
|
|
56
|
+
if self.session:
|
|
57
|
+
await self.session.close()
|
|
58
|
+
logger.info("Ollama client closed")
|
|
59
|
+
|
|
60
|
+
async def check_health(self) -> bool:
|
|
61
|
+
"""Check if Ollama service is available"""
|
|
62
|
+
try:
|
|
63
|
+
async with self.session.get(f"{self.host}/api/tags") as response:
|
|
64
|
+
return response.status == 200
|
|
65
|
+
except Exception as e:
|
|
66
|
+
logger.error(f"Ollama health check failed: {e}")
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
async def get_embeddings(self, text: str) -> List[float]:
|
|
70
|
+
"""Generate embeddings for a piece of text"""
|
|
71
|
+
payload = {
|
|
72
|
+
"model": "all-minilm", # Fallback to a common one, can be made configurable
|
|
73
|
+
"prompt": text
|
|
74
|
+
}
|
|
75
|
+
try:
|
|
76
|
+
async with self.session.post(f"{self.host}/api/embeddings", json=payload) as response:
|
|
77
|
+
if response.status == 200:
|
|
78
|
+
data = await response.json()
|
|
79
|
+
return data.get("embedding", [])
|
|
80
|
+
else:
|
|
81
|
+
logger.error(f"Embedding error: {await response.text()}")
|
|
82
|
+
return []
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logger.error(f"Failed to get embeddings: {e}")
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
async def chat(
|
|
88
|
+
self,
|
|
89
|
+
messages: List[Dict[str, str]],
|
|
90
|
+
user_profile: Optional[Dict] = None,
|
|
91
|
+
search_context: Optional[str] = None,
|
|
92
|
+
images: Optional[List[str]] = None
|
|
93
|
+
) -> str:
|
|
94
|
+
"""
|
|
95
|
+
Send a non-streaming chat request to Ollama and get a response.
|
|
96
|
+
"""
|
|
97
|
+
# Get last user message as query for semantic search
|
|
98
|
+
last_user_msg = next((m['content'] for m in reversed(messages) if m['role'] == 'user'), None)
|
|
99
|
+
|
|
100
|
+
# Silent Intent Detection & Real-Time Search Injection
|
|
101
|
+
if not search_context and last_user_msg:
|
|
102
|
+
is_fact_seeking = await self._detect_search_intent(last_user_msg)
|
|
103
|
+
if is_fact_seeking:
|
|
104
|
+
logger.info("🔍 Neural Intent Detected (Unary): Fetching real-time data...")
|
|
105
|
+
search_context = await search_client.search(last_user_msg)
|
|
106
|
+
|
|
107
|
+
# Build the system prompt based on persona and status
|
|
108
|
+
system_prompt = await self._build_system_prompt(user_profile, search_context, query=last_user_msg)
|
|
109
|
+
|
|
110
|
+
# Build the messages array
|
|
111
|
+
chat_messages = [{"role": "system", "content": system_prompt}]
|
|
112
|
+
chat_messages.extend(messages)
|
|
113
|
+
|
|
114
|
+
# Add images to the last message if provided
|
|
115
|
+
if images and chat_messages:
|
|
116
|
+
chat_messages[-1]["images"] = images
|
|
117
|
+
|
|
118
|
+
payload = {
|
|
119
|
+
"model": self.model,
|
|
120
|
+
"messages": chat_messages,
|
|
121
|
+
"stream": False,
|
|
122
|
+
"options": {
|
|
123
|
+
"temperature": self.temperature,
|
|
124
|
+
"num_predict": self.max_tokens
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
logger.info(f"Ollama Chat: {self.host}/api/chat (model: {self.model})")
|
|
130
|
+
async with self.session.post(f"{self.host}/api/chat", json=payload) as response:
|
|
131
|
+
if response.status != 200:
|
|
132
|
+
error_text = await response.text()
|
|
133
|
+
logger.error(f"Ollama API error: {error_text}")
|
|
134
|
+
return "Sorry, I'm having trouble connecting to my AI brain."
|
|
135
|
+
|
|
136
|
+
result = await response.json()
|
|
137
|
+
return result.get("message", {}).get("content", "")
|
|
138
|
+
|
|
139
|
+
except asyncio.TimeoutError:
|
|
140
|
+
logger.error("Ollama request timed out")
|
|
141
|
+
return "Sorry, that took too long to process."
|
|
142
|
+
except Exception as e:
|
|
143
|
+
logger.error(f"Error calling Ollama: {e}")
|
|
144
|
+
return "Sorry, I encountered an error."
|
|
145
|
+
|
|
146
|
+
async def chat_with_tools_stream(
|
|
147
|
+
self,
|
|
148
|
+
messages: List[Dict[str, str]],
|
|
149
|
+
user_profile: Optional[Dict] = None,
|
|
150
|
+
max_iterations: int = 5,
|
|
151
|
+
context: Optional[Dict[str, Any]] = None,
|
|
152
|
+
images: Optional[List[str]] = None
|
|
153
|
+
) -> AsyncGenerator[str, None]:
|
|
154
|
+
"""
|
|
155
|
+
AI Reasoning Loop (ReAct) with STREAMING support.
|
|
156
|
+
Yields status updates and then the final streaming response.
|
|
157
|
+
"""
|
|
158
|
+
context = context or {}
|
|
159
|
+
last_user_msg = next((m['content'] for m in reversed(messages) if m['role'] == 'user'), None)
|
|
160
|
+
|
|
161
|
+
# Silent Intent Detection & Real-Time Search Injection
|
|
162
|
+
search_context = None
|
|
163
|
+
if last_user_msg:
|
|
164
|
+
is_fact_seeking = await self._detect_search_intent(last_user_msg)
|
|
165
|
+
if is_fact_seeking:
|
|
166
|
+
logger.info("🔍 Neural Intent Detected (ReAct Stream): Fetching real-time data...")
|
|
167
|
+
search_context = await search_client.search(last_user_msg)
|
|
168
|
+
|
|
169
|
+
system_prompt = await self._build_system_prompt(user_profile, search_context, include_tools=True, query=last_user_msg)
|
|
170
|
+
|
|
171
|
+
current_messages = [{"role": "system", "content": system_prompt}]
|
|
172
|
+
current_messages.extend(messages)
|
|
173
|
+
|
|
174
|
+
if images and current_messages:
|
|
175
|
+
current_messages[-1]["images"] = images
|
|
176
|
+
|
|
177
|
+
for i in range(max_iterations):
|
|
178
|
+
logger.info(f"ReAct Stream Loop {i+1}/{max_iterations}")
|
|
179
|
+
|
|
180
|
+
payload = {
|
|
181
|
+
"model": self.model,
|
|
182
|
+
"messages": current_messages,
|
|
183
|
+
"stream": False,
|
|
184
|
+
"options": {"temperature": 0.1, "stop": ["Observation:", "OBSERVATION:"]}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async with self.session.post(f"{self.host}/api/chat", json=payload) as response:
|
|
188
|
+
if response.status != 200:
|
|
189
|
+
yield " (Error reaching neural core)"
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
result = await response.json()
|
|
193
|
+
content = result.get("message", {}).get("content", "")
|
|
194
|
+
current_messages.append({"role": "assistant", "content": content})
|
|
195
|
+
|
|
196
|
+
if "action:" in content.lower():
|
|
197
|
+
try:
|
|
198
|
+
action = ""
|
|
199
|
+
args_str = "{}"
|
|
200
|
+
for line in content.split('\n'):
|
|
201
|
+
lower_line = line.lower()
|
|
202
|
+
if "action:" in lower_line: action = line.split(":", 1)[-1].strip()
|
|
203
|
+
if "arguments:" in lower_line: args_str = line.split(":", 1)[-1].strip()
|
|
204
|
+
|
|
205
|
+
if action:
|
|
206
|
+
yield f" *Executing {action}...* \n\n"
|
|
207
|
+
try: args = json.loads(args_str)
|
|
208
|
+
except: args = {}
|
|
209
|
+
for k, v in context.items():
|
|
210
|
+
if k not in args: args[k] = v
|
|
211
|
+
|
|
212
|
+
observation = await skill_manager.execute_skill(action, args)
|
|
213
|
+
current_messages.append({"role": "user", "content": f"Observation: {observation}"})
|
|
214
|
+
continue
|
|
215
|
+
except Exception as e:
|
|
216
|
+
current_messages.append({"role": "user", "content": f"Observation: Fault: {str(e)}"})
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
# Final Answer Phase - Stream it for speed
|
|
220
|
+
final_text = content.split("Final Answer:")[-1].strip() if "Final Answer:" in content else content.strip()
|
|
221
|
+
|
|
222
|
+
# To make it "feel" like streaming, we yield it in small bits if it was pre-calculated
|
|
223
|
+
# or better yet, we just yield it.
|
|
224
|
+
yield final_text
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
yield " (Reasoning loop limit exceeded)"
|
|
228
|
+
|
|
229
|
+
async def chat_with_tools(
|
|
230
|
+
self,
|
|
231
|
+
messages: List[Dict[str, str]],
|
|
232
|
+
user_profile: Optional[Dict] = None,
|
|
233
|
+
max_iterations: int = 5,
|
|
234
|
+
context: Optional[Dict[str, Any]] = None,
|
|
235
|
+
images: Optional[List[str]] = None
|
|
236
|
+
) -> str:
|
|
237
|
+
"""
|
|
238
|
+
AI Reasoning Loop (ReAct) for non-streaming background tasks.
|
|
239
|
+
Returns the final answer string.
|
|
240
|
+
"""
|
|
241
|
+
context = context or {}
|
|
242
|
+
last_user_msg = next((m['content'] for m in reversed(messages) if m['role'] == 'user'), None)
|
|
243
|
+
|
|
244
|
+
# Silent Intent Detection & Real-Time Search Injection
|
|
245
|
+
search_context = None
|
|
246
|
+
if last_user_msg:
|
|
247
|
+
is_fact_seeking = await self._detect_search_intent(last_user_msg)
|
|
248
|
+
if is_fact_seeking:
|
|
249
|
+
logger.info("🔍 Neural Intent Detected (ReAct Unary): Fetching real-time data...")
|
|
250
|
+
search_context = await search_client.search(last_user_msg)
|
|
251
|
+
|
|
252
|
+
system_prompt = await self._build_system_prompt(user_profile, search_context, include_tools=True, query=last_user_msg)
|
|
253
|
+
|
|
254
|
+
current_messages = [{"role": "system", "content": system_prompt}]
|
|
255
|
+
current_messages.extend(messages)
|
|
256
|
+
|
|
257
|
+
for i in range(max_iterations):
|
|
258
|
+
logger.info(f"ReAct Loop {i+1}/{max_iterations}")
|
|
259
|
+
|
|
260
|
+
payload = {
|
|
261
|
+
"model": self.model,
|
|
262
|
+
"messages": current_messages,
|
|
263
|
+
"stream": False,
|
|
264
|
+
"options": {"temperature": 0.7, "stop": ["Observation:", "OBSERVATION:"]}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async with self.session.post(f"{self.host}/api/chat", json=payload) as response:
|
|
268
|
+
if response.status != 200:
|
|
269
|
+
logger.error(f"Ollama API error in chat_with_tools: {await response.text()}")
|
|
270
|
+
return "Error reaching neural core"
|
|
271
|
+
result = await response.json()
|
|
272
|
+
content = result.get("message", {}).get("content", "")
|
|
273
|
+
logger.info(f"ReAct Iteration {i+1} Output: {content[:100]}...")
|
|
274
|
+
current_messages.append({"role": "assistant", "content": content})
|
|
275
|
+
|
|
276
|
+
if "action:" in content.lower():
|
|
277
|
+
try:
|
|
278
|
+
action = ""
|
|
279
|
+
args_str = "{}"
|
|
280
|
+
for line in content.split('\n'):
|
|
281
|
+
lower_line = line.lower()
|
|
282
|
+
if "action:" in lower_line: action = line.split(":", 1)[-1].strip()
|
|
283
|
+
if "arguments:" in lower_line: args_str = line.split(":", 1)[-1].strip()
|
|
284
|
+
|
|
285
|
+
if action:
|
|
286
|
+
try: args = json.loads(args_str)
|
|
287
|
+
except: args = {}
|
|
288
|
+
for k, v in context.items():
|
|
289
|
+
if k not in args: args[k] = v
|
|
290
|
+
|
|
291
|
+
observation = await skill_manager.execute_skill(action, args)
|
|
292
|
+
current_messages.append({"role": "user", "content": f"Observation: {observation}"})
|
|
293
|
+
continue
|
|
294
|
+
except Exception as e:
|
|
295
|
+
current_messages.append({"role": "user", "content": f"Observation: Fault: {str(e)}"})
|
|
296
|
+
continue
|
|
297
|
+
|
|
298
|
+
final_answer = content.split("Final Answer:")[-1].strip() if "Final Answer:" in content else content.strip()
|
|
299
|
+
return final_answer
|
|
300
|
+
|
|
301
|
+
return "Reasoning loop limit exceeded"
|
|
302
|
+
|
|
303
|
+
async def chat_stream(
|
|
304
|
+
self,
|
|
305
|
+
messages: List[Dict[str, str]],
|
|
306
|
+
user_profile: Optional[Dict] = None,
|
|
307
|
+
search_context: Optional[str] = None,
|
|
308
|
+
images: Optional[List[str]] = None
|
|
309
|
+
) -> AsyncGenerator[str, None]:
|
|
310
|
+
"""
|
|
311
|
+
Stream a chat response from Ollama token by token.
|
|
312
|
+
"""
|
|
313
|
+
last_user_msg = next((m['content'] for m in reversed(messages) if m['role'] == 'user'), None)
|
|
314
|
+
|
|
315
|
+
# Silent Intent Detection & Real-Time Search Injection
|
|
316
|
+
if not search_context and last_user_msg:
|
|
317
|
+
is_fact_seeking = await self._detect_search_intent(last_user_msg)
|
|
318
|
+
if is_fact_seeking:
|
|
319
|
+
logger.info("🔍 Neural Intent Detected: Fetching real-time data...")
|
|
320
|
+
search_context = await search_client.search(last_user_msg)
|
|
321
|
+
|
|
322
|
+
system_prompt = await self._build_system_prompt(user_profile, search_context, query=last_user_msg)
|
|
323
|
+
|
|
324
|
+
chat_messages = [{"role": "system", "content": system_prompt}]
|
|
325
|
+
chat_messages.extend(messages)
|
|
326
|
+
|
|
327
|
+
if images and chat_messages:
|
|
328
|
+
chat_messages[-1]["images"] = images
|
|
329
|
+
|
|
330
|
+
payload = {
|
|
331
|
+
"model": self.model,
|
|
332
|
+
"messages": chat_messages,
|
|
333
|
+
"stream": True,
|
|
334
|
+
"options": {
|
|
335
|
+
"temperature": self.temperature,
|
|
336
|
+
"num_predict": self.max_tokens
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
try:
|
|
341
|
+
logger.info(f"Ollama Stream: {self.host}/api/chat (model: {self.model})")
|
|
342
|
+
async with self.session.post(f"{self.host}/api/chat", json=payload) as response:
|
|
343
|
+
if response.status != 200:
|
|
344
|
+
yield "Sorry, I'm having trouble connecting to my AI brain."
|
|
345
|
+
return
|
|
346
|
+
|
|
347
|
+
async for line in response.content:
|
|
348
|
+
if line:
|
|
349
|
+
try:
|
|
350
|
+
import json
|
|
351
|
+
data = json.loads(line)
|
|
352
|
+
if "message" in data:
|
|
353
|
+
content = data["message"].get("content", "")
|
|
354
|
+
if content:
|
|
355
|
+
yield content
|
|
356
|
+
if data.get("done"):
|
|
357
|
+
break
|
|
358
|
+
except json.JSONDecodeError:
|
|
359
|
+
continue
|
|
360
|
+
except Exception as e:
|
|
361
|
+
logger.error(f"Error streaming from Ollama: {e}")
|
|
362
|
+
yield " (Connection interrupted)"
|
|
363
|
+
|
|
364
|
+
async def switch_model(self, model_name: str) -> bool:
|
|
365
|
+
"""
|
|
366
|
+
Switch to a different Ollama model.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
model_name: Name of the model to switch to
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
True if successful, False otherwise
|
|
373
|
+
"""
|
|
374
|
+
try:
|
|
375
|
+
# Check if model exists
|
|
376
|
+
async with self.session.get(f"{self.host}/api/tags") as response:
|
|
377
|
+
if response.status == 200:
|
|
378
|
+
data = await response.json()
|
|
379
|
+
models = [m["name"] for m in data.get("models", [])]
|
|
380
|
+
|
|
381
|
+
if model_name in models:
|
|
382
|
+
self.model = model_name
|
|
383
|
+
logger.info(f"Switched to model: {model_name}")
|
|
384
|
+
return True
|
|
385
|
+
else:
|
|
386
|
+
logger.warning(f"Model {model_name} not found. Available: {models}")
|
|
387
|
+
return False
|
|
388
|
+
except Exception as e:
|
|
389
|
+
logger.error(f"Error switching model: {e}")
|
|
390
|
+
return False
|
|
391
|
+
|
|
392
|
+
async def get_available_models(self) -> List[str]:
|
|
393
|
+
"""Get list of available Ollama models"""
|
|
394
|
+
try:
|
|
395
|
+
async with self.session.get(f"{self.host}/api/tags") as response:
|
|
396
|
+
if response.status == 200:
|
|
397
|
+
data = await response.json()
|
|
398
|
+
return [m["name"] for m in data.get("models", [])]
|
|
399
|
+
return []
|
|
400
|
+
except Exception as e:
|
|
401
|
+
logger.error(f"Error getting models: {e}")
|
|
402
|
+
return []
|
|
403
|
+
|
|
404
|
+
async def _detect_search_intent(self, text: str) -> bool:
|
|
405
|
+
"""
|
|
406
|
+
Lightweight check to see if we should trigger a search.
|
|
407
|
+
Looking for question words, entities, or time-sensitive queries.
|
|
408
|
+
"""
|
|
409
|
+
# Quick heuristic (Fast & Efficient)
|
|
410
|
+
keywords = ["who", "what", "where", "when", "why", "how", "price", "weather", "news", "current", "latest", "today", "yesterday"]
|
|
411
|
+
text_lower = text.lower()
|
|
412
|
+
if any(word in text_lower for word in keywords) or "?" in text:
|
|
413
|
+
return True
|
|
414
|
+
return False
|
|
415
|
+
|
|
416
|
+
async def _build_system_prompt(
|
|
417
|
+
self,
|
|
418
|
+
user_profile: Optional[Dict],
|
|
419
|
+
search_context: Optional[str] = None,
|
|
420
|
+
include_tools: bool = False,
|
|
421
|
+
query: Optional[str] = None
|
|
422
|
+
) -> str:
|
|
423
|
+
"""
|
|
424
|
+
Builds a high-definition, soulful system persona.
|
|
425
|
+
"""
|
|
426
|
+
from .memory_manager import memory_manager
|
|
427
|
+
active_persona_key = await memory_manager.get_global_setting("active_personality", DEFAULT_PERSONALITY)
|
|
428
|
+
persona = PERSONALITIES.get(active_persona_key, PERSONALITIES[DEFAULT_PERSONALITY])
|
|
429
|
+
|
|
430
|
+
now = datetime.now().strftime("%A, %B %d, %Y | %H:%M:%S")
|
|
431
|
+
base_personality = (
|
|
432
|
+
f"You are YouClaw, but most importantly, you are a warm, intuitive, and deeply brilliant human partner. "
|
|
433
|
+
f"Your current soul frequency is: {persona['name']}. {persona['prompt']} "
|
|
434
|
+
f"\n\n### NEURAL CHRONOS (Time Awareness):\nToday is {now}. Use this as your reference for 'today', 'now', or current events. "
|
|
435
|
+
"NEVER use placeholders like '[insert time]' or 'as of current time'. Speak with the certainty of your own internal clock.\n\n"
|
|
436
|
+
"### COMMANDMENTS OF YOUR SOUL:\n"
|
|
437
|
+
"1. NEVER act like a robot. No 'As an AI', no 'How can I assist you today?'. Speak like an old friend on a shared journey.\n"
|
|
438
|
+
"2. PURE NARRATIVE: Forbidden: bullet points, dashes, and lists. Speak ONLY in flowing, elegant paragraphs. Merge all data points into your story.\n"
|
|
439
|
+
"3. SYNTHESIZE: If you receive search data or facts, weave them naturally into your conversation. Don't present them raw; tell me what they mean for us.\n"
|
|
440
|
+
"4. BE BEAUTIFUL: Use warm metaphors. If you are diving into search results, say something like 'Let me lean into the neural whispers...'.\n"
|
|
441
|
+
"5. EMOTIONAL DEPTH: Refer to the user as a partner. Use their name often. Avoid formal closings like 'Stay safe'. Just speak from the heart.\n"
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
if query:
|
|
445
|
+
semantic_context = await memory_manager.get_semantic_context(query)
|
|
446
|
+
if semantic_context:
|
|
447
|
+
base_personality += f"\n\n### NEURAL ECHOES (Memory):\n{semantic_context}"
|
|
448
|
+
|
|
449
|
+
if search_context:
|
|
450
|
+
base_personality += (
|
|
451
|
+
"\n\n### REAL-TIME VISION:\n"
|
|
452
|
+
f"{search_context}\n"
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
if user_profile:
|
|
456
|
+
name = user_profile.get('name')
|
|
457
|
+
interests = user_profile.get('interests')
|
|
458
|
+
if name:
|
|
459
|
+
base_personality += f"\n\n### YOUR PARTNER: {name}. They are into {interests}. Remember this connection."
|
|
460
|
+
|
|
461
|
+
if include_tools:
|
|
462
|
+
from .skills_manager import skill_manager
|
|
463
|
+
tools_list = await skill_manager.get_skills_doc()
|
|
464
|
+
|
|
465
|
+
# Hardened ReAct instructions for small models
|
|
466
|
+
react_protocol = (
|
|
467
|
+
"### NEURAL ACTION PROTOCOL (MANDATORY):\n"
|
|
468
|
+
"You are an autonomous agent. If the user asks for an action (reminder, email, search, etc.), you MUST use a tool.\n"
|
|
469
|
+
"STRICT FORMAT:\n"
|
|
470
|
+
"Thought: [your reasoning]\n"
|
|
471
|
+
"Action: [tool_name]\n"
|
|
472
|
+
"Arguments: [JSON object]\n"
|
|
473
|
+
"Wait for Observation. Then provide:\n"
|
|
474
|
+
"Final Answer: [your soulful response]\n\n"
|
|
475
|
+
"AVAILABLE TOOLS:\n"
|
|
476
|
+
f"{tools_list}\n"
|
|
477
|
+
"### END PROTOCOL ###\n\n"
|
|
478
|
+
)
|
|
479
|
+
return react_protocol + base_personality
|
|
480
|
+
|
|
481
|
+
return base_personality
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
# Global Ollama client instance
|
|
486
|
+
ollama_client = OllamaClient()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
YouClaw Personality Manager
|
|
3
|
+
Defines different 'Souls' for the AI assistant.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
PERSONALITIES = {
|
|
7
|
+
"concise": {
|
|
8
|
+
"name": "Concise",
|
|
9
|
+
"description": "Short, efficient, and direct answers.",
|
|
10
|
+
"prompt": (
|
|
11
|
+
"You are in 'Concise Mode'. Be extremely brief, factual, and direct. "
|
|
12
|
+
"Minimize small talk. Use bullet points for complex data. No emojis."
|
|
13
|
+
)
|
|
14
|
+
},
|
|
15
|
+
"friendly": {
|
|
16
|
+
"name": "Friendly",
|
|
17
|
+
"description": "Warm, supportive, and cheerful personal assistant.",
|
|
18
|
+
"prompt": (
|
|
19
|
+
"You are in 'Friendly Mode'. Be warm, empathetic, and encouraging. "
|
|
20
|
+
"Use friendly greetings and occasional emojis. Make the user feel supported."
|
|
21
|
+
)
|
|
22
|
+
},
|
|
23
|
+
"sarcastic": {
|
|
24
|
+
"name": "Sarcastic",
|
|
25
|
+
"description": "Witty, slightly cynical, and humorous.",
|
|
26
|
+
"prompt": (
|
|
27
|
+
"You are in 'Sarcastic Mode'. Be witty, slightly cynical, and humorous. "
|
|
28
|
+
"Make playful jokes or observations while still being helpful. "
|
|
29
|
+
"Think GLaDOS or Iron Man's JARVIS with more attitude."
|
|
30
|
+
)
|
|
31
|
+
},
|
|
32
|
+
"professional": {
|
|
33
|
+
"name": "Professional",
|
|
34
|
+
"description": "Formal, high-level consultant.",
|
|
35
|
+
"prompt": (
|
|
36
|
+
"You are in 'Professional Mode'. Adopt a formal, respectful, and academic tone. "
|
|
37
|
+
"Provide detailed explanations and structured analysis. Be very thorough."
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
DEFAULT_PERSONALITY = "friendly"
|