symbolicai 1.0.0__py3-none-any.whl → 1.1.1__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 (129) 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 +35 -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/__init__.py +4 -0
  39. symai/backend/mixin/anthropic.py +48 -40
  40. symai/backend/mixin/cerebras.py +9 -0
  41. symai/backend/mixin/deepseek.py +4 -5
  42. symai/backend/mixin/google.py +5 -4
  43. symai/backend/mixin/groq.py +2 -4
  44. symai/backend/mixin/openai.py +132 -110
  45. symai/backend/settings.py +14 -14
  46. symai/chat.py +164 -94
  47. symai/collect/dynamic.py +13 -11
  48. symai/collect/pipeline.py +39 -31
  49. symai/collect/stats.py +109 -69
  50. symai/components.py +578 -238
  51. symai/constraints.py +14 -5
  52. symai/core.py +1495 -1210
  53. symai/core_ext.py +55 -50
  54. symai/endpoints/api.py +113 -58
  55. symai/extended/api_builder.py +22 -17
  56. symai/extended/arxiv_pdf_parser.py +13 -5
  57. symai/extended/bibtex_parser.py +8 -4
  58. symai/extended/conversation.py +88 -69
  59. symai/extended/document.py +40 -27
  60. symai/extended/file_merger.py +45 -7
  61. symai/extended/graph.py +38 -24
  62. symai/extended/html_style_template.py +17 -11
  63. symai/extended/interfaces/blip_2.py +1 -1
  64. symai/extended/interfaces/clip.py +4 -2
  65. symai/extended/interfaces/console.py +5 -3
  66. symai/extended/interfaces/dall_e.py +3 -1
  67. symai/extended/interfaces/file.py +2 -0
  68. symai/extended/interfaces/flux.py +3 -1
  69. symai/extended/interfaces/gpt_image.py +15 -6
  70. symai/extended/interfaces/input.py +2 -1
  71. symai/extended/interfaces/llava.py +1 -1
  72. symai/extended/interfaces/{naive_webscraping.py → naive_scrape.py} +3 -2
  73. symai/extended/interfaces/naive_vectordb.py +2 -2
  74. symai/extended/interfaces/ocr.py +4 -2
  75. symai/extended/interfaces/openai_search.py +2 -0
  76. symai/extended/interfaces/parallel.py +30 -0
  77. symai/extended/interfaces/perplexity.py +2 -0
  78. symai/extended/interfaces/pinecone.py +6 -4
  79. symai/extended/interfaces/python.py +2 -0
  80. symai/extended/interfaces/serpapi.py +2 -0
  81. symai/extended/interfaces/terminal.py +0 -1
  82. symai/extended/interfaces/tts.py +2 -1
  83. symai/extended/interfaces/whisper.py +2 -1
  84. symai/extended/interfaces/wolframalpha.py +1 -0
  85. symai/extended/metrics/__init__.py +1 -1
  86. symai/extended/metrics/similarity.py +5 -2
  87. symai/extended/os_command.py +31 -22
  88. symai/extended/packages/symdev.py +39 -34
  89. symai/extended/packages/sympkg.py +30 -27
  90. symai/extended/packages/symrun.py +46 -35
  91. symai/extended/repo_cloner.py +10 -9
  92. symai/extended/seo_query_optimizer.py +15 -12
  93. symai/extended/solver.py +104 -76
  94. symai/extended/summarizer.py +8 -7
  95. symai/extended/taypan_interpreter.py +10 -9
  96. symai/extended/vectordb.py +28 -15
  97. symai/formatter/formatter.py +39 -31
  98. symai/formatter/regex.py +46 -44
  99. symai/functional.py +184 -86
  100. symai/imports.py +85 -51
  101. symai/interfaces.py +1 -1
  102. symai/memory.py +33 -24
  103. symai/menu/screen.py +28 -19
  104. symai/misc/console.py +27 -27
  105. symai/misc/loader.py +4 -3
  106. symai/models/base.py +147 -76
  107. symai/models/errors.py +1 -1
  108. symai/ops/__init__.py +1 -1
  109. symai/ops/measures.py +17 -14
  110. symai/ops/primitives.py +933 -635
  111. symai/post_processors.py +28 -24
  112. symai/pre_processors.py +58 -52
  113. symai/processor.py +15 -9
  114. symai/prompts.py +714 -649
  115. symai/server/huggingface_server.py +115 -32
  116. symai/server/llama_cpp_server.py +14 -6
  117. symai/server/qdrant_server.py +206 -0
  118. symai/shell.py +98 -39
  119. symai/shellsv.py +307 -223
  120. symai/strategy.py +135 -81
  121. symai/symbol.py +276 -225
  122. symai/utils.py +62 -46
  123. {symbolicai-1.0.0.dist-info → symbolicai-1.1.1.dist-info}/METADATA +19 -9
  124. symbolicai-1.1.1.dist-info/RECORD +169 -0
  125. symbolicai-1.0.0.dist-info/RECORD +0 -163
  126. {symbolicai-1.0.0.dist-info → symbolicai-1.1.1.dist-info}/WHEEL +0 -0
  127. {symbolicai-1.0.0.dist-info → symbolicai-1.1.1.dist-info}/entry_points.txt +0 -0
  128. {symbolicai-1.0.0.dist-info → symbolicai-1.1.1.dist-info}/licenses/LICENSE +0 -0
  129. {symbolicai-1.0.0.dist-info → symbolicai-1.1.1.dist-info}/top_level.txt +0 -0
