symbolicai 1.0.0__py3-none-any.whl → 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. symai/__init__.py +198 -134
  2. symai/backend/base.py +51 -51
  3. symai/backend/engines/drawing/engine_bfl.py +33 -33
  4. symai/backend/engines/drawing/engine_gpt_image.py +4 -10
  5. symai/backend/engines/embedding/engine_llama_cpp.py +50 -35
  6. symai/backend/engines/embedding/engine_openai.py +22 -16
  7. symai/backend/engines/execute/engine_python.py +16 -16
  8. symai/backend/engines/files/engine_io.py +51 -49
  9. symai/backend/engines/imagecaptioning/engine_blip2.py +27 -23
  10. symai/backend/engines/imagecaptioning/engine_llavacpp_client.py +53 -46
  11. symai/backend/engines/index/engine_pinecone.py +116 -88
  12. symai/backend/engines/index/engine_qdrant.py +1011 -0
  13. symai/backend/engines/index/engine_vectordb.py +78 -52
  14. symai/backend/engines/lean/engine_lean4.py +65 -25
  15. symai/backend/engines/neurosymbolic/__init__.py +28 -28
  16. symai/backend/engines/neurosymbolic/engine_anthropic_claudeX_chat.py +137 -135
  17. symai/backend/engines/neurosymbolic/engine_anthropic_claudeX_reasoning.py +145 -152
  18. symai/backend/engines/neurosymbolic/engine_cerebras.py +328 -0
  19. symai/backend/engines/neurosymbolic/engine_deepseekX_reasoning.py +75 -49
  20. symai/backend/engines/neurosymbolic/engine_google_geminiX_reasoning.py +199 -155
  21. symai/backend/engines/neurosymbolic/engine_groq.py +106 -72
  22. symai/backend/engines/neurosymbolic/engine_huggingface.py +100 -67
  23. symai/backend/engines/neurosymbolic/engine_llama_cpp.py +121 -93
  24. symai/backend/engines/neurosymbolic/engine_openai_gptX_chat.py +213 -132
  25. symai/backend/engines/neurosymbolic/engine_openai_gptX_reasoning.py +180 -137
  26. symai/backend/engines/ocr/engine_apilayer.py +18 -20
  27. symai/backend/engines/output/engine_stdout.py +9 -9
  28. symai/backend/engines/{webscraping → scrape}/engine_requests.py +25 -11
  29. symai/backend/engines/search/engine_openai.py +95 -83
  30. symai/backend/engines/search/engine_parallel.py +665 -0
  31. symai/backend/engines/search/engine_perplexity.py +40 -41
  32. symai/backend/engines/search/engine_serpapi.py +33 -28
  33. symai/backend/engines/speech_to_text/engine_local_whisper.py +37 -27
  34. symai/backend/engines/symbolic/engine_wolframalpha.py +14 -8
  35. symai/backend/engines/text_to_speech/engine_openai.py +15 -19
  36. symai/backend/engines/text_vision/engine_clip.py +34 -28
  37. symai/backend/engines/userinput/engine_console.py +3 -4
  38. symai/backend/mixin/anthropic.py +48 -40
  39. symai/backend/mixin/deepseek.py +4 -5
  40. symai/backend/mixin/google.py +5 -4
  41. symai/backend/mixin/groq.py +2 -4
  42. symai/backend/mixin/openai.py +132 -110
  43. symai/backend/settings.py +14 -14
  44. symai/chat.py +164 -94
  45. symai/collect/dynamic.py +13 -11
  46. symai/collect/pipeline.py +39 -31
  47. symai/collect/stats.py +109 -69
  48. symai/components.py +556 -238
  49. symai/constraints.py +14 -5
  50. symai/core.py +1495 -1210
  51. symai/core_ext.py +55 -50
  52. symai/endpoints/api.py +113 -58
  53. symai/extended/api_builder.py +22 -17
  54. symai/extended/arxiv_pdf_parser.py +13 -5
  55. symai/extended/bibtex_parser.py +8 -4
  56. symai/extended/conversation.py +88 -69
  57. symai/extended/document.py +40 -27
  58. symai/extended/file_merger.py +45 -7
  59. symai/extended/graph.py +38 -24
  60. symai/extended/html_style_template.py +17 -11
  61. symai/extended/interfaces/blip_2.py +1 -1
  62. symai/extended/interfaces/clip.py +4 -2
  63. symai/extended/interfaces/console.py +5 -3
  64. symai/extended/interfaces/dall_e.py +3 -1
  65. symai/extended/interfaces/file.py +2 -0
  66. symai/extended/interfaces/flux.py +3 -1
  67. symai/extended/interfaces/gpt_image.py +15 -6
  68. symai/extended/interfaces/input.py +2 -1
  69. symai/extended/interfaces/llava.py +1 -1
  70. symai/extended/interfaces/{naive_webscraping.py → naive_scrape.py} +3 -2
  71. symai/extended/interfaces/naive_vectordb.py +2 -2
  72. symai/extended/interfaces/ocr.py +4 -2
  73. symai/extended/interfaces/openai_search.py +2 -0
  74. symai/extended/interfaces/parallel.py +30 -0
  75. symai/extended/interfaces/perplexity.py +2 -0
  76. symai/extended/interfaces/pinecone.py +6 -4
  77. symai/extended/interfaces/python.py +2 -0
  78. symai/extended/interfaces/serpapi.py +2 -0
  79. symai/extended/interfaces/terminal.py +0 -1
  80. symai/extended/interfaces/tts.py +2 -1
  81. symai/extended/interfaces/whisper.py +2 -1
  82. symai/extended/interfaces/wolframalpha.py +1 -0
  83. symai/extended/metrics/__init__.py +1 -1
  84. symai/extended/metrics/similarity.py +5 -2
  85. symai/extended/os_command.py +31 -22
  86. symai/extended/packages/symdev.py +39 -34
  87. symai/extended/packages/sympkg.py +30 -27
  88. symai/extended/packages/symrun.py +46 -35
  89. symai/extended/repo_cloner.py +10 -9
  90. symai/extended/seo_query_optimizer.py +15 -12
  91. symai/extended/solver.py +104 -76
  92. symai/extended/summarizer.py +8 -7
  93. symai/extended/taypan_interpreter.py +10 -9
  94. symai/extended/vectordb.py +28 -15
  95. symai/formatter/formatter.py +39 -31
  96. symai/formatter/regex.py +46 -44
  97. symai/functional.py +184 -86
  98. symai/imports.py +85 -51
  99. symai/interfaces.py +1 -1
  100. symai/memory.py +33 -24
  101. symai/menu/screen.py +28 -19
  102. symai/misc/console.py +27 -27
  103. symai/misc/loader.py +4 -3
  104. symai/models/base.py +147 -76
  105. symai/models/errors.py +1 -1
  106. symai/ops/__init__.py +1 -1
  107. symai/ops/measures.py +17 -14
  108. symai/ops/primitives.py +933 -635
  109. symai/post_processors.py +28 -24
  110. symai/pre_processors.py +58 -52
  111. symai/processor.py +15 -9
  112. symai/prompts.py +714 -649
  113. symai/server/huggingface_server.py +115 -32
  114. symai/server/llama_cpp_server.py +14 -6
  115. symai/server/qdrant_server.py +206 -0
  116. symai/shell.py +98 -39
  117. symai/shellsv.py +307 -223
  118. symai/strategy.py +135 -81
  119. symai/symbol.py +276 -225
  120. symai/utils.py +62 -46
  121. {symbolicai-1.0.0.dist-info → symbolicai-1.1.0.dist-info}/METADATA +19 -9
  122. symbolicai-1.1.0.dist-info/RECORD +168 -0
  123. symbolicai-1.0.0.dist-info/RECORD +0 -163
  124. {symbolicai-1.0.0.dist-info → symbolicai-1.1.0.dist-info}/WHEEL +0 -0
  125. {symbolicai-1.0.0.dist-info → symbolicai-1.1.0.dist-info}/entry_points.txt +0 -0
  126. {symbolicai-1.0.0.dist-info → symbolicai-1.1.0.dist-info}/licenses/LICENSE +0 -0
  127. {symbolicai-1.0.0.dist-info → symbolicai-1.1.0.dist-info}/top_level.txt +0 -0
