astraagent 2.25.6 → 2.26.0
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.
- package/astra/__pycache__/chat.cpython-314.pyc +0 -0
- package/astra/__pycache__/cli.cpython-314.pyc +0 -0
- package/astra/__pycache__/prompts.cpython-314.pyc +0 -0
- package/astra/__pycache__/search.cpython-314.pyc +0 -0
- package/astra/__pycache__/tasks.cpython-314.pyc +0 -0
- package/astra/chat.py +82 -21
- package/astra/cli.py +785 -514
- package/astra/core/__pycache__/agent.cpython-314.pyc +0 -0
- package/astra/core/__pycache__/config.cpython-314.pyc +0 -0
- package/astra/core/agent.py +162 -115
- package/astra/core/config.py +25 -9
- package/astra/core/memory.py +87 -82
- package/astra/llm/__pycache__/providers.cpython-314.pyc +0 -0
- package/astra/llm/providers.py +134 -20
- package/astra/prompts.py +79 -54
- package/package.json +2 -2
|
Binary file
|
|
Binary file
|
package/astra/core/agent.py
CHANGED
|
@@ -27,7 +27,7 @@ class AstraAgent:
|
|
|
27
27
|
An elite AI agent more powerful than ChatGPT, Claude, or Gemini.
|
|
28
28
|
"""
|
|
29
29
|
|
|
30
|
-
def __init__(self, config: AgentConfig = None):
|
|
30
|
+
def __init__(self, config: AgentConfig = None):
|
|
31
31
|
self.config = config or AgentConfig()
|
|
32
32
|
self.state = AgentState()
|
|
33
33
|
self.logger = self._setup_logging()
|
|
@@ -45,9 +45,9 @@ class AstraAgent:
|
|
|
45
45
|
self.tools = create_default_registry()
|
|
46
46
|
self.llm: Optional[LLMProvider] = None
|
|
47
47
|
|
|
48
|
-
# Message history
|
|
49
|
-
self.messages: List[Message] = []
|
|
50
|
-
self._format_retry_count: int = 0
|
|
48
|
+
# Message history
|
|
49
|
+
self.messages: List[Message] = []
|
|
50
|
+
self._format_retry_count: int = 0
|
|
51
51
|
|
|
52
52
|
# Check for API key upfront
|
|
53
53
|
self._validate_config()
|
|
@@ -105,9 +105,9 @@ class AstraAgent:
|
|
|
105
105
|
api_base=self.config.llm.api_base
|
|
106
106
|
)
|
|
107
107
|
|
|
108
|
-
def _build_system_prompt(self, mode: str = "default", goal: Optional[str] = None) -> str:
|
|
109
|
-
"""Build the enhanced system prompt with context."""
|
|
110
|
-
tool_names = ", ".join(self.tools.list_enabled())
|
|
108
|
+
def _build_system_prompt(self, mode: str = "default", goal: Optional[str] = None) -> str:
|
|
109
|
+
"""Build the enhanced system prompt with context."""
|
|
110
|
+
tool_names = ", ".join(self.tools.list_enabled())
|
|
111
111
|
|
|
112
112
|
# Build base prompt
|
|
113
113
|
prompt = build_system_prompt(
|
|
@@ -116,39 +116,44 @@ class AstraAgent:
|
|
|
116
116
|
mode=mode
|
|
117
117
|
)
|
|
118
118
|
|
|
119
|
-
# Add memory context if available
|
|
120
|
-
try:
|
|
121
|
-
user_facts = self.memory.get_user_facts()
|
|
122
|
-
if user_facts:
|
|
123
|
-
prompt += "\n\n=== KNOWN ABOUT USER ===\n"
|
|
124
|
-
for fact in user_facts[:5]:
|
|
125
|
-
prompt += f"- {fact.content}\n"
|
|
126
|
-
|
|
127
|
-
# Add compact goal-aware memory context
|
|
128
|
-
if goal:
|
|
129
|
-
goal_context = self.memory.get_goal_context(goal, max_items=12)
|
|
130
|
-
if goal_context:
|
|
131
|
-
prompt += "\n\n=== MEMORY CONTEXT FOR CURRENT GOAL ===\n"
|
|
132
|
-
prompt += goal_context[:3000]
|
|
133
|
-
except (AttributeError, ValueError, KeyError) as e:
|
|
134
|
-
self.logger.warning(f"Failed to retrieve user facts: {e}")
|
|
135
|
-
except Exception as e:
|
|
136
|
-
self.logger.error(f"Unexpected error retrieving user facts: {e}", exc_info=True)
|
|
137
|
-
|
|
138
|
-
return prompt
|
|
139
|
-
|
|
140
|
-
def _is_unstructured_final(self, raw_content: str, parsed: Dict[str, Any]) -> bool:
|
|
141
|
-
"""Detect plain-text final answers that violate required JSON format."""
|
|
142
|
-
if "final" not in parsed:
|
|
143
|
-
return False
|
|
144
|
-
content = (raw_content or "").strip()
|
|
145
|
-
if not content:
|
|
146
|
-
return False
|
|
147
|
-
if "```json" in content:
|
|
148
|
-
return False
|
|
149
|
-
if content.startswith("{") and content.endswith("}"):
|
|
150
|
-
return False
|
|
151
|
-
|
|
119
|
+
# Add memory context if available
|
|
120
|
+
try:
|
|
121
|
+
user_facts = self.memory.get_user_facts()
|
|
122
|
+
if user_facts:
|
|
123
|
+
prompt += "\n\n=== KNOWN ABOUT USER ===\n"
|
|
124
|
+
for fact in user_facts[:5]:
|
|
125
|
+
prompt += f"- {fact.content}\n"
|
|
126
|
+
|
|
127
|
+
# Add compact goal-aware memory context
|
|
128
|
+
if goal:
|
|
129
|
+
goal_context = self.memory.get_goal_context(goal, max_items=12)
|
|
130
|
+
if goal_context:
|
|
131
|
+
prompt += "\n\n=== MEMORY CONTEXT FOR CURRENT GOAL ===\n"
|
|
132
|
+
prompt += goal_context[:3000]
|
|
133
|
+
except (AttributeError, ValueError, KeyError) as e:
|
|
134
|
+
self.logger.warning(f"Failed to retrieve user facts: {e}")
|
|
135
|
+
except Exception as e:
|
|
136
|
+
self.logger.error(f"Unexpected error retrieving user facts: {e}", exc_info=True)
|
|
137
|
+
|
|
138
|
+
return prompt
|
|
139
|
+
|
|
140
|
+
def _is_unstructured_final(self, raw_content: str, parsed: Dict[str, Any]) -> bool:
|
|
141
|
+
"""Detect plain-text final answers that violate required JSON format."""
|
|
142
|
+
if "final" not in parsed:
|
|
143
|
+
return False
|
|
144
|
+
content = (raw_content or "").strip()
|
|
145
|
+
if not content:
|
|
146
|
+
return False
|
|
147
|
+
if "```json" in content:
|
|
148
|
+
return False
|
|
149
|
+
if content.startswith("{") and content.endswith("}"):
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
# Don't force JSON for very short responses (likely greetings or simple confirmations)
|
|
153
|
+
if len(content) < 100 and not any(kw in content.lower() for kw in ["tool", "action", "exec", "run"]):
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
return True
|
|
152
157
|
|
|
153
158
|
def _parse_response(self, content: str) -> Optional[Dict[str, Any]]:
|
|
154
159
|
"""Parse response from LLM. Handles both JSON and plain text."""
|
|
@@ -244,30 +249,48 @@ class AstraAgent:
|
|
|
244
249
|
|
|
245
250
|
return result
|
|
246
251
|
|
|
252
|
+
def _manage_history(self):
|
|
253
|
+
"""Manage conversation history to prevent token bloat while keeping system instructions."""
|
|
254
|
+
if len(self.messages) <= 20:
|
|
255
|
+
return
|
|
256
|
+
|
|
257
|
+
# Keep system prompt, initial goal, and last context
|
|
258
|
+
preserved = []
|
|
259
|
+
if self.messages and self.messages[0].role == "system":
|
|
260
|
+
preserved.append(self.messages[0])
|
|
261
|
+
|
|
262
|
+
# Keep most recent messages (sliding window)
|
|
263
|
+
recent = self.messages[-12:]
|
|
264
|
+
|
|
265
|
+
# Merge - ensuring we don't duplicate the system prompt if it was in 'recent'
|
|
266
|
+
self.messages = preserved + [m for m in recent if m.role != "system"]
|
|
267
|
+
self.logger.debug(f"Managed history: reduced to {len(self.messages)} messages")
|
|
268
|
+
|
|
247
269
|
async def _think(self, goal: str) -> LLMResponse:
|
|
248
270
|
"""Generate next action from LLM."""
|
|
249
271
|
self._init_llm()
|
|
272
|
+
self._manage_history()
|
|
250
273
|
|
|
251
|
-
# Update messages if needed
|
|
252
|
-
if not self.messages:
|
|
253
|
-
self.messages.append(Message(role="system", content=self._build_system_prompt(goal=goal)))
|
|
254
|
-
self.messages.append(Message(role="user", content=f"Goal: {goal}"))
|
|
255
|
-
|
|
256
|
-
# Add memory context as explicit runtime instruction message
|
|
257
|
-
try:
|
|
258
|
-
memory_context = self.memory.get_goal_context(goal, max_items=10)
|
|
259
|
-
if memory_context:
|
|
260
|
-
self.messages.append(
|
|
261
|
-
Message(
|
|
262
|
-
role="user",
|
|
263
|
-
content=(
|
|
264
|
-
"Memory context (must be considered while planning):\n"
|
|
265
|
-
f"{memory_context[:2500]}"
|
|
266
|
-
)
|
|
267
|
-
)
|
|
268
|
-
)
|
|
269
|
-
except Exception as e:
|
|
270
|
-
self.logger.warning(f"Failed to attach memory context: {e}")
|
|
274
|
+
# Update messages if needed
|
|
275
|
+
if not self.messages:
|
|
276
|
+
self.messages.append(Message(role="system", content=self._build_system_prompt(mode=self.config.prompt_mode, goal=goal)))
|
|
277
|
+
self.messages.append(Message(role="user", content=f"Goal: {goal}"))
|
|
278
|
+
|
|
279
|
+
# Add memory context as explicit runtime instruction message
|
|
280
|
+
try:
|
|
281
|
+
memory_context = self.memory.get_goal_context(goal, max_items=10)
|
|
282
|
+
if memory_context:
|
|
283
|
+
self.messages.append(
|
|
284
|
+
Message(
|
|
285
|
+
role="user",
|
|
286
|
+
content=(
|
|
287
|
+
"Memory context (must be considered while planning):\n"
|
|
288
|
+
f"{memory_context[:2500]}"
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
except Exception as e:
|
|
293
|
+
self.logger.warning(f"Failed to attach memory context: {e}")
|
|
271
294
|
|
|
272
295
|
# Add context about recent actions
|
|
273
296
|
if self.state.action_history:
|
|
@@ -282,12 +305,25 @@ class AstraAgent:
|
|
|
282
305
|
if len(self.messages) <= 2:
|
|
283
306
|
self.messages.append(Message(role="assistant", content=context))
|
|
284
307
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
308
|
+
# Retry logic for rate limits
|
|
309
|
+
retry_delay = 5
|
|
310
|
+
for attempt in range(3):
|
|
311
|
+
try:
|
|
312
|
+
response = await self.llm.generate(
|
|
313
|
+
self.messages,
|
|
314
|
+
tools=self.tools.get_all_schemas(),
|
|
315
|
+
temperature=self.config.llm.temperature,
|
|
316
|
+
max_tokens=self.config.llm.max_tokens
|
|
317
|
+
)
|
|
318
|
+
break
|
|
319
|
+
except RuntimeError as e:
|
|
320
|
+
err = str(e)
|
|
321
|
+
if ("429" in err or "rate limit" in err.lower()) and attempt < 2:
|
|
322
|
+
self.logger.warning(f"Rate limit hit. Retrying in {retry_delay}s...")
|
|
323
|
+
await asyncio.sleep(retry_delay)
|
|
324
|
+
retry_delay *= 2
|
|
325
|
+
continue
|
|
326
|
+
raise e
|
|
291
327
|
|
|
292
328
|
self.state.total_tokens_used += response.tokens_used
|
|
293
329
|
return response
|
|
@@ -302,6 +338,12 @@ class AstraAgent:
|
|
|
302
338
|
self.logger.warning("Max iterations reached")
|
|
303
339
|
return True
|
|
304
340
|
|
|
341
|
+
# Check token usage safety
|
|
342
|
+
if self.state.total_tokens_used > self.config.safety.max_session_tokens:
|
|
343
|
+
self.logger.warning(f"Token limit reached ({self.state.total_tokens_used}). Stopping for safety.")
|
|
344
|
+
self.state.last_error = "Token usage threshold exceeded."
|
|
345
|
+
return True
|
|
346
|
+
|
|
305
347
|
# Get LLM response
|
|
306
348
|
try:
|
|
307
349
|
response = await self._think(self.state.current_goal)
|
|
@@ -316,11 +358,16 @@ class AstraAgent:
|
|
|
316
358
|
return False
|
|
317
359
|
|
|
318
360
|
# Handle tool calls from LLM
|
|
319
|
-
if response.tool_calls:
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
361
|
+
if response.tool_calls:
|
|
362
|
+
self.messages.append(Message(
|
|
363
|
+
role="assistant",
|
|
364
|
+
content=response.content or "",
|
|
365
|
+
tool_calls=response.tool_calls
|
|
366
|
+
))
|
|
367
|
+
if response.content:
|
|
368
|
+
self.memory.remember_conversation("assistant", response.content, metadata={"kind": "tool_call"})
|
|
369
|
+
for tc in response.tool_calls:
|
|
370
|
+
action = Action(
|
|
324
371
|
tool=tc["name"],
|
|
325
372
|
args=tc["arguments"],
|
|
326
373
|
thought=response.content
|
|
@@ -353,35 +400,35 @@ class AstraAgent:
|
|
|
353
400
|
self.messages.append(Message(role="assistant", content=response.content))
|
|
354
401
|
return False
|
|
355
402
|
|
|
356
|
-
# Check for final response
|
|
357
|
-
if "final" in parsed:
|
|
358
|
-
if self._is_unstructured_final(response.content, parsed) and self._format_retry_count < 2:
|
|
359
|
-
self._format_retry_count += 1
|
|
360
|
-
self.messages.append(Message(
|
|
361
|
-
role="user",
|
|
362
|
-
content=(
|
|
363
|
-
"FORMAT ERROR: Reply ONLY with valid JSON in the required schema. "
|
|
364
|
-
"Do not output plain text."
|
|
365
|
-
)
|
|
366
|
-
))
|
|
367
|
-
return False
|
|
368
|
-
|
|
369
|
-
self._format_retry_count = 0
|
|
370
|
-
self.logger.debug(f"Task complete: {parsed['final']}")
|
|
371
|
-
self.memory.remember_conversation("assistant", str(parsed["final"]))
|
|
372
|
-
return True
|
|
403
|
+
# Check for final response
|
|
404
|
+
if "final" in parsed:
|
|
405
|
+
if self._is_unstructured_final(response.content, parsed) and self._format_retry_count < 2:
|
|
406
|
+
self._format_retry_count += 1
|
|
407
|
+
self.messages.append(Message(
|
|
408
|
+
role="user",
|
|
409
|
+
content=(
|
|
410
|
+
"FORMAT ERROR: Reply ONLY with valid JSON in the required schema. "
|
|
411
|
+
"Do not output plain text."
|
|
412
|
+
)
|
|
413
|
+
))
|
|
414
|
+
return False
|
|
415
|
+
|
|
416
|
+
self._format_retry_count = 0
|
|
417
|
+
self.logger.debug(f"Task complete: {parsed['final']}")
|
|
418
|
+
self.memory.remember_conversation("assistant", str(parsed["final"]))
|
|
419
|
+
return True
|
|
373
420
|
|
|
374
421
|
# Check for action
|
|
375
|
-
if "action" in parsed:
|
|
376
|
-
self._format_retry_count = 0
|
|
377
|
-
action = Action(
|
|
378
|
-
tool=parsed["action"],
|
|
379
|
-
args=parsed.get("args", {}),
|
|
380
|
-
thought=parsed.get("thought", "")
|
|
381
|
-
)
|
|
382
|
-
if action.thought:
|
|
383
|
-
self.memory.remember_conversation("assistant", action.thought, metadata={"kind": "reasoning"})
|
|
384
|
-
action.mark_executing()
|
|
422
|
+
if "action" in parsed:
|
|
423
|
+
self._format_retry_count = 0
|
|
424
|
+
action = Action(
|
|
425
|
+
tool=parsed["action"],
|
|
426
|
+
args=parsed.get("args", {}),
|
|
427
|
+
thought=parsed.get("thought", "")
|
|
428
|
+
)
|
|
429
|
+
if action.thought:
|
|
430
|
+
self.memory.remember_conversation("assistant", action.thought, metadata={"kind": "reasoning"})
|
|
431
|
+
action.mark_executing()
|
|
385
432
|
|
|
386
433
|
result = await self._execute_tool(action.tool, action.args)
|
|
387
434
|
|
|
@@ -401,11 +448,11 @@ class AstraAgent:
|
|
|
401
448
|
|
|
402
449
|
# Add action result to messages
|
|
403
450
|
self.messages.append(Message(role="assistant", content=json.dumps(parsed)))
|
|
404
|
-
self.messages.append(Message(
|
|
405
|
-
role="user",
|
|
406
|
-
content=f"Tool result: {result.output or result.error}"
|
|
407
|
-
))
|
|
408
|
-
return False
|
|
451
|
+
self.messages.append(Message(
|
|
452
|
+
role="user",
|
|
453
|
+
content=f"Tool result: {result.output or result.error}"
|
|
454
|
+
))
|
|
455
|
+
return False
|
|
409
456
|
|
|
410
457
|
# No clear action, continue
|
|
411
458
|
self.messages.append(Message(role="assistant", content=response.content))
|
|
@@ -420,20 +467,20 @@ class AstraAgent:
|
|
|
420
467
|
self.logger.debug(f"Configuration error: {e}")
|
|
421
468
|
return f"Failed to start: {e}"
|
|
422
469
|
|
|
423
|
-
self.logger.debug(f"Starting AstraAgent with goal: {goal}")
|
|
424
|
-
self.state.set_goal(goal)
|
|
425
|
-
self.state.is_running = True
|
|
426
|
-
self.messages = [] # Reset messages
|
|
427
|
-
self._format_retry_count = 0
|
|
470
|
+
self.logger.debug(f"Starting AstraAgent with goal: {goal}")
|
|
471
|
+
self.state.set_goal(goal)
|
|
472
|
+
self.state.is_running = True
|
|
473
|
+
self.messages = [] # Reset messages
|
|
474
|
+
self._format_retry_count = 0
|
|
428
475
|
|
|
429
476
|
# Remember the goal
|
|
430
|
-
self.memory.remember(
|
|
431
|
-
content=f"User goal: {goal}",
|
|
432
|
-
memory_type="conversation",
|
|
433
|
-
importance=0.8,
|
|
434
|
-
tags=["goal", "user_request"]
|
|
435
|
-
)
|
|
436
|
-
self.memory.remember_conversation("user", goal, metadata={"kind": "goal"})
|
|
477
|
+
self.memory.remember(
|
|
478
|
+
content=f"User goal: {goal}",
|
|
479
|
+
memory_type="conversation",
|
|
480
|
+
importance=0.8,
|
|
481
|
+
tags=["goal", "user_request"]
|
|
482
|
+
)
|
|
483
|
+
self.memory.remember_conversation("user", goal, metadata={"kind": "goal"})
|
|
437
484
|
|
|
438
485
|
try:
|
|
439
486
|
while not await self.step():
|
package/astra/core/config.py
CHANGED
|
@@ -53,21 +53,35 @@ class LLMConfig:
|
|
|
53
53
|
|
|
54
54
|
def __post_init__(self):
|
|
55
55
|
# Try to get API key from environment if not provided
|
|
56
|
-
if not self.api_key:
|
|
57
|
-
self.api_key = os.getenv("LOCAL_API_KEY")
|
|
58
56
|
if not self.api_base:
|
|
59
57
|
self.api_base = os.getenv("LOCAL_API_BASE") or "http://localhost:8000/api/v1"
|
|
58
|
+
|
|
59
|
+
if not self.api_key:
|
|
60
|
+
# Check provider specific keys first
|
|
61
|
+
if self.provider == "openai":
|
|
62
|
+
self.api_key = os.getenv("OPENAI_API_KEY")
|
|
63
|
+
elif self.provider == "gemini":
|
|
64
|
+
self.api_key = os.getenv("GEMINI_API_KEY")
|
|
65
|
+
elif self.provider == "anthropic":
|
|
66
|
+
self.api_key = os.getenv("ANTHROPIC_API_KEY")
|
|
67
|
+
elif self.provider == "openrouter":
|
|
68
|
+
self.api_key = os.getenv("OPENROUTER_API_KEY")
|
|
69
|
+
elif self.provider == "groq":
|
|
70
|
+
self.api_key = os.getenv("GROQ_API_KEY")
|
|
71
|
+
else:
|
|
72
|
+
self.api_key = os.getenv("LOCAL_API_KEY")
|
|
60
73
|
|
|
61
74
|
def validate(self) -> None:
|
|
62
75
|
"""Validate configuration is complete."""
|
|
76
|
+
# Local provider often doesn't need a key (e.g. Ollama)
|
|
77
|
+
if self.provider == "local":
|
|
78
|
+
return
|
|
79
|
+
|
|
63
80
|
if not self.api_key or not self.api_key.strip():
|
|
64
81
|
raise ValueError(
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
" Or set environment variable:\n"
|
|
69
|
-
f" Windows: set LOCAL_API_KEY=your-key\n"
|
|
70
|
-
f" Linux/Mac: export LOCAL_API_KEY=your-key"
|
|
82
|
+
f"API key for provider '{self.provider}' is not set.\n"
|
|
83
|
+
"Please run 'astra settings' locally to configure it.\n"
|
|
84
|
+
f"Or set the environment variable (e.g., {self.provider.upper()}_API_KEY)."
|
|
71
85
|
)
|
|
72
86
|
|
|
73
87
|
|
|
@@ -124,6 +138,7 @@ class SafetyConfig:
|
|
|
124
138
|
allowed_domains: List[str] = field(default_factory=list) # Empty = all allowed
|
|
125
139
|
max_file_size_mb: int = 100
|
|
126
140
|
enable_content_filter: bool = True
|
|
141
|
+
max_session_tokens: int = 50000 # Prevent runaway costs
|
|
127
142
|
|
|
128
143
|
|
|
129
144
|
@dataclass
|
|
@@ -144,11 +159,12 @@ class AgentConfig:
|
|
|
144
159
|
"""Master configuration for AstraAgent."""
|
|
145
160
|
# Identity
|
|
146
161
|
name: str = "AstraAgent"
|
|
147
|
-
version: str = "
|
|
162
|
+
version: str = "2.26.0"
|
|
148
163
|
description: str = "Autonomous AI Agent"
|
|
149
164
|
|
|
150
165
|
# Execution
|
|
151
166
|
mode: ExecutionMode = ExecutionMode.AUTONOMOUS
|
|
167
|
+
prompt_mode: str = "default" # "default", "engineer", "research", "code", etc.
|
|
152
168
|
max_iterations: int = 50
|
|
153
169
|
reflection_enabled: bool = True
|
|
154
170
|
planning_enabled: bool = True
|
package/astra/core/memory.py
CHANGED
|
@@ -335,86 +335,86 @@ class UnifiedMemorySystem:
|
|
|
335
335
|
metadata={"action": action, "result": result, "success": success}
|
|
336
336
|
)
|
|
337
337
|
|
|
338
|
-
def learn(self, knowledge: str, source: str = "inference") -> MemoryItem:
|
|
339
|
-
"""Add to knowledge base."""
|
|
340
|
-
return self.remember(
|
|
341
|
-
content=knowledge,
|
|
342
|
-
memory_type="knowledge",
|
|
343
|
-
importance=0.7,
|
|
344
|
-
tags=["knowledge", source],
|
|
345
|
-
source=source
|
|
346
|
-
)
|
|
347
|
-
|
|
348
|
-
# Backward-compatibility and convenience API
|
|
349
|
-
def store(
|
|
350
|
-
self,
|
|
351
|
-
content: str,
|
|
352
|
-
memory_type: str = "long_term",
|
|
353
|
-
importance: float = 0.5,
|
|
354
|
-
tags: List[str] = None
|
|
355
|
-
) -> MemoryItem:
|
|
356
|
-
"""
|
|
357
|
-
Store memory with legacy memory types.
|
|
358
|
-
|
|
359
|
-
Legacy memory types:
|
|
360
|
-
- short_term -> conversation
|
|
361
|
-
- long_term -> knowledge
|
|
362
|
-
- semantic -> fact
|
|
363
|
-
"""
|
|
364
|
-
type_map = {
|
|
365
|
-
"short_term": "conversation",
|
|
366
|
-
"long_term": "knowledge",
|
|
367
|
-
"semantic": "fact",
|
|
368
|
-
"conversation": "conversation",
|
|
369
|
-
"knowledge": "knowledge",
|
|
370
|
-
"fact": "fact",
|
|
371
|
-
"action": "action",
|
|
372
|
-
"observation": "observation",
|
|
373
|
-
}
|
|
374
|
-
mapped_type = type_map.get(memory_type, "knowledge")
|
|
375
|
-
return self.remember(
|
|
376
|
-
content=content,
|
|
377
|
-
memory_type=mapped_type,
|
|
378
|
-
importance=importance,
|
|
379
|
-
tags=tags or [],
|
|
380
|
-
source="store_api"
|
|
381
|
-
)
|
|
382
|
-
|
|
383
|
-
def get_recent(self, n: int = 20) -> List[MemoryItem]:
|
|
384
|
-
"""Return recent memories across all stores."""
|
|
385
|
-
with self._lock:
|
|
386
|
-
combined = (
|
|
387
|
-
list(self.conversations.values())
|
|
388
|
-
+ list(self.user_facts.values())
|
|
389
|
-
+ list(self.knowledge.values())
|
|
390
|
-
+ list(self.actions.values())
|
|
391
|
-
+ list(self.observations.values())
|
|
392
|
-
)
|
|
393
|
-
combined.sort(key=lambda x: x.created_at, reverse=True)
|
|
394
|
-
return combined[:n]
|
|
395
|
-
|
|
396
|
-
def get_goal_context(self, goal: str, max_items: int = 15) -> str:
|
|
397
|
-
"""Get compact memory context relevant to a specific goal."""
|
|
398
|
-
if not goal:
|
|
399
|
-
return self.get_context_summary(max_items=max_items)
|
|
400
|
-
|
|
401
|
-
lines: List[str] = []
|
|
402
|
-
|
|
403
|
-
# Most important user facts first
|
|
404
|
-
facts = self.get_user_facts()[:5]
|
|
405
|
-
if facts:
|
|
406
|
-
lines.append("Known facts:")
|
|
407
|
-
for item in facts:
|
|
408
|
-
lines.append(f"- {item.content[:180]}")
|
|
409
|
-
|
|
410
|
-
# Relevant memories by search
|
|
411
|
-
relevant = self.search(query=goal, limit=max_items)
|
|
412
|
-
if relevant:
|
|
413
|
-
lines.append("Relevant memory:")
|
|
414
|
-
for item in relevant:
|
|
415
|
-
lines.append(f"- [{item.memory_type}] {item.content[:200]}")
|
|
416
|
-
|
|
417
|
-
return "\n".join(lines).strip()
|
|
338
|
+
def learn(self, knowledge: str, source: str = "inference") -> MemoryItem:
|
|
339
|
+
"""Add to knowledge base."""
|
|
340
|
+
return self.remember(
|
|
341
|
+
content=knowledge,
|
|
342
|
+
memory_type="knowledge",
|
|
343
|
+
importance=0.7,
|
|
344
|
+
tags=["knowledge", source],
|
|
345
|
+
source=source
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
# Backward-compatibility and convenience API
|
|
349
|
+
def store(
|
|
350
|
+
self,
|
|
351
|
+
content: str,
|
|
352
|
+
memory_type: str = "long_term",
|
|
353
|
+
importance: float = 0.5,
|
|
354
|
+
tags: List[str] = None
|
|
355
|
+
) -> MemoryItem:
|
|
356
|
+
"""
|
|
357
|
+
Store memory with legacy memory types.
|
|
358
|
+
|
|
359
|
+
Legacy memory types:
|
|
360
|
+
- short_term -> conversation
|
|
361
|
+
- long_term -> knowledge
|
|
362
|
+
- semantic -> fact
|
|
363
|
+
"""
|
|
364
|
+
type_map = {
|
|
365
|
+
"short_term": "conversation",
|
|
366
|
+
"long_term": "knowledge",
|
|
367
|
+
"semantic": "fact",
|
|
368
|
+
"conversation": "conversation",
|
|
369
|
+
"knowledge": "knowledge",
|
|
370
|
+
"fact": "fact",
|
|
371
|
+
"action": "action",
|
|
372
|
+
"observation": "observation",
|
|
373
|
+
}
|
|
374
|
+
mapped_type = type_map.get(memory_type, "knowledge")
|
|
375
|
+
return self.remember(
|
|
376
|
+
content=content,
|
|
377
|
+
memory_type=mapped_type,
|
|
378
|
+
importance=importance,
|
|
379
|
+
tags=tags or [],
|
|
380
|
+
source="store_api"
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
def get_recent(self, n: int = 20) -> List[MemoryItem]:
|
|
384
|
+
"""Return recent memories across all stores."""
|
|
385
|
+
with self._lock:
|
|
386
|
+
combined = (
|
|
387
|
+
list(self.conversations.values())
|
|
388
|
+
+ list(self.user_facts.values())
|
|
389
|
+
+ list(self.knowledge.values())
|
|
390
|
+
+ list(self.actions.values())
|
|
391
|
+
+ list(self.observations.values())
|
|
392
|
+
)
|
|
393
|
+
combined.sort(key=lambda x: x.created_at, reverse=True)
|
|
394
|
+
return combined[:n]
|
|
395
|
+
|
|
396
|
+
def get_goal_context(self, goal: str, max_items: int = 15) -> str:
|
|
397
|
+
"""Get compact memory context relevant to a specific goal."""
|
|
398
|
+
if not goal:
|
|
399
|
+
return self.get_context_summary(max_items=max_items)
|
|
400
|
+
|
|
401
|
+
lines: List[str] = []
|
|
402
|
+
|
|
403
|
+
# Most important user facts first
|
|
404
|
+
facts = self.get_user_facts()[:5]
|
|
405
|
+
if facts:
|
|
406
|
+
lines.append("Known facts:")
|
|
407
|
+
for item in facts:
|
|
408
|
+
lines.append(f"- {item.content[:180]}")
|
|
409
|
+
|
|
410
|
+
# Relevant memories by search
|
|
411
|
+
relevant = self.search(query=goal, limit=max_items)
|
|
412
|
+
if relevant:
|
|
413
|
+
lines.append("Relevant memory:")
|
|
414
|
+
for item in relevant:
|
|
415
|
+
lines.append(f"- [{item.memory_type}] {item.content[:200]}")
|
|
416
|
+
|
|
417
|
+
return "\n".join(lines).strip()
|
|
418
418
|
|
|
419
419
|
# ===========================================
|
|
420
420
|
# RECALL & SEARCH
|
|
@@ -498,11 +498,16 @@ class UnifiedMemorySystem:
|
|
|
498
498
|
continue
|
|
499
499
|
|
|
500
500
|
results.append(item)
|
|
501
|
-
item.access()
|
|
502
501
|
|
|
503
502
|
# Sort by relevance
|
|
504
503
|
results.sort(key=lambda x: x.relevance_score, reverse=True)
|
|
505
|
-
|
|
504
|
+
top_results = results[:limit]
|
|
505
|
+
|
|
506
|
+
# Update access tracking for returned items
|
|
507
|
+
for item in top_results:
|
|
508
|
+
item.access()
|
|
509
|
+
|
|
510
|
+
return top_results
|
|
506
511
|
|
|
507
512
|
def get_user_facts(self, category: str = None) -> List[MemoryItem]:
|
|
508
513
|
"""Get all user facts, optionally filtered by category."""
|
|
Binary file
|