@@ -39,6 +39,7 @@ class GPTXChatEngine(Engine, OpenAIMixin):
39
39
  "gpt-4.1-mini",
40
40
  "gpt-4.1-nano",
41
41
  "gpt-5-chat-latest",
42
+ "gpt-5.1-chat-latest",
42
43
  }
43
44
  _VISION_PREVIEW_MODEL = "gpt-4-vision-preview"
44
45
  _VISION_IMAGE_URL_MODELS: ClassVar[set[str]] = {
@@ -51,6 +52,7 @@ class GPTXChatEngine(Engine, OpenAIMixin):
51
52
  "gpt-4.1-mini",
52
53
  "gpt-4.1-nano",
53
54
  "gpt-5-chat-latest",
55
+ "gpt-5.1-chat-latest",
54
56
  }
55
57
  _NON_VERBOSE_OUTPUT = """<META_INSTRUCTION/>\nYou do not output anything else, like verbose preambles or post explanation, such as "Sure, let me...", "Hope that was helpful...", "Yes, I can help you with that...", etc. Consider well formatted output, e.g. for sentences use punctuation, spaces etc. or for code use indentation, etc. Never add meta instructions information to your output!\n\n"""
56
58
 
@@ -59,16 +61,16 @@ class GPTXChatEngine(Engine, OpenAIMixin):
59
61
  self.config = deepcopy(SYMAI_CONFIG)
60
62
  # In case we use EngineRepository.register to inject the api_key and model => dynamically change the engine at runtime
61
63
  if api_key is not None and model is not None:
62
- self.config['NEUROSYMBOLIC_ENGINE_API_KEY'] = api_key
63
- self.config['NEUROSYMBOLIC_ENGINE_MODEL'] = model
64
- if self.id() != 'neurosymbolic':
65
- return # do not initialize if not neurosymbolic; avoids conflict with llama.cpp check in EngineRepository.register_from_package
66
- openai.api_key = self.config['NEUROSYMBOLIC_ENGINE_API_KEY']
67
- self.model = self.config['NEUROSYMBOLIC_ENGINE_MODEL']
64
+ self.config["NEUROSYMBOLIC_ENGINE_API_KEY"] = api_key
65
+ self.config["NEUROSYMBOLIC_ENGINE_MODEL"] = model
66
+ if self.id() != "neurosymbolic":
67
+ return # do not initialize if not neurosymbolic; avoids conflict with llama.cpp check in EngineRepository.register_from_package
68
+ openai.api_key = self.config["NEUROSYMBOLIC_ENGINE_API_KEY"]
69
+ self.model = self.config["NEUROSYMBOLIC_ENGINE_MODEL"]
68
70
  try:
69
71
  self.tokenizer = tiktoken.encoding_for_model(self.model)
70
72
  except Exception:
71
- self.tokenizer = tiktoken.get_encoding('o200k_base')
73
+ self.tokenizer = tiktoken.get_encoding("o200k_base")
72
74
  self.max_context_tokens = self.api_max_context_tokens()
73
75
  self.max_response_tokens = self.api_max_response_tokens()
74
76
  self.seed = None
@@ -77,26 +79,31 @@ class GPTXChatEngine(Engine, OpenAIMixin):
77
79
  try:
78
80
  self.client = openai.Client(api_key=openai.api_key)
79
81
  except Exception as e:
80
- UserMessage(f'Failed to initialize OpenAI client. Please check your OpenAI library version. Caused by: {e}', raise_with=ValueError)
82
+ UserMessage(
83
+ f"Failed to initialize OpenAI client. Please check your OpenAI library version. Caused by: {e}",
84
+ raise_with=ValueError,
85
+ )
81
86
 
82
87
  def id(self) -> str:
83
- if self.config.get('NEUROSYMBOLIC_ENGINE_MODEL') and \
84
- (self.config.get('NEUROSYMBOLIC_ENGINE_MODEL').startswith('gpt-3.5') or \
85
- self.config.get('NEUROSYMBOLIC_ENGINE_MODEL').startswith('gpt-4') or \
86
- self.config.get('NEUROSYMBOLIC_ENGINE_MODEL').startswith('chatgpt-4o') or \
87
- self.config.get('NEUROSYMBOLIC_ENGINE_MODEL').startswith('gpt-4.1') or \
88
- self.config.get('NEUROSYMBOLIC_ENGINE_MODEL') == 'gpt-5-chat-latest'):
89
- return 'neurosymbolic'
90
- return super().id() # default to unregistered
88
+ if self.config.get("NEUROSYMBOLIC_ENGINE_MODEL") and (
89
+ self.config.get("NEUROSYMBOLIC_ENGINE_MODEL").startswith("gpt-3.5")
90
+ or self.config.get("NEUROSYMBOLIC_ENGINE_MODEL").startswith("gpt-4")
91
+ or self.config.get("NEUROSYMBOLIC_ENGINE_MODEL").startswith("chatgpt-4o")
92
+ or self.config.get("NEUROSYMBOLIC_ENGINE_MODEL").startswith("gpt-4.1")
93
+ or self.config.get("NEUROSYMBOLIC_ENGINE_MODEL") == "gpt-5-chat-latest"
94
+ or self.config.get("NEUROSYMBOLIC_ENGINE_MODEL") == "gpt-5.1-chat-latest"
95
+ ):
96
+ return "neurosymbolic"
97
+ return super().id() # default to unregistered
91
98
 