@@ -26,17 +26,17 @@ class GPTXReasoningEngine(Engine, OpenAIMixin):
26
26
  self.config = deepcopy(SYMAI_CONFIG)
27
27
  # In case we use EngineRepository.register to inject the api_key and model => dynamically change the engine at runtime
28
28
  if api_key is not None and model is not None:
29
- self.config['NEUROSYMBOLIC_ENGINE_API_KEY'] = api_key
30
- self.config['NEUROSYMBOLIC_ENGINE_MODEL'] = model
31
- if self.id() != 'neurosymbolic':
32
- return # do not initialize if not neurosymbolic; avoids conflict with llama.cpp check in EngineRepository.register_from_package
33
- openai.api_key = self.config['NEUROSYMBOLIC_ENGINE_API_KEY']
34
- self.model = self.config['NEUROSYMBOLIC_ENGINE_MODEL']
29
+ self.config["NEUROSYMBOLIC_ENGINE_API_KEY"] = api_key
30
+ self.config["NEUROSYMBOLIC_ENGINE_MODEL"] = model
31
+ if self.id() != "neurosymbolic":
32
+ return # do not initialize if not neurosymbolic; avoids conflict with llama.cpp check in EngineRepository.register_from_package
33
+ openai.api_key = self.config["NEUROSYMBOLIC_ENGINE_API_KEY"]
34
+ self.model = self.config["NEUROSYMBOLIC_ENGINE_MODEL"]
35
35
  self.name = self.__class__.__name__
36
36
  try:
37
37
  self.tokenizer = tiktoken.encoding_for_model(self.model)
38
38
  except Exception:
39
- self.tokenizer = tiktoken.get_encoding('o200k_base')
39
+ self.tokenizer = tiktoken.get_encoding("o200k_base")
40
40
  self.max_context_tokens = self.api_max_context_tokens()
41
41
  self.max_response_tokens = self.api_max_response_tokens()
42
42
  self.seed = None
@@ -44,47 +44,53 @@ class GPTXReasoningEngine(Engine, OpenAIMixin):
44
44
  try:
45
45
  self.client = openai.Client(api_key=openai.api_key)
