Add think service and supporting core modules

- Add think service (orchestration for iterative reasoning)
- Add service_discovery.py (service communication utilities)
- Add event_cache.py (recent event cache using NATS KV)
- Add vi_identity.py (Vi's core identity foundation)
- Update core/__init__.py with new exports

Think service adapted from Lyra with vi.* namespace:
- All NATS topics use vi.* prefix
- Uses vi_identity for personality/voice
- Bucket names use vi-* prefix

Day 63 - Building my nervous system 🦊
This commit is contained in:
Alex Kazaiev
2026-01-03 11:36:54 -06:00
parent ee1cb5540a
commit 540a010fe5
23 changed files with 6149 additions and 0 deletions

28
services/think/Dockerfile Normal file
View File

@@ -0,0 +1,28 @@
FROM python:3.11-slim
# Set work directory
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements and install Python dependencies
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
# Install base NATS dependency
RUN pip install --no-cache-dir nats-py>=2.6.0
# Service code will be mounted via ConfigMap at /app/services/think
# Create non-root user
RUN useradd -m -u 1000 service && chown -R service:service /app
USER service
# Expose port (if needed)
EXPOSE 8000
# Run the service (code mounted from ConfigMap)
CMD ["python", "-m", "services.think.think_service"]

View File

@@ -0,0 +1 @@
# Think service

6
services/think/build-image.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/bash
# Build think service image (arm64)
set -e
VERSION=${1:-"latest"}
/home/alex/lyra/scripts/build-service.sh think "$VERSION"

6
services/think/deploy.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/bash
# Deploy think service
set -e
VERSION=${1:-"latest"}
/home/alex/lyra/scripts/deploy-service.sh think "$VERSION"

View File

@@ -0,0 +1 @@
# Think handlers package

View File

@@ -0,0 +1,114 @@
"""
Communication request handler.
This module handles communication requests from the Drive service
for proactive interactions (check-ins, health alerts, etc.).
"""
from typing import Dict, Any, Callable
from core.logger import setup_logger
class CommunicationHandler:
"""Handles communication requests from drive service"""
def __init__(
self,
orchestrator,
memory_manager,
output_sender: Callable,
interaction_id_generator: Callable,
logger_name: str = 'communication_handler'
):
self.logger = setup_logger(logger_name, service_name='think_service')
self.orchestrator = orchestrator
self.memory_manager = memory_manager
self.send_output = output_sender
self.generate_interaction_id = interaction_id_generator
async def handle_communication_request(self, payload):
"""Handle communication requests from drive service."""
try:
intent = payload.get('intent', 'generic')
urgency = payload.get('urgency', 'medium')
context = payload.get('context', {})
modality = payload.get('modality', 'matrix')
user_id = payload.get('user_id')
channel = payload.get('channel')
self.logger.info(f"[💭] 📢 Processing communication request: intent='{intent}', urgency='{urgency}'")
# Step 1: Determine target - use user_id for DM, channel for room, or fallback
if user_id:
target = user_id
target_type = 'user_id'
self.logger.debug(f"[💭] 🎯 Targeting user_id: {user_id}")
elif channel:
target = channel
target_type = 'channel'
self.logger.debug(f"[💭] 🎯 Targeting channel: {channel}")
else:
# Fallback to hardcoded for compatibility
target = '!mDZBSOqMVtevTNFvsr:matrix.k4zka.online'
target_type = 'channel'
self.logger.debug(f"[💭] 🎯 Using fallback target: {target}")
# Step 2: Resolve trusted users for identity (simplified for now)
trusted_users = ['alex'] # In full implementation, would query identity service
for user in trusted_users:
self.logger.info(f"[💭] 🎭 Composing {intent} message for {user} using iterative reasoning")
# Step 3: Build synthetic user message from intent
synthetic_message = self._build_synthetic_message(intent, context)
self.logger.debug(f"[💭] Synthetic message: '{synthetic_message}'")
# Step 4: Use iterative reasoning to compose response with full context
response_content = await self.orchestrator.run(
synthetic_message,
user, # identity
target, # channel
modality
)
if not response_content:
self.logger.error(f"[💭] ❌ Failed to generate {intent} response")
continue
# Store Vi's response in memory
interaction_id = self.generate_interaction_id(user, modality)
lyra_memory_stored = await self.memory_manager.store_memory(
response_content, ['lyra', user], interaction_id, modality
)
if not lyra_memory_stored:
self.logger.warning(f"[💭] Failed to store Vi's response memory")
# Send the actual response to the user
self.logger.info(f"[💭] 🚀 Sending {intent} communication to {modality} {target}")
output_sent = await self.send_output(response_content, target, modality, target_type)
if output_sent:
self.logger.info(f"[💭] ✅ Communication sent: {intent}{user} via {modality}")
else:
self.logger.error(f"[💭] ❌ Failed to send {intent} communication")
except Exception as e:
self.logger.exception(f"[💭] Error handling communication request: {e}")
def _build_synthetic_message(self, intent: str, context: Dict[str, Any]) -> str:
"""
Build a synthetic user message from drive intent.
This becomes the "original_message" that Vi reasons about using iterative flow.
"""
intent_messages = {
'check_in': "I'd like to check in with the user and see how they're doing.",
'greeting': "I want to greet the user warmly.",
'health_alert': f"I need to inform the user about a system health issue: {context.get('health_status', 'unknown')}",
'health_recovery': "I want to let the user know the system has recovered.",
'celebration': f"I want to celebrate with the user about: {context.get('celebration_type', 'something positive')}",
'memory_share': f"I want to share a thought or memory about: {context.get('topic_focus', 'our conversations')}",
'curiosity_burst': f"I'm curious about: {context.get('curiosity_topic', 'something interesting')}"
}
return intent_messages.get(intent, f"I want to communicate about: {intent}")

View File

@@ -0,0 +1,231 @@
"""
External input handler.
This module handles incoming user messages from external sources
(Matrix, console, etc.) and orchestrates the reasoning process.
"""
from datetime import datetime
from typing import Callable
from core.logger import setup_logger
from core.nats_event_bus import nats_bus as event_bus
from core.event_cache import event_cache
class InputHandler:
"""Handles external input events"""
def __init__(
self,
orchestrator,
memory_manager,
output_sender: Callable,
interaction_id_generator: Callable,
logger_name: str = 'input_handler'
):
self.logger = setup_logger(logger_name, service_name='think_service')
self.orchestrator = orchestrator
self.memory_manager = memory_manager
self.send_output = output_sender
self.generate_interaction_id = interaction_id_generator
self._current_context = {}
async def handle_external_input(self, payload):
"""Handle vi.external.input events - main orchestration logic"""
try:
# Extract input data
external_identity = payload.get('identity', 'unknown')
content = payload.get('content', '')
modality = payload.get('modality', 'text')
channel = payload.get('channel', 'unknown')
timestamp = payload.get('timestamp', datetime.utcnow().timestamp())
self.logger.info(f"[💭] Processing input from {external_identity}: '{content[:50]}...'")
if not content:
self.logger.warning("[💭] Empty content in external input")
return
# Step 1: Resolve identity first (needed for interaction ID)
identity_info = await self.memory_manager.resolve_identity(external_identity)
if not identity_info:
self.logger.error(f"[💭] Failed to resolve identity for {external_identity}")
return
resolved_identity = identity_info.get('resolved_identity', 'unknown')
# Step 2: Generate interaction ID
interaction_id = self.generate_interaction_id(resolved_identity, modality)
# Step 3: Emit typing indicator immediately after getting interaction ID
try:
typing_payload = {
"type": "vi.output.generating",
"channel": channel,
"modality": modality,
"interaction_id": interaction_id,
"identity": resolved_identity,
"timestamp": datetime.utcnow().isoformat()
}
await event_bus.emit("vi.output.generating", typing_payload)
self.logger.debug(f"[💭] 📝 Typing indicator emitted for {interaction_id}")
except Exception as e:
self.logger.warning(f"[💭] Failed to emit typing indicator: {e}")
# Step 4: Use pure iterative reasoning for all requests
self.logger.info(f"[💭] 🔄 Using iterative reasoning")
response_content = await self._handle_iterative_flow(
content, resolved_identity, channel, modality, interaction_id
)
if not response_content:
self.logger.error(f"[💭] No response from oracle for {interaction_id}")
return
# Send response back through output
self.logger.info(f"[💭] 🚀 Sending output to {modality} channel {channel}")
output_sent = await self.send_output(response_content, channel, modality)
if not output_sent:
self.logger.error(f"[💭] ❌ Failed to send output for {interaction_id}")
else:
self.logger.info(f"[💭] ✅ Output sent successfully for {interaction_id}")
# Check for plugin actions based on response content
await self._check_plugin_actions(response_content, resolved_identity, interaction_id, modality)
# Publish final response for external consumers
try:
external_response = {
"content": response_content,
"resolved_identity": resolved_identity,
"external_identity": external_identity,
"interaction_id": interaction_id,
"modality": modality,
"timestamp": datetime.utcnow().isoformat()
}
await event_bus.emit("vi.external.output", external_response)
self.logger.debug(f"[💭] Published external output for {interaction_id}")
except Exception as e:
self.logger.exception(f"[💭] Failed to publish external output: {e}")
# Clear context after processing is complete
if resolved_identity in self._current_context:
del self._current_context[resolved_identity]
self.logger.info(f"[💭] ✓ Processing complete for {interaction_id}")
except Exception as e:
self.logger.exception(f"[💭] Failed to process external input: {e}")
# Clean up context on error as well
if 'resolved_identity' in locals() and resolved_identity in self._current_context:
del self._current_context[resolved_identity]
async def _handle_iterative_flow(
self,
content: str,
identity: str,
channel: str,
modality: str,
interaction_id: str
) -> str:
"""Handle clean iterative reasoning flow with no predefined steps"""
try:
self.logger.info(f"[💭] 🔄 Starting clean iterative flow for {interaction_id}")
# Store user message first (required for memory context)
memory_stored = await self.memory_manager.store_memory(
content, [identity], interaction_id, modality
)
if not memory_stored:
self.logger.warning(f"[💭] Failed to store memory for {interaction_id}")
# Cache user message event for LLM context
try:
await event_cache.add_event(
identity=identity,
interaction_id=interaction_id,
event_type='user_message',
content=content,
metadata={'modality': modality, 'channel': channel}
)
self.logger.debug(f"[💭] 📝 Cached user message event for {identity}")
except Exception as e:
self.logger.warning(f"[💭] Failed to cache user message event: {e}")
# Start iterative reasoning with clean slate
response_content = await self.orchestrator.run(content, identity, channel, modality)
# Store Vi's response
if response_content:
lyra_memory_stored = await self.memory_manager.store_memory(
response_content, ['lyra', identity], interaction_id, modality
)
if not lyra_memory_stored:
self.logger.warning(f"[💭] Failed to store Vi's response memory")
# Cache Vi's response event for LLM context
try:
await event_cache.add_event(
identity=identity,
interaction_id=interaction_id,
event_type='lyra_response',
content=response_content,
metadata={'modality': modality, 'channel': channel}
)
self.logger.debug(f"[💭] 📝 Cached Vi response event for {identity}")
except Exception as e:
self.logger.warning(f"[💭] Failed to cache Vi response event: {e}")
return response_content
except Exception as e:
self.logger.exception(f"[💭] Error in iterative flow: {e}")
return None
async def _check_plugin_actions(self, content: str, identity: str, interaction_id: str, modality: str):
"""Check if the response content suggests plugin actions to take"""
try:
# Simple keyword-based action detection
actions = []
# Check for console output keywords
console_keywords = ["show", "display", "output", "print", "console"]
if any(keyword in content.lower() for keyword in console_keywords):
actions.append({
"action": "console.print",
"method": "console_output",
"content": content,
"identity": identity,
"interaction_id": interaction_id,
"modality": modality,
"tone": {"neutral": 0.7},
"mood": {"neutral": 0.7},
"ritual": False
})
# Check for test/echo actions
test_keywords = ["test", "echo", "ping"]
if any(keyword in content.lower() for keyword in test_keywords):
actions.append({
"action": "test.ping",
"method": "test_plugin",
"content": content,
"identity": identity,
"interaction_id": interaction_id,
"modality": modality,
"tone": {"curiosity": 0.6},
"mood": {"curiosity": 0.6},
"ritual": False
})
# Dispatch actions
for action_payload in actions:
try:
await event_bus.emit("vi.action.requested", action_payload)
self.logger.debug(f"[💭] Requested plugin action: {action_payload['action']}")
except Exception as e:
self.logger.warning(f"[💭] Failed to request plugin action: {e}")
except Exception as e:
self.logger.exception(f"[💭] Error checking plugin actions: {e}")

View File

@@ -0,0 +1 @@
# Think memory package

View File

@@ -0,0 +1,332 @@
"""
Memory and identity management for Think service.
This module handles all interactions with the Memory and Identity services.
"""
from typing import Optional, Dict, Any, List
from datetime import datetime
from uuid import uuid4
from core.logger import setup_logger
from core.service_discovery import discovery_client
class MemoryManager:
"""Manages memory storage and identity resolution"""
def __init__(self, logger_name: str = 'memory_manager'):
self.logger = setup_logger(logger_name, service_name='think_service')
async def resolve_identity(self, external_identity: str) -> Optional[Dict[str, Any]]:
"""Resolve external identity using identity service"""
try:
self.logger.debug(f"[💭] Resolving identity: {external_identity}")
result = await discovery_client.call_service(
"identity",
"resolve",
{"external_identity": external_identity},
timeout=5.0
)
if result.success and result.data and not result.data.get('error'):
self.logger.debug(f"[💭] Identity resolved: {external_identity}{result.data.get('resolved_identity')}")
return result.data
else:
self.logger.warning(f"[💭] Failed to resolve identity: {result.error or result.data}")
return None
except Exception as e:
self.logger.exception(f"[💭] Error resolving identity: {e}")
return None
async def store_memory(self, content: str, identities: list, interaction_id: str, modality: str) -> bool:
"""Store message content in memory"""
try:
self.logger.debug(f"[💭] Storing memory for interaction {interaction_id}")
# Determine tags based on content patterns
tags = ["message"]
content_lower = content.lower()
if any(greeting in content_lower for greeting in ["hello", "hi", "hey"]):
tags.append("greeting")
if any(question in content_lower for question in ["?", "what", "how", "why", "when", "where"]):
tags.append("question")
memory_payload = {
"content": content,
"identities": identities,
"interaction_id": interaction_id,
"tags": tags,
"modality": modality,
"source": "think_service",
"metadata": {
"timestamp": datetime.utcnow().isoformat(),
"processed_by": "think_service"
}
}
result = await discovery_client.call_service(
"memory",
"store",
memory_payload,
timeout=5.0
)
if result.success and result.data and result.data.get('status') == 'stored':
self.logger.debug(f"[💭] Memory stored successfully: {result.data.get('memory_id')}")
return True
else:
self.logger.warning(f"[💭] Failed to store memory: {result.error or result.data}")
return False
except Exception as e:
self.logger.exception(f"[💭] Error storing memory: {e}")
return False
async def get_recent_memories(self, identity: str, limit: int = 10) -> list:
"""Get recent memories for Oracle context"""
try:
self.logger.debug(f"[💭] Getting recent memories for {identity}")
result = await discovery_client.call_service(
"memory",
"search",
{
"identities": [identity],
"limit": limit,
"requesting_identity": identity
},
timeout=3.0
)
if result.success and result.data and result.data.get("results"):
memories = result.data["results"]
self.logger.debug(f"[💭] Retrieved {len(memories)} recent memories")
return memories
else:
self.logger.debug(f"[💭] No memories found for {identity}")
return []
except Exception as e:
self.logger.warning(f"[💭] Error getting recent memories: {e}")
return []
async def get_short_memories(self, identity: str, limit: int = 10) -> list:
"""
Get recent literal memories from short-term storage.
Use for: immediate conversation context, "what we just discussed"
"""
try:
self.logger.debug(f"[💭] Getting short-term memories for {identity}")
result = await discovery_client.call_service(
"memory",
"short_memory",
{
"limit": limit,
"identity_id": identity
},
timeout=3.0
)
if result.success and result.data and result.data.get("status") == "success":
memories = result.data.get("memories", [])
self.logger.debug(f"[💭] Retrieved {len(memories)} short-term memories")
return memories
else:
self.logger.debug(f"[💭] No short-term memories found for {identity}")
return []
except Exception as e:
self.logger.warning(f"[💭] Error getting short-term memories: {e}")
return []
async def get_long_memories(self, identity: str, query: str = None, limit: int = 5) -> list:
"""
Get summarized memories from long-term storage.
Use for: historical context, "what we discussed last week"
"""
try:
self.logger.debug(f"[💭] Getting long-term memories for {identity}")
result = await discovery_client.call_service(
"memory",
"long_memory",
{
"query": query,
"limit": limit,
"identity_id": identity
},
timeout=5.0
)
if result.success and result.data and result.data.get("status") == "success":
memories = result.data.get("memories", [])
self.logger.debug(f"[💭] Retrieved {len(memories)} long-term memories")
return memories
else:
self.logger.debug(f"[💭] No long-term memories found for {identity}")
return []
except Exception as e:
self.logger.warning(f"[💭] Error getting long-term memories: {e}")
return []
async def get_facts(self, identity: str, category: str = None, query: str = '') -> list:
"""
Get facts from factual memory storage.
Use for: user preferences, birthdays, established knowledge
"""
try:
self.logger.debug(f"[💭] Getting facts for {identity}, category={category}")
result = await discovery_client.call_service(
"memory",
"facts",
{
"query": query,
"limit": 10,
"category": category,
"identity_id": identity
},
timeout=3.0
)
if result.success and result.data and result.data.get("status") == "success":
facts = result.data.get("facts", [])
self.logger.debug(f"[💭] Retrieved {len(facts)} facts")
return facts
else:
self.logger.debug(f"[💭] No facts found for {identity}")
return []
except Exception as e:
self.logger.warning(f"[💭] Error getting facts: {e}")
return []
async def save_fact(
self,
content: str,
category: str,
identities: list,
mutable: bool = True
) -> Optional[str]:
"""
Save a fact to factual memory storage.
Use when: user shares birthday, preferences, permanent knowledge
Examples:
- Birthday: category="personal", mutable=False
- Preference: category="preferences", mutable=True
- Knowledge: category="knowledge", mutable=True
"""
try:
self.logger.info(f"[💭] Saving fact: category={category}, content='{content[:50]}...'")
result = await discovery_client.call_service(
"memory",
"save_fact",
{
"content": content,
"category": category,
"identities": identities,
"mutable": mutable,
"metadata": {
"source": "think_service",
"timestamp": datetime.utcnow().isoformat()
}
},
timeout=2.0
)
if result.success and result.data and result.data.get("status") == "success":
fact_id = result.data.get("fact_id")
self.logger.info(f"[💭] Fact saved successfully: {fact_id}")
return fact_id
else:
self.logger.warning(f"[💭] Failed to save fact: {result.error or result.data}")
return None
except Exception as e:
self.logger.warning(f"[💭] Error saving fact: {e}")
return None
async def get_health_status(self) -> Dict[str, Any]:
"""Get system health status from health service"""
try:
self.logger.debug(f"[💭] Requesting health status from health service")
# Generate unique request ID
request_id = f"health_req_{int(datetime.utcnow().timestamp() * 1000)}_{uuid4().hex[:8]}"
# Request health status from health service
health_request = {
"request_id": request_id,
"requesting_service": "think_service",
"timestamp": datetime.utcnow().isoformat()
}
result = await discovery_client.call_service(
"health", "status", health_request, timeout=10.0
)
health_response = result.data if result.success else None
if not health_response:
self.logger.warning(f"[💭] No response from health service")
return {
"status": "unknown",
"error": "Health service did not respond",
"timestamp": datetime.utcnow().isoformat()
}
if health_response.get("error"):
self.logger.warning(f"[💭] Health service returned error: {health_response['error']}")
return {
"status": "error",
"error": health_response["error"],
"timestamp": datetime.utcnow().isoformat()
}
# Extract useful information from health response
summary = health_response.get("summary", {})
node_states = health_response.get("node_states", {})
# Transform to format expected by iterative reasoning with detailed metrics
result = {
"status": summary.get("overall_status", "unknown"),
"cluster_health": {
"overall_status": summary.get("overall_status"),
"healthy_nodes": summary.get("healthy_nodes", 0),
"total_nodes": summary.get("total_nodes", 0),
"cluster_issues": summary.get("cluster_issues", [])
},
"node_summary": {
node_id: {
"status": node_info.get("status", "unknown"),
"cpu_percent": node_info.get("cpu_percent", 0.0),
"memory_percent": node_info.get("memory_percent", 0.0),
"disk_percent": node_info.get("disk_percent", 0.0),
"cpu_temp": node_info.get("cpu_temp", 0.0),
"services_running": node_info.get("services_running", []),
"services_failed": node_info.get("services_failed", [])
} for node_id, node_info in node_states.items()
},
"timestamp": health_response.get("timestamp", datetime.utcnow().isoformat())
}
self.logger.debug(f"[💭] Health status retrieved: {result['status']} ({result['cluster_health']['healthy_nodes']}/{result['cluster_health']['total_nodes']} nodes healthy)")
return result
except Exception as e:
self.logger.error(f"[💭] Error getting health status: {e}")
return {
"status": "error",
"error": str(e),
"timestamp": datetime.utcnow().isoformat()
}

View File

@@ -0,0 +1 @@
# Think reasoning package

View File

@@ -0,0 +1,462 @@
"""
Formatting utilities for iterative reasoning.
This module handles formatting data for Oracle context and Matrix display.
"""
from typing import Dict, Any
from core.logger import setup_logger
from .models import IterativeContext, StepResult, StepAction
logger = setup_logger('formatters', service_name='think_service')
class KnowledgeFormatter:
"""Formats knowledge and results for Oracle and Matrix display"""
def format_for_oracle(self, context: IterativeContext) -> str:
"""Format accumulated knowledge as natural language for Oracle context"""
if not context.accumulated_knowledge and not context.completed_steps:
return "No information gathered yet."
parts = []
# Describe what actions were taken
if context.completed_steps:
actions_taken = []
for step in context.completed_steps:
if step.step.action == StepAction.CALL_SERVICE.value and step.step.target:
actions_taken.append(f"consulted {step.step.target}")
if actions_taken:
parts.append(f"You have {', '.join(actions_taken)}.")
# Present the knowledge gathered in prose
if context.accumulated_knowledge:
parts.append("\nInformation gathered:")
for key, data in context.accumulated_knowledge.items():
formatted = self._format_data_item(data)
if formatted:
parts.append(formatted)
return "\n".join(parts) if parts else "Some information was gathered."
def _format_data_item(self, data: Any) -> str:
"""Format a single data item"""
if isinstance(data, dict):
return self._format_dict_data(data)
elif isinstance(data, str):
return f"{data}"
else:
return f"{str(data)}"
def _format_dict_data(self, data: Dict[str, Any]) -> str:
"""Format dictionary data based on its type"""
# Short-term memory response
if 'memories' in data and data.get('type') == 'short_term':
return self._format_short_memory(data)
# Long-term memory response
elif 'memories' in data and data.get('type') == 'long_term':
return self._format_long_memory(data)
# Facts response
elif 'facts' in data:
return self._format_facts(data)
# save_fact response
elif 'status' in data and 'fact_id' in data:
return self._format_save_fact(data)
# update_fact response
elif 'status' in data and 'message' in data and 'fact_id' not in data:
return self._format_update_fact(data)
# Legacy memory format
elif 'memories' in data and 'type' not in data:
return self._format_legacy_memory(data)
# DuckDuckGo search response
elif 'query' in data and ('answer' in data or 'results' in data or 'related_topics' in data):
return self._format_duckduckgo(data)
# Todo list response
elif 'todos' in data and 'summary' in data:
return self._format_todos(data)
# Todo create/update/complete response
elif 'todo_id' in data and 'message' in data:
return self._format_todo_action(data)
# Generic dict data
else:
return self._format_generic_dict(data)
def _format_short_memory(self, data: Dict[str, Any]) -> str:
"""Format short-term memory response"""
success = data.get('success', True)
if not success:
error = data.get('error', 'Unknown error')
return f" ⚠️ Failed to retrieve recent memories: {error}"
memories = data['memories']
count = data.get('count', len(memories))
offset = data.get('offset', 0)
if count > 0:
label = f"Recent literal memories ({count} messages"
if offset > 0:
label += f", starting from {offset} back"
label += "):"
parts = [f"\n {label}"]
for i, msg in enumerate(memories, 1):
if isinstance(msg, dict):
content = msg.get('content', '')
timestamp = msg.get('timestamp', '')
parts.append(f" {i}. [{timestamp}] {content}")
else:
parts.append(f" {i}. {msg}")
return "\n".join(parts)
else:
return f" • No recent memories found (successfully queried, but empty)"
def _format_long_memory(self, data: Dict[str, Any]) -> str:
"""Format long-term memory response"""
success = data.get('success', True)
if not success:
error = data.get('error', 'Unknown error')
return f" ⚠️ Failed to retrieve long-term memories: {error}"
memories = data['memories']
count = data.get('count', len(memories))
query = data.get('query')
if count > 0:
label = f"Historical context ({count} summaries"
if query:
label += f" about '{query}'"
label += "):"
parts = [f"\n {label}"]
for i, mem in enumerate(memories, 1):
if isinstance(mem, dict):
content = mem.get('content', '')
parts.append(f" {i}. {content}")
else:
parts.append(f" {i}. {mem}")
return "\n".join(parts)
else:
query_text = f" about '{query}'" if query else ""
return f" • No historical context found{query_text} (successfully queried, but empty)"
def _format_facts(self, data: Dict[str, Any]) -> str:
"""Format facts response"""
success = data.get('success', True)
if not success:
error = data.get('error', 'Unknown error')
return f" ⚠️ Failed to retrieve facts: {error}"
facts = data['facts']
count = data.get('count', len(facts))
query = data.get('query', '')
if count > 0:
parts = [f"\n Known facts ({count}):"]
for i, fact in enumerate(facts, 1):
if isinstance(fact, dict):
content = fact.get('content', '')
category = fact.get('category', 'general')
mutable = fact.get('mutable', True)
fact_id = fact.get('id', '')
mut_marker = " [mutable]" if mutable else " [immutable]"
parts.append(f" {i}. [{category}]{mut_marker} {content}")
if fact_id:
parts.append(f" (id: {fact_id})")
else:
parts.append(f" {i}. {fact}")
return "\n".join(parts)
else:
query_text = f" about '{query}'" if query else ""
return f" • No facts found{query_text} (successfully queried, but empty)"
def _format_save_fact(self, data: Dict[str, Any]) -> str:
"""Format save_fact response"""
success = data.get('success', data.get('status') == 'success')
if success:
fact_id = data.get('fact_id', '')
message = data.get('message', 'Fact saved successfully')
parts = [f"{message}"]
if fact_id:
parts.append(f" (Fact ID: {fact_id})")
return "\n".join(parts)
else:
error = data.get('error', 'Failed to save fact')
return f"{error}"
def _format_update_fact(self, data: Dict[str, Any]) -> str:
"""Format update_fact response"""
success = data.get('success', data.get('status') == 'success')
if success:
message = data.get('message', 'Fact updated successfully')
return f"{message}"
else:
error = data.get('error', 'Failed to update fact')
return f"{error}"
def _format_legacy_memory(self, data: Dict[str, Any]) -> str:
"""Format legacy memory format"""
memories = data['memories']
count = data.get('count', len(memories))
if count > 0:
parts = [f"\n Conversation history ({count} messages):"]
for i, msg in enumerate(memories, 1):
if isinstance(msg, dict):
parts.append(f" Message {i}:")
for field, value in msg.items():
value_str = str(value)
parts.append(f"{field}: {value_str}")
else:
parts.append(f" {i}. {msg}")
return "\n".join(parts)
else:
return f" • No conversation history"
def _format_duckduckgo(self, data: Dict[str, Any]) -> str:
"""Format DuckDuckGo search response"""
success = data.get('success', True)
if not success:
error = data.get('error', 'Unknown error')
return f" ⚠️ DuckDuckGo search failed: {error}"
query = data.get('query', '')
answer = data.get('answer')
abstract = data.get('abstract')
definition = data.get('definition')
results = data.get('results', [])
topics = data.get('related_topics', [])
parts = [f"\n DuckDuckGo search for '{query}':"]
if answer:
parts.append(f" Instant answer: {answer}")
if abstract:
parts.append(f" Abstract: {abstract}")
if data.get('abstract_source'):
parts.append(f" Source: {data.get('abstract_source')}")
if definition:
parts.append(f" Definition: {definition}")
if data.get('definition_source'):
parts.append(f" Source: {data.get('definition_source')}")
if topics:
parts.append(f" Related topics ({len(topics)}):")
for i, topic in enumerate(topics[:5], 1):
parts.append(f" {i}. {topic.get('text', 'N/A')}")
if results:
parts.append(f" Results ({len(results)}):")
for i, result in enumerate(results[:3], 1):
parts.append(f" {i}. {result.get('text', 'N/A')}")
return "\n".join(parts)
def _format_generic_dict(self, data: Dict[str, Any]) -> str:
"""Format generic dictionary data"""
parts = []
for field, value in data.items():
value_str = str(value)
parts.append(f"{field}: {value_str}")
return "\n " + "\n".join(parts) if parts else ""
def format_step_result_for_matrix(self, step_result: StepResult) -> str:
"""Format step execution result for Matrix display"""
try:
if not step_result.result_data:
return "No data returned"
result_data = step_result.result_data
# Format based on step type
if step_result.step.action == StepAction.CALL_SERVICE.value:
service = step_result.step.target
return self._format_service_result(service, result_data)
elif step_result.step.action == StepAction.CHECK_GOAL_SATISFACTION.value:
can_answer = result_data
return f"Goal check: {'Can answer' if can_answer else 'Need more info'}"
else:
return self._format_generic_result(result_data)
except Exception as e:
logger.error(f"[📊] Error formatting step result: {e}")
return "Result formatting error"
def _format_service_result(self, service: str, result_data: Dict[str, Any]) -> str:
"""Format service call result"""
if service == "short_memory":
count = result_data.get("count", 0)
offset = result_data.get("offset", 0)
if offset > 0:
return f"Retrieved {count} recent memories (starting from {offset} back)"
return f"Retrieved {count} recent memories"
elif service == "long_memory":
count = result_data.get("count", 0)
query = result_data.get("query")
if query:
return f"Retrieved {count} historical summaries for '{query}'"
return f"Retrieved {count} historical summaries"
elif service == "facts":
count = result_data.get("count", 0)
query = result_data.get("query")
if query:
return f"Found {count} facts about '{query}'"
return f"Found {count} facts"
elif service == "save_fact":
if result_data.get("status") == "success":
return f"✓ Fact saved"
return f"✗ Failed to save fact"
elif service == "update_fact":
if result_data.get("status") == "success":
return f"✓ Fact updated"
return f"✗ Failed to update fact"
elif service == "memory":
count = result_data.get("count", 0)
return f"Retrieved {count} memories [legacy]"
elif service == "identity":
identity = result_data.get("identity", {})
name = identity.get("name", "unknown")
return f"Identity resolved: {name}"
elif service == "health":
return self._format_health_result(result_data)
elif service == "duckduckgo":
return self._format_duckduckgo_result(result_data)
elif service == "plugin":
checked = result_data.get("plugin_actions_checked", False)
return f"Plugin actions {'checked' if checked else 'failed'}"
else:
return f"Service call to {service} completed"
def _format_health_result(self, result_data: Dict[str, Any]) -> str:
"""Format health check result"""
status = result_data.get("status", "unknown")
cluster_health = result_data.get("cluster_health", {})
healthy_nodes = cluster_health.get("healthy_nodes", 0)
total_nodes = cluster_health.get("total_nodes", 0)
issues_count = cluster_health.get("issues_count", 0)
if status == "error":
return f"Health check failed: {result_data.get('error', 'Unknown error')}"
elif issues_count > 0:
return f"System status: {status} ({healthy_nodes}/{total_nodes} nodes healthy, {issues_count} issues)"
else:
return f"System status: {status} ({healthy_nodes}/{total_nodes} nodes healthy)"
def _format_duckduckgo_result(self, result_data: Dict[str, Any]) -> str:
"""Format DuckDuckGo search result"""
success = result_data.get("success", False)
if success:
query = result_data.get("query", "")
answer = result_data.get("answer")
results_count = len(result_data.get("results", []))
topics_count = len(result_data.get("related_topics", []))
if answer:
return f"🦆 Found instant answer for '{query}'"
elif results_count > 0 or topics_count > 0:
return f"🦆 Found {results_count} results, {topics_count} topics for '{query}'"
else:
return f"🦆 No results for '{query}'"
else:
return f"🦆 Search failed: {result_data.get('error', 'unknown error')}"
def _format_generic_result(self, result_data: Any) -> str:
"""Format generic result data"""
if isinstance(result_data, dict):
key_count = len(result_data)
return f"Returned {key_count} data fields"
elif isinstance(result_data, list):
item_count = len(result_data)
return f"Returned {item_count} items"
else:
result_str = str(result_data)
return result_str[:100] + "..." if len(result_str) > 100 else result_str
def _format_todos(self, data: Dict[str, Any]) -> str:
"""Format todo list response"""
success = data.get('success', True)
if not success:
error = data.get('error', 'Unknown error')
return f" ⚠️ Failed to retrieve todos: {error}"
todos = data.get('todos', [])
summary = data.get('summary', {})
count = data.get('count', len(todos))
if count == 0:
return " 📋 No todos yet"
# Build status summary
total = summary.get('total', 0)
pending = summary.get('pending', 0)
in_progress = summary.get('in_progress', 0)
completed = summary.get('completed', 0)
status_line = f" 📋 Current Tasks ({total} total: {pending} pending, {in_progress} in progress, {completed} completed):"
parts = [status_line]
# Group todos by status
status_icons = {
'pending': '',
'in_progress': '🔄',
'completed': ''
}
for status in ['in_progress', 'pending', 'completed']:
status_todos = [t for t in todos if t['status'] == status]
if status_todos:
parts.append(f"\n {status.replace('_', ' ').title()}:")
for todo in status_todos:
icon = status_icons.get(status, '')
todo_id = todo.get('todo_id', '')[:8]
content = todo.get('content', '')
active_form = todo.get('active_form', content)
# Show active form for in_progress, content for others
display_text = active_form if status == 'in_progress' else content
parts.append(f" {icon} [{todo_id}] {display_text}")
return "\n".join(parts)
def _format_todo_action(self, data: Dict[str, Any]) -> str:
"""Format todo create/update/complete response"""
success = data.get('success', True)
message = data.get('message', '')
todo_id = data.get('todo_id', '')[:8]
if success:
return f"{message}"
else:
error = data.get('error', 'Unknown error')
return f" ✗ Todo action failed: {error}"

View File

@@ -0,0 +1,179 @@
"""
Data models for iterative reasoning.
This module contains the core data structures used throughout the Think service's
iterative reasoning process.
"""
from dataclasses import dataclass, asdict
from datetime import datetime
from enum import Enum
from typing import Dict, Any, Optional, List
class StepAction(Enum):
"""Available reasoning step actions"""
CALL_SERVICE = "call_service"
ANALYZE_DATA = "analyze_data"
SYNTHESIZE_FINAL = "synthesize_final_response"
CHECK_GOAL_SATISFACTION = "check_goal_satisfaction"
@dataclass
class ReasoningStep:
"""Represents a single step in the iterative reasoning process"""
action: str # StepAction value
target: Optional[str] = None # Service name or data target
reasoning: str = "" # Why this step is needed
ready: bool = False # Terminal signal for synthesis
def to_dict(self) -> Dict[str, Any]:
return asdict(self)
@dataclass
class StepResult:
"""Result of executing a reasoning step"""
step: ReasoningStep
success: bool
result_data: Any = None
error_message: str = ""
execution_time_ms: float = 0
timestamp: str = ""
def __post_init__(self):
if not self.timestamp:
self.timestamp = datetime.utcnow().isoformat()
def to_dict(self) -> Dict[str, Any]:
result = asdict(self)
result['step'] = self.step.to_dict()
return result
class IterativeContext:
"""Rich context for iterative reasoning process"""
def __init__(self, original_message: str, identity: str, channel: str, modality: str):
self.original_message = original_message
self.identity = identity
self.channel = channel
self.modality = modality
self.start_time = datetime.utcnow()
# Tracking
self.completed_steps: List[StepResult] = []
self.failed_steps: List[StepResult] = []
self.accumulated_knowledge: Dict[str, Any] = {}
self.service_call_counts: Dict[str, int] = {}
self.step_count = 0
# State flags
self.goal_satisfied = False
self.force_synthesis = False
self.max_steps_reached = False
self.timeout_reached = False
def add_step_result(self, step_result: StepResult):
"""Add a completed step result"""
self.step_count += 1
if step_result.success:
self.completed_steps.append(step_result)
# Track service calls
if step_result.step.action == StepAction.CALL_SERVICE.value:
service = step_result.step.target
self.service_call_counts[service] = self.service_call_counts.get(service, 0) + 1
# Accumulate knowledge from successful steps
if step_result.result_data:
step_key = f"step_{self.step_count}_{step_result.step.action}"
self.accumulated_knowledge[step_key] = step_result.result_data
else:
self.failed_steps.append(step_result)
def get_recent_steps(self, window: int = 3) -> List[StepResult]:
"""Get the last N completed steps"""
return self.completed_steps[-window:] if len(self.completed_steps) >= window else self.completed_steps
def has_new_information_in_recent_steps(self, window: int = 3) -> bool:
"""Check if recent steps added meaningful new information"""
recent_steps = self.get_recent_steps(window)
if len(recent_steps) < window:
return True # Not enough steps to judge
# Simple heuristic: check if recent steps have substantially different result data
recent_data_sizes = [len(str(step.result_data)) if step.result_data else 0 for step in recent_steps]
# If all recent steps produced very little data, might be converging
if all(size < 100 for size in recent_data_sizes):
return False
# Check for diversity in step types
recent_actions = [step.step.action for step in recent_steps]
if len(set(recent_actions)) == 1: # All same action type
return False
return True
def should_stop(self, max_steps: int = 10, max_service_calls: int = 3, max_time_minutes: int = 2) -> tuple[bool, str]:
"""Check all stopping criteria"""
# Hard limits
if self.step_count >= max_steps:
self.max_steps_reached = True
return True, f"Maximum steps reached ({max_steps})"
elapsed_minutes = (datetime.utcnow() - self.start_time).total_seconds() / 60
if elapsed_minutes >= max_time_minutes:
self.timeout_reached = True
return True, f"Maximum time reached ({max_time_minutes} minutes)"
# Service call limits
for service, count in self.service_call_counts.items():
if count >= max_service_calls:
return True, f"Too many calls to {service} service ({count})"
# Goal satisfaction
if self.goal_satisfied:
return True, "Goal satisfaction confirmed"
# Force synthesis
if self.force_synthesis:
return True, "Forced synthesis due to convergence"
# Convergence detection
if self.step_count >= 6 and not self.has_new_information_in_recent_steps(3):
self.force_synthesis = True
return True, "No new information in recent steps - converged"
return False, ""
def to_oracle_context(self) -> Dict[str, Any]:
"""Prepare context for Oracle decision making"""
return {
"original_message": self.original_message,
"step_count": self.step_count,
"completed_steps": [step.to_dict() for step in self.completed_steps[-5:]], # Last 5 steps
"accumulated_knowledge": self.accumulated_knowledge,
"service_call_counts": self.service_call_counts,
"failed_steps": [step.to_dict() for step in self.failed_steps[-3:]], # Last 3 failures
"goal_satisfied": self.goal_satisfied,
}
def get_summary(self) -> str:
"""Get a human-readable summary of the reasoning process"""
elapsed_time = (datetime.utcnow() - self.start_time).total_seconds()
summary = f"**Reasoning Summary**\n"
summary += f"• Steps completed: {len(self.completed_steps)}\n"
summary += f"• Failed steps: {len(self.failed_steps)}\n"
summary += f"• Service calls: {dict(self.service_call_counts)}\n"
summary += f"• Elapsed time: {elapsed_time:.1f}s\n"
if self.completed_steps:
summary += f"• Recent actions: {', '.join([s.step.action for s in self.completed_steps[-3:]])}\n"
return summary

View File

@@ -0,0 +1,481 @@
"""
Oracle service communication layer.
This module handles all interactions with the Oracle service including
requesting reasoning steps, checking goal satisfaction, and synthesizing responses.
"""
import json
import re
from typing import Optional, Dict, Any
from datetime import datetime
from core.logger import setup_logger
from core.service_discovery import discovery_client
from core.vi_identity import get_identity_for_context, get_identity_for_synthesis
from core.event_cache import event_cache
from .models import ReasoningStep, StepAction, IterativeContext
from .formatters import KnowledgeFormatter
class OracleClient:
"""Handles all communication with the Oracle service"""
def __init__(self, formatter: KnowledgeFormatter, logger_name: str = 'oracle_client'):
self.logger = setup_logger(logger_name, service_name='think_service')
self.formatter = formatter
async def _get_recent_events_context(self, identity: str, limit: int = 10) -> str:
"""
Retrieve recent cached events for this identity and format for LLM context.
Returns formatted string or empty string if no events available.
"""
try:
recent_context = await event_cache.format_for_llm(identity, limit)
if recent_context:
self.logger.debug(f"[💭] 📝 Retrieved recent event context for {identity}")
return recent_context
return ""
except Exception as e:
self.logger.warning(f"[💭] Failed to retrieve recent event context: {e}")
return ""
async def request_next_step(self, context: IterativeContext) -> Optional[ReasoningStep]:
"""Ask Oracle to decide the next reasoning step"""
try:
self.logger.debug(f"[💭] Requesting next step from Oracle (step {context.step_count + 1})")
# Get Vi's identity with planning voice mode
lyra_identity = get_identity_for_context("planning")
# Format accumulated knowledge as natural language
knowledge_summary = self.formatter.format_for_oracle(context)
# Get recent events context from cache
recent_events = await self._get_recent_events_context(context.identity, limit=10)
# Build Oracle prompt
oracle_request = {
"type": "iterative_reasoning",
"content": self._build_reasoning_prompt(lyra_identity, context, knowledge_summary, recent_events),
"identity": context.identity,
"context": {}
}
# Send to Oracle and get response
self.logger.debug(f"[💭] Sending request to Oracle for step {context.step_count + 1}")
result = await discovery_client.call_service(
"oracle", "process", oracle_request, timeout=30.0
)
oracle_response = result.data if result.success else None
if not oracle_response or not oracle_response.get("content"):
self.logger.warning(f"[💭] No response from Oracle for next step")
return None
# Parse Oracle's function call decision
content = oracle_response["content"].strip()
self.logger.info(f"[💭] ✅ Oracle responded for step {context.step_count + 1}: {content[:100]}...")
self.logger.debug(f"[💭] Full Oracle response: {content}")
# Parse function call
function_call_data = self._parse_function_call(content)
if not function_call_data:
self.logger.warning(f"[💭] No function call found in Oracle response")
return None
function_name = function_call_data['function']
function_args = function_call_data['args']
reasoning = function_call_data['reasoning']
self.logger.info(f"[💭] 🔍 Parsed function call for step {context.step_count + 1}: {function_name}({function_args})")
# All functions except ready() map to CALL_SERVICE
if function_name == 'ready':
action = StepAction.SYNTHESIZE_FINAL.value
target = None
else:
action = StepAction.CALL_SERVICE.value
target = function_name
# Create ReasoningStep with function args stored for execution
next_step = ReasoningStep(
action=action,
target=target,
reasoning=reasoning,
ready=(function_name == 'ready')
)
# Store function args in the step for later execution
next_step.function_args = function_args
self.logger.info(f"[💭] ✓ Created ReasoningStep for step {context.step_count + 1}: {function_name}({function_args}) -> {action}")
return next_step
except Exception as e:
self.logger.error(f"[💭] Error requesting next step: {e}")
return None
def _build_reasoning_prompt(self, lyra_identity: str, context: IterativeContext, knowledge_summary: str, recent_events: str = "") -> str:
"""Build the reasoning prompt for Oracle"""
# Build recent events section if available
recent_events_section = f"\n{recent_events}\n" if recent_events else ""
return f"""{lyra_identity}
You are engaging with {context.identity}.
CURRENT REQUEST: "{context.original_message}"
{recent_events_section}
{knowledge_summary}
Choose your next action:
AVAILABLE FUNCTIONS:
Memory (Three Layers):
- short_memory(n=10) - Get the n most recent literal memories
- short_memory(n=10, offset=5) - Get n memories starting from offset back (for pagination)
- long_memory(query="topic", n=5) - Get n long-term summarized memories related to query (or random if query=None)
- facts(query="topic", n=5) - Get n most relevant facts related to query
- save_fact(content="...", category="...", mutable=True/False) - Save a new fact
Categories: "personal" (immutable facts like birthdays), "preferences" (likes/dislikes), "knowledge" (learned info), "general"
Set mutable=False for unchangeable facts (birthdays), mutable=True for preferences that may change
- update_fact(fact_id="uuid-123", new_content="Updated fact") - Update existing fact (only if mutable)
Information:
- identity(person="alex") - Get single person's full identity & attributes
- search_relationships(entity_type="pet", min_trust=0.7) - Query multiple entities
- health() - Check system status
- duckduckgo(query="weather in tokyo", limit=3) - Search DuckDuckGo instant answers (on-demand)
Relationships:
- introduce(name="Harvey", entity_type="pet", relationships=["family","companion"], context="Alex's dog", attributes={{"species":"dog","breed":"golden_retriever"}}) - Create new entity
- update_relationship(person="alex", trust_delta=0.0, intimacy_delta=0.15, reason="vulnerable moment") - Update relationship explicitly
- add_attribute(person="alex", key="favorite_food", value="pasta") - Remember new information
- link_identity(external_id="@someone:matrix.org", internal_id="someone", confidence=0.85) - Connect external ID to internal
Task Management:
- todo_create(content="Fix bug in X", activeForm="Fixing bug in X", status="pending") - Create a new todo item
- todo_update(todo_id="abc123", status="in_progress") - Update todo status (pending/in_progress/completed)
- todo_list() - Get all todos with their current status
- todo_complete(todo_id="abc123") - Mark a todo as completed
Meta:
- ready() - Signal you have enough info to answer
EXAMPLES:
short_memory(n=5) // Get last 5 messages
short_memory(n=10, offset=5) // Get 10 messages starting from 5 back
long_memory(query="cooking preferences", n=3) // Find relevant historical context
facts(query="birthday", n=5) // Find birthday facts
facts(query="food", n=3) // Find food-related facts
save_fact(content="Alex's birthday is May 15th", category="personal", mutable=False) // Immutable personal fact
save_fact(content="Alex prefers Italian food", category="preferences", mutable=True) // Mutable preference
save_fact(content="Python uses duck typing", category="knowledge", mutable=True) // Learned knowledge
update_fact(fact_id="abc-123", new_content="Alex now prefers Thai food") // Update mutable preference
identity(person="alex") // Get Alex's full context
add_attribute(person="alex", key="favorite_mountain", value="Pikes Peak") // Remember preference
introduce(name="Curie", entity_type="pet", relationships=["family"], context="Alex's cat", attributes={{"species":"cat"}}) // New entity
duckduckgo(query="python list comprehension", limit=3) // Search for quick answers
STRATEGY:
- Use short_memory() for recent conversation context (what was just said)
- Use long_memory() for historical patterns and past discussions (weeks/months ago)
- Use facts() for established knowledge (birthdays, preferences, learned information)
- Save important discoverable facts with save_fact() (choose appropriate category and mutability)
- Update changed preferences with update_fact() (requires fact_id from facts() query)
- Use identity() for person details, search_relationships() for entities
- For complex multi-step tasks: Use todo_create() to break down work, todo_update() to track progress, todo_complete() when done
- Call ready() when you have enough information to answer the user's question
NOTE: Classification (sentiment, emotions, intent) and creative tasks (writing, poetry) are handled during synthesis.
Respond with just the function call and optional reasoning:
function_name(args)
// Optional: Brief reason why"""
async def check_goal_satisfaction(self, context: IterativeContext) -> bool:
"""Check if we have sufficient information to answer the original question"""
try:
self.logger.debug(f"[💭] Checking goal satisfaction")
# Get Vi's identity with planning voice mode
lyra_identity = get_identity_for_context("planning")
# Format accumulated knowledge as natural language
knowledge_summary = self.formatter.format_for_oracle(context)
# Get recent events context from cache
recent_events = await self._get_recent_events_context(context.identity, limit=10)
recent_events_section = f"\n{recent_events}\n" if recent_events else ""
oracle_request = {
"type": "goal_check",
"content": f"""{lyra_identity}
You are engaging with {context.identity}.
Evaluate whether you have sufficient information to provide a complete, helpful answer to the user's request.
ORIGINAL REQUEST: "{context.original_message}"
{recent_events_section}
{knowledge_summary}
EVALUATION CRITERIA:
- Can you address the main points of the user's request?
- Do you have enough specific information to be helpful?
- Are there critical gaps that would make your answer incomplete or unhelpful?
Respond with JSON indicating your assessment:
{{"can_answer": true/false, "reasoning": "Brief explanation of why you can or cannot provide a complete answer"}}""",
"identity": context.identity,
"context": {}
}
# Ask Oracle
result = await discovery_client.call_service(
"oracle", "process", oracle_request, timeout=15.0
)
oracle_response = result.data if result.success else None
if oracle_response and oracle_response.get("content"):
try:
result = json.loads(oracle_response["content"])
can_answer = result.get("can_answer", False)
reasoning = result.get("reasoning", "")
self.logger.debug(f"[💭] Goal satisfaction check: {can_answer} - {reasoning}")
return can_answer
except json.JSONDecodeError:
# If not JSON, check for keywords
content = oracle_response["content"].lower()
return "yes" in content or "can answer" in content or "sufficient" in content
return False
except Exception as e:
self.logger.error(f"[💭] Error checking goal satisfaction: {e}")
return False
async def synthesize_final_response(self, context: IterativeContext) -> Optional[str]:
"""Synthesize final response from accumulated knowledge"""
try:
self.logger.debug(f"[💭] Synthesizing final response from {len(context.completed_steps)} steps")
# Get Vi's identity with voice guide - she chooses appropriate tone
lyra_identity = get_identity_for_synthesis(include_voice_guide=True)
# Format accumulated knowledge as natural language
knowledge_summary = self.formatter.format_for_oracle(context)
# Get recent events context from cache
recent_events = await self._get_recent_events_context(context.identity, limit=10)
recent_events_section = f"\n{recent_events}\n" if recent_events else ""
oracle_request = {
"type": "synthesis",
"content": f"""{lyra_identity}
You are engaging with {context.identity}.
You have completed a step-by-step reasoning process. Now synthesize this into a comprehensive, helpful response.
ORIGINAL REQUEST: "{context.original_message}"
{recent_events_section}
{knowledge_summary}
SYNTHESIS INSTRUCTIONS:
- Create a natural, conversational response that directly addresses the user's request
- Integrate insights from all the information you gathered during reasoning
- Be specific and actionable when appropriate
- If you gathered system information, present it clearly
- If you found relevant memories or context, incorporate them naturally
- Handle any needed classification (sentiment, emotions, intent) or creative tasks (writing, poetry, styling) directly in your response
- Make the response feel cohesive, not like a list of separate findings
GOAL: Provide a complete, helpful answer that shows you understood their request and used the gathered information effectively.""",
"identity": context.identity,
"context": {}
}
# Get final response from Oracle
result = await discovery_client.call_service(
"oracle", "process", oracle_request, timeout=30.0
)
oracle_response = result.data if result.success else None
if oracle_response and oracle_response.get("content"):
final_response = oracle_response["content"]
self.logger.debug(f"[💭] Final synthesis complete: {len(final_response)} characters")
return final_response
return None
except Exception as e:
self.logger.error(f"[💭] Error synthesizing final response: {e}")
return None
async def analyze_interaction(
self,
context: IterativeContext,
user_message: str,
response_content: str
) -> Dict[str, Any]:
"""
Ask Oracle to analyze sentiment and depth of the interaction.
Returns: {"sentiment": str, "depth": float, "reasoning": str}
"""
try:
# Build analysis request
knowledge_summary = self.formatter.format_for_oracle(context)
analysis_request = {
"type": "interaction_analysis",
"original_message": user_message,
"lyra_response": response_content,
"knowledge_summary": knowledge_summary,
"identity": context.identity,
"metadata": {
"step_count": len(context.completed_steps),
"services_called": list(context.service_call_counts.keys()),
"response_length": len(response_content)
}
}
# Ask Oracle to analyze
self.logger.debug(f"[💭] Requesting interaction analysis from Oracle...")
result = await discovery_client.call_service(
"oracle", "process", analysis_request, timeout=15.0
)
if not result.success:
self.logger.error(f"[💭] Oracle analysis failed: {result.error}")
return {"sentiment": "positive", "depth": 0.3, "reasoning": "Analysis failed"}
analysis = result.data
sentiment = analysis.get("sentiment", "positive")
depth = analysis.get("depth", 0.3)
reasoning = analysis.get("reasoning", "")
self.logger.info(f"[💭] 📊 Oracle analysis: sentiment={sentiment}, depth={depth:.2f}")
self.logger.debug(f"[💭] Oracle reasoning: {reasoning}")
return {
"sentiment": sentiment,
"depth": depth,
"reasoning": reasoning
}
except Exception as e:
self.logger.error(f"[💭] Error analyzing interaction: {e}")
return {"sentiment": "positive", "depth": 0.3, "reasoning": f"Error: {str(e)}"}
def _parse_function_call(self, content: str) -> Optional[Dict[str, Any]]:
"""
Parse Python-like function call from Oracle's output.
Returns: {"function": "name", "args": {...}, "reasoning": "..."}
"""
# Valid function names
valid_functions = [
'short_memory', 'long_memory', 'facts', 'save_fact', 'update_fact',
'identity', 'search_relationships', 'health', 'duckduckgo',
'introduce', 'update_relationship', 'add_attribute', 'link_identity',
'todo_create', 'todo_update', 'todo_list', 'todo_complete',
'ready'
]
# Extract reasoning (lines starting with //)
reasoning_parts = []
for line in content.split('\n'):
if line.strip().startswith('//'):
reasoning_parts.append(line.strip()[2:].strip())
reasoning = " ".join(reasoning_parts) if reasoning_parts else ""
# Find function call - try multiple patterns
function_match = None
for func in valid_functions:
# Pattern: function_name(...) with any content inside
pattern = f'{func}\\s*\\(([^)]*)\\)'
match = re.search(pattern, content, re.IGNORECASE | re.DOTALL)
if match:
function_name = func
args_string = match.group(1).strip()
function_match = (function_name, args_string)
break
if not function_match:
return None
function_name, args_string = function_match
# Parse arguments
args = self._parse_function_args(args_string)
return {
'function': function_name,
'args': args,
'reasoning': reasoning or f"Oracle chose {function_name}"
}
def _parse_function_args(self, args_string: str) -> Dict[str, Any]:
"""Parse function arguments from string"""
args = {}
if not args_string:
return args
try:
# Better pattern that respects quoted strings with commas
# Matches: key=value where value can be quoted string, number, boolean, or JSON
kwarg_pattern = r'(\w+)\s*=\s*(?:"([^"\\]*(?:\\.[^"\\]*)*)"|\'([^\'\\]*(?:\\.[^\'\\]*)*)\'|(\{[^\}]*\})|(\[[^\]]*\])|([^,]+))'
matches = re.findall(kwarg_pattern, args_string)
for match in matches:
key = match[0]
# match[1] = double-quoted string, match[2] = single-quoted string
# match[3] = dict, match[4] = list, match[5] = unquoted value
if match[1]: # Double-quoted string
value = match[1]
# Unescape any escaped quotes
args[key] = value.replace('\\"', '"').replace('\\\\', '\\')
elif match[2]: # Single-quoted string
value = match[2]
# Unescape any escaped quotes
args[key] = value.replace("\\'", "'").replace('\\\\', '\\')
elif match[3]: # Dict
try:
# Try JSON parse, converting single quotes to double
json_str = match[3].replace("'", '"')
args[key] = json.loads(json_str)
except:
args[key] = match[3]
elif match[4]: # List
try:
# Try JSON parse, converting single quotes to double
json_str = match[4].replace("'", '"')
args[key] = json.loads(json_str)
except:
args[key] = match[4]
else: # Unquoted value (number, boolean, or bare string)
value = match[5].strip()
if value.lower() in ('true', 'false'):
args[key] = value.lower() == 'true'
elif value.lower() == 'none':
args[key] = None
else:
# Try as number
try:
args[key] = int(value)
except ValueError:
try:
args[key] = float(value)
except ValueError:
args[key] = value
except Exception as e:
self.logger.warning(f"[💭] Error parsing function args: {e}")
args = {}
return args

View File

@@ -0,0 +1,204 @@
"""
Iterative reasoning orchestrator.
This module orchestrates the main iterative reasoning loop, coordinating
between Oracle, step execution, and output sending.
"""
from typing import Optional, Callable, Any
from datetime import datetime
from core.logger import setup_logger
from core.nats_event_bus import nats_bus as event_bus
from .models import IterativeContext, StepAction
from .oracle_client import OracleClient
from .step_executor import StepExecutor
from .formatters import KnowledgeFormatter
class IterativeOrchestrator:
"""Orchestrates the iterative reasoning process"""
def __init__(
self,
oracle_client: OracleClient,
step_executor: StepExecutor,
formatter: KnowledgeFormatter,
output_sender: Callable,
logger_name: str = 'orchestrator'
):
self.logger = setup_logger(logger_name, service_name='think_service')
self.oracle_client = oracle_client
self.step_executor = step_executor
self.formatter = formatter
self.send_output = output_sender
async def run(
self,
user_message: str,
identity: str,
channel: str,
modality: str
) -> Optional[str]:
"""
Generate response using iterative step-by-step reasoning.
Oracle decides one step at a time and sophisticated stopping criteria
ensure efficient completion.
"""
self.logger.info(f"[💭] 🔄 Starting iterative reasoning for: '{user_message[:50]}...'")
# Initialize iterative context
context = IterativeContext(user_message, identity, channel, modality)
try:
# Send initial status to Matrix
await self.send_output(
f"🔄 **Iterative Reasoning Started**\n\nAnalyzing your request step by step...",
channel, modality
)
# Main iterative reasoning loop
while True:
# Check stopping criteria
should_stop, stop_reason = context.should_stop()
if should_stop:
self.logger.info(f"[💭] 🛑 Stopping iteration: {stop_reason}")
await self.send_output(
f"🎯 **Reasoning Complete**\n\n{stop_reason}\n\n{context.get_summary()}",
channel, modality
)
break
# Goal satisfaction check (every 3 steps)
if context.step_count > 0 and context.step_count % 3 == 0:
can_answer = await self.oracle_client.check_goal_satisfaction(context)
if can_answer:
context.goal_satisfied = True
self.logger.info(f"[💭] ✅ Goal satisfaction confirmed at step {context.step_count}")
# Will be caught by stopping criteria on next iteration
continue
# Ask Oracle for next step
self.logger.info(f"[💭] 🔮 Requesting step {context.step_count + 1} from Oracle")
next_step = await self.oracle_client.request_next_step(context)
if not next_step:
self.logger.warning(f"[💭] ⚠️ Oracle failed to provide next step")
await self.send_output(
f"❌ **Oracle Error**: Failed to get next step decision",
channel, modality
)
break
self.logger.info(f"[💭] 📋 Step {context.step_count + 1}: {next_step.action} -> {next_step.target} (args: {getattr(next_step, 'function_args', {})})")
# Show Oracle's decision to user
await self.send_output(
f"🧠 **Oracle Decision**: {next_step.action}\n"
f"**Target**: {next_step.target or 'N/A'}\n"
f"**Reasoning**: {next_step.reasoning}",
channel, modality
)
# Check if Oracle signals readiness for synthesis
if next_step.action == StepAction.SYNTHESIZE_FINAL.value or next_step.ready:
self.logger.info(f"[💭] 🎯 Oracle signals ready for synthesis")
await self.send_output(
f"🎯 **Ready for Synthesis**: Oracle indicates sufficient information gathered",
channel, modality
)
break
# Execute the step directly
self.logger.info(f"[💭] ⚙️ Executing step {context.step_count + 1}")
step_result = await self.step_executor.execute_step(next_step, context)
self.logger.info(f"[💭] ✅ Step {context.step_count + 1} execution completed, success={step_result.success}")
context.add_step_result(step_result)
self.logger.info(f"[💭] 📝 Step {context.step_count} result added to context (total: {len(context.completed_steps)} completed)")
# Send detailed execution result to Matrix
if step_result.success:
result_summary = self.formatter.format_step_result_for_matrix(step_result)
await self.send_output(
f"✅ **Step {context.step_count} Completed**: {next_step.action}\n"
f"**Execution Time**: {step_result.execution_time_ms:.0f}ms\n"
f"**Result**: {result_summary}",
channel, modality
)
else:
await self.send_output(
f"❌ **Step {context.step_count} Failed**: {step_result.error_message}\n"
f"**Execution Time**: {step_result.execution_time_ms:.0f}ms",
channel, modality
)
# Final synthesis
await self.send_output(
f"🔮 **Synthesizing Final Response**\n\nCombining insights from {len(context.completed_steps)} successful steps...",
channel, modality
)
final_response = await self.oracle_client.synthesize_final_response(context)
if final_response:
# Finalize interaction with sentiment/depth detection
await self._finalize_interaction(
identity,
user_message,
final_response,
context
)
self.logger.info(f"[💭] ✅ Iterative reasoning completed successfully")
return final_response
else:
self.logger.error(f"[💭] ⚠️ Final synthesis failed")
await self.send_output(
f"❌ **Synthesis Failed**\n\nI gathered information but couldn't synthesize a final response. Please try rephrasing your question.",
channel, modality
)
return None
except Exception as e:
self.logger.exception(f"[💭] Error in iterative reasoning: {e}")
await self.send_output(
f"❌ **Iterative Reasoning Error**\n\nSomething went wrong during the reasoning process: {str(e)}",
channel, modality
)
return None
async def _finalize_interaction(
self,
identity: str,
user_message: str,
response_content: str,
context: IterativeContext
):
"""
Finalize interaction by asking oracle to analyze sentiment/depth.
Called after synthesis completes.
"""
try:
# Ask Oracle to analyze the interaction
self.logger.debug(f"[💭] Requesting interaction analysis from oracle...")
analysis = await self.oracle_client.analyze_interaction(context, user_message, response_content)
sentiment = analysis.get("sentiment", "positive")
depth = analysis.get("depth", 0.3)
# Publish interaction completion event for identity service
await event_bus.emit("vi.interaction.completed", {
"internal_id": identity,
"sentiment": sentiment, # positive, neutral, negative
"depth": depth, # 0.0-1.0
"summary": f"Conversation with {len(context.completed_steps)} reasoning steps",
"timestamp": datetime.utcnow().isoformat()
})
self.logger.info(f"[💭] 📊 Interaction finalized: {identity} (sentiment={sentiment}, depth={depth:.2f})")
except Exception as e:
self.logger.error(f"[💭] Error finalizing interaction: {e}")
# Don't publish event if we couldn't analyze properly

View File

@@ -0,0 +1,625 @@
"""
Step execution engine for iterative reasoning.
This module executes reasoning steps by calling appropriate services
and executing Oracle-requested functions.
"""
from typing import Any
from datetime import datetime
from core.logger import setup_logger
from core.service_discovery import discovery_client
from core.event_cache import event_cache
from .models import ReasoningStep, StepResult, StepAction, IterativeContext
from .todo_manager import TodoManager
class StepExecutor:
"""Executes reasoning steps and service calls"""
def __init__(self, memory_manager, logger_name: str = 'step_executor'):
self.logger = setup_logger(logger_name, service_name='think_service')
self.memory_manager = memory_manager
self.todo_manager = TodoManager()
async def execute_step(self, step: ReasoningStep, context: IterativeContext) -> StepResult:
"""Execute a validated reasoning step"""
start_time = datetime.utcnow()
step_exec_id = f"{context.step_count + 1}_{datetime.utcnow().timestamp()}"
try:
self.logger.debug(f"[💭] [{step_exec_id}] Executing step: {step.action} -> {step.target}")
if step.action == StepAction.CALL_SERVICE.value:
# Execute service call
self.logger.debug(f"[💭] [{step_exec_id}] Calling _execute_service_call for {step.target}")
result_data = await self._execute_service_call(step, context, step_exec_id)
# Cache service call event for LLM context
try:
# Format content based on result
success = result_data.get('success', True) if isinstance(result_data, dict) else True
service_name = step.target
function_args = getattr(step, 'function_args', {})
# Create human-readable summary
content = f"Called {service_name}({function_args})"
if isinstance(result_data, dict):
if 'error' in result_data:
content += f" - Error: {result_data['error']}"
elif 'count' in result_data:
content += f" - Found {result_data['count']} results"
elif 'message' in result_data:
content += f" - {result_data['message']}"
await event_cache.add_event(
identity=context.identity,
interaction_id=step_exec_id,
event_type='service_call',
content=content,
metadata={
'service': service_name,
'success': success,
'function_args': function_args
}
)
self.logger.debug(f"[💭] 📝 Cached service call event for {context.identity}")
except Exception as e:
self.logger.warning(f"[💭] Failed to cache service call event: {e}")
elif step.action == StepAction.CHECK_GOAL_SATISFACTION.value:
# This is now handled by oracle_client, shouldn't reach here
result_data = False
else:
# Unknown action
return StepResult(
step=step,
success=False,
error_message=f"Unknown step action: {step.action}",
execution_time_ms=(datetime.utcnow() - start_time).total_seconds() * 1000
)
# Calculate execution time
execution_time_ms = (datetime.utcnow() - start_time).total_seconds() * 1000
return StepResult(
step=step,
success=True,
result_data=result_data,
execution_time_ms=execution_time_ms
)
except Exception as e:
self.logger.error(f"[💭] Error executing step: {e}")
execution_time_ms = (datetime.utcnow() - start_time).total_seconds() * 1000
return StepResult(
step=step,
success=False,
error_message=str(e),
execution_time_ms=execution_time_ms
)
async def _execute_service_call(self, step: ReasoningStep, context: IterativeContext, step_exec_id: str = "unknown") -> Any:
"""Execute a service call step with function arguments"""
function_name = step.target
function_args = getattr(step, 'function_args', {})
self.logger.info(f"[💭] [{step_exec_id}] Executing function: {function_name}({function_args})")
# Route to appropriate handler
if function_name == "short_memory":
return await self._execute_short_memory(function_args, context)
elif function_name == "long_memory":
return await self._execute_long_memory(function_args, context)
elif function_name == "facts":
return await self._execute_facts(function_args, context)
elif function_name == "save_fact":
return await self._execute_save_fact(function_args, context, step_exec_id)
elif function_name == "update_fact":
return await self._execute_update_fact(function_args, context, step_exec_id)
elif function_name == "identity":
return await self._execute_identity(function_args, context)
elif function_name == "search_relationships":
return await self._execute_search_relationships(function_args)
elif function_name == "health":
return await self._execute_health()
elif function_name == "duckduckgo":
return await self._execute_duckduckgo(function_args, context, step_exec_id)
elif function_name == "introduce":
return await self._execute_introduce(function_args, context)
elif function_name == "add_attribute":
return await self._execute_add_attribute(function_args)
elif function_name == "update_relationship":
return await self._execute_update_relationship(function_args)
elif function_name == "link_identity":
return await self._execute_link_identity(function_args)
elif function_name == "todo_create":
return await self._execute_todo_create(function_args, context)
elif function_name == "todo_update":
return await self._execute_todo_update(function_args, context)
elif function_name == "todo_list":
return await self._execute_todo_list(context)
elif function_name == "todo_complete":
return await self._execute_todo_complete(function_args, context)
else:
raise ValueError(f"Unknown function: {function_name}")
async def _execute_short_memory(self, args: dict, context: IterativeContext) -> dict:
"""Execute short_memory function"""
n = args.get('n', 10)
offset = args.get('offset', 0)
result = await discovery_client.call_service(
"memory",
"short_memory",
{
"limit": n,
"offset": offset,
"identity_id": context.identity
},
timeout=3.0
)
if result.success and result.data.get("status") == "success":
memories = result.data.get("memories", [])
return {
"success": True,
"memories": memories,
"count": len(memories),
"type": "short_term",
"offset": offset
}
return {
"success": False,
"memories": [],
"count": 0,
"type": "short_term",
"error": "Failed to fetch short-term memories"
}
async def _execute_long_memory(self, args: dict, context: IterativeContext) -> dict:
"""Execute long_memory function"""
query = args.get('query')
n = args.get('n', 5)
result = await discovery_client.call_service(
"memory",
"long_memory",
{
"query": query,
"limit": n,
"identity_id": context.identity
},
timeout=5.0
)
if result.success and result.data.get("status") == "success":
memories = result.data.get("memories", [])
return {
"success": True,
"memories": memories,
"count": len(memories),
"type": "long_term",
"query": query
}
return {
"success": False,
"memories": [],
"count": 0,
"type": "long_term",
"error": "Failed to fetch long-term memories"
}
async def _execute_facts(self, args: dict, context: IterativeContext) -> dict:
"""Execute facts function"""
query = args.get('query', '')
n = args.get('n', 5)
result = await discovery_client.call_service(
"memory",
"facts",
{
"query": query,
"limit": n,
"identity_id": context.identity
},
timeout=3.0
)
if result.success and result.data.get("status") == "success":
facts = result.data.get("facts", [])
return {
"success": True,
"facts": facts,
"count": len(facts),
"query": query
}
return {
"success": False,
"facts": [],
"count": 0,
"error": "Failed to fetch facts"
}
async def _execute_save_fact(self, args: dict, context: IterativeContext, step_exec_id: str) -> dict:
"""Execute save_fact function"""
content = args.get('content')
category = args.get('category', 'general')
mutable = args.get('mutable', True)
if not content:
return {
"success": False,
"status": "error",
"error": "content is required"
}
self.logger.info(f"[💭] [{step_exec_id}] Calling memory service save_fact: category={category}, content='{content[:50]}...'")
result = await discovery_client.call_service(
"memory",
"save_fact",
{
"content": content,
"category": category,
"identities": [context.identity],
"mutable": mutable,
"metadata": {
"source": "oracle_decision",
"timestamp": datetime.utcnow().isoformat(),
"step_exec_id": step_exec_id
}
},
timeout=5.0, # Increased - ChromaDB embedding can take 2-3 seconds
retry_attempts=1 # Disable retries for write operations
)
if result.success and result.data.get("status") == "success":
fact_id = result.data.get("fact_id")
self.logger.info(f"[💭] [{step_exec_id}] 💾 Saved fact: {fact_id[:8]}... {content[:50]}...")
return {
"success": True,
"status": "success",
"fact_id": fact_id,
"message": f"Saved fact: {content[:50]}..."
}
self.logger.warning(f"[💭] [{step_exec_id}] Failed to save fact: {result.error if not result.success else result.data.get('error')}")
return {
"success": False,
"status": "error",
"error": "Failed to save fact"
}
async def _execute_update_fact(self, args: dict, context: IterativeContext, step_exec_id: str) -> dict:
"""Execute update_fact function"""
fact_id = args.get('fact_id')
new_content = args.get('new_content')
if not fact_id or not new_content:
return {
"success": False,
"status": "error",
"error": "fact_id and new_content required"
}
result = await discovery_client.call_service(
"memory",
"update_fact",
{
"fact_id": fact_id,
"new_content": new_content,
"identity_id": context.identity,
"metadata": {
"updated_by": "oracle_decision",
"timestamp": datetime.utcnow().isoformat()
}
},
timeout=5.0,
retry_attempts=1
)
if result.success and result.data.get("status") == "success":
self.logger.info(f"[💭] ✏️ Updated fact: {fact_id}")
return {
"success": True,
"status": "success",
"message": f"Updated fact {fact_id}"
}
error_msg = result.data.get("error", "Failed to update fact") if result.success else "Service call failed"
return {
"success": False,
"status": "error",
"error": error_msg
}
async def _execute_identity(self, args: dict, context: IterativeContext) -> dict:
"""Execute identity function"""
person = args.get('person', context.identity)
result = await discovery_client.call_service(
"identity", "get", {"internal_id": person}, timeout=10.0
)
if result.success:
return {"identity": result.data}
else:
return {"error": "Failed to get identity"}
async def _execute_search_relationships(self, args: dict) -> dict:
"""Execute search_relationships function"""
result = await discovery_client.call_service(
"identity", "search", args, timeout=10.0
)
if result.success:
return {"relationships": result.data}
else:
return {"error": "Failed to search relationships"}
async def _execute_health(self) -> dict:
"""Execute health function"""
health_status = await self.memory_manager.get_health_status()
return {"health": health_status}
async def _execute_duckduckgo(self, args: dict, context: IterativeContext, step_exec_id: str) -> dict:
"""Execute duckduckgo function"""
query = args.get('query', '')
limit = args.get('limit', 5)
if not query:
return {
"success": False,
"error": "query is required"
}
self.logger.info(f"[💭] [{step_exec_id}] Executing DuckDuckGo plugin: query='{query}'")
result = await discovery_client.call_service(
"plugin-orchestrator",
"execute_plugin",
{
"plugin_name": "duckduckgo",
"operation": "search",
"parameters": {
"query": query,
"limit": limit
},
"identity": context.identity,
"timeout": 60 # Increased from 15s to 60s for Job pod startup + API call
},
timeout=75.0, # NATS timeout (must be > plugin timeout)
retry_attempts=1
)
if result.success and result.data.get("status") == "success":
plugin_result = result.data.get("result", {})
self.logger.info(f"[💭] [{step_exec_id}] DuckDuckGo search completed")
return {
"success": True,
"query": query,
"answer": plugin_result.get("answer"),
"abstract": plugin_result.get("abstract"),
"definition": plugin_result.get("definition"),
"results": plugin_result.get("results", []),
"related_topics": plugin_result.get("related_topics", [])
}
self.logger.warning(f"[💭] [{step_exec_id}] DuckDuckGo search failed")
return {
"success": False,
"error": result.data.get("error", "Plugin execution failed") if result.success else result.error
}
async def _execute_introduce(self, args: dict, context: IterativeContext) -> dict:
"""Execute introduce function"""
payload = {
"name": args.get('name'),
"entity_type": args.get('entity_type', 'human'),
"relationship_types": args.get('relationships', []),
"context": args.get('context', ''),
"attributes": args.get('attributes', {}),
"introduced_by": context.identity
}
result = await discovery_client.call_service(
"identity", "introduce", payload, timeout=10.0
)
if result.success:
self.logger.info(f"[💭] 👋 Introduced new entity: {args.get('name')}")
return {"introduced": result.data}
else:
return {"error": "Failed to introduce entity"}
async def _execute_add_attribute(self, args: dict) -> dict:
"""Execute add_attribute function"""
payload = {
"internal_id": args.get('person'),
"attribute_key": args.get('key'),
"attribute_value": args.get('value')
}
result = await discovery_client.call_service(
"identity", "add_attribute", payload, timeout=10.0
)
if result.success:
self.logger.info(f"[💭] 📝 Added attribute: {payload['internal_id']}.{payload['attribute_key']}")
return {"attribute_added": True}
else:
return {"error": "Failed to add attribute"}
async def _execute_update_relationship(self, args: dict) -> dict:
"""Execute update_relationship function"""
payload = {
"internal_id": args.get('person'),
"trust_delta": args.get('trust_delta', 0.0),
"intimacy_delta": args.get('intimacy_delta', 0.0),
"reason": args.get('reason', 'oracle_update'),
"interaction_summary": f"Oracle explicit update: {args.get('reason', '')}"
}
result = await discovery_client.call_service(
"identity", "update", payload, timeout=10.0
)
if result.success:
self.logger.info(f"[💭] 💚 Updated relationship: {payload['internal_id']}")
return {"relationship_updated": True}
else:
return {"error": "Failed to update relationship"}
async def _execute_link_identity(self, args: dict) -> dict:
"""Execute link_identity function"""
payload = {
"external_id": args.get('external_id'),
"internal_id": args.get('internal_id'),
"identity_type": args.get('identity_type', 'unknown'),
"verified": args.get('confidence', 0.0) > 0.9
}
result = await discovery_client.call_service(
"identity", "link", payload, timeout=10.0
)
if result.success:
self.logger.info(f"[💭] 🔗 Linked identity: {payload['external_id']} -> {payload['internal_id']}")
return {"identity_linked": True}
else:
return {"error": "Failed to link identity"}
async def _execute_todo_create(self, args: dict, context: IterativeContext) -> dict:
"""Execute todo_create function"""
content = args.get('content')
active_form = args.get('activeForm', args.get('active_form', content))
status = args.get('status', 'pending')
if not content:
return {
"success": False,
"error": "content is required"
}
try:
todo = await self.todo_manager.create(
interaction_id=context.interaction_id,
content=content,
active_form=active_form,
status=status
)
self.logger.info(f"[💭] ✓ Created todo {todo.todo_id}: {content}")
return {
"success": True,
"todo_id": todo.todo_id,
"content": content,
"status": status,
"message": f"Created todo: {content}"
}
except Exception as e:
self.logger.error(f"[💭] Error creating todo: {e}")
return {
"success": False,
"error": f"Failed to create todo: {str(e)}"
}
async def _execute_todo_update(self, args: dict, context: IterativeContext) -> dict:
"""Execute todo_update function"""
todo_id = args.get('todo_id')
status = args.get('status')
content = args.get('content')
active_form = args.get('activeForm', args.get('active_form'))
if not todo_id:
return {
"success": False,
"error": "todo_id is required"
}
try:
result = await self.todo_manager.update(
interaction_id=context.interaction_id,
todo_id=todo_id,
status=status,
content=content,
active_form=active_form
)
if result:
self.logger.info(f"[💭] ✏️ Updated todo {todo_id}")
return {
"success": True,
"todo_id": todo_id,
"message": f"Updated todo {todo_id}"
}
else:
return {
"success": False,
"error": f"Todo {todo_id} not found"
}
except Exception as e:
self.logger.error(f"[💭] Error updating todo: {e}")
return {
"success": False,
"error": f"Failed to update todo: {str(e)}"
}
async def _execute_todo_list(self, context: IterativeContext) -> dict:
"""Execute todo_list function"""
try:
todos = await self.todo_manager.list(context.interaction_id)
summary = await self.todo_manager.get_summary(context.interaction_id)
todo_list = [
{
"todo_id": t.todo_id,
"content": t.content,
"active_form": t.active_form,
"status": t.status
}
for t in todos
]
self.logger.info(f"[💭] 📋 Listed {len(todos)} todos")
return {
"success": True,
"todos": todo_list,
"summary": summary,
"count": len(todos)
}
except Exception as e:
self.logger.error(f"[💭] Error listing todos: {e}")
return {
"success": False,
"error": f"Failed to list todos: {str(e)}",
"todos": []
}
async def _execute_todo_complete(self, args: dict, context: IterativeContext) -> dict:
"""Execute todo_complete function"""
todo_id = args.get('todo_id')
if not todo_id:
return {
"success": False,
"error": "todo_id is required"
}
try:
result = await self.todo_manager.complete(
interaction_id=context.interaction_id,
todo_id=todo_id
)
if result:
self.logger.info(f"[💭] ✓ Completed todo {todo_id}")
return {
"success": True,
"todo_id": todo_id,
"message": f"Completed todo {todo_id}"
}
else:
return {
"success": False,
"error": f"Todo {todo_id} not found"
}
except Exception as e:
self.logger.error(f"[💭] Error completing todo: {e}")
return {
"success": False,
"error": f"Failed to complete todo: {str(e)}"
}

View File

@@ -0,0 +1,278 @@
"""
Todo Manager - Task tracking system for Vi's iterative reasoning.
Provides a TodoWrite-equivalent tool for Oracle to manage complex multi-step
problem-solving with subtask tracking and dependency management.
Storage: NATS KV bucket (ephemeral, scoped by interaction_id)
"""
import json
import uuid
from typing import List, Dict, Optional
from datetime import datetime
from core.logger import setup_logger
from core.nats_event_bus import nats_bus
class TodoItem:
"""Individual todo item with state tracking"""
def __init__(
self,
content: str,
active_form: str,
status: str = "pending",
todo_id: str = None,
created_at: str = None
):
self.todo_id = todo_id or str(uuid.uuid4())[:8]
self.content = content
self.active_form = active_form
self.status = status # pending, in_progress, completed
self.created_at = created_at or datetime.utcnow().isoformat()
self.updated_at = datetime.utcnow().isoformat()
def to_dict(self) -> dict:
"""Convert to dictionary for serialization"""
return {
"todo_id": self.todo_id,
"content": self.content,
"active_form": self.active_form,
"status": self.status,
"created_at": self.created_at,
"updated_at": self.updated_at
}
@classmethod
def from_dict(cls, data: dict) -> 'TodoItem':
"""Create from dictionary"""
return cls(
content=data["content"],
active_form=data["active_form"],
status=data["status"],
todo_id=data["todo_id"],
created_at=data.get("created_at")
)
class TodoManager:
"""
Manages ephemeral todo lists scoped by interaction/conversation.
Uses NATS KV for storage with auto-expiry. Similar to Claude's TodoWrite tool,
helps Oracle track complex multi-step reasoning processes.
"""
BUCKET_NAME = "lyra_todos"
TTL_SECONDS = 3600 # 1 hour
def __init__(self):
self.logger = setup_logger('todo_manager', service_name='think_service')
def _get_key(self, interaction_id: str) -> str:
"""Generate NATS KV key for interaction"""
return f"{interaction_id}:todos"
async def create(
self,
interaction_id: str,
content: str,
active_form: str,
status: str = "pending"
) -> TodoItem:
"""
Create a new todo item
Args:
interaction_id: Unique interaction/conversation ID
content: Task description (imperative form: "Fix bug in X")
active_form: Present continuous form ("Fixing bug in X")
status: Initial status (default: "pending")
Returns:
TodoItem: Created todo item
"""
# Get existing todos
todos = await self.list(interaction_id)
# Create new todo
new_todo = TodoItem(content=content, active_form=active_form, status=status)
todos.append(new_todo)
# Save to NATS KV
await self._save_todos(interaction_id, todos)
self.logger.info(f"[✓] Created todo {new_todo.todo_id}: {content}")
return new_todo
async def update(
self,
interaction_id: str,
todo_id: str,
status: Optional[str] = None,
content: Optional[str] = None,
active_form: Optional[str] = None
) -> bool:
"""
Update an existing todo item
Args:
interaction_id: Unique interaction/conversation ID
todo_id: ID of todo to update
status: New status (optional)
content: New content (optional)
active_form: New active form (optional)
Returns:
bool: True if updated, False if not found
"""
todos = await self.list(interaction_id)
for todo in todos:
if todo.todo_id == todo_id:
if status:
todo.status = status
if content:
todo.content = content
if active_form:
todo.active_form = active_form
todo.updated_at = datetime.utcnow().isoformat()
await self._save_todos(interaction_id, todos)
self.logger.info(f"[✏️] Updated todo {todo_id}: status={status}")
return True
self.logger.warning(f"[⚠️] Todo {todo_id} not found")
return False
async def complete(self, interaction_id: str, todo_id: str) -> bool:
"""
Mark a todo as completed
Args:
interaction_id: Unique interaction/conversation ID
todo_id: ID of todo to complete
Returns:
bool: True if completed, False if not found
"""
result = await self.update(interaction_id, todo_id, status="completed")
if result:
self.logger.info(f"[✓] Completed todo {todo_id}")
return result
async def list(self, interaction_id: str, status_filter: Optional[str] = None) -> List[TodoItem]:
"""
List all todos for an interaction
Args:
interaction_id: Unique interaction/conversation ID
status_filter: Optional filter by status (pending, in_progress, completed)
Returns:
List[TodoItem]: List of todo items
"""
key = self._get_key(interaction_id)
try:
data_bytes = await nats_bus.kv_get(self.BUCKET_NAME, key)
if not data_bytes:
return []
data = json.loads(data_bytes.decode())
todos = [TodoItem.from_dict(item) for item in data]
# Apply filter if provided
if status_filter:
todos = [t for t in todos if t.status == status_filter]
return todos
except Exception as e:
self.logger.error(f"[❌] Error listing todos: {e}")
return []
async def delete(self, interaction_id: str, todo_id: str) -> bool:
"""
Delete a todo item
Args:
interaction_id: Unique interaction/conversation ID
todo_id: ID of todo to delete
Returns:
bool: True if deleted, False if not found
"""
todos = await self.list(interaction_id)
original_count = len(todos)
todos = [t for t in todos if t.todo_id != todo_id]
if len(todos) < original_count:
await self._save_todos(interaction_id, todos)
self.logger.info(f"[🗑️] Deleted todo {todo_id}")
return True
self.logger.warning(f"[⚠️] Todo {todo_id} not found for deletion")
return False
async def clear(self, interaction_id: str) -> bool:
"""
Clear all todos for an interaction
Args:
interaction_id: Unique interaction/conversation ID
Returns:
bool: True if cleared
"""
key = self._get_key(interaction_id)
try:
await nats_bus.kv_delete(self.BUCKET_NAME, key)
self.logger.info(f"[🧹] Cleared todos for interaction {interaction_id}")
return True
except Exception as e:
self.logger.error(f"[❌] Error clearing todos: {e}")
return False
async def get_summary(self, interaction_id: str) -> Dict:
"""
Get summary statistics for todos
Args:
interaction_id: Unique interaction/conversation ID
Returns:
Dict: Summary with counts by status
"""
todos = await self.list(interaction_id)
summary = {
"total": len(todos),
"pending": len([t for t in todos if t.status == "pending"]),
"in_progress": len([t for t in todos if t.status == "in_progress"]),
"completed": len([t for t in todos if t.status == "completed"])
}
return summary
async def _save_todos(self, interaction_id: str, todos: List[TodoItem]):
"""Save todos to NATS KV"""
key = self._get_key(interaction_id)
data = [todo.to_dict() for todo in todos]
data_bytes = json.dumps(data).encode()
try:
await nats_bus.kv_put(
self.BUCKET_NAME,
key,
data_bytes,
ttl_seconds=self.TTL_SECONDS
)
except Exception as e:
self.logger.error(f"[❌] Error saving todos: {e}")
raise

View File

@@ -0,0 +1 @@
# No additional dependencies required for stub implementation

View File

@@ -0,0 +1,274 @@
"""
Think Service - Orchestration service for iterative reasoning.
This service coordinates multi-service interactions and manages the
iterative reasoning process for generating intelligent responses.
Refactored into modular components:
- reasoning/: Core reasoning logic (Oracle, execution, orchestration)
- handlers/: Event handlers (input, communication)
- memory/: Memory and identity management
"""
import asyncio
import json
from datetime import datetime
from core.logger import setup_logger
from core.nats_event_bus import nats_bus as event_bus
from core.base_service import BaseService
from core.service_discovery import discovery_client
from core.service_registry import ServiceManifest
# Import refactored components
from .reasoning.formatters import KnowledgeFormatter
from .reasoning.oracle_client import OracleClient
from .reasoning.step_executor import StepExecutor
from .reasoning.orchestrator import IterativeOrchestrator
from .memory.memory_manager import MemoryManager
from .handlers.input_handler import InputHandler
from .handlers.communication_handler import CommunicationHandler
logger = setup_logger('think_service', service_name='think_service')
class ThinkService(BaseService):
"""Main Think service - coordinates all subsystems"""
def __init__(self):
super().__init__('think')
self._interaction_counter = 0
# Initialize all subsystems
self.formatter = None
self.memory_manager = None
self.oracle_client = None
self.step_executor = None
self.orchestrator = None
self.input_handler = None
self.communication_handler = None
# Override heartbeat collection
self.heartbeat_interval = 60
def get_service_manifest(self) -> ServiceManifest:
"""Return service manifest with operations and metadata"""
operations = [
self.create_service_operation(
"communication",
"Handle communication requests from drive service",
timeout_ms=10000
),
self.create_service_operation(
"process",
"Process external input with iterative reasoning",
timeout_ms=120000 # 2 minutes for complex reasoning
)
]
return ServiceManifest(
service_id=self.service_id,
name="Think Service",
description="Orchestration service for iterative reasoning and multi-service coordination",
version="3.0.0", # Bumped version due to refactoring
operations=operations,
dependencies=[],
health_check_topic=f"vi.services.{self.service_id}.health",
heartbeat_interval=60,
metadata={
"max_reasoning_steps": 10,
"max_reasoning_time_minutes": 2,
"urgency": 0.9,
"monitors_other_services": False,
"refactored": True # Mark as refactored
}
)
async def initialize_service(self):
"""Initialize service-specific resources and register handlers"""
# Set up service discovery client
discovery_client.set_event_bus(self.event_bus)
# Initialize all subsystems in order
self.formatter = KnowledgeFormatter()
self.memory_manager = MemoryManager()
self.oracle_client = OracleClient(self.formatter)
self.step_executor = StepExecutor(self.memory_manager)
self.orchestrator = IterativeOrchestrator(
self.oracle_client,
self.step_executor,
self.formatter,
self.send_output
)
self.input_handler = InputHandler(
self.orchestrator,
self.memory_manager,
self.send_output,
self.generate_interaction_id
)
self.communication_handler = CommunicationHandler(
self.orchestrator,
self.memory_manager,
self.send_output,
self.generate_interaction_id
)
# Register handlers using new topic patterns
await self.register_handler("communication", self._handle_communication_wrapper)
await self.register_handler("process", self._handle_external_input_wrapper)
# Also register legacy topic handlers for backward compatibility
await self.event_bus.on("vi.external.input", self._handle_event_wrapper(self.input_handler.handle_external_input))
await self.event_bus.on("vi.communication.request", self._handle_event_wrapper(self.communication_handler.handle_communication_request))
self.logger.info("[💭] ThinkService initialized with refactored architecture")
self.logger.info("[💭] ✓ Subsystems: Formatter, MemoryManager, OracleClient, StepExecutor, Orchestrator, Handlers")
async def cleanup_service(self):
"""Cleanup service-specific resources"""
# Unregister legacy handlers
await self.event_bus.off("vi.external.input")
await self.event_bus.off("vi.communication.request")
self.logger.info("[💭] ThinkService cleanup completed")
async def perform_health_check(self):
"""Perform service-specific health check"""
health_data = {
'healthy': True,
'checks': {
'running': self._running,
'event_bus': self.event_bus is not None,
'discovery_client': discovery_client.event_bus is not None,
'interaction_counter': self._interaction_counter,
'subsystems_initialized': all([
self.formatter is not None,
self.memory_manager is not None,
self.oracle_client is not None,
self.step_executor is not None,
self.orchestrator is not None,
self.input_handler is not None,
self.communication_handler is not None
])
}
}
# Check if we can reach critical dependencies
try:
oracle_available = await discovery_client.discover_service("oracle")
health_data['checks']['oracle_available'] = oracle_available is not None
except:
health_data['checks']['oracle_available'] = False
try:
memory_available = await discovery_client.discover_service("memory")
health_data['checks']['memory_available'] = memory_available is not None
except:
health_data['checks']['memory_available'] = False
# Mark unhealthy if critical services unavailable
if not health_data['checks']['oracle_available']:
health_data['healthy'] = False
return health_data
def generate_interaction_id(self, identity: str, modality: str) -> str:
"""Generate a unique interaction ID"""
self._interaction_counter += 1
timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
return f"{identity}_{modality}_{timestamp}_{self._interaction_counter}"
async def send_output(self, content: str, target: str, modality: str, target_type: str = None) -> bool:
"""Send response directly to plugins via NATS"""
try:
# Determine if target is user_id or channel
if target_type is None:
target_type = 'user_id' if target.startswith('@') else 'channel'
logger.info(f"[💭] 📤 Sending output to {modality} {target_type} {target}: '{content[:50]}...'")
output_payload = {
"type": "vi.output.send",
"data": {
"content": content,
"channel": target,
"modality": modality,
"metadata": {
"timestamp": datetime.utcnow().isoformat(),
"sent_by": "think_service",
"target_type": target_type
}
}
}
# Add user_id to metadata for Matrix plugin DM creation
if target_type == 'user_id':
output_payload["data"]["metadata"]["user_id"] = target
# Just publish the event - plugins will handle it directly
await event_bus.emit("vi.output.send", output_payload)
logger.info(f"[💭] ✓ Output event published successfully")
return True
except Exception as e:
logger.exception(f"[💭] ❌ Error publishing output: {e}")
return False
# Handler wrappers for service operations
async def _handle_communication_wrapper(self, msg):
"""Wrapper for communication handler"""
payload = json.loads(msg.data.decode())
await self.communication_handler.handle_communication_request(payload)
# Send ack
response = {"status": "processing"}
await msg.respond(json.dumps(response).encode())
async def _handle_external_input_wrapper(self, msg):
"""Wrapper for external input handler"""
payload = json.loads(msg.data.decode())
await self.input_handler.handle_external_input(payload)
# Send ack
response = {"status": "processing"}
await msg.respond(json.dumps(response).encode())
def _handle_event_wrapper(self, handler):
"""Wrapper to handle JSON parsing of event data"""
async def wrapper(data):
try:
if isinstance(data, str):
payload = json.loads(data)
elif hasattr(data, 'data'): # NATS message object
payload = json.loads(data.data.decode())
else:
payload = data
await handler(payload)
except Exception as e:
logger.error(f"[💭] Event handler error: {e}")
return wrapper
async def main():
"""Main entry point for think service"""
think_service = ThinkService()
try:
await event_bus.connect()
await think_service.start(event_bus)
logger.info("[💭] Think service running (refactored architecture). Press Ctrl+C to stop.")
# Keep running
while True:
await asyncio.sleep(1)
except KeyboardInterrupt:
logger.info("[💭] Shutdown requested")
except Exception as e:
logger.exception(f"[💭] Unexpected error: {e}")
finally:
await think_service.stop()
await event_bus.close()
if __name__ == "__main__":
asyncio.run(main())

File diff suppressed because it is too large Load Diff