92
99
  def command(self, *args, **kwargs):
93
100
  super().command(*args, **kwargs)
94
- if 'NEUROSYMBOLIC_ENGINE_API_KEY' in kwargs:
95
- openai.api_key = kwargs['NEUROSYMBOLIC_ENGINE_API_KEY']
96
- if 'NEUROSYMBOLIC_ENGINE_MODEL' in kwargs:
97
- self.model = kwargs['NEUROSYMBOLIC_ENGINE_MODEL']
98
- if 'seed' in kwargs:
99
- self.seed = kwargs['seed']
101
+ if "NEUROSYMBOLIC_ENGINE_API_KEY" in kwargs:
102
+ openai.api_key = kwargs["NEUROSYMBOLIC_ENGINE_API_KEY"]
103
+ if "NEUROSYMBOLIC_ENGINE_MODEL" in kwargs:
104
+ self.model = kwargs["NEUROSYMBOLIC_ENGINE_MODEL"]
105
+ if "seed" in kwargs:
106
+ self.seed = kwargs["seed"]
100
107
 
101
108
  def _resolve_token_config(self) -> tuple[int, int]:
102
109
  if self.model in self._THREE_TOKEN_MODELS:
@@ -104,16 +111,20 @@ class GPTXChatEngine(Engine, OpenAIMixin):
104
111
  if self.model == "gpt-3.5-turbo-0301":
105
112
  return 4, -1
106
113
  if self.model == "gpt-3.5-turbo":
107
- UserMessage("Warning: gpt-3.5-turbo may update over time. Returning num tokens assuming gpt-3.5-turbo-0613.")
114
+ UserMessage(
115
+ "Warning: gpt-3.5-turbo may update over time. Returning num tokens assuming gpt-3.5-turbo-0613."
116
+ )
108
117
  self.tokenizer = tiktoken.encoding_for_model("gpt-3.5-turbo-0613")
109
118
  return 3, 1
110
119
  if self.model == "gpt-4":
111
- UserMessage("Warning: gpt-4 may update over time. Returning num tokens assuming gpt-4-0613.")
120
+ UserMessage(
121
+ "Warning: gpt-4 may update over time. Returning num tokens assuming gpt-4-0613."
122
+ )
112
123
  self.tokenizer = tiktoken.encoding_for_model("gpt-4-0613")
113
124
  return 3, 1
114
125
  UserMessage(
115
126
  f"""num_tokens_from_messages() is not implemented for model {self.model}. See https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken for information on how messages are converted to tokens.""",
116
- raise_with=NotImplementedError
127
+ raise_with=NotImplementedError,
117
128
  )
118
129
  raise NotImplementedError
119
130
 
@@ -122,8 +133,8 @@ class GPTXChatEngine(Engine, OpenAIMixin):
122
133
  return len(self.tokenizer.encode(value, disallowed_special=()))
123
134
  tokens = 0
124
135
  for item in value:
125
- if item['type'] == 'text':
126
- tokens += len(self.tokenizer.encode(item['text'], disallowed_special=()))
136
+ if item["type"] == "text":
137
+ tokens += len(self.tokenizer.encode(item["text"], disallowed_special=()))
127
138
  return tokens
128
139
 
129
140
  def compute_required_tokens(self, messages):
@@ -138,15 +149,19 @@ class GPTXChatEngine(Engine, OpenAIMixin):
138
149
  if key == "name":
139
150
  num_tokens += tokens_per_name
140
151
  num_tokens += 3 # every reply is primed with <|start|>assistant<|message|>
141
- return num_tokens
152
+ return (
153
+ num_tokens if self.model != "gpt-5.1-chat-latest" else num_tokens - 1
154
+ ) # no idea why -1 but ok
142
155
 
143
156
  def compute_remaining_tokens(self, prompts: list) -> int:
144
157
  val = self.compute_required_tokens(prompts)
145
158
  return min(self.max_context_tokens - val, self.max_response_tokens)
146
159
 
147
160
  def _should_skip_truncation(self, prompts: list[dict]) -> bool:
148
- if len(prompts) != 2 and all(prompt['role'] in ['system', 'user'] for prompt in prompts):
149
- UserMessage(f"Token truncation currently supports only two messages, from 'user' and 'system' (got {len(prompts)}). Returning original prompts.")
161
+ if len(prompts) != 2 and all(prompt["role"] in ["system", "user"] for prompt in prompts):
162
+ UserMessage(
163
+ f"Token truncation currently supports only two messages, from 'user' and 'system' (got {len(prompts)}). Returning original prompts."
164
+ )
150
165
  return True
151
166
  return False
152
167
 
@@ -155,32 +170,53 @@ class GPTXChatEngine(Engine, OpenAIMixin):
155
170
  return truncation_percentage
156
171
  return (self.max_context_tokens - self.max_response_tokens) / self.max_context_tokens
157
172
 
