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:
231
services/think/handlers/input_handler.py
Normal file
231
services/think/handlers/input_handler.py
Normal 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}")
|
||||
Reference in New Issue
Block a user