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)