158
- def _collect_user_tokens(self, user_prompt: dict, prompts: list[dict]) -> tuple[list, object | None]:
173
+ def _collect_user_tokens(
174
+ self, user_prompt: dict, prompts: list[dict]
175
+ ) -> tuple[list, object | None]:
159
176
  user_tokens: list = []
160
- content = user_prompt['content']
177
+ content = user_prompt["content"]
161
178
  if isinstance(content, str):
162
179
  user_tokens.extend(Symbol(content).tokens)
163
180
  return user_tokens, None
164
181
  if isinstance(content, list):
165
182
  for content_item in content:
166
183
  if isinstance(content_item, dict):
167
- if content_item.get('type') == 'text':
168
- user_tokens.extend(Symbol(content_item['text']).tokens)
184
+ if content_item.get("type") == "text":
185
+ user_tokens.extend(Symbol(content_item["text"]).tokens)
169
186
  else:
170
187
  return [], prompts
171
188
  else:
172
- return [], ValueError(f"Invalid content type: {type(content_item)}. Format input according to the documentation. See https://platform.openai.com/docs/api-reference/chat/create?lang=python")
189
+ return [], ValueError(
190
+ f"Invalid content type: {type(content_item)}. Format input according to the documentation. See https://platform.openai.com/docs/api-reference/chat/create?lang=python"
191
+ )
173
192
  return user_tokens, None
174
- UserMessage(f"Unknown content type: {type(content)}. Format input according to the documentation. See https://platform.openai.com/docs/api-reference/chat/create?lang=python", raise_with=ValueError)
193
+ UserMessage(
194
+ f"Unknown content type: {type(content)}. Format input according to the documentation. See https://platform.openai.com/docs/api-reference/chat/create?lang=python",
195
+ raise_with=ValueError,
196
+ )
175
197
  return user_tokens, None
176
198
 
177
- def _user_only_exceeds(self, user_token_count: int, system_token_count: int, max_prompt_tokens: int) -> bool:
178
- return user_token_count > max_prompt_tokens/2 and system_token_count <= max_prompt_tokens/2
199
+ def _user_only_exceeds(
200
+ self, user_token_count: int, system_token_count: int, max_prompt_tokens: int
201
+ ) -> bool:
202
+ return (
203
+ user_token_count > max_prompt_tokens / 2 and system_token_count <= max_prompt_tokens / 2
204
+ )
179
205
 
180
- def _system_only_exceeds(self, system_token_count: int, user_token_count: int, max_prompt_tokens: int) -> bool:
181
- return system_token_count > max_prompt_tokens/2 and user_token_count <= max_prompt_tokens/2
206
+ def _system_only_exceeds(
207
+ self, system_token_count: int, user_token_count: int, max_prompt_tokens: int
208
+ ) -> bool:
209
+ return (
210
+ system_token_count > max_prompt_tokens / 2 and user_token_count <= max_prompt_tokens / 2
211
+ )
182
212
 
183
- def _compute_proportional_lengths(self, system_token_count: int, user_token_count: int, total_tokens: int, max_prompt_tokens: int) -> tuple[int, int]:
213
+ def _compute_proportional_lengths(
214
+ self,
215
+ system_token_count: int,
216
+ user_token_count: int,
217
+ total_tokens: int,
218
+ max_prompt_tokens: int,
219
+ ) -> tuple[int, int]:
184
220
  system_ratio = system_token_count / total_tokens
185
221
  user_ratio = user_token_count / total_tokens
186
222
  new_system_len = int(max_prompt_tokens * system_ratio)
@@ -192,47 +228,55 @@ class GPTXChatEngine(Engine, OpenAIMixin):
192
228
 
193
229
  def _decode_prompt_pair(self, system_tokens, user_tokens) -> list[dict]:
194
230
  return [
195
- {'role': 'system', 'content': self.tokenizer.decode(system_tokens)},
196
- {'role': 'user', 'content': [{'type': 'text', 'text': self.tokenizer.decode(user_tokens)}]}
231
+ {"role": "system", "content": self.tokenizer.decode(system_tokens)},
232
+ {
233
+ "role": "user",
234
+ "content": [{"type": "text", "text": self.tokenizer.decode(user_tokens)}],
235
+ },
197
236
  ]
198
237
 
199
238
  def _handle_image_content(self, content: str) -> list:
200
239
  """Handle image content by processing vision patterns and returning image file data."""
240
+
201
241
  def extract_pattern(text):
202
- pattern = r'<<vision:(.*?):>>'
242
+ pattern = r"<<vision:(.*?):>>"
203
243
  return re.findall(pattern, text)
204
244
 
205
245
  image_files = []
206
246
  # pre-process prompt if contains image url
207
- if (self.model == 'gpt-4-vision-preview' or \
208
- self.model == 'gpt-4-turbo-2024-04-09' or \
209
- self.model == 'gpt-4-turbo' or \
210
- self.model == 'gpt-4o' or \
211
- self.model == 'gpt-4o-mini' or \
212
- self.model == 'chatgpt-4o-latest' or \
213
- self.model == 'gpt-4.1' or \
214
- self.model == 'gpt-4.1-mini' or \
215
- self.model == 'gpt-4.1-nano' or \
216
- self.model == 'gpt-5-chat-latest') \
217
- and '<<vision:' in content:
218
-
247
+ if (
248
+ self.model == "gpt-4-vision-preview"
249
+ or self.model == "gpt-4-turbo-2024-04-09"
250
+ or self.model == "gpt-4-turbo"
251
+ or self.model == "gpt-4o"
252
+ or self.model == "gpt-4o-mini"
253
+ or self.model == "chatgpt-4o-latest"
254
+ or self.model == "gpt-4.1"
255
+ or self.model == "gpt-4.1-mini"
256
+ or self.model == "gpt-4.1-nano"
257
+ or self.model == "gpt-5-chat-latest"
258
+ or self.model == "gpt-5.1-chat-latest"
259
+ ) and "<<vision:" in content:
219
260
  parts = extract_pattern(content)