46
46
  except Exception as e:
47
- UserMessage(f'Failed to initialize OpenAI client. Please check your OpenAI library version. Caused by: {e}', raise_with=ValueError)
47
+ UserMessage(
48
+ f"Failed to initialize OpenAI client. Please check your OpenAI library version. Caused by: {e}",
49
+ raise_with=ValueError,
50
+ )
48
51
 
49
52
  def id(self) -> str:
50
- if self.config.get('NEUROSYMBOLIC_ENGINE_MODEL') and \
51
- (self.config.get('NEUROSYMBOLIC_ENGINE_MODEL').startswith('o1') or \
52
- self.config.get('NEUROSYMBOLIC_ENGINE_MODEL').startswith('o3') or \
53
- self.config.get('NEUROSYMBOLIC_ENGINE_MODEL').startswith('o4') or \
54
- self.config.get('NEUROSYMBOLIC_ENGINE_MODEL') == 'gpt-5' or \
55
- self.config.get('NEUROSYMBOLIC_ENGINE_MODEL') == 'gpt-5-mini' or \
56
- self.config.get('NEUROSYMBOLIC_ENGINE_MODEL') == 'gpt-5-nano'):
57
- return 'neurosymbolic'
58
- return super().id() # default to unregistered
53
+ if self.config.get("NEUROSYMBOLIC_ENGINE_MODEL") and (
54
+ self.config.get("NEUROSYMBOLIC_ENGINE_MODEL").startswith("o1")
55
+ or self.config.get("NEUROSYMBOLIC_ENGINE_MODEL").startswith("o3")
56
+ or self.config.get("NEUROSYMBOLIC_ENGINE_MODEL").startswith("o4")
57
+ or self.config.get("NEUROSYMBOLIC_ENGINE_MODEL") == "gpt-5"
58
+ or self.config.get("NEUROSYMBOLIC_ENGINE_MODEL") == "gpt-5.1"
59
+ or self.config.get("NEUROSYMBOLIC_ENGINE_MODEL") == "gpt-5-mini"
60
+ or self.config.get("NEUROSYMBOLIC_ENGINE_MODEL") == "gpt-5-nano"
61
+ ):
62
+ return "neurosymbolic"
63
+ return super().id() # default to unregistered
59
64
 
60
65
  def command(self, *args, **kwargs):
61
66
  super().command(*args, **kwargs)
62
- if 'NEUROSYMBOLIC_ENGINE_API_KEY' in kwargs:
63
- openai.api_key = kwargs['NEUROSYMBOLIC_ENGINE_API_KEY']
64
- if 'NEUROSYMBOLIC_ENGINE_MODEL' in kwargs:
65
- self.model = kwargs['NEUROSYMBOLIC_ENGINE_MODEL']
66
- if 'seed' in kwargs:
67
- self.seed = kwargs['seed']
67
+ if "NEUROSYMBOLIC_ENGINE_API_KEY" in kwargs:
68
+ openai.api_key = kwargs["NEUROSYMBOLIC_ENGINE_API_KEY"]
69
+ if "NEUROSYMBOLIC_ENGINE_MODEL" in kwargs:
70
+ self.model = kwargs["NEUROSYMBOLIC_ENGINE_MODEL"]
71
+ if "seed" in kwargs:
72
+ self.seed = kwargs["seed"]
68
73
 
69
74
  def compute_required_tokens(self, messages):
70
75
  """Return the number of tokens used by a list of messages."""
71
76
 
72
77
  if self.model in {
73
- 'o1',
74
- 'o3',
75
- 'o3-mini',
76
- 'o4-mini',
77
- 'gpt-5',
78
- 'gpt-5-mini',
79
- 'gpt-5-nano',
80
- }:
78
+ "o1",
79
+ "o3",
80
+ "o3-mini",
81
+ "o4-mini",
82
+ "gpt-5",
83
+ "gpt-5.1",
84
+ "gpt-5-mini",
85
+ "gpt-5-nano",
86
+ }:
81
87
  tokens_per_message = 3
82
88
  tokens_per_name = 1
83
89
  else:
84
90
  UserMessage(
85
91
  f"'num_tokens_from_messages()' is not implemented for model {self.model}. "
86
92
  "See https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken for information on how messages are converted to tokens.",
87
- raise_with=NotImplementedError
93
+ raise_with=NotImplementedError,
88
94
  )
89
95
 
90
96
  num_tokens = 0
@@ -95,12 +101,14 @@ class GPTXReasoningEngine(Engine, OpenAIMixin):
95
101
  num_tokens += len(self.tokenizer.encode(value, disallowed_special=()))
96
102
  else:
97
103
  for v in value:
98
- if v['type'] == 'text':
99
- num_tokens += len(self.tokenizer.encode(v['text'], disallowed_special=()))
104
+ if v["type"] == "text":
105
+ num_tokens += len(
106
+ self.tokenizer.encode(v["text"], disallowed_special=())
107
+ )
100
108
  if key == "name":
101
109
  num_tokens += tokens_per_name
102
110
  num_tokens += 3 # every reply is primed with <|start|>assistant<|message|>
103
- return num_tokens - 1 # don't know where that extra 1 comes from
111
+ return num_tokens - 1 # don't know where that extra 1 comes from
104
112
 
