autonomy phase 2
This commit is contained in:
1
cortex/autonomy/actions/__init__.py
Normal file
1
cortex/autonomy/actions/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Autonomous action execution system."""
|
||||
480
cortex/autonomy/actions/autonomous_actions.py
Normal file
480
cortex/autonomy/actions/autonomous_actions.py
Normal file
@@ -0,0 +1,480 @@
|
||||
"""
|
||||
Autonomous Action Manager - executes safe, self-initiated actions.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import json
|
||||
from typing import Dict, List, Any, Optional
|
||||
from datetime import datetime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AutonomousActionManager:
|
||||
"""
|
||||
Manages safe autonomous actions that Lyra can take without explicit user prompting.
|
||||
|
||||
Whitelist of allowed actions:
|
||||
- create_memory: Store information in NeoMem
|
||||
- update_goal: Modify goal status
|
||||
- schedule_reminder: Create future reminder
|
||||
- summarize_session: Generate conversation summary
|
||||
- learn_topic: Add topic to learning queue
|
||||
- update_focus: Change current focus area
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize action manager with whitelisted actions."""
|
||||
self.allowed_actions = {
|
||||
"create_memory": self._create_memory,
|
||||
"update_goal": self._update_goal,
|
||||
"schedule_reminder": self._schedule_reminder,
|
||||
"summarize_session": self._summarize_session,
|
||||
"learn_topic": self._learn_topic,
|
||||
"update_focus": self._update_focus
|
||||
}
|
||||
|
||||
self.action_log = [] # Track all actions for audit
|
||||
|
||||
async def execute_action(
|
||||
self,
|
||||
action_type: str,
|
||||
parameters: Dict[str, Any],
|
||||
context: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Execute a single autonomous action.
|
||||
|
||||
Args:
|
||||
action_type: Type of action (must be in whitelist)
|
||||
parameters: Action-specific parameters
|
||||
context: Current context state
|
||||
|
||||
Returns:
|
||||
{
|
||||
"success": bool,
|
||||
"action": action_type,
|
||||
"result": action_result,
|
||||
"timestamp": ISO timestamp,
|
||||
"error": optional error message
|
||||
}
|
||||
"""
|
||||
# Safety check: action must be whitelisted
|
||||
if action_type not in self.allowed_actions:
|
||||
logger.error(f"[ACTIONS] Attempted to execute non-whitelisted action: {action_type}")
|
||||
return {
|
||||
"success": False,
|
||||
"action": action_type,
|
||||
"error": f"Action '{action_type}' not in whitelist",
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
try:
|
||||
logger.info(f"[ACTIONS] Executing autonomous action: {action_type}")
|
||||
|
||||
# Execute the action
|
||||
action_func = self.allowed_actions[action_type]
|
||||
result = await action_func(parameters, context)
|
||||
|
||||
# Log successful action
|
||||
action_record = {
|
||||
"success": True,
|
||||
"action": action_type,
|
||||
"result": result,
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"parameters": parameters
|
||||
}
|
||||
|
||||
self.action_log.append(action_record)
|
||||
logger.info(f"[ACTIONS] Action {action_type} completed successfully")
|
||||
|
||||
return action_record
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[ACTIONS] Action {action_type} failed: {e}")
|
||||
|
||||
error_record = {
|
||||
"success": False,
|
||||
"action": action_type,
|
||||
"error": str(e),
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"parameters": parameters
|
||||
}
|
||||
|
||||
self.action_log.append(error_record)
|
||||
return error_record
|
||||
|
||||
async def execute_batch(
|
||||
self,
|
||||
actions: List[Dict[str, Any]],
|
||||
context: Dict[str, Any]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Execute multiple actions sequentially.
|
||||
|
||||
Args:
|
||||
actions: List of {"action": str, "parameters": dict}
|
||||
context: Current context state
|
||||
|
||||
Returns:
|
||||
List of action results
|
||||
"""
|
||||
results = []
|
||||
|
||||
for action_spec in actions:
|
||||
action_type = action_spec.get("action")
|
||||
parameters = action_spec.get("parameters", {})
|
||||
|
||||
result = await self.execute_action(action_type, parameters, context)
|
||||
results.append(result)
|
||||
|
||||
# Stop on first failure if critical
|
||||
if not result["success"] and action_spec.get("critical", False):
|
||||
logger.warning(f"[ACTIONS] Critical action {action_type} failed, stopping batch")
|
||||
break
|
||||
|
||||
return results
|
||||
|
||||
# ========================================
|
||||
# Whitelisted Action Implementations
|
||||
# ========================================
|
||||
|
||||
async def _create_memory(
|
||||
self,
|
||||
parameters: Dict[str, Any],
|
||||
context: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a memory entry in NeoMem.
|
||||
|
||||
Parameters:
|
||||
- text: Memory content (required)
|
||||
- tags: Optional tags for memory
|
||||
- importance: 0.0-1.0 importance score
|
||||
"""
|
||||
text = parameters.get("text")
|
||||
if not text:
|
||||
raise ValueError("Memory text required")
|
||||
|
||||
tags = parameters.get("tags", [])
|
||||
importance = parameters.get("importance", 0.5)
|
||||
session_id = context.get("session_id", "autonomous")
|
||||
|
||||
# Import NeoMem client
|
||||
try:
|
||||
from memory.neomem_client import store_memory
|
||||
|
||||
result = await store_memory(
|
||||
text=text,
|
||||
session_id=session_id,
|
||||
tags=tags,
|
||||
importance=importance
|
||||
)
|
||||
|
||||
return {
|
||||
"memory_id": result.get("id"),
|
||||
"text": text[:50] + "..." if len(text) > 50 else text
|
||||
}
|
||||
|
||||
except ImportError:
|
||||
logger.warning("[ACTIONS] NeoMem client not available, simulating memory storage")
|
||||
return {
|
||||
"memory_id": "simulated",
|
||||
"text": text[:50] + "..." if len(text) > 50 else text,
|
||||
"note": "NeoMem not available, memory not persisted"
|
||||
}
|
||||
|
||||
async def _update_goal(
|
||||
self,
|
||||
parameters: Dict[str, Any],
|
||||
context: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Update goal status in self-state.
|
||||
|
||||
Parameters:
|
||||
- goal_id: Goal identifier (required)
|
||||
- status: New status (pending/in_progress/completed)
|
||||
- progress: Optional progress note
|
||||
"""
|
||||
goal_id = parameters.get("goal_id")
|
||||
if not goal_id:
|
||||
raise ValueError("goal_id required")
|
||||
|
||||
status = parameters.get("status", "in_progress")
|
||||
progress = parameters.get("progress")
|
||||
|
||||
# Import self-state manager
|
||||
from autonomy.self.state import get_self_state_instance
|
||||
|
||||
state = get_self_state_instance()
|
||||
active_goals = state._state.get("active_goals", [])
|
||||
|
||||
# Find and update goal
|
||||
updated = False
|
||||
for goal in active_goals:
|
||||
if isinstance(goal, dict) and goal.get("id") == goal_id:
|
||||
goal["status"] = status
|
||||
if progress:
|
||||
goal["progress"] = progress
|
||||
goal["updated_at"] = datetime.utcnow().isoformat()
|
||||
updated = True
|
||||
break
|
||||
|
||||
if updated:
|
||||
state._save_state()
|
||||
return {
|
||||
"goal_id": goal_id,
|
||||
"status": status,
|
||||
"updated": True
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"goal_id": goal_id,
|
||||
"updated": False,
|
||||
"note": "Goal not found"
|
||||
}
|
||||
|
||||
async def _schedule_reminder(
|
||||
self,
|
||||
parameters: Dict[str, Any],
|
||||
context: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Schedule a future reminder.
|
||||
|
||||
Parameters:
|
||||
- message: Reminder text (required)
|
||||
- delay_minutes: Minutes until reminder
|
||||
- priority: 0.0-1.0 priority score
|
||||
"""
|
||||
message = parameters.get("message")
|
||||
if not message:
|
||||
raise ValueError("Reminder message required")
|
||||
|
||||
delay_minutes = parameters.get("delay_minutes", 60)
|
||||
priority = parameters.get("priority", 0.5)
|
||||
|
||||
# For now, store in self-state's learning queue
|
||||
# In future: integrate with scheduler/cron system
|
||||
from autonomy.self.state import get_self_state_instance
|
||||
|
||||
state = get_self_state_instance()
|
||||
|
||||
reminder = {
|
||||
"type": "reminder",
|
||||
"message": message,
|
||||
"scheduled_at": datetime.utcnow().isoformat(),
|
||||
"trigger_at_minutes": delay_minutes,
|
||||
"priority": priority
|
||||
}
|
||||
|
||||
# Add to learning queue as placeholder
|
||||
state._state.setdefault("reminders", []).append(reminder)
|
||||
state._save_state(state._state) # Pass state dict as argument
|
||||
|
||||
logger.info(f"[ACTIONS] Reminder scheduled: {message} (in {delay_minutes}min)")
|
||||
|
||||
return {
|
||||
"message": message,
|
||||
"delay_minutes": delay_minutes,
|
||||
"note": "Reminder stored in self-state (scheduler integration pending)"
|
||||
}
|
||||
|
||||
async def _summarize_session(
|
||||
self,
|
||||
parameters: Dict[str, Any],
|
||||
context: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate a summary of current session.
|
||||
|
||||
Parameters:
|
||||
- max_length: Max summary length in words
|
||||
- focus_topics: Optional list of topics to emphasize
|
||||
"""
|
||||
max_length = parameters.get("max_length", 200)
|
||||
session_id = context.get("session_id", "unknown")
|
||||
|
||||
# Import summarizer (from deferred_summary or create simple one)
|
||||
try:
|
||||
from utils.deferred_summary import summarize_conversation
|
||||
|
||||
summary = await summarize_conversation(
|
||||
session_id=session_id,
|
||||
max_words=max_length
|
||||
)
|
||||
|
||||
return {
|
||||
"summary": summary,
|
||||
"word_count": len(summary.split())
|
||||
}
|
||||
|
||||
except ImportError:
|
||||
# Fallback: simple summary
|
||||
message_count = context.get("message_count", 0)
|
||||
focus = context.get("monologue", {}).get("intent", "general")
|
||||
|
||||
summary = f"Session {session_id}: {message_count} messages exchanged, focused on {focus}."
|
||||
|
||||
return {
|
||||
"summary": summary,
|
||||
"word_count": len(summary.split()),
|
||||
"note": "Simple summary (full summarizer not available)"
|
||||
}
|
||||
|
||||
async def _learn_topic(
|
||||
self,
|
||||
parameters: Dict[str, Any],
|
||||
context: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Add topic to learning queue.
|
||||
|
||||
Parameters:
|
||||
- topic: Topic name (required)
|
||||
- reason: Why this topic
|
||||
- priority: 0.0-1.0 priority score
|
||||
"""
|
||||
topic = parameters.get("topic")
|
||||
if not topic:
|
||||
raise ValueError("Topic required")
|
||||
|
||||
reason = parameters.get("reason", "autonomous learning")
|
||||
priority = parameters.get("priority", 0.5)
|
||||
|
||||
# Import self-state manager
|
||||
from autonomy.self.state import get_self_state_instance
|
||||
|
||||
state = get_self_state_instance()
|
||||
state.add_learning_goal(topic) # Only pass topic parameter
|
||||
|
||||
logger.info(f"[ACTIONS] Added to learning queue: {topic} (reason: {reason})")
|
||||
|
||||
return {
|
||||
"topic": topic,
|
||||
"reason": reason,
|
||||
"queue_position": len(state._state.get("learning_queue", []))
|
||||
}
|
||||
|
||||
async def _update_focus(
|
||||
self,
|
||||
parameters: Dict[str, Any],
|
||||
context: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Update current focus area.
|
||||
|
||||
Parameters:
|
||||
- focus: New focus area (required)
|
||||
- reason: Why this focus
|
||||
"""
|
||||
focus = parameters.get("focus")
|
||||
if not focus:
|
||||
raise ValueError("Focus required")
|
||||
|
||||
reason = parameters.get("reason", "autonomous update")
|
||||
|
||||
# Import self-state manager
|
||||
from autonomy.self.state import get_self_state_instance
|
||||
|
||||
state = get_self_state_instance()
|
||||
old_focus = state._state.get("focus", "none")
|
||||
|
||||
state._state["focus"] = focus
|
||||
state._state["focus_updated_at"] = datetime.utcnow().isoformat()
|
||||
state._state["focus_reason"] = reason
|
||||
state._save_state(state._state) # Pass state dict as argument
|
||||
|
||||
logger.info(f"[ACTIONS] Focus updated: {old_focus} -> {focus}")
|
||||
|
||||
return {
|
||||
"old_focus": old_focus,
|
||||
"new_focus": focus,
|
||||
"reason": reason
|
||||
}
|
||||
|
||||
# ========================================
|
||||
# Utility Methods
|
||||
# ========================================
|
||||
|
||||
def get_allowed_actions(self) -> List[str]:
|
||||
"""Get list of all allowed action types."""
|
||||
return list(self.allowed_actions.keys())
|
||||
|
||||
def get_action_log(self, limit: int = 50) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get recent action log.
|
||||
|
||||
Args:
|
||||
limit: Max number of entries to return
|
||||
|
||||
Returns:
|
||||
List of action records
|
||||
"""
|
||||
return self.action_log[-limit:]
|
||||
|
||||
def clear_action_log(self) -> None:
|
||||
"""Clear action log."""
|
||||
self.action_log = []
|
||||
logger.info("[ACTIONS] Action log cleared")
|
||||
|
||||
def validate_action(self, action_type: str, parameters: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Validate an action without executing it.
|
||||
|
||||
Args:
|
||||
action_type: Type of action
|
||||
parameters: Action parameters
|
||||
|
||||
Returns:
|
||||
{
|
||||
"valid": bool,
|
||||
"action": action_type,
|
||||
"errors": [error messages] or []
|
||||
}
|
||||
"""
|
||||
errors = []
|
||||
|
||||
# Check whitelist
|
||||
if action_type not in self.allowed_actions:
|
||||
errors.append(f"Action '{action_type}' not in whitelist")
|
||||
|
||||
# Check required parameters (basic validation)
|
||||
if action_type == "create_memory" and not parameters.get("text"):
|
||||
errors.append("Memory 'text' parameter required")
|
||||
|
||||
if action_type == "update_goal" and not parameters.get("goal_id"):
|
||||
errors.append("Goal 'goal_id' parameter required")
|
||||
|
||||
if action_type == "schedule_reminder" and not parameters.get("message"):
|
||||
errors.append("Reminder 'message' parameter required")
|
||||
|
||||
if action_type == "learn_topic" and not parameters.get("topic"):
|
||||
errors.append("Learning 'topic' parameter required")
|
||||
|
||||
if action_type == "update_focus" and not parameters.get("focus"):
|
||||
errors.append("Focus 'focus' parameter required")
|
||||
|
||||
return {
|
||||
"valid": len(errors) == 0,
|
||||
"action": action_type,
|
||||
"errors": errors
|
||||
}
|
||||
|
||||
|
||||
# Singleton instance
|
||||
_action_manager_instance = None
|
||||
|
||||
|
||||
def get_action_manager() -> AutonomousActionManager:
|
||||
"""
|
||||
Get singleton action manager instance.
|
||||
|
||||
Returns:
|
||||
AutonomousActionManager instance
|
||||
"""
|
||||
global _action_manager_instance
|
||||
if _action_manager_instance is None:
|
||||
_action_manager_instance = AutonomousActionManager()
|
||||
return _action_manager_instance
|
||||
Reference in New Issue
Block a user