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.
Files changed (123) hide show
  1. symai/__init__.py +96 -64
  2. symai/backend/base.py +93 -80
  3. symai/backend/engines/drawing/engine_bfl.py +12 -11
  4. symai/backend/engines/drawing/engine_gpt_image.py +108 -87
  5. symai/backend/engines/embedding/engine_llama_cpp.py +25 -28
  6. symai/backend/engines/embedding/engine_openai.py +3 -5
  7. symai/backend/engines/execute/engine_python.py +6 -5
  8. symai/backend/engines/files/engine_io.py +74 -67
  9. symai/backend/engines/imagecaptioning/engine_blip2.py +3 -3
  10. symai/backend/engines/imagecaptioning/engine_llavacpp_client.py +54 -38
  11. symai/backend/engines/index/engine_pinecone.py +23 -24
  12. symai/backend/engines/index/engine_vectordb.py +16 -14
  13. symai/backend/engines/lean/engine_lean4.py +38 -34
  14. symai/backend/engines/neurosymbolic/__init__.py +41 -13
  15. symai/backend/engines/neurosymbolic/engine_anthropic_claudeX_chat.py +262 -182
  16. symai/backend/engines/neurosymbolic/engine_anthropic_claudeX_reasoning.py +263 -191
  17. symai/backend/engines/neurosymbolic/engine_deepseekX_reasoning.py +53 -49
  18. symai/backend/engines/neurosymbolic/engine_google_geminiX_reasoning.py +212 -211
  19. symai/backend/engines/neurosymbolic/engine_groq.py +87 -63
  20. symai/backend/engines/neurosymbolic/engine_huggingface.py +21 -24
  21. symai/backend/engines/neurosymbolic/engine_llama_cpp.py +117 -48
  22. symai/backend/engines/neurosymbolic/engine_openai_gptX_chat.py +256 -229
  23. symai/backend/engines/neurosymbolic/engine_openai_gptX_reasoning.py +270 -150
  24. symai/backend/engines/ocr/engine_apilayer.py +6 -8
  25. symai/backend/engines/output/engine_stdout.py +1 -4
  26. symai/backend/engines/search/engine_openai.py +7 -7
  27. symai/backend/engines/search/engine_perplexity.py +5 -5
  28. symai/backend/engines/search/engine_serpapi.py +12 -14
  29. symai/backend/engines/speech_to_text/engine_local_whisper.py +20 -27
  30. symai/backend/engines/symbolic/engine_wolframalpha.py +3 -3
  31. symai/backend/engines/text_to_speech/engine_openai.py +5 -7
  32. symai/backend/engines/text_vision/engine_clip.py +7 -11
  33. symai/backend/engines/userinput/engine_console.py +3 -3
  34. symai/backend/engines/webscraping/engine_requests.py +81 -48
  35. symai/backend/mixin/__init__.py +13 -0
  36. symai/backend/mixin/anthropic.py +4 -2
  37. symai/backend/mixin/deepseek.py +2 -0
  38. symai/backend/mixin/google.py +2 -0
  39. symai/backend/mixin/openai.py +11 -3
  40. symai/backend/settings.py +83 -16
  41. symai/chat.py +101 -78
  42. symai/collect/__init__.py +7 -1
  43. symai/collect/dynamic.py +77 -69
  44. symai/collect/pipeline.py +35 -27
  45. symai/collect/stats.py +75 -63
  46. symai/components.py +198 -169
  47. symai/constraints.py +15 -12
  48. symai/core.py +698 -359
  49. symai/core_ext.py +32 -34
  50. symai/endpoints/api.py +80 -73
  51. symai/extended/.DS_Store +0 -0
  52. symai/extended/__init__.py +46 -12
  53. symai/extended/api_builder.py +11 -8
  54. symai/extended/arxiv_pdf_parser.py +13 -12
  55. symai/extended/bibtex_parser.py +2 -3
  56. symai/extended/conversation.py +101 -90
  57. symai/extended/document.py +17 -10
  58. symai/extended/file_merger.py +18 -13
  59. symai/extended/graph.py +18 -13
  60. symai/extended/html_style_template.py +2 -4
  61. symai/extended/interfaces/blip_2.py +1 -2
  62. symai/extended/interfaces/clip.py +1 -2
  63. symai/extended/interfaces/console.py +7 -1
  64. symai/extended/interfaces/dall_e.py +1 -1
  65. symai/extended/interfaces/flux.py +1 -1
  66. symai/extended/interfaces/gpt_image.py +1 -1
  67. symai/extended/interfaces/input.py +1 -1
  68. symai/extended/interfaces/llava.py +0 -1
  69. symai/extended/interfaces/naive_vectordb.py +7 -8
  70. symai/extended/interfaces/naive_webscraping.py +1 -1
  71. symai/extended/interfaces/ocr.py +1 -1
  72. symai/extended/interfaces/pinecone.py +6 -5
  73. symai/extended/interfaces/serpapi.py +1 -1
  74. symai/extended/interfaces/terminal.py +2 -3
  75. symai/extended/interfaces/tts.py +1 -1
  76. symai/extended/interfaces/whisper.py +1 -1
  77. symai/extended/interfaces/wolframalpha.py +1 -1
  78. symai/extended/metrics/__init__.py +11 -1
  79. symai/extended/metrics/similarity.py +11 -13
  80. symai/extended/os_command.py +17 -16
  81. symai/extended/packages/__init__.py +29 -3
  82. symai/extended/packages/symdev.py +19 -16
  83. symai/extended/packages/sympkg.py +12 -9
  84. symai/extended/packages/symrun.py +21 -19
  85. symai/extended/repo_cloner.py +11 -10
  86. symai/extended/seo_query_optimizer.py +1 -2
  87. symai/extended/solver.py +20 -23
  88. symai/extended/summarizer.py +4 -3
  89. symai/extended/taypan_interpreter.py +10 -12
  90. symai/extended/vectordb.py +99 -82
  91. symai/formatter/__init__.py +9 -1
  92. symai/formatter/formatter.py +12 -16
  93. symai/formatter/regex.py +62 -63
  94. symai/functional.py +176 -122
  95. symai/imports.py +136 -127
  96. symai/interfaces.py +56 -27
  97. symai/memory.py +14 -13
  98. symai/misc/console.py +49 -39
  99. symai/misc/loader.py +5 -3
  100. symai/models/__init__.py +17 -1
  101. symai/models/base.py +269 -181
  102. symai/models/errors.py +0 -1
  103. symai/ops/__init__.py +32 -22
  104. symai/ops/measures.py +11 -15
  105. symai/ops/primitives.py +348 -228
  106. symai/post_processors.py +32 -28
  107. symai/pre_processors.py +39 -41
  108. symai/processor.py +6 -4
  109. symai/prompts.py +59 -45
  110. symai/server/huggingface_server.py +23 -20
  111. symai/server/llama_cpp_server.py +7 -5
  112. symai/shell.py +3 -4
  113. symai/shellsv.py +499 -375
  114. symai/strategy.py +517 -287
  115. symai/symbol.py +111 -116
  116. symai/utils.py +42 -36
  117. {symbolicai-0.20.2.dist-info → symbolicai-1.0.0.dist-info}/METADATA +4 -2
  118. symbolicai-1.0.0.dist-info/RECORD +163 -0
  119. symbolicai-0.20.2.dist-info/RECORD +0 -162
  120. {symbolicai-0.20.2.dist-info → symbolicai-1.0.0.dist-info}/WHEEL +0 -0
  121. {symbolicai-0.20.2.dist-info → symbolicai-1.0.0.dist-info}/entry_points.txt +0 -0
  122. {symbolicai-0.20.2.dist-info → symbolicai-1.0.0.dist-info}/licenses/LICENSE +0 -0
  123. {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 List, Optional
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 CustomUserWarning, encode_media_frames
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
- def __init__(self, api_key: Optional[str] = None, model: Optional[str] = None):
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 as e:
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
- CustomUserWarning(f'Failed to initialize OpenAI client. Please check your OpenAI library version. Caused by: {e}', raise_with=ValueError)
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 compute_required_tokens(self, messages):
71
- """Return the number of tokens used by a list of messages."""
72
-
73
- if self.model in {
74
- "gpt-3.5-turbo-0613",
75
- "gpt-3.5-turbo-16k-0613",
76
- "gpt-4-1106-preview",
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
- elif self.model == "gpt-4":
102
- tokens_per_message = 3
103
- tokens_per_name = 1
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
- else:
107
- CustomUserWarning(
108
- 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.""",
109
- raise_with=NotImplementedError
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
- if type(value) == str:
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
- CustomUserWarning(f"Invalid max_used_frames value: {max_used_frames}. Expected value between 1 and {max_frames_spacing}", raise_with=ValueError)
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
- print('No frames found or error in encoding frames')
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 len(prompts) != 2 and all(prompt['role'] in ['system', 'user'] for prompt in prompts):
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
- if truncation_percentage is None:
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
- if isinstance(user_prompt['content'], str):
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
- CustomUserWarning(
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 > max_prompt_tokens/2 and system_token_count <= max_prompt_tokens/2:
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 > max_prompt_tokens/2 and user_token_count <= max_prompt_tokens/2:
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
- system_ratio = system_token_count / total_tokens
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
- logging.error(msg)
324
+ UserMessage(msg)
301
325
  if self.config['NEUROSYMBOLIC_ENGINE_API_KEY'] is None or self.config['NEUROSYMBOLIC_ENGINE_API_KEY'] == '':
302
- CustomUserWarning(msg, raise_with=ValueError)
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['model'] if 'model' in kwargs else self.model
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
- CustomUserWarning(f'Error during generation. Caused by: {e}', raise_with=ValueError)
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
- CustomUserWarning('Need to provide a prompt instruction to the engine if raw_input is enabled.', raise_with=ValueError)
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 type(value) != list:
329
- if type(value) != dict:
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 prepare(self, argument):
335
- if argument.prop.raw_input:
336
- argument.prop.prepared_input = self._prepare_raw_input(argument)
337
- return
338
-
339
- _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"""
340
- user: str = ""
341
- system: str = ""
342
-
343
- if argument.prop.suppress_verbose_output:
344
- system += _non_verbose_output
345
- system = f'{system}\n' if system and len(system) > 0 else ''
346
-
347
- if argument.prop.response_format:
348
- _rsp_fmt = argument.prop.response_format
349
- 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'
350
- if _rsp_fmt["type"] == "json_object":
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
- system += f"<STATIC CONTEXT/>\n{static_ctxt}\n\n"
360
-
378
+ sections.append(f"<STATIC CONTEXT/>\n{static_ctxt}\n\n")
361
379
  if len(dyn_ctxt) > 0:
362
- system += f"<DYNAMIC CONTEXT/>\n{dyn_ctxt}\n\n"
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
- if argument.prop.payload:
366
- system += f"<ADDITIONAL CONTEXT/>\n{str(payload)}\n\n"
367
-
368
- examples: List[str] = argument.prop.examples
369
- if examples and len(examples) > 0:
370
- system += f"<EXAMPLES/>\n{str(examples)}\n\n"
371
-
372
- image_files = self._handle_image_content(str(argument.prop.processed_input))
373
-
374
- if argument.prop.prompt is not None and len(argument.prop.prompt) > 0:
375
- val = str(argument.prop.prompt)
376
- if len(image_files) > 0:
377
- val = self._remove_vision_pattern(val)
378
- system += f"<INSTRUCTION/>\n{val}\n\n"
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
- user += f"{suffix}"
385
-
386
- if argument.prop.template_suffix:
387
- system += f' You will only generate content for the placeholder `{str(argument.prop.template_suffix)}` following the instructions and the provided context information.\n\n'
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
- system = res['system']
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 hasattr(res, 'choices') and res.choices:
439
- choice = res.choices[0]
440
- if hasattr(choice, 'message') and choice.message:
441
- if hasattr(choice.message, 'tool_calls') and choice.message.tool_calls:
442
- for tool_call in choice.message.tool_calls:
443
- if hasattr(tool_call, 'function') and tool_call.function:
444
- if hit:
445
- CustomUserWarning("Multiple function calls detected in the response but only the first one will be processed.")
446
- break
447
- try:
448
- args_dict = json.loads(tool_call.function.arguments)
449
- except json.JSONDecodeError:
450
- args_dict = {}
451
- metadata['function_call'] = {
452
- 'name': tool_call.function.name,
453
- 'arguments': args_dict
454
- }
455
- hit = True
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
- CustomUserWarning(
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
- CustomUserWarning(
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
- if max_completion_tokens > self.max_response_tokens:
484
- CustomUserWarning(
485
- f"Provided 'max_completion_tokens' ({max_completion_tokens}) exceeds max response tokens ({self.max_response_tokens}). "
486
- f"Truncating to {remaining_tokens} to avoid API failure."
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,