105
113
  def compute_remaining_tokens(self, prompts: list) -> int:
106
114
  val = self.compute_required_tokens(prompts)
@@ -108,33 +116,40 @@ class GPTXReasoningEngine(Engine, OpenAIMixin):
108
116
 
109
117
  def _handle_image_content(self, content: str) -> list:
110
118
  """Handle image content by processing vision patterns and returning image file data."""
119
+
111
120
  def _extract_pattern(text):
112
- pattern = r'<<vision:(.*?):>>'
121
+ pattern = r"<<vision:(.*?):>>"
113
122
  return re.findall(pattern, text)
114
123
 
115
124
  image_files = []
116
125
  # pre-process prompt if contains image url
117
- if (self.model == 'o1' or \
118
- self.model == 'gpt-5' or \
119
- self.model == 'gpt-5-mini' or \
120
- self.model == 'gpt-5-nano') and '<<vision:' in content:
126
+ if (
127
+ self.model == "o1"
128
+ or self.model == "gpt-5"
129
+ or self.model == "gpt-5.1"
130
+ or self.model == "gpt-5-mini"
131
+ or self.model == "gpt-5-nano"
132
+ ) and "<<vision:" in content:
121
133
  parts = _extract_pattern(content)
122
134
  for p in parts:
123
135
  img_ = p.strip()
124
- if img_.startswith('http') or img_.startswith('data:image'):
136
+ if img_.startswith("http") or img_.startswith("data:image"):
125
137
  image_files.append(img_)
126
138
  else:
127
139
  max_frames_spacing = 50
128
140
  max_used_frames = 10
129
- if img_.startswith('frames:'):
130
- img_ = img_.replace('frames:', '')
131
- max_used_frames, img_ = img_.split(':')
141
+ if img_.startswith("frames:"):
142
+ img_ = img_.replace("frames:", "")
143
+ max_used_frames, img_ = img_.split(":")
132
144
  max_used_frames = int(max_used_frames)
133
145
  if max_used_frames < 1 or max_used_frames > max_frames_spacing:
134
- UserMessage(f"Invalid max_used_frames value: {max_used_frames}. Expected value between 1 and {max_frames_spacing}", raise_with=ValueError)
146
+ UserMessage(
147
+ f"Invalid max_used_frames value: {max_used_frames}. Expected value between 1 and {max_frames_spacing}",
148
+ raise_with=ValueError,
149
+ )
135
150
  buffer, ext = encode_media_frames(img_)
136
151
  if len(buffer) > 1:
137
- step = len(buffer) // max_frames_spacing # max frames spacing
152
+ step = len(buffer) // max_frames_spacing # max frames spacing
138
153
  frames = []
139
154
  indices = list(range(0, len(buffer), step))[:max_used_frames]
140
155
  for i in indices:
@@ -143,22 +158,22 @@ class GPTXReasoningEngine(Engine, OpenAIMixin):
143
158
  elif len(buffer) == 1:
144
159
  image_files.append(f"data:image/{ext};base64,{buffer[0]}")
145
160
  else:
146
- UserMessage('No frames found or error in encoding frames')
161
+ UserMessage("No frames found or error in encoding frames")
147
162
  return image_files
148
163
 
149
164
  def _remove_vision_pattern(self, text: str) -> str:
150
165
  """Remove vision patterns from text."""
151
- pattern = r'<<vision:(.*?):>>'
152
- return re.sub(pattern, '', text)
166
+ pattern = r"<<vision:(.*?):>>"
167
+ return re.sub(pattern, "", text)
153
168
 
154
169
  def _slice_tokens(self, tokens, new_len, truncation_type):
155
170
  """Slice tokens based on truncation type."""
156
171
  new_len = max(100, new_len) # Ensure minimum token length
157
- return tokens[-new_len:] if truncation_type == 'head' else tokens[:new_len] # else 'tail'
172
+ return tokens[-new_len:] if truncation_type == "head" else tokens[:new_len] # else 'tail'
158
173
 
159
174
  def _validate_truncation_prompts(self, prompts: list[dict]) -> bool:
160
175
  """Validate prompt structure before truncation."""
161
- if len(prompts) != 2 and all(prompt['role'] in ['developer', 'user'] for prompt in prompts):
176
+ if len(prompts) != 2 and all(prompt["role"] in ["developer", "user"] for prompt in prompts):
162
177
  # Only support developer and user prompts
163
178
  UserMessage(
164
179
  f"Token truncation currently supports only two messages, from 'user' and 'developer' (got {len(prompts)}). Returning original prompts."
@@ -172,15 +187,15 @@ class GPTXReasoningEngine(Engine, OpenAIMixin):
172
187
  ) -> tuple[list[int], bool]:
173
188
  """Collect user tokens and detect unsupported content."""
174
189
  user_tokens: list[int] = []
175
- user_content = user_prompt['content']
190
+ user_content = user_prompt["content"]
176
191
  if isinstance(user_content, str):
177
192
  user_tokens.extend(Symbol(user_content).tokens)
178
193
  return user_tokens, False
179
194
  if isinstance(user_content, list):
180
195
  for content_item in user_content:
181
196
  if isinstance(content_item, dict):
