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.
- symai/__init__.py +198 -134
- symai/backend/base.py +51 -51
- symai/backend/engines/drawing/engine_bfl.py +33 -33
- symai/backend/engines/drawing/engine_gpt_image.py +4 -10
- symai/backend/engines/embedding/engine_llama_cpp.py +50 -35
- symai/backend/engines/embedding/engine_openai.py +22 -16
- symai/backend/engines/execute/engine_python.py +16 -16
- symai/backend/engines/files/engine_io.py +51 -49
- symai/backend/engines/imagecaptioning/engine_blip2.py +27 -23
- symai/backend/engines/imagecaptioning/engine_llavacpp_client.py +53 -46
- symai/backend/engines/index/engine_pinecone.py +116 -88
- symai/backend/engines/index/engine_qdrant.py +1011 -0
- symai/backend/engines/index/engine_vectordb.py +78 -52
- symai/backend/engines/lean/engine_lean4.py +65 -25
- symai/backend/engines/neurosymbolic/__init__.py +28 -28
- symai/backend/engines/neurosymbolic/engine_anthropic_claudeX_chat.py +137 -135
- symai/backend/engines/neurosymbolic/engine_anthropic_claudeX_reasoning.py +145 -152
- symai/backend/engines/neurosymbolic/engine_cerebras.py +328 -0
- symai/backend/engines/neurosymbolic/engine_deepseekX_reasoning.py +75 -49
- symai/backend/engines/neurosymbolic/engine_google_geminiX_reasoning.py +199 -155
- symai/backend/engines/neurosymbolic/engine_groq.py +106 -72
- symai/backend/engines/neurosymbolic/engine_huggingface.py +100 -67
- symai/backend/engines/neurosymbolic/engine_llama_cpp.py +121 -93
- symai/backend/engines/neurosymbolic/engine_openai_gptX_chat.py +213 -132
- symai/backend/engines/neurosymbolic/engine_openai_gptX_reasoning.py +180 -137
- symai/backend/engines/ocr/engine_apilayer.py +18 -20
- symai/backend/engines/output/engine_stdout.py +9 -9
- symai/backend/engines/{webscraping → scrape}/engine_requests.py +25 -11
- symai/backend/engines/search/engine_openai.py +95 -83
- symai/backend/engines/search/engine_parallel.py +665 -0
- symai/backend/engines/search/engine_perplexity.py +40 -41
- symai/backend/engines/search/engine_serpapi.py +33 -28
- symai/backend/engines/speech_to_text/engine_local_whisper.py +37 -27
- symai/backend/engines/symbolic/engine_wolframalpha.py +14 -8
- symai/backend/engines/text_to_speech/engine_openai.py +15 -19
- symai/backend/engines/text_vision/engine_clip.py +34 -28
- symai/backend/engines/userinput/engine_console.py +3 -4
- symai/backend/mixin/anthropic.py +48 -40
- symai/backend/mixin/deepseek.py +4 -5
- symai/backend/mixin/google.py +5 -4
- symai/backend/mixin/groq.py +2 -4
- symai/backend/mixin/openai.py +132 -110
- symai/backend/settings.py +14 -14
- symai/chat.py +164 -94
- symai/collect/dynamic.py +13 -11
- symai/collect/pipeline.py +39 -31
- symai/collect/stats.py +109 -69
- symai/components.py +556 -238
- symai/constraints.py +14 -5
- symai/core.py +1495 -1210
- symai/core_ext.py +55 -50
- symai/endpoints/api.py +113 -58
- symai/extended/api_builder.py +22 -17
- symai/extended/arxiv_pdf_parser.py +13 -5
- symai/extended/bibtex_parser.py +8 -4
- symai/extended/conversation.py +88 -69
- symai/extended/document.py +40 -27
- symai/extended/file_merger.py +45 -7
- symai/extended/graph.py +38 -24
- symai/extended/html_style_template.py +17 -11
- symai/extended/interfaces/blip_2.py +1 -1
- symai/extended/interfaces/clip.py +4 -2
- symai/extended/interfaces/console.py +5 -3
- symai/extended/interfaces/dall_e.py +3 -1
- symai/extended/interfaces/file.py +2 -0
- symai/extended/interfaces/flux.py +3 -1
- symai/extended/interfaces/gpt_image.py +15 -6
- symai/extended/interfaces/input.py +2 -1
- symai/extended/interfaces/llava.py +1 -1
- symai/extended/interfaces/{naive_webscraping.py → naive_scrape.py} +3 -2
- symai/extended/interfaces/naive_vectordb.py +2 -2
- symai/extended/interfaces/ocr.py +4 -2
- symai/extended/interfaces/openai_search.py +2 -0
- symai/extended/interfaces/parallel.py +30 -0
- symai/extended/interfaces/perplexity.py +2 -0
- symai/extended/interfaces/pinecone.py +6 -4
- symai/extended/interfaces/python.py +2 -0
- symai/extended/interfaces/serpapi.py +2 -0
- symai/extended/interfaces/terminal.py +0 -1
- symai/extended/interfaces/tts.py +2 -1
- symai/extended/interfaces/whisper.py +2 -1
- symai/extended/interfaces/wolframalpha.py +1 -0
- symai/extended/metrics/__init__.py +1 -1
- symai/extended/metrics/similarity.py +5 -2
- symai/extended/os_command.py +31 -22
- symai/extended/packages/symdev.py +39 -34
- symai/extended/packages/sympkg.py +30 -27
- symai/extended/packages/symrun.py +46 -35
- symai/extended/repo_cloner.py +10 -9
- symai/extended/seo_query_optimizer.py +15 -12
- symai/extended/solver.py +104 -76
- symai/extended/summarizer.py +8 -7
- symai/extended/taypan_interpreter.py +10 -9
- symai/extended/vectordb.py +28 -15
- symai/formatter/formatter.py +39 -31
- symai/formatter/regex.py +46 -44
- symai/functional.py +184 -86
- symai/imports.py +85 -51
- symai/interfaces.py +1 -1
- symai/memory.py +33 -24
- symai/menu/screen.py +28 -19
- symai/misc/console.py +27 -27
- symai/misc/loader.py +4 -3
- symai/models/base.py +147 -76
- symai/models/errors.py +1 -1
- symai/ops/__init__.py +1 -1
- symai/ops/measures.py +17 -14
- symai/ops/primitives.py +933 -635
- symai/post_processors.py +28 -24
- symai/pre_processors.py +58 -52
- symai/processor.py +15 -9
- symai/prompts.py +714 -649
- symai/server/huggingface_server.py +115 -32
- symai/server/llama_cpp_server.py +14 -6
- symai/server/qdrant_server.py +206 -0
- symai/shell.py +98 -39
- symai/shellsv.py +307 -223
- symai/strategy.py +135 -81
- symai/symbol.py +276 -225
- symai/utils.py +62 -46
- {symbolicai-1.0.0.dist-info → symbolicai-1.1.0.dist-info}/METADATA +19 -9
- symbolicai-1.1.0.dist-info/RECORD +168 -0
- symbolicai-1.0.0.dist-info/RECORD +0 -163
- {symbolicai-1.0.0.dist-info → symbolicai-1.1.0.dist-info}/WHEEL +0 -0
- {symbolicai-1.0.0.dist-info → symbolicai-1.1.0.dist-info}/entry_points.txt +0 -0
- {symbolicai-1.0.0.dist-info → symbolicai-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {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[
|
|
30
|
-
self.config[
|
|
31
|
-
if self.id() !=
|
|
32
|
-
return
|
|
33
|
-
openai.api_key = self.config[
|
|
34
|
-
self.model = self.config[
|
|
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(
|
|
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(
|
|
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(
|
|
51
|
-
|
|
52
|
-
self.config.get(
|
|
53
|
-
self.config.get(
|
|
54
|
-
self.config.get(
|
|
55
|
-
self.config.get(
|
|
56
|
-
self.config.get(
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
63
|
-
openai.api_key = kwargs[
|
|
64
|
-
if
|
|
65
|
-
self.model = kwargs[
|
|
66
|
-
if
|
|
67
|
-
self.seed = kwargs[
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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[
|
|
99
|
-
num_tokens += len(
|
|
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
|
|
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
|
|
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 (
|
|
118
|
-
self.model ==
|
|
119
|
-
self.model ==
|
|
120
|
-
self.model ==
|
|
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(
|
|
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(
|
|
130
|
-
img_ = img_.replace(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
152
|
-
return re.sub(pattern,
|
|
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 ==
|
|
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[
|
|
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[
|
|
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(
|
|
183
|
-
user_tokens.extend(Symbol(content_item[
|
|
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
|
-
{
|
|
213
|
-
{
|
|
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
|
-
{
|
|
220
|
-
{
|
|
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(
|
|
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[
|
|
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
|
-
{
|
|
293
|
-
{
|
|
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(
|
|
299
|
-
|
|
300
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
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[
|
|
346
|
+
openai.api_key = self.config["NEUROSYMBOLIC_ENGINE_API_KEY"]
|
|
314
347
|
|
|
315
348
|
callback = self.client.chat.completions.create
|
|
316
|
-
kwargs[
|
|
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
|
|
354
|
+
UserMessage(f"Error during generation. Caused by: {e}", raise_with=ValueError)
|
|
322
355
|
|
|
323
|
-
metadata = {
|
|
324
|
-
if payload.get(
|
|
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(
|
|
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 = {
|
|
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
|
-
"
|
|
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(
|
|
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
|
|
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
|
|
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 +=
|
|
446
|
+
developer += "".join(part for part in parts if part)
|
|
409
447
|
|
|
410
448
|
if argument.prop.template_suffix:
|
|
411
449
|
developer += (
|
|
412
|
-
f
|
|
413
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
{
|
|
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(
|
|
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({
|
|
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 =
|
|
467
|
-
|
|
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
|
-
{
|
|
514
|
+
{"type": "text", "text": res["user"]},
|
|
474
515
|
],
|
|
475
516
|
}
|
|
476
517
|
else:
|
|
477
|
-
user_prompt = {"role": "user", "content": res[
|
|
518
|
+
user_prompt = {"role": "user", "content": res["user"]}
|
|
478
519
|
|
|
479
|
-
return user_prompt, res[
|
|
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
|
-
{
|
|
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,
|
|
549
|
+
hasattr(res, "choices")
|
|
509
550
|
and res.choices
|
|
510
|
-
and hasattr(res.choices[0],
|
|
551
|
+
and hasattr(res.choices[0], "message")
|
|
511
552
|
and res.choices[0].message
|
|
512
|
-
and hasattr(res.choices[0].message,
|
|
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(
|
|
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,
|
|
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[
|
|
525
|
-
|
|
526
|
-
|
|
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(
|
|
536
|
-
max_completion_tokens = kwargs.get(
|
|
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[
|
|
593
|
+
kwargs["max_completion_tokens"] = remaining_tokens
|
|
551
594
|
else:
|
|
552
|
-
kwargs[
|
|
553
|
-
del kwargs[
|
|
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[
|
|
603
|
+
kwargs["max_completion_tokens"] = remaining_tokens
|
|
561
604
|
|
|
562
605
|
payload = {
|
|
563
606
|
"messages": messages,
|
|
564
|
-
"model": kwargs.get(
|
|
565
|
-
"seed": kwargs.get(
|
|
566
|
-
"reasoning_effort": kwargs.get(
|
|
567
|
-
"max_completion_tokens": kwargs.get(
|
|
568
|
-
"stop": kwargs.get(
|
|
569
|
-
"temperature": kwargs.get(
|
|
570
|
-
"frequency_penalty": kwargs.get(
|
|
571
|
-
"presence_penalty": kwargs.get(
|
|
572
|
-
"top_p": kwargs.get(
|
|
573
|
-
"n": kwargs.get(
|
|
574
|
-
"logit_bias": kwargs.get(
|
|
575
|
-
"tools": kwargs.get(
|
|
576
|
-
"tool_choice": kwargs.get(
|
|
577
|
-
"response_format": kwargs.get(
|
|
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":
|