1
- # SocraticReasoning.py
2
1
import logging
2
+ import os
3
+ import pathlib
4
+ import ujson
5
+ from datetime import datetime
3
6
from chatter import GPT4o , GroqModel , OllamaModel
7
+ from logic import LogicTables
8
+ from memory .memory import create_memory_folders , store_in_stm , DialogEntry
9
+ from api import APIManager
4
10
5
11
class SocraticReasoning :
6
12
def __init__ (self , chatter ):
7
- self .premises = []
13
+ """
14
+ Initializes the SocraticReasoning instance with necessary configurations.
15
+
16
+ Args:
17
+ chatter: An instance of the model used for generating responses.
18
+ """
19
+ self .premises = [] # List to hold premises
8
20
self .logger = logging .getLogger ('SocraticReasoning' )
9
- self .logger .setLevel (logging .INFO )
10
- self .max_tokens = 100
11
- self .chatter = chatter
21
+ self .logger .setLevel (logging .DEBUG ) # Set to DEBUG to capture all SocraticReasoning logs
22
+
23
+ # Ensure the logs directory exists
24
+ logs_dir = './memory/logs'
25
+ os .makedirs (logs_dir , exist_ok = True )
26
+
27
+ # File handler for saving Socratic Reasoning logs
28
+ file_handler = logging .FileHandler (os .path .join (logs_dir , 'socraticlogs.txt' ))
29
+ file_handler .setLevel (logging .DEBUG )
30
+ file_formatter = logging .Formatter ('%(asctime)s - %(name)s - %(levelname)s - %(message)s' )
31
+ file_handler .setFormatter (file_formatter )
32
+
33
+ # Stream handler to suppress lower-level logs in the terminal
34
+ stream_handler = logging .StreamHandler ()
35
+ stream_handler .setLevel (logging .CRITICAL ) # Show only critical logs in the terminal
36
+ stream_formatter = logging .Formatter ('%(message)s' )
37
+ stream_handler .setFormatter (stream_formatter )
38
+
39
+ # Adding handlers to the logger
40
+ self .logger .addHandler (file_handler )
41
+ self .logger .addHandler (stream_handler )
42
+
43
+ # File paths for saving premises, non-premises, conclusions, and truth tables
44
+ self .premises_file = './memory/logs/premises.json'
45
+ self .not_premises_file = './memory/logs/notpremise.json'
46
+ self .conclusions_file = './memory/logs/conclusions.txt' # Path to save conclusions
47
+ self .truth_tables_file = './memory/logs/truth_tables.json' # Path to save truth tables
48
+
49
+ self .max_tokens = 100 # Default max tokens for Socratic premise from add_premise(statement)
50
+ self .chatter = chatter # Chatter model for generating responses
51
+ self .logic_tables = LogicTables () # Logic tables for reasoning
52
+ self .dialogue_history = [] # List to hold the history of dialogues
53
+ self .logical_conclusion = "" # Variable to store the conclusion
54
+
55
+ create_memory_folders () # Ensure memory folders are created
12
56
13
57
def log (self , message , level = 'info' ):
58
+ """
59
+ Logs a message with a specified level.
60
+
61
+ Args:
62
+ message: The message to be logged.
63
+ level: The level of logging ('info' or 'error').
64
+ """
14
65
if level == 'info' :
15
66
self .logger .info (message )
16
67
elif level == 'error' :
17
68
self .logger .error (message )
18
- print (message )
69
+ self .log_errors (message , level ) # Save Socratic reasoning to ./memory/logs/socraticlogs.txt
70
+
71
+ def log_errors (self , message , level ):
72
+ """
73
+ Stores error logs in ./memory/logs/errorlogs.txt.
74
+
75
+ Args:
76
+ message: The error message to be logged.
77
+ level: The level of the error.
78
+ """
79
+ error_logs_path = './memory/logs/errorlogs.txt'
80
+ pathlib .Path (error_logs_path ).parent .mkdir (parents = True , exist_ok = True )
81
+ with open (error_logs_path , 'a' ) as file :
82
+ file .write (f"{ level .upper ()} : { message } \n " )
83
+
84
+ def log_not_premise (self , message , level = 'info' ):
85
+ """
86
+ Logs messages that are not considered premises.
87
+
88
+ Args:
89
+ message: The message to be logged.
90
+ level: The level of logging.
91
+ """
92
+ not_premises_path = self .not_premises_file
93
+ pathlib .Path (not_premises_path ).parent .mkdir (parents = True , exist_ok = True )
94
+ entry = {"level" : level .upper (), "message" : message }
95
+ try :
96
+ with open (not_premises_path , 'r' ) as file :
97
+ logs = ujson .load (file )
98
+ except (FileNotFoundError , ValueError ):
99
+ logs = []
100
+
101
+ logs .append (entry )
102
+ with open (not_premises_path , 'w' ) as file :
103
+ ujson .dump (logs , file , indent = 2 )
104
+
105
+ def save_premises (self ):
106
+ """
107
+ Saves the current list of premises to a JSON file.
108
+ """
109
+ pathlib .Path (self .premises_file ).parent .mkdir (parents = True , exist_ok = True )
110
+ with open (self .premises_file , 'w' ) as file :
111
+ ujson .dump (self .premises , file , indent = 2 )
19
112
20
113
def add_premise (self , premise ):
21
- self .premises .append (premise )
22
- self .log (f'Added premise: { premise } ' )
114
+ """
115
+ Adds a premise to the list if valid and saves the premises.
116
+
117
+ Args:
118
+ premise: The premise to be added.
119
+ """
120
+ if self .parse_statement (premise ): # Check if the premise is valid
121
+ self .premises .append (premise ) # Add the premise to the list
122
+ self .save_premises () # Save the updated list of premises
123
+ else :
124
+ self .log_not_premise (f'Invalid premise: { premise } ' , level = 'error' ) # Log invalid premise
125
+
126
+ def parse_statement (self , statement ):
127
+ """
128
+ Validates a statement to check if it can be a premise.
129
+
130
+ Args:
131
+ statement: The statement to be validated.
132
+
133
+ Returns:
134
+ bool: True if the statement is valid, False otherwise.
135
+ """
136
+ return isinstance (statement , str ) and len (statement ) > 0 # Check if the statement is a non-empty string
137
+
138
+ def generate_new_premise (self , current_premises ):
139
+ """
140
+ Generates a new premise based on the current premises.
141
+
142
+ Args:
143
+ current_premises: The list of current premises.
144
+
145
+ Returns:
146
+ str: A new premise generated from the current premises.
147
+ """
148
+ premise_text = " " .join (f"{ premise } " for premise in current_premises )
149
+ new_premise = self .chatter .generate_response (premise_text )
150
+ return new_premise .strip ()
23
151
24
152
def challenge_premise (self , premise ):
25
- if premise in self .premises :
26
- self .premises .remove (premise )
27
- self .log (f'Challenged and removed premise: { premise } ' )
153
+ """
154
+ Challenges and removes a premise from the list if it exists.
155
+
156
+ Args:
157
+ premise: The premise to be challenged.
158
+ """
159
+ if premise in self .premises : # Check if the premise exists in the list
160
+ self .premises .remove (premise ) # Remove the premise from the list
161
+ self .log (f'Challenged and removed premise: { premise } ' ) # Log the removal
162
+ self .remove_equivalent_premises (premise ) # Remove equivalent premises
163
+ self .save_premises () # Save the updated list of premises
28
164
else :
29
- self .log (f'Premise not found: { premise } ' , level = 'error' )
165
+ self .log_not_premise (f'Premise not found: { premise } ' , level = 'error' ) # Log if premise not found
166
+
167
+ def remove_equivalent_premises (self , premise ):
168
+ """
169
+ Removes premises that are logically equivalent to the challenged premise.
170
+
171
+ Args:
172
+ premise: The premise to be checked for equivalence.
173
+ """
174
+ equivalent_premises = [p for p in self .premises if self .logic_tables .unify_variables (premise , p )]
175
+ for p in equivalent_premises :
176
+ self .premises .remove (p ) # Remove equivalent premise
177
+ self .log_not_premise (f'Removed equivalent premise: { p } ' ) # Log removal of equivalent premise
178
+ self .save_premises () # Save the updated list of premises
30
179
31
180
def draw_conclusion (self ):
32
- if not self .premises :
33
- self .log ('No premises available for logic as conclusion.' , level = 'error' )
34
- return
181
+ """
182
+ Draws a conclusion based on the current list of premises.
183
+
184
+ Returns:
185
+ str: The conclusion derived from the premises.
186
+ """
187
+ if not self .premises : # Check if there are no premises
188
+ self .log ('No premises available for logic as conclusion.' , level = 'error' ) # Log the absence of premises
189
+ return "No premises available for logic as conclusion."
35
190
36
- premise_text = "\n " .join (f"- { premise } " for premise in self .premises )
37
- prompt = f"Based on the premises:\n { premise_text } \n Provide a logical conclusion."
191
+ additional_premises_count = 0 # Counter for additional premises
38
192
39
- # Use the appropriate model to generate the response
40
- conclusion = self .chatter .generate_response (prompt )
41
- self .log (f"Conclusion:\n { conclusion } " )
193
+ # Generate new premises until a valid conclusion is drawn or the maximum limit is reached
194
+ while additional_premises_count < 5 :
195
+ new_premise = self .generate_new_premise (self .premises )
196
+ if not self .parse_statement (new_premise ):
197
+ self .log_not_premise (f'Invalid generated premise: { new_premise } ' , level = 'error' )
198
+ continue
199
+ self .premises .append (new_premise )
200
+ self .save_premises ()
201
+ additional_premises_count += 1
202
+
203
+ # Create a single string from the premises
204
+ premise_text = " " .join (f"{ premise } " for premise in self .premises )
205
+
206
+ # Use the premise_text as the input (knowledge) for generating a response
207
+ raw_response = self .chatter .generate_response (premise_text )
208
+
209
+ # Process the response to get the conclusion
210
+ conclusion = raw_response .strip ()
211
+
212
+ self .logical_conclusion = conclusion # Store the conclusion
213
+
214
+ if self .validate_conclusion (): # Validate the conclusion
215
+ break
216
+ else :
217
+ self .log_not_premise ('Invalid conclusion. Generating more premises.' , level = 'error' )
218
+
219
+ # Save the conclusion along with premises
220
+ conclusion_entry = {"premises" : self .premises , "conclusion" : self .logical_conclusion }
221
+ pathlib .Path (self .premises_file ).parent .mkdir (parents = True , exist_ok = True )
222
+ with open (self .premises_file , 'w' ) as file :
223
+ ujson .dump (conclusion_entry , file , indent = 2 )
224
+
225
+ # Log the conclusion to conclusions.txt
226
+ pathlib .Path (self .conclusions_file ).parent .mkdir (parents = True , exist_ok = True )
227
+ with open (self .conclusions_file , 'a' ) as file :
228
+ file .write (f"Premises: { self .premises } \n Conclusion: { self .logical_conclusion } \n " )
229
+
230
+ # Log the final premises to conclusion
231
+ self .log (f"Final Premises to Conclusion:\n Premises: { self .premises } \n Conclusion: { self .logical_conclusion } " )
232
+
233
+ # Save the valid conclusion as a truth
234
+ self .save_truth (self .logical_conclusion )
235
+
236
+ # Clear the premises list for the next round
237
+ self .premises = []
238
+
239
+ return self .logical_conclusion # Return the conclusion
240
+
241
+ def validate_conclusion (self ):
242
+ """
243
+ Validates the logical conclusion.
244
+
245
+ Returns:
246
+ bool: True if the conclusion is valid, False otherwise.
247
+ """
248
+ return self .logic_tables .tautology (self .logical_conclusion ) # Validate using logic tables
249
+
250
+ def save_truth (self , truth ):
251
+ """
252
+ Saves the valid conclusion as a truth in the truth tables.
253
+
254
+ Args:
255
+ truth: The truth to be saved.
256
+ """
257
+ truth_tables_entry = {
258
+ "truth" : truth ,
259
+ "timestamp" : datetime .now ().strftime ('%Y-%m-%d %H:%M:%S' )
260
+ }
261
+ pathlib .Path (self .truth_tables_file ).parent .mkdir (parents = True , exist_ok = True )
262
+ with open (self .truth_tables_file , 'a' ) as file :
263
+ ujson .dump (truth_tables_entry , file , indent = 2 )
264
+ file .write ("\n " )
265
+
266
+ def update_logic_tables (self , variables , expressions , valid_truths ):
267
+ """
268
+ Updates the logic tables with new variables, expressions, and valid truths.
269
+
270
+ Args:
271
+ variables: The logical variables.
272
+ expressions: The logical expressions.
273
+ valid_truths: The valid truths for the logic table.
274
+ """
275
+ self .logic_tables .variables = variables
276
+ self .logic_tables .expressions = expressions
277
+ self .logic_tables .valid_truths = valid_truths
278
+
279
+ # Log the truth tables to truth_tables.json
280
+ truth_tables_entry = {
281
+ "variables" : variables ,
282
+ "expressions" : expressions ,
283
+ "valid_truths" : valid_truths
284
+ }
285
+ pathlib .Path (self .truth_tables_file ).parent .mkdir (parents = True , exist_ok = True )
286
+ with open (self .truth_tables_file , 'w' ) as file :
287
+ ujson .dump (truth_tables_entry , file , indent = 2 )
288
+
289
+ # Save a timestamped file in ./memory/truth
290
+ timestamp = datetime .now ().strftime ('%Y%m%d%H%M%S' )
291
+ belief_timestamp_file = f'./memory/truth/belief_{ timestamp } .json'
292
+ pathlib .Path (belief_timestamp_file ).parent .mkdir (parents = True , exist_ok = True )
293
+ with open (belief_timestamp_file , 'w' ) as file :
294
+ ujson .dump (truth_tables_entry , file , indent = 2 )
295
+
296
+ # Add a log entry to confirm the update
297
+ self .logger .info ("Updated logic tables: %s" , truth_tables_entry )
42
298
43
299
def set_max_tokens (self , max_tokens ):
300
+ """
301
+ Sets the maximum number of tokens for generating a response.
302
+
303
+ Args:
304
+ max_tokens: The maximum number of tokens.
305
+ """
44
306
self .max_tokens = max_tokens
45
307
self .log (f"Max tokens set to: { max_tokens } " )
46
308
47
309
def interact (self ):
310
+ """
311
+ Interacts with the user to add, challenge premises, and draw conclusions.
312
+ """
48
313
while True :
49
314
self .log ("\n Commands: add, challenge, conclude, set_tokens, exit" )
50
315
cmd = input ("> " ).strip ().lower ()
51
-
316
+
52
317
if cmd == 'exit' :
53
- self .log ('Exiting SocraticReasoning .' )
318
+ self .log ('Exiting Socratic Reasoning .' )
54
319
break
55
320
elif cmd == 'add' :
56
321
premise = input ("Enter the premise: " ).strip ()
@@ -59,25 +324,42 @@ def interact(self):
59
324
premise = input ("Enter the premise to challenge: " ).strip ()
60
325
self .challenge_premise (premise )
61
326
elif cmd == 'conclude' :
62
- self .draw_conclusion ()
327
+ conclusion = self .draw_conclusion ()
328
+ print (conclusion )
63
329
elif cmd == 'set_tokens' :
64
330
tokens = input ("Enter the maximum number of tokens for the conclusion: " ).strip ()
65
331
if tokens .isdigit ():
66
332
self .set_max_tokens (int (tokens ))
67
333
else :
68
334
self .log ("Invalid number of tokens." , level = 'error' )
335
+ self .log_not_premise ("Invalid number of tokens." , level = 'error' )
69
336
else :
70
- self .log ('Invalid command.' , level = 'error' )
337
+ self .log_not_premise ('Invalid command.' , level = 'error' )
338
+
339
+ if __name__ == "__main__" :
340
+ logging .basicConfig (level = logging .INFO )
341
+ api_manager = APIManager () # Initialize API manager to handle API keys
342
+ openai_key = api_manager .get_api_key ('openai' )
343
+ groq_key = api_manager .get_api_key ('groq' )
344
+
345
+ if openai_key :
346
+ chatter = GPT4o (openai_key ) # Use OpenAI model if key is available
347
+ elif groq_key :
348
+ chatter = GroqModel (groq_key ) # Use Groq model if key is available
349
+ else :
350
+ raise ValueError ("No suitable API key found. Please add an API key." )
351
+
352
+ socratic_reasoning = SocraticReasoning (chatter ) # Initialize SocraticReasoning with the selected model
71
353
72
- def main ():
73
- # Replace 'your_api_key_here' with the actual API key or retrieve it as needed
74
- api_key = 'your_api_key_here'
75
- api_provider = 'openai' # or 'groq' or 'ollama'
76
- chatter = GPT4o (api_key ) # or GroqModel(api_key), OllamaModel()
77
- reasoner = SocraticReasoning (chatter )
78
- reasoner .log ('SocraticReasoning initialized.' )
79
- reasoner .interact ()
354
+ # Example usage
355
+ statements = [
356
+ "All humans are mortal." ,
357
+ "Socrates is a human."
358
+ ]
80
359
81
- if __name__ == '__main__' :
82
- main ()
360
+ for statement in statements :
361
+ socratic_reasoning . add_premise ( statement ) # Add each statement as a premise
83
362
363
+ conclusion = socratic_reasoning .draw_conclusion () # Draw a conclusion based on the premises
364
+ print (conclusion )
365
+ socratic_reasoning .interact () # Start the interactive loop
0 commit comments