182
- if content_item.get('type') == 'text':
183
- user_tokens.extend(Symbol(content_item['text']).tokens)
197
+ if content_item.get("type") == "text":
198
+ user_tokens.extend(Symbol(content_item["text"]).tokens)
184
199
  else:
185
200
  return user_tokens, True
186
201
  else:
@@ -209,19 +224,27 @@ class GPTXReasoningEngine(Engine, OpenAIMixin):
209
224
  new_user_len = max_prompt_tokens - system_token_count
210
225
  new_user_tokens = self._slice_tokens(user_tokens, new_user_len, truncation_type)
211
226
  return [
212
- {'role': 'developer', 'content': self.tokenizer.decode(system_tokens)},
213
- {'role': 'user', 'content': [{'type': 'text', 'text': self.tokenizer.decode(new_user_tokens)}]},
227
+ {"role": "developer", "content": self.tokenizer.decode(system_tokens)},
228
+ {
229
+ "role": "user",
230
+ "content": [{"type": "text", "text": self.tokenizer.decode(new_user_tokens)}],
231
+ },
214
232
  ]
215
233
  if system_token_count > half_limit and user_token_count <= half_limit:
216
234
  new_system_len = max_prompt_tokens - user_token_count
217
235
  new_system_tokens = self._slice_tokens(system_tokens, new_system_len, truncation_type)
218
236
  return [
219
- {'role': 'developer', 'content': self.tokenizer.decode(new_system_tokens)},
220
- {'role': 'user', 'content': [{'type': 'text', 'text': self.tokenizer.decode(user_tokens)}]},
237
+ {"role": "developer", "content": self.tokenizer.decode(new_system_tokens)},
238
+ {
239
+ "role": "user",
240
+ "content": [{"type": "text", "text": self.tokenizer.decode(user_tokens)}],
241
+ },
221
242
  ]
222
243
  return None
223
244
 
224
- def truncate(self, prompts: list[dict], truncation_percentage: float | None, truncation_type: str) -> list[dict]:
245
+ def truncate(
246
+ self, prompts: list[dict], truncation_percentage: float | None, truncation_type: str
247
+ ) -> list[dict]:
225
248
  """Main truncation method"""
226
249
  if not self._validate_truncation_prompts(prompts):
227
250
  return prompts
@@ -234,7 +257,7 @@ class GPTXReasoningEngine(Engine, OpenAIMixin):
234
257
  user_prompt = prompts[1]
235
258
 
236
259
  # Get token counts
237
- system_tokens = Symbol(system_prompt['content']).tokens
260
+ system_tokens = Symbol(system_prompt["content"]).tokens
238
261
  user_tokens = []
239
262
 
240
263
  user_tokens, should_return_original = self._collect_user_tokens(user_prompt)
