symbolicai 0.20.2__py3-none-any.whl → 1.0.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 +96 -64
- symai/backend/base.py +93 -80
- symai/backend/engines/drawing/engine_bfl.py +12 -11
- symai/backend/engines/drawing/engine_gpt_image.py +108 -87
- symai/backend/engines/embedding/engine_llama_cpp.py +25 -28
- symai/backend/engines/embedding/engine_openai.py +3 -5
- symai/backend/engines/execute/engine_python.py +6 -5
- symai/backend/engines/files/engine_io.py +74 -67
- symai/backend/engines/imagecaptioning/engine_blip2.py +3 -3
- symai/backend/engines/imagecaptioning/engine_llavacpp_client.py +54 -38
- symai/backend/engines/index/engine_pinecone.py +23 -24
- symai/backend/engines/index/engine_vectordb.py +16 -14
- symai/backend/engines/lean/engine_lean4.py +38 -34
- symai/backend/engines/neurosymbolic/__init__.py +41 -13
- symai/backend/engines/neurosymbolic/engine_anthropic_claudeX_chat.py +262 -182
- symai/backend/engines/neurosymbolic/engine_anthropic_claudeX_reasoning.py +263 -191
- symai/backend/engines/neurosymbolic/engine_deepseekX_reasoning.py +53 -49
- symai/backend/engines/neurosymbolic/engine_google_geminiX_reasoning.py +212 -211
- symai/backend/engines/neurosymbolic/engine_groq.py +87 -63
- symai/backend/engines/neurosymbolic/engine_huggingface.py +21 -24
- symai/backend/engines/neurosymbolic/engine_llama_cpp.py +117 -48
- symai/backend/engines/neurosymbolic/engine_openai_gptX_chat.py +256 -229
- symai/backend/engines/neurosymbolic/engine_openai_gptX_reasoning.py +270 -150
- symai/backend/engines/ocr/engine_apilayer.py +6 -8
- symai/backend/engines/output/engine_stdout.py +1 -4
- symai/backend/engines/search/engine_openai.py +7 -7
- symai/backend/engines/search/engine_perplexity.py +5 -5
- symai/backend/engines/search/engine_serpapi.py +12 -14
- symai/backend/engines/speech_to_text/engine_local_whisper.py +20 -27
- symai/backend/engines/symbolic/engine_wolframalpha.py +3 -3
- symai/backend/engines/text_to_speech/engine_openai.py +5 -7
- symai/backend/engines/text_vision/engine_clip.py +7 -11
- symai/backend/engines/userinput/engine_console.py +3 -3
- symai/backend/engines/webscraping/engine_requests.py +81 -48
- symai/backend/mixin/__init__.py +13 -0
- symai/backend/mixin/anthropic.py +4 -2
- symai/backend/mixin/deepseek.py +2 -0
- symai/backend/mixin/google.py +2 -0
- symai/backend/mixin/openai.py +11 -3
- symai/backend/settings.py +83 -16
- symai/chat.py +101 -78
- symai/collect/__init__.py +7 -1
- symai/collect/dynamic.py +77 -69
- symai/collect/pipeline.py +35 -27
- symai/collect/stats.py +75 -63
- symai/components.py +198 -169
- symai/constraints.py +15 -12
- symai/core.py +698 -359
- symai/core_ext.py +32 -34
- symai/endpoints/api.py +80 -73
- symai/extended/.DS_Store +0 -0
- symai/extended/__init__.py +46 -12
- symai/extended/api_builder.py +11 -8
- symai/extended/arxiv_pdf_parser.py +13 -12
- symai/extended/bibtex_parser.py +2 -3
- symai/extended/conversation.py +101 -90
- symai/extended/document.py +17 -10
- symai/extended/file_merger.py +18 -13
- symai/extended/graph.py +18 -13
- symai/extended/html_style_template.py +2 -4
- symai/extended/interfaces/blip_2.py +1 -2
- symai/extended/interfaces/clip.py +1 -2
- symai/extended/interfaces/console.py +7 -1
- symai/extended/interfaces/dall_e.py +1 -1
- symai/extended/interfaces/flux.py +1 -1
- symai/extended/interfaces/gpt_image.py +1 -1
- symai/extended/interfaces/input.py +1 -1
- symai/extended/interfaces/llava.py +0 -1
- symai/extended/interfaces/naive_vectordb.py +7 -8
- symai/extended/interfaces/naive_webscraping.py +1 -1
- symai/extended/interfaces/ocr.py +1 -1
- symai/extended/interfaces/pinecone.py +6 -5
- symai/extended/interfaces/serpapi.py +1 -1
- symai/extended/interfaces/terminal.py +2 -3
- symai/extended/interfaces/tts.py +1 -1
- symai/extended/interfaces/whisper.py +1 -1
- symai/extended/interfaces/wolframalpha.py +1 -1
- symai/extended/metrics/__init__.py +11 -1
- symai/extended/metrics/similarity.py +11 -13
- symai/extended/os_command.py +17 -16
- symai/extended/packages/__init__.py +29 -3
- symai/extended/packages/symdev.py +19 -16
- symai/extended/packages/sympkg.py +12 -9
- symai/extended/packages/symrun.py +21 -19
- symai/extended/repo_cloner.py +11 -10
- symai/extended/seo_query_optimizer.py +1 -2
- symai/extended/solver.py +20 -23
- symai/extended/summarizer.py +4 -3
- symai/extended/taypan_interpreter.py +10 -12
- symai/extended/vectordb.py +99 -82
- symai/formatter/__init__.py +9 -1
- symai/formatter/formatter.py +12 -16
- symai/formatter/regex.py +62 -63
- symai/functional.py +176 -122
- symai/imports.py +136 -127
- symai/interfaces.py +56 -27
- symai/memory.py +14 -13
- symai/misc/console.py +49 -39
- symai/misc/loader.py +5 -3
- symai/models/__init__.py +17 -1
- symai/models/base.py +269 -181
- symai/models/errors.py +0 -1
- symai/ops/__init__.py +32 -22
- symai/ops/measures.py +11 -15
- symai/ops/primitives.py +348 -228
- symai/post_processors.py +32 -28
- symai/pre_processors.py +39 -41
- symai/processor.py +6 -4
- symai/prompts.py +59 -45
- symai/server/huggingface_server.py +23 -20
- symai/server/llama_cpp_server.py +7 -5
- symai/shell.py +3 -4
- symai/shellsv.py +499 -375
- symai/strategy.py +517 -287
- symai/symbol.py +111 -116
- symai/utils.py +42 -36
- {symbolicai-0.20.2.dist-info → symbolicai-1.0.0.dist-info}/METADATA +4 -2
- symbolicai-1.0.0.dist-info/RECORD +163 -0
- symbolicai-0.20.2.dist-info/RECORD +0 -162
- {symbolicai-0.20.2.dist-info → symbolicai-1.0.0.dist-info}/WHEEL +0 -0
- {symbolicai-0.20.2.dist-info → symbolicai-1.0.0.dist-info}/entry_points.txt +0 -0
- {symbolicai-0.20.2.dist-info → symbolicai-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {symbolicai-0.20.2.dist-info → symbolicai-1.0.0.dist-info}/top_level.txt +0 -0
|
@@ -2,15 +2,14 @@ import json
|
|
|
2
2
|
import logging
|
|
3
3
|
import re
|
|
4
4
|
from copy import deepcopy
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import ClassVar
|
|
6
6
|
|
|
7
7
|
import openai
|
|
8
8
|
import tiktoken
|
|
9
9
|
|
|
10
10
|
from ....components import SelfPrompt
|
|
11
|
-
from ....misc.console import ConsoleStyle
|
|
12
11
|
from ....symbol import Symbol
|
|
13
|
-
from ....utils import
|
|
12
|
+
from ....utils import UserMessage, encode_media_frames
|
|
14
13
|
from ...base import Engine
|
|
15
14
|
from ...mixin.openai import OpenAIMixin
|
|
16
15
|
from ...settings import SYMAI_CONFIG
|
|
@@ -23,7 +22,39 @@ logging.getLogger("httpcore").setLevel(logging.ERROR)
|
|
|
23
22
|
|
|
24
23
|
|
|
25
24
|
class GPTXChatEngine(Engine, OpenAIMixin):
|
|
26
|
-
|
|
25
|
+
_THREE_TOKEN_MODELS: ClassVar[set[str]] = {
|
|
26
|
+
"gpt-3.5-turbo-0613",
|
|
27
|
+
"gpt-3.5-turbo-16k-0613",
|
|
28
|
+
"gpt-4-1106-preview",
|
|
29
|
+
"gpt-4-0314",
|
|
30
|
+
"gpt-4-32k-0314",
|
|
31
|
+
"gpt-4-0613",
|
|
32
|
+
"gpt-4-32k-0613",
|
|
33
|
+
"gpt-4-turbo",
|
|
34
|
+
"gpt-4o",
|
|
35
|
+
"gpt-4o-2024-11-20",
|
|
36
|
+
"gpt-4o-mini",
|
|
37
|
+
"chatgpt-4o-latest",
|
|
38
|
+
"gpt-4.1",
|
|
39
|
+
"gpt-4.1-mini",
|
|
40
|
+
"gpt-4.1-nano",
|
|
41
|
+
"gpt-5-chat-latest",
|
|
42
|
+
}
|
|
43
|
+
_VISION_PREVIEW_MODEL = "gpt-4-vision-preview"
|
|
44
|
+
_VISION_IMAGE_URL_MODELS: ClassVar[set[str]] = {
|
|
45
|
+
"gpt-4-turbo-2024-04-09",
|
|
46
|
+
"gpt-4-turbo",
|
|
47
|
+
"gpt-4o",
|
|
48
|
+
"gpt-4o-mini",
|
|
49
|
+
"chatgpt-4o-latest",
|
|
50
|
+
"gpt-4.1",
|
|
51
|
+
"gpt-4.1-mini",
|
|
52
|
+
"gpt-4.1-nano",
|
|
53
|
+
"gpt-5-chat-latest",
|
|
54
|
+
}
|
|
55
|
+
_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
|
+
|
|
57
|
+
def __init__(self, api_key: str | None = None, model: str | None = None):
|
|
27
58
|
super().__init__()
|
|
28
59
|
self.config = deepcopy(SYMAI_CONFIG)
|
|
29
60
|
# In case we use EngineRepository.register to inject the api_key and model => dynamically change the engine at runtime
|
|
@@ -36,7 +67,7 @@ class GPTXChatEngine(Engine, OpenAIMixin):
|
|
|
36
67
|
self.model = self.config['NEUROSYMBOLIC_ENGINE_MODEL']
|
|
37
68
|
try:
|
|
38
69
|
self.tokenizer = tiktoken.encoding_for_model(self.model)
|
|
39
|
-
except Exception
|
|
70
|
+
except Exception:
|
|
40
71
|
self.tokenizer = tiktoken.get_encoding('o200k_base')
|
|
41
72
|
self.max_context_tokens = self.api_max_context_tokens()
|
|
42
73
|
self.max_response_tokens = self.api_max_response_tokens()
|
|
@@ -46,7 +77,7 @@ class GPTXChatEngine(Engine, OpenAIMixin):
|
|
|
46
77
|
try:
|
|
47
78
|
self.client = openai.Client(api_key=openai.api_key)
|
|
48
79
|
except Exception as e:
|
|
49
|
-
|
|
80
|
+
UserMessage(f'Failed to initialize OpenAI client. Please check your OpenAI library version. Caused by: {e}', raise_with=ValueError)
|
|
50
81
|
|
|
51
82
|
def id(self) -> str:
|
|
52
83
|
if self.config.get('NEUROSYMBOLIC_ENGINE_MODEL') and \
|
|
@@ -67,58 +98,43 @@ class GPTXChatEngine(Engine, OpenAIMixin):
|
|
|
67
98
|
if 'seed' in kwargs:
|
|
68
99
|
self.seed = kwargs['seed']
|
|
69
100
|
|
|
70
|
-
def
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if self.model
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
"gpt-
|
|
77
|
-
"gpt-4-0314",
|
|
78
|
-
"gpt-4-32k-0314",
|
|
79
|
-
"gpt-4-0613",
|
|
80
|
-
"gpt-4-32k-0613",
|
|
81
|
-
"gpt-4-turbo",
|
|
82
|
-
"gpt-4o",
|
|
83
|
-
"gpt-4o-2024-11-20",
|
|
84
|
-
"gpt-4o-mini",
|
|
85
|
-
"chatgpt-4o-latest",
|
|
86
|
-
"gpt-4.1",
|
|
87
|
-
"gpt-4.1-mini",
|
|
88
|
-
"gpt-4.1-nano",
|
|
89
|
-
"gpt-5-chat-latest"
|
|
90
|
-
}:
|
|
91
|
-
tokens_per_message = 3
|
|
92
|
-
tokens_per_name = 1
|
|
93
|
-
elif self.model == "gpt-3.5-turbo-0301":
|
|
94
|
-
tokens_per_message = 4 # every message follows <|start|>{role/name}\n{content}<|end|>\n
|
|
95
|
-
tokens_per_name = -1 # if there's a name, the role is omitted
|
|
96
|
-
elif self.model == "gpt-3.5-turbo":
|
|
97
|
-
CustomUserWarning("Warning: gpt-3.5-turbo may update over time. Returning num tokens assuming gpt-3.5-turbo-0613.")
|
|
98
|
-
tokens_per_message = 3
|
|
99
|
-
tokens_per_name = 1
|
|
101
|
+
def _resolve_token_config(self) -> tuple[int, int]:
|
|
102
|
+
if self.model in self._THREE_TOKEN_MODELS:
|
|
103
|
+
return 3, 1
|
|
104
|
+
if self.model == "gpt-3.5-turbo-0301":
|
|
105
|
+
return 4, -1
|
|
106
|
+
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.")
|
|
100
108
|
self.tokenizer = tiktoken.encoding_for_model("gpt-3.5-turbo-0613")
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
CustomUserWarning("Warning: gpt-4 may update over time. Returning num tokens assuming gpt-4-0613.")
|
|
109
|
+
return 3, 1
|
|
110
|
+
if self.model == "gpt-4":
|
|
111
|
+
UserMessage("Warning: gpt-4 may update over time. Returning num tokens assuming gpt-4-0613.")
|
|
105
112
|
self.tokenizer = tiktoken.encoding_for_model("gpt-4-0613")
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
113
|
+
return 3, 1
|
|
114
|
+
UserMessage(
|
|
115
|
+
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
|
|
117
|
+
)
|
|
118
|
+
raise NotImplementedError
|
|
119
|
+
|
|
120
|
+
def _count_tokens_in_value(self, value) -> int:
|
|
121
|
+
if isinstance(value, str):
|
|
122
|
+
return len(self.tokenizer.encode(value, disallowed_special=()))
|
|
123
|
+
tokens = 0
|
|
124
|
+
for item in value:
|
|
125
|
+
if item['type'] == 'text':
|
|
126
|
+
tokens += len(self.tokenizer.encode(item['text'], disallowed_special=()))
|
|
127
|
+
return tokens
|
|
128
|
+
|
|
129
|
+
def compute_required_tokens(self, messages):
|
|
130
|
+
"""Return the number of tokens used by a list of messages."""
|
|
111
131
|
|
|
132
|
+
tokens_per_message, tokens_per_name = self._resolve_token_config()
|
|
112
133
|
num_tokens = 0
|
|
113
134
|
for message in messages:
|
|
114
135
|
num_tokens += tokens_per_message
|
|
115
136
|
for key, value in message.items():
|
|
116
|
-
|
|
117
|
-
num_tokens += len(self.tokenizer.encode(value, disallowed_special=()))
|
|
118
|
-
else:
|
|
119
|
-
for v in value:
|
|
120
|
-
if v['type'] == 'text':
|
|
121
|
-
num_tokens += len(self.tokenizer.encode(v['text'], disallowed_special=()))
|
|
137
|
+
num_tokens += self._count_tokens_in_value(value)
|
|
122
138
|
if key == "name":
|
|
123
139
|
num_tokens += tokens_per_name
|
|
124
140
|
num_tokens += 3 # every reply is primed with <|start|>assistant<|message|>
|
|
@@ -128,6 +144,58 @@ class GPTXChatEngine(Engine, OpenAIMixin):
|
|
|
128
144
|
val = self.compute_required_tokens(prompts)
|
|
129
145
|
return min(self.max_context_tokens - val, self.max_response_tokens)
|
|
130
146
|
|
|
147
|
+
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.")
|
|
150
|
+
return True
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
def _resolve_truncation_percentage(self, truncation_percentage: float | None) -> float:
|
|
154
|
+
if truncation_percentage is not None:
|
|
155
|
+
return truncation_percentage
|
|
156
|
+
return (self.max_context_tokens - self.max_response_tokens) / self.max_context_tokens
|
|
157
|
+
|
|
158
|
+
def _collect_user_tokens(self, user_prompt: dict, prompts: list[dict]) -> tuple[list, object | None]:
|
|
159
|
+
user_tokens: list = []
|
|
160
|
+
content = user_prompt['content']
|
|
161
|
+
if isinstance(content, str):
|
|
162
|
+
user_tokens.extend(Symbol(content).tokens)
|
|
163
|
+
return user_tokens, None
|
|
164
|
+
if isinstance(content, list):
|
|
165
|
+
for content_item in content:
|
|
166
|
+
if isinstance(content_item, dict):
|
|
167
|
+
if content_item.get('type') == 'text':
|
|
168
|
+
user_tokens.extend(Symbol(content_item['text']).tokens)
|
|
169
|
+
else:
|
|
170
|
+
return [], prompts
|
|
171
|
+
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")
|
|
173
|
+
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)
|
|
175
|
+
return user_tokens, None
|
|
176
|
+
|
|
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
|
|
179
|
+
|
|
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
|
|
182
|
+
|
|
183
|
+
def _compute_proportional_lengths(self, system_token_count: int, user_token_count: int, total_tokens: int, max_prompt_tokens: int) -> tuple[int, int]:
|
|
184
|
+
system_ratio = system_token_count / total_tokens
|
|
185
|
+
user_ratio = user_token_count / total_tokens
|
|
186
|
+
new_system_len = int(max_prompt_tokens * system_ratio)
|
|
187
|
+
new_user_len = int(max_prompt_tokens * user_ratio)
|
|
188
|
+
distribute_tokens = max_prompt_tokens - new_system_len - new_user_len
|
|
189
|
+
new_system_len += distribute_tokens // 2
|
|
190
|
+
new_user_len += distribute_tokens // 2
|
|
191
|
+
return new_system_len, new_user_len
|
|
192
|
+
|
|
193
|
+
def _decode_prompt_pair(self, system_tokens, user_tokens) -> list[dict]:
|
|
194
|
+
return [
|
|
195
|
+
{'role': 'system', 'content': self.tokenizer.decode(system_tokens)},
|
|
196
|
+
{'role': 'user', 'content': [{'type': 'text', 'text': self.tokenizer.decode(user_tokens)}]}
|
|
197
|
+
]
|
|
198
|
+
|
|
131
199
|
def _handle_image_content(self, content: str) -> list:
|
|
132
200
|
"""Handle image content by processing vision patterns and returning image file data."""
|
|
133
201
|
def extract_pattern(text):
|
|
@@ -151,9 +219,7 @@ class GPTXChatEngine(Engine, OpenAIMixin):
|
|
|
151
219
|
parts = extract_pattern(content)
|
|
152
220
|
for p in parts:
|
|
153
221
|
img_ = p.strip()
|
|
154
|
-
if img_.startswith('http'):
|
|
155
|
-
image_files.append(img_)
|
|
156
|
-
elif img_.startswith('data:image'):
|
|
222
|
+
if img_.startswith('http') or img_.startswith('data:image'):
|
|
157
223
|
image_files.append(img_)
|
|
158
224
|
else:
|
|
159
225
|
max_frames_spacing = 50
|
|
@@ -163,7 +229,7 @@ class GPTXChatEngine(Engine, OpenAIMixin):
|
|
|
163
229
|
max_used_frames, img_ = img_.split(':')
|
|
164
230
|
max_used_frames = int(max_used_frames)
|
|
165
231
|
if max_used_frames < 1 or max_used_frames > max_frames_spacing:
|
|
166
|
-
|
|
232
|
+
UserMessage(f"Invalid max_used_frames value: {max_used_frames}. Expected value between 1 and {max_frames_spacing}", raise_with=ValueError)
|
|
167
233
|
buffer, ext = encode_media_frames(img_)
|
|
168
234
|
if len(buffer) > 1:
|
|
169
235
|
step = len(buffer) // max_frames_spacing # max frames spacing
|
|
@@ -175,7 +241,7 @@ class GPTXChatEngine(Engine, OpenAIMixin):
|
|
|
175
241
|
elif len(buffer) == 1:
|
|
176
242
|
image_files.append(f"data:image/{ext};base64,{buffer[0]}")
|
|
177
243
|
else:
|
|
178
|
-
|
|
244
|
+
UserMessage('No frames found or error in encoding frames')
|
|
179
245
|
return image_files
|
|
180
246
|
|
|
181
247
|
def _remove_vision_pattern(self, text: str) -> str:
|
|
@@ -190,41 +256,16 @@ class GPTXChatEngine(Engine, OpenAIMixin):
|
|
|
190
256
|
new_len = max(100, new_len) # Ensure minimum token length
|
|
191
257
|
return tokens[-new_len:] if truncation_type == 'head' else tokens[:new_len] # else 'tail'
|
|
192
258
|
|
|
193
|
-
if
|
|
194
|
-
# Only support system and user prompts
|
|
195
|
-
CustomUserWarning(f"Token truncation currently supports only two messages, from 'user' and 'system' (got {len(prompts)}). Returning original prompts.")
|
|
259
|
+
if self._should_skip_truncation(prompts):
|
|
196
260
|
return prompts
|
|
197
261
|
|
|
198
|
-
|
|
199
|
-
# Calculate smart truncation percentage based on model's max messages and completion tokens
|
|
200
|
-
truncation_percentage = (self.max_context_tokens - self.max_response_tokens) / self.max_context_tokens
|
|
201
|
-
|
|
262
|
+
truncation_percentage = self._resolve_truncation_percentage(truncation_percentage)
|
|
202
263
|
system_prompt = prompts[0]
|
|
203
264
|
user_prompt = prompts[1]
|
|
204
|
-
|
|
205
|
-
# Get token counts
|
|
206
265
|
system_tokens = Symbol(system_prompt['content']).tokens
|
|
207
|
-
user_tokens =
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
# Default input format
|
|
211
|
-
user_tokens.extend(Symbol(user_prompt['content']).tokens)
|
|
212
|
-
elif isinstance(user_prompt['content'], list):
|
|
213
|
-
for content_item in user_prompt['content']:
|
|
214
|
-
# Image input format
|
|
215
|
-
if isinstance(content_item, dict):
|
|
216
|
-
if content_item.get('type') == 'text':
|
|
217
|
-
user_tokens.extend(Symbol(content_item['text']).tokens)
|
|
218
|
-
else:
|
|
219
|
-
# Image content; return original since not supported
|
|
220
|
-
return prompts
|
|
221
|
-
else:
|
|
222
|
-
# Unknown input format
|
|
223
|
-
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")
|
|
224
|
-
else:
|
|
225
|
-
# Unknown input format
|
|
226
|
-
CustomUserWarning(f"Unknown content type: {type(user_prompt['content'])}. Format input according to the documentation. See https://platform.openai.com/docs/api-reference/chat/create?lang=python", raise_with=ValueError)
|
|
227
|
-
|
|
266
|
+
user_tokens, fallback = self._collect_user_tokens(user_prompt, prompts)
|
|
267
|
+
if fallback is not None:
|
|
268
|
+
return fallback
|
|
228
269
|
system_token_count = len(system_tokens)
|
|
229
270
|
user_token_count = len(user_tokens)
|
|
230
271
|
artifacts = self.compute_required_tokens(prompts) - (system_token_count + user_token_count)
|
|
@@ -238,7 +279,7 @@ class GPTXChatEngine(Engine, OpenAIMixin):
|
|
|
238
279
|
if total_tokens <= max_prompt_tokens:
|
|
239
280
|
return prompts
|
|
240
281
|
|
|
241
|
-
|
|
282
|
+
UserMessage(
|
|
242
283
|
f"Executing {truncation_type} truncation to fit within {max_prompt_tokens} tokens. "
|
|
243
284
|
f"Combined prompts ({total_tokens} tokens) exceed maximum allowed tokens "
|
|
244
285
|
f"of {max_prompt_tokens} ({truncation_percentage*100:.1f}% of context). "
|
|
@@ -248,40 +289,23 @@ class GPTXChatEngine(Engine, OpenAIMixin):
|
|
|
248
289
|
f"Choose 'truncation_type' as 'head' to keep the end of prompts or 'tail' to keep the beginning."
|
|
249
290
|
)
|
|
250
291
|
# Case 1: Only user prompt exceeds
|
|
251
|
-
if user_token_count
|
|
292
|
+
if self._user_only_exceeds(user_token_count, system_token_count, max_prompt_tokens):
|
|
252
293
|
new_user_len = max_prompt_tokens - system_token_count
|
|
253
294
|
new_user_tokens = _slice_tokens(user_tokens, new_user_len, truncation_type)
|
|
254
|
-
return
|
|
255
|
-
{'role': 'system', 'content': self.tokenizer.decode(system_tokens)},
|
|
256
|
-
{'role': 'user', 'content': [{'type': 'text', 'text': self.tokenizer.decode(new_user_tokens)}]}
|
|
257
|
-
]
|
|
295
|
+
return self._decode_prompt_pair(system_tokens, new_user_tokens)
|
|
258
296
|
|
|
259
297
|
# Case 2: Only system prompt exceeds
|
|
260
|
-
if system_token_count
|
|
298
|
+
if self._system_only_exceeds(system_token_count, user_token_count, max_prompt_tokens):
|
|
261
299
|
new_system_len = max_prompt_tokens - user_token_count
|
|
262
300
|
new_system_tokens = _slice_tokens(system_tokens, new_system_len, truncation_type)
|
|
263
|
-
return
|
|
264
|
-
{'role': 'system', 'content': self.tokenizer.decode(new_system_tokens)},
|
|
265
|
-
{'role': 'user', 'content': [{'type': 'text', 'text': self.tokenizer.decode(user_tokens)}]}
|
|
266
|
-
]
|
|
301
|
+
return self._decode_prompt_pair(new_system_tokens, user_tokens)
|
|
267
302
|
|
|
268
303
|
# Case 3: Both exceed - reduce proportionally
|
|
269
|
-
|
|
270
|
-
user_ratio = user_token_count / total_tokens
|
|
271
|
-
|
|
272
|
-
new_system_len = int(max_prompt_tokens * system_ratio)
|
|
273
|
-
new_user_len = int(max_prompt_tokens * user_ratio)
|
|
274
|
-
distribute_tokens = max_prompt_tokens - new_system_len - new_user_len
|
|
275
|
-
new_system_len += distribute_tokens // 2
|
|
276
|
-
new_user_len += distribute_tokens // 2
|
|
277
|
-
|
|
304
|
+
new_system_len, new_user_len = self._compute_proportional_lengths(system_token_count, user_token_count, total_tokens, max_prompt_tokens)
|
|
278
305
|
new_system_tokens = _slice_tokens(system_tokens, new_system_len, truncation_type)
|
|
279
306
|
new_user_tokens = _slice_tokens(user_tokens, new_user_len, truncation_type)
|
|
280
307
|
|
|
281
|
-
return
|
|
282
|
-
{'role': 'system', 'content': self.tokenizer.decode(new_system_tokens)},
|
|
283
|
-
{'role': 'user', 'content': [{'type': 'text', 'text': self.tokenizer.decode(new_user_tokens)}]}
|
|
284
|
-
]
|
|
308
|
+
return self._decode_prompt_pair(new_system_tokens, new_user_tokens)
|
|
285
309
|
|
|
286
310
|
def forward(self, argument):
|
|
287
311
|
kwargs = argument.kwargs
|
|
@@ -297,18 +321,18 @@ class GPTXChatEngine(Engine, OpenAIMixin):
|
|
|
297
321
|
except Exception as e:
|
|
298
322
|
if openai.api_key is None or openai.api_key == '':
|
|
299
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.'
|
|
300
|
-
|
|
324
|
+
UserMessage(msg)
|
|
301
325
|
if self.config['NEUROSYMBOLIC_ENGINE_API_KEY'] is None or self.config['NEUROSYMBOLIC_ENGINE_API_KEY'] == '':
|
|
302
|
-
|
|
326
|
+
UserMessage(msg, raise_with=ValueError)
|
|
303
327
|
openai.api_key = self.config['NEUROSYMBOLIC_ENGINE_API_KEY']
|
|
304
328
|
|
|
305
329
|
callback = self.client.chat.completions.create
|
|
306
|
-
kwargs['model'] = kwargs
|
|
330
|
+
kwargs['model'] = kwargs.get('model', self.model)
|
|
307
331
|
|
|
308
332
|
if except_remedy is not None:
|
|
309
333
|
res = except_remedy(self, e, callback, argument)
|
|
310
334
|
else:
|
|
311
|
-
|
|
335
|
+
UserMessage(f'Error during generation. Caused by: {e}', raise_with=ValueError)
|
|
312
336
|
|
|
313
337
|
metadata = {'raw_output': res}
|
|
314
338
|
if payload.get('tools'):
|
|
@@ -322,111 +346,111 @@ class GPTXChatEngine(Engine, OpenAIMixin):
|
|
|
322
346
|
|
|
323
347
|
def _prepare_raw_input(self, argument):
|
|
324
348
|
if not argument.prop.processed_input:
|
|
325
|
-
|
|
349
|
+
UserMessage('Need to provide a prompt instruction to the engine if raw_input is enabled.', raise_with=ValueError)
|
|
326
350
|
value = argument.prop.processed_input
|
|
327
351
|
# convert to dict if not already
|
|
328
|
-
if
|
|
329
|
-
if
|
|
352
|
+
if not isinstance(value, list):
|
|
353
|
+
if not isinstance(value, dict):
|
|
330
354
|
value = {'role': 'user', 'content': str(value)}
|
|
331
355
|
value = [value]
|
|
332
356
|
return value
|
|
333
357
|
|
|
334
|
-
def
|
|
335
|
-
if argument.prop.
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
# OpenAI docs:
|
|
352
|
-
# "Important: when using JSON mode, you must also instruct the model
|
|
353
|
-
# to produce JSON yourself via a system or user message"
|
|
354
|
-
system += f'<RESPONSE_FORMAT/>\nYou are a helpful assistant designed to output JSON.\n\n'
|
|
355
|
-
|
|
358
|
+
def _build_non_verbose_prefix(self, argument) -> list[str]:
|
|
359
|
+
if not argument.prop.suppress_verbose_output:
|
|
360
|
+
return []
|
|
361
|
+
prefix = f'{self._NON_VERBOSE_OUTPUT}\n'
|
|
362
|
+
return [prefix]
|
|
363
|
+
|
|
364
|
+
def _response_format_section(self, argument) -> list[str]:
|
|
365
|
+
if not argument.prop.response_format:
|
|
366
|
+
return []
|
|
367
|
+
_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'
|
|
369
|
+
if _rsp_fmt["type"] != "json_object":
|
|
370
|
+
return []
|
|
371
|
+
return ['<RESPONSE_FORMAT/>\nYou are a helpful assistant designed to output JSON.\n\n']
|
|
372
|
+
|
|
373
|
+
def _context_sections(self, argument) -> list[str]:
|
|
374
|
+
sections: list[str] = []
|
|
356
375
|
ref = argument.prop.instance
|
|
357
376
|
static_ctxt, dyn_ctxt = ref.global_context
|
|
358
377
|
if len(static_ctxt) > 0:
|
|
359
|
-
|
|
360
|
-
|
|
378
|
+
sections.append(f"<STATIC CONTEXT/>\n{static_ctxt}\n\n")
|
|
361
379
|
if len(dyn_ctxt) > 0:
|
|
362
|
-
|
|
380
|
+
sections.append(f"<DYNAMIC CONTEXT/>\n{dyn_ctxt}\n\n")
|
|
381
|
+
return sections
|
|
363
382
|
|
|
383
|
+
def _payload_section(self, argument) -> list[str]:
|
|
384
|
+
if not argument.prop.payload:
|
|
385
|
+
return []
|
|
364
386
|
payload = argument.prop.payload
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
examples:
|
|
369
|
-
if examples and len(examples) > 0:
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
if argument.prop.prompt is
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
387
|
+
return [f"<ADDITIONAL CONTEXT/>\n{payload!s}\n\n"]
|
|
388
|
+
|
|
389
|
+
def _examples_section(self, argument) -> list[str]:
|
|
390
|
+
examples: list[str] = argument.prop.examples
|
|
391
|
+
if not (examples and len(examples) > 0):
|
|
392
|
+
return []
|
|
393
|
+
return [f"<EXAMPLES/>\n{examples!s}\n\n"]
|
|
394
|
+
|
|
395
|
+
def _instruction_section(self, argument, image_files: list[str]) -> list[str]:
|
|
396
|
+
if argument.prop.prompt is None or len(argument.prop.prompt) == 0:
|
|
397
|
+
return []
|
|
398
|
+
val = str(argument.prop.prompt)
|
|
399
|
+
if len(image_files) > 0:
|
|
400
|
+
val = self._remove_vision_pattern(val)
|
|
401
|
+
return [f"<INSTRUCTION/>\n{val}\n\n"]
|
|
402
|
+
|
|
403
|
+
def _template_suffix_section(self, argument) -> list[str]:
|
|
404
|
+
if not argument.prop.template_suffix:
|
|
405
|
+
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']
|
|
407
|
+
|
|
408
|
+
def _build_system_message(self, argument, image_files: list[str]) -> str:
|
|
409
|
+
sections: list[str] = []
|
|
410
|
+
sections.extend(self._build_non_verbose_prefix(argument))
|
|
411
|
+
sections.extend(self._response_format_section(argument))
|
|
412
|
+
sections.extend(self._context_sections(argument))
|
|
413
|
+
sections.extend(self._payload_section(argument))
|
|
414
|
+
sections.extend(self._examples_section(argument))
|
|
415
|
+
sections.extend(self._instruction_section(argument, image_files))
|
|
416
|
+
sections.extend(self._template_suffix_section(argument))
|
|
417
|
+
return "".join(sections)
|
|
418
|
+
|
|
419
|
+
def _build_user_text(self, argument, image_files: list[str]) -> str:
|
|
380
420
|
suffix: str = str(argument.prop.processed_input)
|
|
381
421
|
if len(image_files) > 0:
|
|
382
422
|
suffix = self._remove_vision_pattern(suffix)
|
|
423
|
+
return f"{suffix}"
|
|
424
|
+
|
|
425
|
+
def _create_user_prompt(self, user_text: str, image_files: list[str]) -> dict:
|
|
426
|
+
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}]}
|
|
429
|
+
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}]}
|
|
432
|
+
return {"role": "user", "content": user_text}
|
|
433
|
+
|
|
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):
|
|
436
|
+
return system, user_prompt
|
|
437
|
+
self_prompter = SelfPrompt()
|
|
438
|
+
res = self_prompter({'user': user_text, 'system': system})
|
|
439
|
+
if res is None:
|
|
440
|
+
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
|
|
383
443
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
if self.model == 'gpt-4-vision-preview':
|
|
390
|
-
images = [{ 'type': 'image', "image_url": { "url": file }} for file in image_files]
|
|
391
|
-
user_prompt = { "role": "user", "content": [
|
|
392
|
-
*images,
|
|
393
|
-
{ 'type': 'text', 'text': user }
|
|
394
|
-
]}
|
|
395
|
-
elif self.model == 'gpt-4-turbo-2024-04-09' or \
|
|
396
|
-
self.model == 'gpt-4-turbo' or \
|
|
397
|
-
self.model == 'gpt-4o' or \
|
|
398
|
-
self.model == 'gpt-4o-mini' or \
|
|
399
|
-
self.model == 'chatgpt-4o-latest' or \
|
|
400
|
-
self.model == 'gpt-4.1' or \
|
|
401
|
-
self.model == 'gpt-4.1-mini' or \
|
|
402
|
-
self.model == 'gpt-4.1-nano' or \
|
|
403
|
-
self.model == 'gpt-5-chat-latest':
|
|
404
|
-
|
|
405
|
-
images = [{ 'type': 'image_url', "image_url": { "url": file }} for file in image_files]
|
|
406
|
-
user_prompt = { "role": "user", "content": [
|
|
407
|
-
*images,
|
|
408
|
-
{ 'type': 'text', 'text': user }
|
|
409
|
-
]}
|
|
410
|
-
else:
|
|
411
|
-
user_prompt = { "role": "user", "content": user }
|
|
412
|
-
|
|
413
|
-
# First check if the `Symbol` instance has the flag set, otherwise check if it was passed as an argument to a method
|
|
414
|
-
if argument.prop.instance._kwargs.get('self_prompt', False) or argument.prop.self_prompt:
|
|
415
|
-
self_prompter = SelfPrompt()
|
|
416
|
-
|
|
417
|
-
res = self_prompter({'user': user, 'system': system})
|
|
418
|
-
if res is None:
|
|
419
|
-
CustomUserWarning("Self-prompting failed!", raise_with=ValueError)
|
|
420
|
-
|
|
421
|
-
if len(image_files) > 0:
|
|
422
|
-
user_prompt = { "role": "user", "content": [
|
|
423
|
-
*images,
|
|
424
|
-
{ 'type': 'text', 'text': res['user'] }
|
|
425
|
-
]}
|
|
426
|
-
else:
|
|
427
|
-
user_prompt = { "role": "user", "content": res['user'] }
|
|
444
|
+
def prepare(self, argument):
|
|
445
|
+
if argument.prop.raw_input:
|
|
446
|
+
argument.prop.prepared_input = self._prepare_raw_input(argument)
|
|
447
|
+
return
|
|
428
448
|
|
|
429
|
-
|
|
449
|
+
image_files = self._handle_image_content(str(argument.prop.processed_input))
|
|
450
|
+
system = self._build_system_message(argument, image_files)
|
|
451
|
+
user_text = self._build_user_text(argument, image_files)
|
|
452
|
+
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)
|
|
430
454
|
|
|
431
455
|
argument.prop.prepared_input = [
|
|
432
456
|
{ "role": "system", "content": system },
|
|
@@ -435,24 +459,28 @@ class GPTXChatEngine(Engine, OpenAIMixin):
|
|
|
435
459
|
|
|
436
460
|
def _process_function_calls(self, res, metadata):
|
|
437
461
|
hit = False
|
|
438
|
-
if
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
462
|
+
if (
|
|
463
|
+
hasattr(res, 'choices')
|
|
464
|
+
and res.choices
|
|
465
|
+
and hasattr(res.choices[0], 'message')
|
|
466
|
+
and res.choices[0].message
|
|
467
|
+
and hasattr(res.choices[0].message, 'tool_calls')
|
|
468
|
+
and res.choices[0].message.tool_calls
|
|
469
|
+
):
|
|
470
|
+
for tool_call in res.choices[0].message.tool_calls:
|
|
471
|
+
if hasattr(tool_call, 'function') and tool_call.function:
|
|
472
|
+
if hit:
|
|
473
|
+
UserMessage("Multiple function calls detected in the response but only the first one will be processed.")
|
|
474
|
+
break
|
|
475
|
+
try:
|
|
476
|
+
args_dict = json.loads(tool_call.function.arguments)
|
|
477
|
+
except json.JSONDecodeError:
|
|
478
|
+
args_dict = {}
|
|
479
|
+
metadata['function_call'] = {
|
|
480
|
+
'name': tool_call.function.name,
|
|
481
|
+
'arguments': args_dict
|
|
482
|
+
}
|
|
483
|
+
hit = True
|
|
456
484
|
return metadata
|
|
457
485
|
|
|
458
486
|
def _prepare_request_payload(self, messages, argument):
|
|
@@ -464,13 +492,13 @@ class GPTXChatEngine(Engine, OpenAIMixin):
|
|
|
464
492
|
remaining_tokens = self.compute_remaining_tokens(messages)
|
|
465
493
|
|
|
466
494
|
if max_tokens is not None:
|
|
467
|
-
|
|
495
|
+
UserMessage(
|
|
468
496
|
"'max_tokens' is now deprecated in favor of 'max_completion_tokens', and is not compatible with o1 series models. "
|
|
469
497
|
"We handle this conversion by default for you for now but we won't in the future. "
|
|
470
498
|
"See: https://platform.openai.com/docs/api-reference/chat/create"
|
|
471
499
|
)
|
|
472
500
|
if max_tokens > self.max_response_tokens:
|
|
473
|
-
|
|
501
|
+
UserMessage(
|
|
474
502
|
f"Provided 'max_tokens' ({max_tokens}) exceeds max response tokens ({self.max_response_tokens}). "
|
|
475
503
|
f"Truncating to {remaining_tokens} to avoid API failure."
|
|
476
504
|
)
|
|
@@ -479,13 +507,12 @@ class GPTXChatEngine(Engine, OpenAIMixin):
|
|
|
479
507
|
kwargs['max_completion_tokens'] = max_tokens
|
|
480
508
|
del kwargs['max_tokens']
|
|
481
509
|
|
|
482
|
-
if max_completion_tokens is not None:
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
kwargs['max_completion_tokens'] = remaining_tokens
|
|
510
|
+
if max_completion_tokens is not None and max_completion_tokens > self.max_response_tokens:
|
|
511
|
+
UserMessage(
|
|
512
|
+
f"Provided 'max_completion_tokens' ({max_completion_tokens}) exceeds max response tokens ({self.max_response_tokens}). "
|
|
513
|
+
f"Truncating to {remaining_tokens} to avoid API failure."
|
|
514
|
+
)
|
|
515
|
+
kwargs['max_completion_tokens'] = remaining_tokens
|
|
489
516
|
|
|
490
517
|
payload = {
|
|
491
518
|
"messages": messages,
|