220
261
  for p in parts:
221
262
  img_ = p.strip()
222
- if img_.startswith('http') or img_.startswith('data:image'):
263
+ if img_.startswith("http") or img_.startswith("data:image"):
223
264
  image_files.append(img_)
224
265
  else:
225
266
  max_frames_spacing = 50
226
267
  max_used_frames = 10
227
- if img_.startswith('frames:'):
228
- img_ = img_.replace('frames:', '')
229
- max_used_frames, img_ = img_.split(':')
268
+ if img_.startswith("frames:"):
269
+ img_ = img_.replace("frames:", "")
270
+ max_used_frames, img_ = img_.split(":")
230
271
  max_used_frames = int(max_used_frames)
231
272
  if max_used_frames < 1 or max_used_frames > max_frames_spacing:
232
- UserMessage(f"Invalid max_used_frames value: {max_used_frames}. Expected value between 1 and {max_frames_spacing}", raise_with=ValueError)
273
+ UserMessage(
274
+ f"Invalid max_used_frames value: {max_used_frames}. Expected value between 1 and {max_frames_spacing}",
275
+ raise_with=ValueError,
276
+ )
233
277
  buffer, ext = encode_media_frames(img_)
234
278
  if len(buffer) > 1:
235
- step = len(buffer) // max_frames_spacing # max frames spacing
279
+ step = len(buffer) // max_frames_spacing # max frames spacing
236
280
  frames = []
237
281
  indices = list(range(0, len(buffer), step))[:max_used_frames]
238
282
  for i in indices:
@@ -241,20 +285,25 @@ class GPTXChatEngine(Engine, OpenAIMixin):
241
285
  elif len(buffer) == 1:
242
286
  image_files.append(f"data:image/{ext};base64,{buffer[0]}")
243
287
  else:
244
- UserMessage('No frames found or error in encoding frames')
288
+ UserMessage("No frames found or error in encoding frames")
245
289
  return image_files
246
290
 
247
291
  def _remove_vision_pattern(self, text: str) -> str:
248
292
  """Remove vision patterns from text."""
249
- pattern = r'<<vision:(.*?):>>'
250
- return re.sub(pattern, '', text)
293
+ pattern = r"<<vision:(.*?):>>"
294
+ return re.sub(pattern, "", text)
251
295
 
252
- def truncate(self, prompts: list[dict], truncation_percentage: float | None, truncation_type: str) -> list[dict]:
296
+ def truncate(
297
+ self, prompts: list[dict], truncation_percentage: float | None, truncation_type: str
298
+ ) -> list[dict]:
253
299
  """Main truncation method"""
300
+
254
301
  def _slice_tokens(tokens, new_len, truncation_type):
255
302
  """Slice tokens based on truncation type"""
256
303
  new_len = max(100, new_len) # Ensure minimum token length
257
- return tokens[-new_len:] if truncation_type == 'head' else tokens[:new_len] # else 'tail'
304
+ return (
305
+ tokens[-new_len:] if truncation_type == "head" else tokens[:new_len]
306
+ ) # else 'tail'
258
307
 
259
308
  if self._should_skip_truncation(prompts):
260
309
  return prompts
@@ -262,7 +311,7 @@ class GPTXChatEngine(Engine, OpenAIMixin):
262
311
  truncation_percentage = self._resolve_truncation_percentage(truncation_percentage)
263
312
  system_prompt = prompts[0]
264
313
  user_prompt = prompts[1]
265
- system_tokens = Symbol(system_prompt['content']).tokens
314
+ system_tokens = Symbol(system_prompt["content"]).tokens
266
315
  user_tokens, fallback = self._collect_user_tokens(user_prompt, prompts)
267
316
  if fallback is not None:
268
317
  return fallback