@@ -257,7 +280,7 @@ class GPTXReasoningEngine(Engine, OpenAIMixin):
257
280
  UserMessage(
258
281
  f"Executing {truncation_type} truncation to fit within {max_prompt_tokens} tokens. "
259
282
  f"Combined prompts ({total_tokens} tokens) exceed maximum allowed tokens "
260
- f"of {max_prompt_tokens} ({truncation_percentage*100:.1f}% of context). "
283
+ f"of {max_prompt_tokens} ({truncation_percentage * 100:.1f}% of context). "
261
284
  f"You can control this behavior by setting 'truncation_percentage' (current: {truncation_percentage:.2f}) "
262
285
  f"and 'truncation_type' (current: '{truncation_type}') parameters. "
263
286
  f"Set 'truncation_percentage=1.0' to deactivate truncation (will fail if exceeding context window). "
@@ -289,39 +312,49 @@ class GPTXReasoningEngine(Engine, OpenAIMixin):
289
312
  new_user_tokens = self._slice_tokens(user_tokens, new_user_len, truncation_type)
290
313
 
291
314
  return [
292
- {'role': 'developer', 'content': self.tokenizer.decode(new_system_tokens)},
293
- {'role': 'user', 'content': [{'type': 'text', 'text': self.tokenizer.decode(new_user_tokens)}]}
315
+ {"role": "developer", "content": self.tokenizer.decode(new_system_tokens)},
316
+ {
317
+ "role": "user",
318
+ "content": [{"type": "text", "text": self.tokenizer.decode(new_user_tokens)}],
319
+ },
294
320
  ]
295
321
 
296
322
  def forward(self, argument):
297
323
  kwargs = argument.kwargs
298
- truncation_percentage = kwargs.get('truncation_percentage', argument.prop.truncation_percentage)
299
- truncation_type = kwargs.get('truncation_type', argument.prop.truncation_type)
300
- messages = self.truncate(argument.prop.prepared_input, truncation_percentage, truncation_type)
324
+ truncation_percentage = kwargs.get(
325
+ "truncation_percentage", argument.prop.truncation_percentage
326
+ )
327
+ truncation_type = kwargs.get("truncation_type", argument.prop.truncation_type)
328
+ messages = self.truncate(
329
+ argument.prop.prepared_input, truncation_percentage, truncation_type
330
+ )
301
331
  payload = self._prepare_request_payload(messages, argument)
302
- except_remedy = kwargs.get('except_remedy')
332
+ except_remedy = kwargs.get("except_remedy")
303
333
 
304
334
  try:
305
335
  res = self.client.chat.completions.create(**payload)
306
336
 
307
337
  except Exception as e:
308
- if openai.api_key is None or openai.api_key == '':
309
- msg = 'OpenAI API key is not set. Please set it in the config file or pass it as an argument to the command method.'
338
+ if openai.api_key is None or openai.api_key == "":
339
+ msg = "OpenAI API key is not set. Please set it in the config file or pass it as an argument to the command method."
310
340
  UserMessage(msg)
311
- if self.config['NEUROSYMBOLIC_ENGINE_API_KEY'] is None or self.config['NEUROSYMBOLIC_ENGINE_API_KEY'] == '':
341
+ if (
342
+ self.config["NEUROSYMBOLIC_ENGINE_API_KEY"] is None
343
+ or self.config["NEUROSYMBOLIC_ENGINE_API_KEY"] == ""
344
+ ):
312
345
  UserMessage(msg, raise_with=ValueError)
313
- openai.api_key = self.config['NEUROSYMBOLIC_ENGINE_API_KEY']
346
+ openai.api_key = self.config["NEUROSYMBOLIC_ENGINE_API_KEY"]
314
347
 
315
348
  callback = self.client.chat.completions.create
316
- kwargs['model'] = kwargs.get('model', self.model)
349
+ kwargs["model"] = kwargs.get("model", self.model)
317
350
 
318
351
  if except_remedy is not None:
319
352
  res = except_remedy(self, e, callback, argument)
320
353
  else:
321
- UserMessage(f'Error during generation. Caused by: {e}', raise_with=ValueError)
354
+ UserMessage(f"Error during generation. Caused by: {e}", raise_with=ValueError)
322
355
 
323
- metadata = {'raw_output': res}
324
- if payload.get('tools'):
356
+ metadata = {"raw_output": res}
357
+ if payload.get("tools"):
325
358
  metadata = self._process_function_calls(res, metadata)
326
359
  output = [r.message.content for r in res.choices]
327
360
 
@@ -329,12 +362,15 @@ class GPTXReasoningEngine(Engine, OpenAIMixin):
329
362
 
330
363
  def _prepare_raw_input(self, argument):
331
364
  if not argument.prop.processed_input:
332
- UserMessage('Need to provide a prompt instruction to the engine if raw_input is enabled.', raise_with=ValueError)
365
+ UserMessage(
366
+ "Need to provide a prompt instruction to the engine if raw_input is enabled.",
367
+ raise_with=ValueError,
368
+ )
333
369
  value = argument.prop.processed_input
334
370
  # convert to dict if not already
335
371
  if not isinstance(value, list):
336
372
  if not isinstance(value, dict):
337
- value = {'role': 'user', 'content': str(value)}
373
+ value = {"role": "user", "content": str(value)}
338
374
  value = [value]
339
375
  return value
340
376
 
@@ -344,21 +380,23 @@ class GPTXReasoningEngine(Engine, OpenAIMixin):
344
380
  return (
345
381
  "<META_INSTRUCTION/>\n"
346
382
  "You do not output anything else, like verbose preambles or post explanation, such as "
347
- "\"Sure, let me...\", \"Hope that was helpful...\", \"Yes, I can help you with that...\", etc. "
383
+ '"Sure, let me...", "Hope that was helpful...", "Yes, I can help you with that...", etc. '
348
384
  "Consider well formatted output, e.g. for sentences use punctuation, spaces etc. or for code use "
349
385
  "indentation, etc. Never add meta instructions information to your output!\n\n"
350
386
  )
351
- return ''
387
+ return ""
352
388
 
353
389
  def _response_format_section(self, argument) -> str:
354
390
  """Return response format instructions if provided."""
355
391
  if not argument.prop.response_format:
356
- return ''
392
+ return ""
357
393
  response_format = argument.prop.response_format
358
- assert response_format.get('type') is not None, 'Expected format `{ "type": "json_object" }`! See https://platform.openai.com/docs/api-reference/chat/create#chat-create-response_format'
394
+ assert response_format.get("type") is not None, (
395
+ 'Expected format `{ "type": "json_object" }`! See https://platform.openai.com/docs/api-reference/chat/create#chat-create-response_format'
396
+ )
359
397
  if response_format["type"] == "json_object":
360
- return '<RESPONSE_FORMAT/>\nYou are a helpful assistant designed to output JSON.\n\n'
361
- return ''
398
+ return "<RESPONSE_FORMAT/>\nYou are a helpful assistant designed to output JSON.\n\n"
399
+ return ""
362
400
 
363
401
  def _context_sections(self, argument) -> list[str]:
364
402
  """Return static and dynamic context sections."""
@@ -374,20 +412,20 @@ class GPTXReasoningEngine(Engine, OpenAIMixin):
374
412
  """Return additional payload context if any."""
375
413
  if argument.prop.payload:
376
414
  return f"<ADDITIONAL CONTEXT/>\n{argument.prop.payload!s}\n\n"
377
- return ''
415
+ return ""
378
416
 
379
417
  def _examples_section(self, argument) -> str:
380
418
  """Return examples section if provided."""
381
419
  examples: list[str] = argument.prop.examples
382
420
  if examples and len(examples) > 0:
383
421
  return f"<EXAMPLES/>\n{examples!s}\n\n"
384
- return ''
422
+ return ""
385
423
 
386
424
  def _instruction_section(self, argument, image_files: list[str]) -> str:
387
425
  """Return instruction section, removing vision patterns when needed."""
388
426
  prompt = argument.prop.prompt
389
427
  if prompt is None or len(prompt) == 0:
390
- return ''
428
+ return ""
391
429
  value = str(prompt)
392
430
  if len(image_files) > 0:
393
431
  value = self._remove_vision_pattern(value)
@@ -396,7 +434,7 @@ class GPTXReasoningEngine(Engine, OpenAIMixin):
396
434
  def _build_developer_prompt(self, argument, image_files: list[str]) -> str:
397
435
  """Assemble developer prompt content."""
398
436
  developer = self._non_verbose_section(argument)
399
- developer = f'{developer}\n' if developer else ''
437
+ developer = f"{developer}\n" if developer else ""
400
438
 
401
439
  parts = [
402
440
  self._response_format_section(argument),
@@ -405,12 +443,12 @@ class GPTXReasoningEngine(Engine, OpenAIMixin):
405
443
  self._examples_section(argument),
406
444
  self._instruction_section(argument, image_files),
407
445
  ]
408
- developer += ''.join(part for part in parts if part)
446
+ developer += "".join(part for part in parts if part)
409
447
 
410
448
  if argument.prop.template_suffix:
411
449
  developer += (
412
- f' You will only generate content for the placeholder `{argument.prop.template_suffix!s}` '
413
- 'following the instructions and the provided context information.\n\n'
450
+ f" You will only generate content for the placeholder `{argument.prop.template_suffix!s}` "
451
+ "following the instructions and the provided context information.\n\n"
414
452
  )
415
453
  return developer
416
454
 
@@ -424,20 +462,21 @@ class GPTXReasoningEngine(Engine, OpenAIMixin):
424
462
  def _construct_user_prompt(self, user_text: str, image_files: list[str]):
425
463
  """Construct user prompt payload."""
426
464
  if self.model in {
427
- 'o1',
428
- 'o3',
429
- 'o3-mini',
430
- 'o4-mini',
431
- 'gpt-5',
432
- 'gpt-5-mini',
433
- 'gpt-5-nano',
434
- }:
435
- images = [{'type': 'image_url', 'image_url': {'url': file}} for file in image_files]
465
+ "o1",
466
+ "o3",
467
+ "o3-mini",
468
+ "o4-mini",
469
+ "gpt-5",
470
+ "gpt-5.1",
471
+ "gpt-5-mini",
472
+ "gpt-5-nano",
473
+ }:
474
+ images = [{"type": "image_url", "image_url": {"url": file}} for file in image_files]
436
475
  user_prompt = {
437
476
  "role": "user",
438
477
  "content": [
439
478
  *images,
440
- {'type': 'text', 'text': user_text},
479
+ {"type": "text", "text": user_text},
441
480
  ],
442
481
  }
