antonypamo commited on
Commit
e993aba
·
verified ·
1 Parent(s): 45348ee

Upload engine.py

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