File size: 20,524 Bytes
e993aba |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 |
"""
engine.py
Orquestador principal del motor Savant Simbiótico RRF.
Expone:
- handle_query(text): detecta intención (map/resonance/music/chat) y responde
- access to SimpleTrainer, SelfImprover, MemoryStore for external control
"""
import time # Import time
from .mappings import IcosaMap, DodecaMap
from .resonance import ResonanceSimulator
from .music import MusicAdapter
from .memory import MemoryStore
from .self_improvement import SelfImprover
# from .trainer import SimpleTrainer # Avoid circular import, trainer can be instantiated externally
from .api_helpers import chat_refine
import os # Import os
import pandas as pd # Import pandas
import json # Import json
import pickle # Import pickle
class SavantEngine:
def __init__(self, structured_data_paths=None):
self.memory = MemoryStore("SAVANT_memory.jsonl")
# Load structured data if paths are provided
self.structured_data = {}
if structured_data_paths:
print("Engine: Loading structured data...")
try:
self.structured_data['equations'] = self._load_json_data(structured_data_paths.get('equations'))
nodes_raw = self._load_json_data(structured_data_paths.get('icosahedron_nodes'))
self.structured_data['icosahedron_nodes'] = nodes_raw.get('nodes', []) if isinstance(nodes_raw, dict) else []
self.structured_data['frequencies'] = self._load_csv_data(structured_data_paths.get('frequencies'))
self.structured_data['constants'] = self._load_csv_data(structured_data_paths.get('constants'))
print("Engine loaded structured data: Equations={}, Nodes={}, Frequencies={}, Constants={}".format(
len(self.structured_data['equations']) if self.structured_data['equations'] else 0,
len(self.structured_data['icosahedron_nodes']),
len(self.structured_data['frequencies']),
len(self.structured_data['constants'])
))
except Exception as e:
print(f"Engine: Error loading structured data: {e}")
self.structured_data = {} # Reset if loading fails
# Instantiate components, passing relevant structured data
self.icosa = IcosaMap(node_data=self.structured_data.get('icosahedron_nodes')) # Pass node data
self.dodeca = DodecaMap() # No dodecahedron data provided in list
self.resonator = ResonanceSimulator(frequencies_data=self.structured_data.get('frequencies'), constants_data=self.structured_data.get('constants')) # Pass freq/const data
self.music = MusicAdapter(frequencies_data=self.structured_data.get('frequencies')) # Pass frequencies data
self.self_improver = SelfImprover(self.memory, structured_data=self.structured_data) # Pass structured data to SelfImprover
self._interaction_count = 0 # Initialize interaction count for self-improvement trigger
# Helper methods for loading data within the Engine (copied from Trainer for self-containment)
def _load_json_data(self, file_path):
"""Loads data from a JSON file."""
if not file_path or not os.path.exists(file_path):
# print(f"JSON file not found or path not provided: {file_path}") # Suppress not found for optional files
return None
try:
with open(file_path, "r", encoding="utf-8") as f:
data = json.load(f)
# print(f"Successfully loaded JSON data from {file_path}") # Suppress success for cleaner output
return data
except json.JSONDecodeError as e:
print(f"Error decoding JSON from {file_path}: {e}")
return None
except Exception as e:
print(f"An unexpected error occurred while loading JSON data: {e}")
return None
def _load_csv_data(self, file_path):
"""Loads data from a CSV file using pandas."""
if not file_path or not os.path.exists(file_path):
# print(f"CSV file not found or path not provided: {file_path}") # Suppress not found for optional files
return []
try:
df = pd.read_csv(file_path)
# print(f"Successfully loaded CSV data from {file_path}") # Suppress success for cleaner output
return df.to_dict(orient='records')
except Exception as e:
print(f"An error occurred while loading CSV data from {file_path}: {e}")
return []
def _classify(self, text):
t = text.lower()
# Enhanced classification based on structured data keywords and patterns
if any(k in t for k in ("equation", "ecuacion", "hamiltoniano", "dirac", "formula", "formulae", "formulas")): # Added formula variations
return "equation_query"
if any(k in t for k in ("node", "nodo", "icosahedron", "dodecahedron", "poly", "vertex", "point", "map")): # Added map keyword to node query
# Check for patterns like "node X" where X is a number
words = t.split()
if len(words) > 1 and words[-1].isdigit() and words[-2] in ("node", "nodo"):
return "node_query"
return "node_query"
if any(k in t for k in ("frecuen", "freq", "music", "nota", "melod", "tono", "pitch", "scale", "musical", "sound", "audio")): # Added sound, audio
return "music_resonance" # Combine music and resonance intent for simplicity here
if any(k in t for k in ("constant", "constante", "valor", "unidad", "define", "what is the value of")): # Added "what is the value of"
return "constant_query"
if any(k in t for k in ("resonance", "resonar", "resonant", "vibration", "oscilla")): # Specific keywords for resonance without music
return "resonance_only"
# Existing classifications (kept as fallbacks or for broader terms)
# Removed redundant 'reson' and 'sinton' mapping to music_resonance as specific resonance_only added
if any(k in t for k in ("chat", "hola", "qué", "como", "explica", "tell me", "what is", "describe", "info", "information")): # Added info, information
return "chat"
return "chat" # Default to chat
def handle_query(self, text, base_model_output=None):
kind = self._classify(text)
# Handle query types based on structured data
if kind == "equation_query":
relevant_eqs = []
if self.structured_data.get('equations'):
# Find equations related to the query (more robust keyword matching)
query_words = text.lower().split()
relevant_eqs = [eq for eq in self.structured_data['equations'] if any(word in eq.get('nombre', '').lower() or word in eq.get('descripcion', '').lower() or any(comp.lower() in word for comp in eq.get('componentes', [])) for word in query_words)]
if relevant_eqs:
# Provide information about found equations
response_parts = ["Based on the RRF Equations data, I found the following relevant equations:"]
for eq in relevant_eqs[:3]: # Limit to first 3 for brevity
response_parts.append(f"- '{eq.get('nombre', 'N/A')}' ({eq.get('tipo', 'Equation')}): {eq.get('ecuacion', 'N/A')} (Components: {', '.join(eq.get('componentes', []))})")
if len(relevant_eqs) > 3:
response_parts.append("...")
response = "\n".join(response_parts)
self._log_interaction(text, base_model_output, response, type="equation_query")
return {"type": "equation_query", "query": text, "result": relevant_eqs, "response": response}
else:
response = "I couldn't find any relevant equations in the loaded data for that query."
self._log_interaction(text, base_model_output, response, type="equation_query_not_found")
return {"type": "equation_query", "query": text, "result": [], "response": response}
if kind == "node_query":
relevant_nodes = []
if self.structured_data.get('icosahedron_nodes'):
query_words = text.lower().split()
# Try to find by ID first if query contains a number
try:
node_id = int(query_words[-1]) if query_words and query_words[-1].isdigit() else None
if node_id is not None:
relevant_nodes = [node for node in self.structured_data['icosahedron_nodes'] if node.get('id') == node_id]
except (ValueError, IndexError):
pass # Not a number query
# If not found by ID or not a number query, search by keyword in description/name
if not relevant_nodes:
relevant_nodes = [node for node in self.structured_data['icosahedron_nodes'] if any(word in node.get('description', '').lower() or word in node.get('name', '').lower() for word in query_words)]
if relevant_nodes:
response_parts = ["Based on the Icosahedron Nodes data, I found the following relevant nodes:"]
for node in relevant_nodes[:3]: # Limit to first 3
response_parts.append(f"- Node {node.get('id', 'N/A')}: {node.get('description', node.get('name', 'No description'))} (Coords: ({node.get('x', 'N/A')}, {node.get('y', 'N/A')}, {node.get('z', 'N/A')}))") # Added N/A checks
if len(relevant_nodes) > 3:
response_parts.append("...")
response = "\n".join(response_parts)
self._log_interaction(text, base_model_output, response, type="node_query")
return {"type": "node_query", "query": text, "result": relevant_nodes, "response": response}
else:
response = "I couldn't find any relevant nodes in the loaded data for that query."
self._log_interaction(text, base_model_output, response, type="node_query_not_found")
return {"type": "node_query", "query": text, "result": [], "response": response}
if kind == "music_resonance":
# Can still trigger resonance simulation and music adaptation
# Enhance response with information from frequencies/constants if relevant keywords are used
response_parts = []
if self.structured_data.get('frequencies') and any(k in text.lower() for k in ("frecuen", "freq", "nota", "pitch", "scale", "musical", "sound", "audio")):
query_words = text.lower().split()
relevant_freqs = [f for f in self.structured_data['frequencies'] if any(word in f.get('note', '').lower() or word in f.get('role', '').lower() for word in query_words)]
if relevant_freqs:
response_parts.append("Based on the Frequencies data, I found:")
for freq in relevant_freqs[:3]:
response_parts.append(f"- Note: {freq.get('note', 'N/A')}, Frequency: {freq.get('frequency', 'N/A')} Hz, Role: {freq.get('role', 'N/A')}") # Added N/A checks
if len(relevant_freqs) > 3: response_parts.append("...")
if self.structured_data.get('constants') and any(k in text.lower() for k in ("constant", "constante")):
query_words = text.lower().split()
relevant_constants = [c for c in self.structured_data['constants'] if any(word in c.get('name', '').lower() for word in query_words)]
if relevant_constants:
response_parts.append("Based on the Constants data, I found:")
for const in relevant_constants[:3]:
response_parts.append(f"- Constant: {const.get('name', 'N/A')}, Value: {const.get('value', 'N/A')}, Units: {const.get('units', 'N/A')}") # Added N/A checks
if len(relevant_constants) > 3: response_parts.append("...")
# Always run resonance simulation and music adaptation for this type
r = self.resonator.simulate(text)
seq = self.music.adapt_text_to_music(text)
response_parts.append(f"Resonance simulation summary: Dominant Frequency={r['summary'].get('dom_freq', 0.0):.4f} Hz, Max Power={r['summary'].get('max_power', 0.0):.4f}.") # Added default values
response_parts.append(f"Adapted to music sequence (first 5 notes: pitch, duration): {seq[:5]}...")
response = "\n".join(response_parts) if response_parts else "Processing music and resonance query..."
self._log_interaction(text, base_model_output, response, type="music_resonance")
return {"type":"music_resonance","query":text,"resonance_result":r,"music_result":seq, "response": response}
if kind == "resonance_only": # New handler for resonance-only queries
# Can still trigger resonance simulation
response_parts = []
if self.structured_data.get('constants') and any(k in text.lower() for k in ("constant", "constante")):
query_words = text.lower().split()
relevant_constants = [c for c in self.structured_data['constants'] if any(word in c.get('name', '').lower() for word in query_words)]
if relevant_constants:
response_parts.append("Based on the Constants data, I found:")
for const in relevant_constants[:3]:
response_parts.append(f"- Constant: {const.get('name', 'N/A')}, Value: {const.get('value', 'N/A')}, Units: {const.get('units', 'N/A')}") # Added N/A checks
if len(relevant_constants) > 3: response_parts.append("...")
r = self.resonator.simulate(text)
response_parts.append(f"Resonance simulation summary: Dominant Frequency={r['summary'].get('dom_freq', 0.0):.4f} Hz, Max Power={r['summary'].get('max_power', 0.0):.4f}.") # Added default values
response = "\n".join(response_parts) if response_parts else "Processing resonance query..."
self._log_interaction(text, base_model_output, response, type="resonance_only")
return {"type":"resonance_only","query":text,"resonance_result":r, "response": response}
if kind == "constant_query":
relevant_constants = []
if self.structured_data.get('constants'):
query_words = text.lower().split()
relevant_constants = [c for c in self.structured_data['constants'] if any(word in c.get('name', '').lower() or word in c.get('units', '').lower() for word in query_words)]
if relevant_constants:
response_parts = ["Based on the RRF Constants data, I found the following relevant constants:"]
for const in relevant_constants[:3]:
response_parts.append(f"- Name: {const.get('name', 'N/A')}, Value: {const.get('value', 'N/A')}, Units: {const.get('units', 'N/A')}") # Added N/A checks
if len(relevant_constants) > 3: response_parts.append("...")
response = "\n".join(response_parts)
self._log_interaction(text, base_model_output, response, type="constant_query")
return {"type": "constant_query", "query": text, "result": relevant_constants, "response": response}
else:
response = "I couldn't find any relevant constants in the loaded data for that query."
self._log_interaction(text, base_model_output, response, type="constant_query_not_found")
return {"type": "constant_query", "query": text, "result": [], "response": response}
if kind == "map":
# Use icosahedron_nodes data in mapping (already done in IcosaMap)
node_label = self.icosa.closest_node(text)
response = f"Mapping query '{text}' to closest node: {node_label}"
# If we have node data, try to find details about the mapped node
if self.structured_data.get('icosahedron_nodes'):
# Assuming node_label is the description or name from node_data used for embedding
# A more robust mapping is needed here to link label back to original node dict by ID
# For now, let's just find the node with a matching description/name if possible
mapped_node_data = next((node for node in self.structured_data['icosahedron_nodes'] if node.get('description', '').lower() == node_label.lower() or node.get('name', '').lower() == node_label.lower()), None)
if mapped_node_data:
response += f" (ID: {mapped_node_data.get('id', 'N/A')}, Coords: ({mapped_node_data.get('x', 'N/A')}, {mapped_node_data.get('y', 'N/A')}, {mapped_node_data.get('z', 'N/A')}))" # Added N/A checks
self._log_interaction(text, base_model_output, response, type="map")
return {"type":"map","query":text,"node":node_label, "response": response}
# chat fallback: if base_model_output provided, refine it using self_improver
if kind == "chat":
if base_model_output is None:
# default echo
base = "Echo: " + text
else:
base = base_model_output
refined = chat_refine(text, base, self_improver=self.self_improver)
response = refined # Use refined output as the main response for chat
self._log_interaction(text, base_model_output, refined, type="chat_interaction") # Log chat interaction
return {"type":"chat","query":text,"base":base,"refined":refined, "response": response}
# Fallback for unhandled types (shouldn't be reached with current classify)
response = "I'm not sure how to handle that query based on the available data and functions."
self._log_interaction(text, base_model_output, response, type="unhandled_query")
return {"type": "unhandled", "query": text, "response": response}
def _log_interaction(self, user_input, base_output, final_output, type="interaction"):
"""Logs interaction details to memory and triggers self-improvement if needed."""
interaction_record = {
"type": type, # Use the specified type (e.g., chat_interaction, equation_query)
"user_input": user_input,
"base_model_output": base_output, # Might be None for non-chat types
"final_output": final_output, # The response generated by handle_query
"_ts": time.time() # Add timestamp
}
self.memory.add(interaction_record)
# Periodically trigger self-improvement (e.g., every 10 interactions)
self._interaction_count = getattr(self, '_interaction_count', 0) + 1
if self._interaction_count % 10 == 0:
print("SAVANT: Triggering self-improvement cycle...")
try:
proposal = self.self_improver.propose()
accepted, metric = self.self_improver.evaluate_and_apply(proposal)
print(f"SAVANT: Self-improvement proposal accepted: {accepted}, New metric: {metric}")
self.memory.add({
"type": "self_improvement_triggered",
"proposal": proposal,
"accepted": accepted,
"metric": metric,
"_ts": time.time()
})
except Exception as si_error:
# Log the error and continue
error_message = f"Error during self-improvement: {si_error}"
print(f"SAVANT: {error_message}")
self.memory.add({
"type": "self_improvement_error",
"error": error_message,
"_ts": time.time()
})
# trainer helpers (these are now called externally via SimpleTrainer instance)
# def run_training_epochs(self, stimuli, epochs=3):
# return self.trainer.run_epochs(stimuli, epochs)
def propose_improvement(self):
return self.self_improver.propose()
def apply_improvement(self, proposal):
return self.self_improver.evaluate_and_apply(proposal)
|