@@ -282,7 +331,7 @@ class GPTXChatEngine(Engine, OpenAIMixin):
282
331
  UserMessage(
283
332
  f"Executing {truncation_type} truncation to fit within {max_prompt_tokens} tokens. "
284
333
  f"Combined prompts ({total_tokens} tokens) exceed maximum allowed tokens "
285
- f"of {max_prompt_tokens} ({truncation_percentage*100:.1f}% of context). "
334
+ f"of {max_prompt_tokens} ({truncation_percentage * 100:.1f}% of context). "
286
335
  f"You can control this behavior by setting 'truncation_percentage' (current: {truncation_percentage:.2f}) "
287
336
  f"and 'truncation_type' (current: '{truncation_type}') parameters. "
288
337
  f"Set 'truncation_percentage=1.0' to deactivate truncation (will fail if exceeding context window). "
@@ -301,7 +350,9 @@ class GPTXChatEngine(Engine, OpenAIMixin):
301
350
  return self._decode_prompt_pair(new_system_tokens, user_tokens)
302
351
 
303
352
  # Case 3: Both exceed - reduce proportionally
304
- new_system_len, new_user_len = self._compute_proportional_lengths(system_token_count, user_token_count, total_tokens, max_prompt_tokens)
353
+ new_system_len, new_user_len = self._compute_proportional_lengths(
354
+ system_token_count, user_token_count, total_tokens, max_prompt_tokens
355
+ )
305
356
  new_system_tokens = _slice_tokens(system_tokens, new_system_len, truncation_type)
306
357
  new_user_tokens = _slice_tokens(user_tokens, new_user_len, truncation_type)
307
358
 
@@ -309,66 +360,78 @@ class GPTXChatEngine(Engine, OpenAIMixin):
309
360
 
310
361
  def forward(self, argument):
311
362
  kwargs = argument.kwargs
312
- truncation_percentage = kwargs.get('truncation_percentage', argument.prop.truncation_percentage)
313
- truncation_type = kwargs.get('truncation_type', argument.prop.truncation_type)
314
- messages = self.truncate(argument.prop.prepared_input, truncation_percentage, truncation_type)
363
+ truncation_percentage = kwargs.get(
364
+ "truncation_percentage", argument.prop.truncation_percentage
365
+ )
366
+ truncation_type = kwargs.get("truncation_type", argument.prop.truncation_type)
367
+ messages = self.truncate(
368
+ argument.prop.prepared_input, truncation_percentage, truncation_type
369
+ )
315
370
  payload = self._prepare_request_payload(messages, argument)
316
- except_remedy = kwargs.get('except_remedy')
371
+ except_remedy = kwargs.get("except_remedy")
317
372
 
318
373
  try:
319
374
  res = self.client.chat.completions.create(**payload)
320
375
 
321
376
  except Exception as e:
322
- if openai.api_key is None or openai.api_key == '':
323
- msg = 'OpenAI API key is not set. Please set it in the config file or pass it as an argument to the command method.'
377
+ if openai.api_key is None or openai.api_key == "":
378
+ msg = "OpenAI API key is not set. Please set it in the config file or pass it as an argument to the command method."
324
379
  UserMessage(msg)
325
- if self.config['NEUROSYMBOLIC_ENGINE_API_KEY'] is None or self.config['NEUROSYMBOLIC_ENGINE_API_KEY'] == '':
380
+ if (
381
+ self.config["NEUROSYMBOLIC_ENGINE_API_KEY"] is None
382
+ or self.config["NEUROSYMBOLIC_ENGINE_API_KEY"] == ""
383
+ ):
326
384
  UserMessage(msg, raise_with=ValueError)
327
- openai.api_key = self.config['NEUROSYMBOLIC_ENGINE_API_KEY']
385
+ openai.api_key = self.config["NEUROSYMBOLIC_ENGINE_API_KEY"]
328
386
 
329
387
  callback = self.client.chat.completions.create
330
- kwargs['model'] = kwargs.get('model', self.model)
388
+ kwargs["model"] = kwargs.get("model", self.model)
331
389
 
332
390
  if except_remedy is not None:
333
391
  res = except_remedy(self, e, callback, argument)
334
392
  else:
335
- UserMessage(f'Error during generation. Caused by: {e}', raise_with=ValueError)
393
+ UserMessage(f"Error during generation. Caused by: {e}", raise_with=ValueError)
336
394
 
337
- metadata = {'raw_output': res}
338
- if payload.get('tools'):
395
+ metadata = {"raw_output": res}
396
+ if payload.get("tools"):
339
397
  metadata = self._process_function_calls(res, metadata)
340
398
  output = [r.message.content for r in res.choices]
341
399
 
342
- #@TODO: Normalize the output across engines to result something like Result object
400
+ # @TODO: Normalize the output across engines to result something like Result object
343
401
  # I like the Rust Ok Result object, there's something similar in Python
344
402
  # (https://github.com/rustedpy/result)
345
403
  return output, metadata
346
404
 
347
405
  def _prepare_raw_input(self, argument):
348
406
  if not argument.prop.processed_input:
349
- UserMessage('Need to provide a prompt instruction to the engine if raw_input is enabled.', raise_with=ValueError)
407
+ UserMessage(
408
+ "Need to provide a prompt instruction to the engine if raw_input is enabled.",
409
+ raise_with=ValueError,
410
+ )
350
411
  value = argument.prop.processed_input
351
412
  # convert to dict if not already
352
413
  if not isinstance(value, list):
353
414
  if not isinstance(value, dict):
354
- value = {'role': 'user', 'content': str(value)}
415
+ value = {"role": "user", "content": str(value)}
355
416
  value = [value]
356
417
  return value
357
418
 
358
419
  def _build_non_verbose_prefix(self, argument) -> list[str]:
359
420
  if not argument.prop.suppress_verbose_output:
360
421
  return []
361
- prefix = f'{self._NON_VERBOSE_OUTPUT}\n'
422
+ prefix = f"{self._NON_VERBOSE_OUTPUT}\n"
362
423
  return [prefix]
363
424
 
364
425
  def _response_format_section(self, argument) -> list[str]:
365
426
  if not argument.prop.response_format:
366
427
  return []
367
428
  _rsp_fmt = argument.prop.response_format
368
- assert _rsp_fmt.get('type') is not None, 'Expected format `{ "type": "json_object" }`! See https://platform.openai.com/docs/api-reference/chat/create#chat-create-response_format'
429
+ assert _rsp_fmt.get("type") is not None, (
430
+ 'Expected format `{ "type": "json_object" }`! See https://platform.openai.com/docs/api-reference/chat/create#chat-create-response_format'
431
+ )
369
432
  if _rsp_fmt["type"] != "json_object":
370
433
  return []
371
- return ['<RESPONSE_FORMAT/>\nYou are a helpful assistant designed to output JSON.\n\n']
434
+ return ["<RESPONSE_FORMAT/>\nYou are a helpful assistant designed to output JSON.\n\n"]
372
435
 
373
436
  def _context_sections(self, argument) -> list[str]:
374
437
  sections: list[str] = []
@@ -403,7 +466,9 @@ class GPTXChatEngine(Engine, OpenAIMixin):
403
466
  def _template_suffix_section(self, argument) -> list[str]:
404
467
  if not argument.prop.template_suffix:
405
468
  return []
406
- return [f' You will only generate content for the placeholder `{argument.prop.template_suffix!s}` following the instructions and the provided context information.\n\n']
469
+ return [
470
+ f" You will only generate content for the placeholder `{argument.prop.template_suffix!s}` following the instructions and the provided context information.\n\n"
471
+ ]
407
472
 
408
473
  def _build_system_message(self, argument, image_files: list[str]) -> str:
409
474
  sections: list[str] = []
@@ -424,22 +489,26 @@ class GPTXChatEngine(Engine, OpenAIMixin):
424
489
 
425
490
  def _create_user_prompt(self, user_text: str, image_files: list[str]) -> dict:
426
491
  if self.model == self._VISION_PREVIEW_MODEL:
427
- images = [{'type': 'image', "image_url": {"url": file}} for file in image_files]
428
- return {"role": "user", "content": [*images, {'type': 'text', 'text': user_text}]}
492
+ images = [{"type": "image", "image_url": {"url": file}} for file in image_files]
493
+ return {"role": "user", "content": [*images, {"type": "text", "text": user_text}]}
429
494
  if self.model in self._VISION_IMAGE_URL_MODELS:
430
- images = [{'type': 'image_url', "image_url": {"url": file}} for file in image_files]
431
- return {"role": "user", "content": [*images, {'type': 'text', 'text': user_text}]}
495
+ images = [{"type": "image_url", "image_url": {"url": file}} for file in image_files]
496
+ return {"role": "user", "content": [*images, {"type": "text", "text": user_text}]}
432
497
  return {"role": "user", "content": user_text}
433
498
 
434
- def _apply_self_prompt_if_needed(self, argument, system: str, user_prompt: dict, user_text: str, image_files: list[str]) -> tuple[str, dict]:
435
- if not (argument.prop.instance._kwargs.get('self_prompt', False) or argument.prop.self_prompt):
499
+ def _apply_self_prompt_if_needed(
500
+ self, argument, system: str, user_prompt: dict, user_text: str, image_files: list[str]
501
+ ) -> tuple[str, dict]:
502
+ if not (
503
+ argument.prop.instance._kwargs.get("self_prompt", False) or argument.prop.self_prompt
504
+ ):
436
505
  return system, user_prompt
437
506
  self_prompter = SelfPrompt()
438
- res = self_prompter({'user': user_text, 'system': system})
507
+ res = self_prompter({"user": user_text, "system": system})
439
508
  if res is None:
440
509
  UserMessage("Self-prompting failed!", raise_with=ValueError)
441
- new_user_prompt = self._create_user_prompt(res['user'], image_files)
442
- return res['system'], new_user_prompt
510
+ new_user_prompt = self._create_user_prompt(res["user"], image_files)
511
+ return res["system"], new_user_prompt
443
512
 
444
513
  def prepare(self, argument):
445
514
  if argument.prop.raw_input:
@@ -450,35 +519,39 @@ class GPTXChatEngine(Engine, OpenAIMixin):
450
519
  system = self._build_system_message(argument, image_files)
451
520
  user_text = self._build_user_text(argument, image_files)
452
521
  user_prompt = self._create_user_prompt(user_text, image_files)
453
- system, user_prompt = self._apply_self_prompt_if_needed(argument, system, user_prompt, user_text, image_files)
522
+ system, user_prompt = self._apply_self_prompt_if_needed(
523
+ argument, system, user_prompt, user_text, image_files
524
+ )
454
525
 
455
526
  argument.prop.prepared_input = [
456
- { "role": "system", "content": system },
527
+ {"role": "system", "content": system},
457
528
  user_prompt,
458
529
  ]
459
530
 
460
531
  def _process_function_calls(self, res, metadata):
461
532
  hit = False
462
533
  if (
463
- hasattr(res, 'choices')
534
+ hasattr(res, "choices")
464
535
  and res.choices
465
- and hasattr(res.choices[0], 'message')
536
+ and hasattr(res.choices[0], "message")
466
537
  and res.choices[0].message
467
- and hasattr(res.choices[0].message, 'tool_calls')
538
+ and hasattr(res.choices[0].message, "tool_calls")
468
539
  and res.choices[0].message.tool_calls
469
540
  ):
470
541
  for tool_call in res.choices[0].message.tool_calls:
471
- if hasattr(tool_call, 'function') and tool_call.function:
542
+ if hasattr(tool_call, "function") and tool_call.function:
472
543
  if hit:
473
- UserMessage("Multiple function calls detected in the response but only the first one will be processed.")
544
+ UserMessage(
545
+ "Multiple function calls detected in the response but only the first one will be processed."
546
+ )
474
547
  break
475
548
  try:
476
549
  args_dict = json.loads(tool_call.function.arguments)
477
550
  except json.JSONDecodeError:
478
551
  args_dict = {}
479
- metadata['function_call'] = {
480
- 'name': tool_call.function.name,
481
- 'arguments': args_dict
552
+ metadata["function_call"] = {
553
+ "name": tool_call.function.name,
554
+ "arguments": args_dict,
482
555
  }
483
556
  hit = True
484
557
  return metadata
@@ -487,8 +560,8 @@ class GPTXChatEngine(Engine, OpenAIMixin):
487
560
  """Prepares the request payload from the argument."""
488
561
  kwargs = argument.kwargs
489
562
 
490
- max_tokens = kwargs.get('max_tokens', None)
491
- max_completion_tokens = kwargs.get('max_completion_tokens', None)
563
+ max_tokens = kwargs.get("max_tokens", None)
564
+ max_completion_tokens = kwargs.get("max_completion_tokens", None)
492
565
  remaining_tokens = self.compute_remaining_tokens(messages)
493
566
 
494
567
  if max_tokens is not None:
@@ -502,39 +575,47 @@ class GPTXChatEngine(Engine, OpenAIMixin):
502
575
  f"Provided 'max_tokens' ({max_tokens}) exceeds max response tokens ({self.max_response_tokens}). "
503
576
  f"Truncating to {remaining_tokens} to avoid API failure."
504
577
  )
505
- kwargs['max_completion_tokens'] = remaining_tokens
578
+ kwargs["max_completion_tokens"] = remaining_tokens
506
579
  else:
507
- kwargs['max_completion_tokens'] = max_tokens
508
- del kwargs['max_tokens']
580
+ kwargs["max_completion_tokens"] = max_tokens
581
+ del kwargs["max_tokens"]
509
582
 
510
583
  if max_completion_tokens is not None and max_completion_tokens > self.max_response_tokens:
511
584
  UserMessage(
512
585
  f"Provided 'max_completion_tokens' ({max_completion_tokens}) exceeds max response tokens ({self.max_response_tokens}). "
513
586
  f"Truncating to {remaining_tokens} to avoid API failure."
514
587
  )
515
- kwargs['max_completion_tokens'] = remaining_tokens
588
+ kwargs["max_completion_tokens"] = remaining_tokens
516
589
 
517
590
  payload = {
518
591
  "messages": messages,
519
- "model": kwargs.get('model', self.model),
520
- "seed": kwargs.get('seed', self.seed),
521
- "max_completion_tokens": kwargs.get('max_completion_tokens'),
522
- "stop": kwargs.get('stop', ''),
523
- "temperature": kwargs.get('temperature', 1),
524
- "frequency_penalty": kwargs.get('frequency_penalty', 0),
525
- "presence_penalty": kwargs.get('presence_penalty', 0),
526
- "top_p": kwargs.get('top_p', 1),
527
- "n": kwargs.get('n', 1),
528
- "logit_bias": kwargs.get('logit_bias'),
529
- "tools": kwargs.get('tools'),
530
- "tool_choice": kwargs.get('tool_choice'),
531
- "response_format": kwargs.get('response_format'),
532
- "logprobs": kwargs.get('logprobs'),
533
- "top_logprobs": kwargs.get('top_logprobs'),
592
+ "model": kwargs.get("model", self.model),
593
+ "seed": kwargs.get("seed", self.seed),
594
+ "max_completion_tokens": kwargs.get("max_completion_tokens"),
595
+ "stop": kwargs.get("stop", ""),
596
+ "temperature": kwargs.get("temperature", 1),
597
+ "frequency_penalty": kwargs.get("frequency_penalty", 0),
598
+ "presence_penalty": kwargs.get("presence_penalty", 0),
599
+ "top_p": kwargs.get("top_p", 1),
600
+ "n": kwargs.get("n", 1),
601
+ "logit_bias": kwargs.get("logit_bias"),
602
+ "tools": kwargs.get("tools"),
603
+ "tool_choice": kwargs.get("tool_choice"),
604
+ "response_format": kwargs.get("response_format"),
605
+ "logprobs": kwargs.get("logprobs"),
606
+ "top_logprobs": kwargs.get("top_logprobs"),
534
607
  }
535
608
 
536
- if self.model == "chatgpt-4o-latest" or self.model == "gpt-5-chat-latest":
537
- del payload['tools']
538
- del payload['tool_choice']
609
+ if (
610
+ self.model == "chatgpt-4o-latest"
611
+ or self.model == "gpt-5-chat-latest"
612
+ or self.model == "gpt-5.1-chat-latest"
613
+ ):
614
+ del payload["tools"]
615
+ del payload["tool_choice"]
616
+ if (
617
+ self.model == "gpt-5.1-chat-latest"
618
+ ): # requires same behavior as for reasoning models
619
+ del payload["stop"]
539
620
 
540
621
  return payload