443
482
  return user_prompt, images
@@ -454,29 +493,31 @@ class GPTXReasoningEngine(Engine, OpenAIMixin):
454
493
  ):
455
494
  """Apply self-prompting when requested."""
456
495
  instance = argument.prop.instance
457
- if not (instance._kwargs.get('self_prompt', False) or argument.prop.self_prompt):
496
+ if not (instance._kwargs.get("self_prompt", False) or argument.prop.self_prompt):
458
497
  return user_prompt, developer
459
498
 
460
499
  self_prompter = SelfPrompt()
461
- res = self_prompter({'user': user_text, 'developer': developer})
500
+ res = self_prompter({"user": user_text, "developer": developer})
462
501
  if res is None:
463
502
  UserMessage("Self-prompting failed!", raise_with=ValueError)
464
503
 
465
504
  if len(image_files) > 0:
466
- image_content = images if images is not None else [
467
- {'type': 'image_url', 'image_url': {'url': file}} for file in image_files
468
- ]
505
+ image_content = (
506
+ images
507
+ if images is not None
508
+ else [{"type": "image_url", "image_url": {"url": file}} for file in image_files]
509
+ )
469
510
  user_prompt = {
470
511
  "role": "user",
471
512
  "content": [
472
513
  *image_content,
473
- {'type': 'text', 'text': res['user']},
514
+ {"type": "text", "text": res["user"]},
474
515
  ],
475
516
  }
476
517
  else:
477
- user_prompt = {"role": "user", "content": res['user']}
518
+ user_prompt = {"role": "user", "content": res["user"]}
478
519
 
479
- return user_prompt, res['developer']
520
+ return user_prompt, res["developer"]
480
521
 
481
522
  def prepare(self, argument):
482
523
  if argument.prop.raw_input:
@@ -498,32 +539,34 @@ class GPTXReasoningEngine(Engine, OpenAIMixin):
498
539
  )
499
540
 
500
541
  argument.prop.prepared_input = [
501
- { "role": "developer", "content": developer },
542
+ {"role": "developer", "content": developer},
502
543
  user_prompt,
503
544
  ]
504
545
 
505
546
  def _process_function_calls(self, res, metadata):
