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.
- 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 +35 -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/__init__.py +4 -0
- symai/backend/mixin/anthropic.py +48 -40
- symai/backend/mixin/cerebras.py +9 -0
- 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 +578 -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.1.dist-info}/METADATA +19 -9
- symbolicai-1.1.1.dist-info/RECORD +169 -0
- symbolicai-1.0.0.dist-info/RECORD +0 -163
- {symbolicai-1.0.0.dist-info → symbolicai-1.1.1.dist-info}/WHEEL +0 -0
- {symbolicai-1.0.0.dist-info → symbolicai-1.1.1.dist-info}/entry_points.txt +0 -0
- {symbolicai-1.0.0.dist-info → symbolicai-1.1.1.dist-info}/licenses/LICENSE +0 -0
- {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[
|
|
63
|
-
self.config[
|
|
64
|
-
if self.id() !=
|
|
65
|
-
return
|
|
66
|
-
openai.api_key = self.config[
|
|
67
|
-
self.model = self.config[
|
|
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(
|
|
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(
|
|
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(
|
|
84
|
-
|
|
85
|
-
self.config.get(
|
|
86
|
-
self.config.get(
|
|
87
|
-
self.config.get(
|
|
88
|
-
self.config.get(
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
95
|
-
openai.api_key = kwargs[
|
|
96
|
-
if
|
|
97
|
-
self.model = kwargs[
|
|
98
|
-
if
|
|
99
|
-
self.seed = kwargs[
|
|
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(
|
|
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(
|
|
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[
|
|
126
|
-
tokens += len(self.tokenizer.encode(item[
|
|
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
|
|
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[
|
|
149
|
-
UserMessage(
|
|
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(
|
|
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[
|
|
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(
|
|
168
|
-
user_tokens.extend(Symbol(content_item[
|
|
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(
|
|
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(
|
|
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(
|
|
178
|
-
|
|
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(
|
|
181
|
-
|
|
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(
|
|
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
|
-
{
|
|
196
|
-
{
|
|
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
|
|
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 (
|
|
208
|
-
self.model ==
|
|
209
|
-
self.model ==
|
|
210
|
-
self.model ==
|
|
211
|
-
self.model ==
|
|
212
|
-
self.model ==
|
|
213
|
-
self.model ==
|
|
214
|
-
self.model ==
|
|
215
|
-
self.model ==
|
|
216
|
-
self.model ==
|
|
217
|
-
|
|
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(
|
|
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(
|
|
228
|
-
img_ = img_.replace(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
250
|
-
return re.sub(pattern,
|
|
293
|
+
pattern = r"<<vision:(.*?):>>"
|
|
294
|
+
return re.sub(pattern, "", text)
|
|
251
295
|
|
|
252
|
-
def truncate(
|
|
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
|
|
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[
|
|
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(
|
|
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(
|
|
313
|
-
|
|
314
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
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[
|
|
385
|
+
openai.api_key = self.config["NEUROSYMBOLIC_ENGINE_API_KEY"]
|
|
328
386
|
|
|
329
387
|
callback = self.client.chat.completions.create
|
|
330
|
-
kwargs[
|
|
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
|
|
393
|
+
UserMessage(f"Error during generation. Caused by: {e}", raise_with=ValueError)
|
|
336
394
|
|
|
337
|
-
metadata = {
|
|
338
|
-
if payload.get(
|
|
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
|
-
|
|
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(
|
|
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 = {
|
|
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
|
|
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(
|
|
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 [
|
|
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 [
|
|
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 = [{
|
|
428
|
-
return {"role": "user", "content": [*images, {
|
|
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 = [{
|
|
431
|
-
return {"role": "user", "content": [*images, {
|
|
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(
|
|
435
|
-
|
|
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({
|
|
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[
|
|
442
|
-
return res[
|
|
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(
|
|
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
|
-
{
|
|
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,
|
|
534
|
+
hasattr(res, "choices")
|
|
464
535
|
and res.choices
|
|
465
|
-
and hasattr(res.choices[0],
|
|
536
|
+
and hasattr(res.choices[0], "message")
|
|
466
537
|
and res.choices[0].message
|
|
467
|
-
and hasattr(res.choices[0].message,
|
|
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,
|
|
542
|
+
if hasattr(tool_call, "function") and tool_call.function:
|
|
472
543
|
if hit:
|
|
473
|
-
UserMessage(
|
|
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[
|
|
480
|
-
|
|
481
|
-
|
|
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(
|
|
491
|
-
max_completion_tokens = kwargs.get(
|
|
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[
|
|
578
|
+
kwargs["max_completion_tokens"] = remaining_tokens
|
|
506
579
|
else:
|
|
507
|
-
kwargs[
|
|
508
|
-
del kwargs[
|
|
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[
|
|
588
|
+
kwargs["max_completion_tokens"] = remaining_tokens
|
|
516
589
|
|
|
517
590
|
payload = {
|
|
518
591
|
"messages": messages,
|
|
519
|
-
"model": kwargs.get(
|
|
520
|
-
"seed": kwargs.get(
|
|
521
|
-
"max_completion_tokens": kwargs.get(
|
|
522
|
-
"stop": kwargs.get(
|
|
523
|
-
"temperature": kwargs.get(
|
|
524
|
-
"frequency_penalty": kwargs.get(
|
|
525
|
-
"presence_penalty": kwargs.get(
|
|
526
|
-
"top_p": kwargs.get(
|
|
527
|
-
"n": kwargs.get(
|
|
528
|
-
"logit_bias": kwargs.get(
|
|
529
|
-
"tools": kwargs.get(
|
|
530
|
-
"tool_choice": kwargs.get(
|
|
531
|
-
"response_format": kwargs.get(
|
|
532
|
-
"logprobs": kwargs.get(
|
|
533
|
-
"top_logprobs": kwargs.get(
|
|
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
|
|
537
|
-
|
|
538
|
-
|
|
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
|