506
547
  hit = False
507
548
  if (
508
- hasattr(res, 'choices')
549
+ hasattr(res, "choices")
509
550
  and res.choices
510
- and hasattr(res.choices[0], 'message')
551
+ and hasattr(res.choices[0], "message")
511
552
  and res.choices[0].message
512
- and hasattr(res.choices[0].message, 'tool_calls')
553
+ and hasattr(res.choices[0].message, "tool_calls")
513
554
  and res.choices[0].message.tool_calls
514
555
  ):
515
556
  for tool_call in res.choices[0].message.tool_calls:
516
557
  if hit:
517
- UserMessage("Multiple function calls detected in the response but only the first one will be processed.")
558
+ UserMessage(
559
+ "Multiple function calls detected in the response but only the first one will be processed."
560
+ )
518
561
  break
519
- if hasattr(tool_call, 'function') and tool_call.function:
562
+ if hasattr(tool_call, "function") and tool_call.function:
520
563
  try:
521
564
  args_dict = json.loads(tool_call.function.arguments)
522
565
  except json.JSONDecodeError:
523
566
  args_dict = {}
524
- metadata['function_call'] = {
525
- 'name': tool_call.function.name,
526
- 'arguments': args_dict
567
+ metadata["function_call"] = {
568
+ "name": tool_call.function.name,
569
+ "arguments": args_dict,
527
570
  }
528
571
  hit = True
529
572
  return metadata
@@ -532,8 +575,8 @@ class GPTXReasoningEngine(Engine, OpenAIMixin):
532
575
  """Prepares the request payload from the argument."""
533
576
  kwargs = argument.kwargs
534
577
 
535
- max_tokens = kwargs.get('max_tokens', None)
536
- max_completion_tokens = kwargs.get('max_completion_tokens', None)
578
+ max_tokens = kwargs.get("max_tokens", None)
579
+ max_completion_tokens = kwargs.get("max_completion_tokens", None)
537
580
  remaining_tokens = self.compute_remaining_tokens(messages)
538
581
 
539
582
  if max_tokens is not None:
@@ -547,34 +590,34 @@ class GPTXReasoningEngine(Engine, OpenAIMixin):
547
590
  f"Provided 'max_tokens' ({max_tokens}) exceeds max response tokens ({self.max_response_tokens}). "
548
591
  f"Truncating to {remaining_tokens} to avoid API failure."
549
592
  )
550
- kwargs['max_completion_tokens'] = remaining_tokens
593
+ kwargs["max_completion_tokens"] = remaining_tokens
551
594
  else:
552
- kwargs['max_completion_tokens'] = max_tokens
553
- del kwargs['max_tokens']
595
+ kwargs["max_completion_tokens"] = max_tokens
596
+ del kwargs["max_tokens"]
554
597
 
555
598
  if max_completion_tokens is not None and max_completion_tokens > self.max_response_tokens:
556
599
  UserMessage(
557
600
  f"Provided 'max_completion_tokens' ({max_completion_tokens}) exceeds max response tokens ({self.max_response_tokens}). "
558
601
  f"Truncating to {remaining_tokens} to avoid API failure."
559
602
  )
560
- kwargs['max_completion_tokens'] = remaining_tokens
603
+ kwargs["max_completion_tokens"] = remaining_tokens
561
604
 
562
605
  payload = {
563
606
  "messages": messages,
564
- "model": kwargs.get('model', self.model),
565
- "seed": kwargs.get('seed', self.seed),
566
- "reasoning_effort": kwargs.get('reasoning_effort', 'medium'),
567
- "max_completion_tokens": kwargs.get('max_completion_tokens'),
568
- "stop": kwargs.get('stop', ''),
569
- "temperature": kwargs.get('temperature', 1),
570
- "frequency_penalty": kwargs.get('frequency_penalty', 0),
571
- "presence_penalty": kwargs.get('presence_penalty', 0),
572
- "top_p": kwargs.get('top_p', 1),
573
- "n": kwargs.get('n', 1),
574
- "logit_bias": kwargs.get('logit_bias'),
575
- "tools": kwargs.get('tools'),
576
- "tool_choice": kwargs.get('tool_choice'),
577
- "response_format": kwargs.get('response_format'),
607
+ "model": kwargs.get("model", self.model),
608
+ "seed": kwargs.get("seed", self.seed),
609
+ "reasoning_effort": kwargs.get("reasoning_effort", "medium"),
610
+ "max_completion_tokens": kwargs.get("max_completion_tokens"),
611
+ "stop": kwargs.get("stop", ""),
612
+ "temperature": kwargs.get("temperature", 1),
613
+ "frequency_penalty": kwargs.get("frequency_penalty", 0),
614
+ "presence_penalty": kwargs.get("presence_penalty", 0),
615
+ "top_p": kwargs.get("top_p", 1),
616
+ "n": kwargs.get("n", 1),
617
+ "logit_bias": kwargs.get("logit_bias"),
618
+ "tools": kwargs.get("tools"),
619
+ "tool_choice": kwargs.get("tool_choice"),
620
+ "response_format": kwargs.get("response_format"),
578
621
  }
579
622
 
580
623
  if self.model == "o4-mini" or self.model == "o3":