symbolicai 1.5.0__py3-none-any.whl → 1.6.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 +21 -71
- symai/backend/base.py +0 -26
- symai/backend/engines/drawing/engine_gemini_image.py +101 -0
- symai/backend/engines/embedding/engine_openai.py +11 -8
- symai/backend/engines/neurosymbolic/__init__.py +8 -0
- symai/backend/engines/neurosymbolic/engine_google_geminiX_reasoning.py +14 -1
- symai/backend/engines/neurosymbolic/engine_openrouter.py +294 -0
- symai/backend/mixin/__init__.py +4 -0
- symai/backend/mixin/openrouter.py +2 -0
- symai/components.py +203 -13
- symai/extended/interfaces/nanobanana.py +23 -0
- symai/interfaces.py +2 -0
- symai/ops/primitives.py +0 -18
- symai/shellsv.py +2 -7
- {symbolicai-1.5.0.dist-info → symbolicai-1.6.0.dist-info}/METADATA +2 -9
- {symbolicai-1.5.0.dist-info → symbolicai-1.6.0.dist-info}/RECORD +20 -43
- {symbolicai-1.5.0.dist-info → symbolicai-1.6.0.dist-info}/WHEEL +1 -1
- symai/backend/driver/webclient.py +0 -217
- symai/backend/engines/crawler/engine_selenium.py +0 -94
- symai/backend/engines/drawing/engine_dall_e.py +0 -131
- symai/backend/engines/embedding/engine_plugin_embeddings.py +0 -12
- symai/backend/engines/experiments/engine_bard_wrapper.py +0 -131
- symai/backend/engines/experiments/engine_gptfinetuner.py +0 -32
- symai/backend/engines/experiments/engine_llamacpp_completion.py +0 -142
- symai/backend/engines/neurosymbolic/engine_openai_gptX_completion.py +0 -277
- symai/collect/__init__.py +0 -8
- symai/collect/dynamic.py +0 -117
- symai/collect/pipeline.py +0 -156
- symai/collect/stats.py +0 -434
- symai/extended/crawler.py +0 -21
- symai/extended/interfaces/selenium.py +0 -18
- symai/extended/interfaces/vectordb.py +0 -21
- symai/extended/personas/__init__.py +0 -3
- symai/extended/personas/builder.py +0 -105
- symai/extended/personas/dialogue.py +0 -126
- symai/extended/personas/persona.py +0 -154
- symai/extended/personas/research/__init__.py +0 -1
- symai/extended/personas/research/yann_lecun.py +0 -62
- symai/extended/personas/sales/__init__.py +0 -1
- symai/extended/personas/sales/erik_james.py +0 -62
- symai/extended/personas/student/__init__.py +0 -1
- symai/extended/personas/student/max_tenner.py +0 -51
- symai/extended/strategies/__init__.py +0 -1
- symai/extended/strategies/cot.py +0 -40
- {symbolicai-1.5.0.dist-info → symbolicai-1.6.0.dist-info}/entry_points.txt +0 -0
- {symbolicai-1.5.0.dist-info → symbolicai-1.6.0.dist-info}/licenses/LICENSE +0 -0
- {symbolicai-1.5.0.dist-info → symbolicai-1.6.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
|
|
5
|
+
import openai
|
|
6
|
+
|
|
7
|
+
from ....components import SelfPrompt
|
|
8
|
+
from ....core_ext import retry
|
|
9
|
+
from ....utils import UserMessage
|
|
10
|
+
from ...base import Engine
|
|
11
|
+
from ...settings import SYMAI_CONFIG
|
|
12
|
+
|
|
13
|
+
logging.getLogger("openai").setLevel(logging.ERROR)
|
|
14
|
+
logging.getLogger("requests").setLevel(logging.ERROR)
|
|
15
|
+
logging.getLogger("urllib").setLevel(logging.ERROR)
|
|
16
|
+
logging.getLogger("httpx").setLevel(logging.ERROR)
|
|
17
|
+
logging.getLogger("httpcore").setLevel(logging.ERROR)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_NON_VERBOSE_OUTPUT = (
|
|
21
|
+
"<META_INSTRUCTION/>\n"
|
|
22
|
+
"You do not output anything else, like verbose preambles or post explanation, such as "
|
|
23
|
+
'"Sure, let me...", "Hope that was helpful...", "Yes, I can help you with that...", etc. '
|
|
24
|
+
"Consider well formatted output, e.g. for sentences use punctuation, spaces etc. or for code use "
|
|
25
|
+
"indentation, etc. Never add meta instructions information to your output!\n\n"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class OpenRouterEngine(Engine):
|
|
30
|
+
def __init__(self, api_key: str | None = None, model: str | None = None):
|
|
31
|
+
super().__init__()
|
|
32
|
+
self.config = deepcopy(SYMAI_CONFIG)
|
|
33
|
+
# In case we use EngineRepository.register to inject the api_key and model => dynamically change the engine at runtime
|
|
34
|
+
if api_key is not None and model is not None:
|
|
35
|
+
self.config["NEUROSYMBOLIC_ENGINE_API_KEY"] = api_key
|
|
36
|
+
self.config["NEUROSYMBOLIC_ENGINE_MODEL"] = model
|
|
37
|
+
if self.id() != "neurosymbolic":
|
|
38
|
+
return # do not initialize if not neurosymbolic; avoids conflict with llama.cpp check in EngineRepository.register_from_package
|
|
39
|
+
openai.api_key = self.config["NEUROSYMBOLIC_ENGINE_API_KEY"]
|
|
40
|
+
self.model = self.config["NEUROSYMBOLIC_ENGINE_MODEL"]
|
|
41
|
+
self.seed = None
|
|
42
|
+
self.name = self.__class__.__name__
|
|
43
|
+
self._last_prompt_tokens = None
|
|
44
|
+
self._last_messages = None
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
self.client = openai.OpenAI(
|
|
48
|
+
api_key=openai.api_key, base_url="https://openrouter.ai/api/v1"
|
|
49
|
+
)
|
|
50
|
+
except Exception as exc:
|
|
51
|
+
UserMessage(
|
|
52
|
+
f"Failed to initialize OpenRouter client. Please check your OpenAI library version. Caused by: {exc}",
|
|
53
|
+
raise_with=ValueError,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def id(self) -> str:
|
|
57
|
+
model_name = self.config.get("NEUROSYMBOLIC_ENGINE_MODEL")
|
|
58
|
+
if model_name and model_name.startswith("openrouter"):
|
|
59
|
+
return "neurosymbolic"
|
|
60
|
+
return super().id()
|
|
61
|
+
|
|
62
|
+
def command(self, *args, **kwargs):
|
|
63
|
+
super().command(*args, **kwargs)
|
|
64
|
+
if "NEUROSYMBOLIC_ENGINE_API_KEY" in kwargs:
|
|
65
|
+
openai.api_key = kwargs["NEUROSYMBOLIC_ENGINE_API_KEY"]
|
|
66
|
+
if "NEUROSYMBOLIC_ENGINE_MODEL" in kwargs:
|
|
67
|
+
self.model = kwargs["NEUROSYMBOLIC_ENGINE_MODEL"]
|
|
68
|
+
if "seed" in kwargs:
|
|
69
|
+
self.seed = kwargs["seed"]
|
|
70
|
+
|
|
71
|
+
def compute_required_tokens(self, messages):
|
|
72
|
+
if self._last_prompt_tokens is not None and self._last_messages == messages:
|
|
73
|
+
return self._last_prompt_tokens
|
|
74
|
+
UserMessage(
|
|
75
|
+
"Token counting not implemented for this engine.", raise_with=NotImplementedError
|
|
76
|
+
)
|
|
77
|
+
return 0
|
|
78
|
+
|
|
79
|
+
def compute_remaining_tokens(self, _prompts: list) -> int:
|
|
80
|
+
UserMessage(
|
|
81
|
+
"Token counting not implemented for this engine.", raise_with=NotImplementedError
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def _handle_prefix(self, model_name: str) -> str:
|
|
85
|
+
"""Handle prefix for model name."""
|
|
86
|
+
return model_name.replace("openrouter:", "")
|
|
87
|
+
|
|
88
|
+
def _extract_thinking_content(self, output: list[str]) -> tuple[str | None, list[str]]:
|
|
89
|
+
"""Extract thinking content from textual output using <think>...</think> tags if present."""
|
|
90
|
+
if not output or not output[0]:
|
|
91
|
+
return None, output
|
|
92
|
+
|
|
93
|
+
content = output[0]
|
|
94
|
+
start = content.find("<think>")
|
|
95
|
+
if start == -1:
|
|
96
|
+
return None, output
|
|
97
|
+
|
|
98
|
+
end = content.find("</think>", start + 7)
|
|
99
|
+
if end == -1:
|
|
100
|
+
return None, output
|
|
101
|
+
|
|
102
|
+
thinking_content = content[start + 7 : end].strip() or None
|
|
103
|
+
cleaned_content = (content[:start] + content[end + 8 :]).strip()
|
|
104
|
+
cleaned_output = [cleaned_content, *output[1:]]
|
|
105
|
+
|
|
106
|
+
return thinking_content, cleaned_output
|
|
107
|
+
|
|
108
|
+
# cumulative wait time is < 30s
|
|
109
|
+
@retry(tries=8, delay=0.5, backoff=1.5, max_delay=5, jitter=(0, 0.5))
|
|
110
|
+
def forward(self, argument):
|
|
111
|
+
kwargs = argument.kwargs
|
|
112
|
+
messages = argument.prop.prepared_input
|
|
113
|
+
payload = self._prepare_request_payload(messages, argument)
|
|
114
|
+
except_remedy = kwargs.get("except_remedy")
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
res = self.client.chat.completions.create(**payload)
|
|
118
|
+
except Exception as exc:
|
|
119
|
+
if openai.api_key is None or openai.api_key == "":
|
|
120
|
+
msg = (
|
|
121
|
+
"OpenRouter API key is not set. Please set it in the config file or "
|
|
122
|
+
"pass it as an argument to the command method."
|
|
123
|
+
)
|
|
124
|
+
UserMessage(msg)
|
|
125
|
+
if (
|
|
126
|
+
self.config["NEUROSYMBOLIC_ENGINE_API_KEY"] is None
|
|
127
|
+
or self.config["NEUROSYMBOLIC_ENGINE_API_KEY"] == ""
|
|
128
|
+
):
|
|
129
|
+
UserMessage(msg, raise_with=ValueError)
|
|
130
|
+
openai.api_key = self.config["NEUROSYMBOLIC_ENGINE_API_KEY"]
|
|
131
|
+
|
|
132
|
+
callback = self.client.chat.completions.create
|
|
133
|
+
kwargs["model"] = (
|
|
134
|
+
self._handle_prefix(kwargs["model"])
|
|
135
|
+
if "model" in kwargs
|
|
136
|
+
else self._handle_prefix(self.model)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if except_remedy is not None:
|
|
140
|
+
res = except_remedy(self, exc, callback, argument)
|
|
141
|
+
else:
|
|
142
|
+
UserMessage(f"Error during generation. Caused by: {exc}", raise_with=ValueError)
|
|
143
|
+
|
|
144
|
+
prompt_tokens = getattr(res.usage, "prompt_tokens", None)
|
|
145
|
+
if prompt_tokens is None:
|
|
146
|
+
prompt_tokens = getattr(res.usage, "input_tokens", None)
|
|
147
|
+
self._last_prompt_tokens = prompt_tokens
|
|
148
|
+
self._last_messages = messages
|
|
149
|
+
|
|
150
|
+
metadata = {"raw_output": res}
|
|
151
|
+
if payload.get("tools"):
|
|
152
|
+
metadata = self._process_function_calls(res, metadata)
|
|
153
|
+
|
|
154
|
+
output = [r.message.content for r in res.choices]
|
|
155
|
+
thinking, output = self._extract_thinking_content(output)
|
|
156
|
+
if thinking:
|
|
157
|
+
metadata["thinking"] = thinking
|
|
158
|
+
|
|
159
|
+
return output, metadata
|
|
160
|
+
|
|
161
|
+
def _prepare_raw_input(self, argument):
|
|
162
|
+
if not argument.prop.processed_input:
|
|
163
|
+
UserMessage(
|
|
164
|
+
"Need to provide a prompt instruction to the engine if raw_input is enabled.",
|
|
165
|
+
raise_with=ValueError,
|
|
166
|
+
)
|
|
167
|
+
value = argument.prop.processed_input
|
|
168
|
+
if not isinstance(value, list):
|
|
169
|
+
if not isinstance(value, dict):
|
|
170
|
+
value = {"role": "user", "content": str(value)}
|
|
171
|
+
value = [value]
|
|
172
|
+
return value
|
|
173
|
+
|
|
174
|
+
def prepare(self, argument):
|
|
175
|
+
if argument.prop.raw_input:
|
|
176
|
+
argument.prop.prepared_input = self._prepare_raw_input(argument)
|
|
177
|
+
return
|
|
178
|
+
self._validate_response_format(argument)
|
|
179
|
+
|
|
180
|
+
system = self._build_system_message(argument)
|
|
181
|
+
user_content = self._build_user_content(argument)
|
|
182
|
+
user_prompt = {"role": "user", "content": user_content}
|
|
183
|
+
system, user_prompt = self._apply_self_prompt_if_needed(argument, system, user_prompt)
|
|
184
|
+
|
|
185
|
+
argument.prop.prepared_input = [
|
|
186
|
+
{"role": "system", "content": system},
|
|
187
|
+
user_prompt,
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
def _validate_response_format(self, argument) -> None:
|
|
191
|
+
if argument.prop.response_format:
|
|
192
|
+
response_format = argument.prop.response_format
|
|
193
|
+
assert response_format.get("type") is not None, (
|
|
194
|
+
'Expected format `{ "type": "json_object" }`! We are using the OpenAI compatible '
|
|
195
|
+
"API for OpenRouter."
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
def _build_system_message(self, argument) -> str:
|
|
199
|
+
system: str = ""
|
|
200
|
+
if argument.prop.suppress_verbose_output:
|
|
201
|
+
system += _NON_VERBOSE_OUTPUT
|
|
202
|
+
if system:
|
|
203
|
+
system = f"{system}\n"
|
|
204
|
+
|
|
205
|
+
ref = argument.prop.instance
|
|
206
|
+
static_ctxt, dyn_ctxt = ref.global_context
|
|
207
|
+
if len(static_ctxt) > 0:
|
|
208
|
+
system += f"<STATIC CONTEXT/>\n{static_ctxt}\n\n"
|
|
209
|
+
|
|
210
|
+
if len(dyn_ctxt) > 0:
|
|
211
|
+
system += f"<DYNAMIC CONTEXT/>\n{dyn_ctxt}\n\n"
|
|
212
|
+
|
|
213
|
+
if argument.prop.payload:
|
|
214
|
+
system += f"<ADDITIONAL CONTEXT/>\n{argument.prop.payload!s}\n\n"
|
|
215
|
+
|
|
216
|
+
examples = argument.prop.examples
|
|
217
|
+
if examples and len(examples) > 0:
|
|
218
|
+
system += f"<EXAMPLES/>\n{examples!s}\n\n"
|
|
219
|
+
|
|
220
|
+
if argument.prop.prompt is not None and len(argument.prop.prompt) > 0:
|
|
221
|
+
val = str(argument.prop.prompt)
|
|
222
|
+
system += f"<INSTRUCTION/>\n{val}\n\n"
|
|
223
|
+
|
|
224
|
+
if argument.prop.template_suffix:
|
|
225
|
+
system += (
|
|
226
|
+
" You will only generate content for the placeholder "
|
|
227
|
+
f"`{argument.prop.template_suffix!s}` following the instructions and the provided context "
|
|
228
|
+
"information.\n\n"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
return system
|
|
232
|
+
|
|
233
|
+
def _build_user_content(self, argument) -> str:
|
|
234
|
+
return str(argument.prop.processed_input)
|
|
235
|
+
|
|
236
|
+
def _apply_self_prompt_if_needed(self, argument, system, user_prompt):
|
|
237
|
+
if argument.prop.instance._kwargs.get("self_prompt", False) or argument.prop.self_prompt:
|
|
238
|
+
self_prompter = SelfPrompt()
|
|
239
|
+
res = self_prompter({"user": user_prompt["content"], "system": system})
|
|
240
|
+
if res is None:
|
|
241
|
+
UserMessage("Self-prompting failed!", raise_with=ValueError)
|
|
242
|
+
return res["system"], {"role": "user", "content": res["user"]}
|
|
243
|
+
return system, user_prompt
|
|
244
|
+
|
|
245
|
+
def _process_function_calls(self, res, metadata):
|
|
246
|
+
hit = False
|
|
247
|
+
if (
|
|
248
|
+
hasattr(res, "choices")
|
|
249
|
+
and res.choices
|
|
250
|
+
and hasattr(res.choices[0], "message")
|
|
251
|
+
and res.choices[0].message
|
|
252
|
+
and hasattr(res.choices[0].message, "tool_calls")
|
|
253
|
+
and res.choices[0].message.tool_calls
|
|
254
|
+
):
|
|
255
|
+
for tool_call in res.choices[0].message.tool_calls:
|
|
256
|
+
if hasattr(tool_call, "function") and tool_call.function:
|
|
257
|
+
if hit:
|
|
258
|
+
UserMessage(
|
|
259
|
+
"Multiple function calls detected in the response but only the first one will be processed."
|
|
260
|
+
)
|
|
261
|
+
break
|
|
262
|
+
try:
|
|
263
|
+
args_dict = json.loads(tool_call.function.arguments)
|
|
264
|
+
except json.JSONDecodeError:
|
|
265
|
+
args_dict = {}
|
|
266
|
+
metadata["function_call"] = {
|
|
267
|
+
"name": tool_call.function.name,
|
|
268
|
+
"arguments": args_dict,
|
|
269
|
+
}
|
|
270
|
+
hit = True
|
|
271
|
+
return metadata
|
|
272
|
+
|
|
273
|
+
# TODO: requires updates for reasoning
|
|
274
|
+
def _prepare_request_payload(self, messages, argument):
|
|
275
|
+
kwargs = argument.kwargs
|
|
276
|
+
max_tokens = kwargs.get("max_tokens")
|
|
277
|
+
if max_tokens is None:
|
|
278
|
+
max_tokens = kwargs.get("max_completion_tokens")
|
|
279
|
+
return {
|
|
280
|
+
"messages": messages,
|
|
281
|
+
"model": self._handle_prefix(kwargs.get("model", self.model)),
|
|
282
|
+
"seed": kwargs.get("seed", self.seed),
|
|
283
|
+
"max_tokens": max_tokens,
|
|
284
|
+
"stop": kwargs.get("stop"),
|
|
285
|
+
"temperature": kwargs.get("temperature", 1),
|
|
286
|
+
"frequency_penalty": kwargs.get("frequency_penalty", 0),
|
|
287
|
+
"presence_penalty": kwargs.get("presence_penalty", 0),
|
|
288
|
+
"top_p": kwargs.get("top_p", 1),
|
|
289
|
+
"n": kwargs.get("n", 1),
|
|
290
|
+
"tools": kwargs.get("tools"),
|
|
291
|
+
"tool_choice": kwargs.get("tool_choice"),
|
|
292
|
+
"response_format": kwargs.get("response_format"),
|
|
293
|
+
"stream": kwargs.get("stream", False),
|
|
294
|
+
}
|
symai/backend/mixin/__init__.py
CHANGED
|
@@ -11,6 +11,8 @@ from .groq import SUPPORTED_REASONING_MODELS as GROQ_REASONING_MODELS
|
|
|
11
11
|
from .openai import SUPPORTED_CHAT_MODELS as OPENAI_CHAT_MODELS
|
|
12
12
|
from .openai import SUPPORTED_REASONING_MODELS as OPENAI_REASONING_MODELS
|
|
13
13
|
from .openai import SUPPORTED_RESPONSES_MODELS as OPENAI_RESPONSES_MODELS
|
|
14
|
+
from .openrouter import SUPPORTED_CHAT_MODELS as OPENROUTER_CHAT_MODELS
|
|
15
|
+
from .openrouter import SUPPORTED_REASONING_MODELS as OPENROUTER_REASONING_MODELS
|
|
14
16
|
|
|
15
17
|
__all__ = [
|
|
16
18
|
"ANTHROPIC_CHAT_MODELS",
|
|
@@ -26,4 +28,6 @@ __all__ = [
|
|
|
26
28
|
"OPENAI_CHAT_MODELS",
|
|
27
29
|
"OPENAI_REASONING_MODELS",
|
|
28
30
|
"OPENAI_RESPONSES_MODELS",
|
|
31
|
+
"OPENROUTER_CHAT_MODELS",
|
|
32
|
+
"OPENROUTER_REASONING_MODELS",
|
|
29
33
|
]
|
symai/components.py
CHANGED
|
@@ -1229,6 +1229,7 @@ class MetadataTracker(Expression):
|
|
|
1229
1229
|
and frame.f_code.co_name == "forward"
|
|
1230
1230
|
and "self" in frame.f_locals
|
|
1231
1231
|
and isinstance(frame.f_locals["self"], Engine)
|
|
1232
|
+
and arg is not None # Ensure arg is not None to avoid unpacking error on exceptions
|
|
1232
1233
|
):
|
|
1233
1234
|
_, metadata = arg # arg contains return value on 'return' event
|
|
1234
1235
|
engine_name = frame.f_locals["self"].__class__.__name__
|
|
@@ -1350,6 +1351,116 @@ class MetadataTracker(Expression):
|
|
|
1350
1351
|
token_details[(engine_name, model_name)]["completion_breakdown"][
|
|
1351
1352
|
"reasoning_tokens"
|
|
1352
1353
|
] += 0
|
|
1354
|
+
elif engine_name in ("ClaudeXChatEngine", "ClaudeXReasoningEngine"):
|
|
1355
|
+
raw_output = metadata["raw_output"]
|
|
1356
|
+
usage = self._extract_claude_usage(raw_output)
|
|
1357
|
+
if usage is None:
|
|
1358
|
+
# Skip if we can't extract usage (shouldn't happen normally)
|
|
1359
|
+
logger.warning(f"Could not extract usage from {engine_name} response.")
|
|
1360
|
+
token_details[(engine_name, model_name)]["usage"]["total_calls"] += 1
|
|
1361
|
+
token_details[(engine_name, model_name)]["prompt_breakdown"][
|
|
1362
|
+
"cached_tokens"
|
|
1363
|
+
] += 0
|
|
1364
|
+
token_details[(engine_name, model_name)]["completion_breakdown"][
|
|
1365
|
+
"reasoning_tokens"
|
|
1366
|
+
] += 0
|
|
1367
|
+
continue
|
|
1368
|
+
input_tokens = getattr(usage, "input_tokens", 0) or 0
|
|
1369
|
+
output_tokens = getattr(usage, "output_tokens", 0) or 0
|
|
1370
|
+
token_details[(engine_name, model_name)]["usage"]["prompt_tokens"] += (
|
|
1371
|
+
input_tokens
|
|
1372
|
+
)
|
|
1373
|
+
token_details[(engine_name, model_name)]["usage"]["completion_tokens"] += (
|
|
1374
|
+
output_tokens
|
|
1375
|
+
)
|
|
1376
|
+
# Calculate total tokens
|
|
1377
|
+
total = input_tokens + output_tokens
|
|
1378
|
+
token_details[(engine_name, model_name)]["usage"]["total_tokens"] += total
|
|
1379
|
+
token_details[(engine_name, model_name)]["usage"]["total_calls"] += 1
|
|
1380
|
+
# Track cache tokens if available
|
|
1381
|
+
cache_creation = getattr(usage, "cache_creation_input_tokens", 0) or 0
|
|
1382
|
+
cache_read = getattr(usage, "cache_read_input_tokens", 0) or 0
|
|
1383
|
+
token_details[(engine_name, model_name)]["prompt_breakdown"][
|
|
1384
|
+
"cache_creation_tokens"
|
|
1385
|
+
] += cache_creation
|
|
1386
|
+
token_details[(engine_name, model_name)]["prompt_breakdown"][
|
|
1387
|
+
"cache_read_tokens"
|
|
1388
|
+
] += cache_read
|
|
1389
|
+
# For backward compatibility, also track as cached_tokens
|
|
1390
|
+
token_details[(engine_name, model_name)]["prompt_breakdown"][
|
|
1391
|
+
"cached_tokens"
|
|
1392
|
+
] += cache_read
|
|
1393
|
+
# Track reasoning/thinking tokens for ClaudeXReasoningEngine
|
|
1394
|
+
if engine_name == "ClaudeXReasoningEngine":
|
|
1395
|
+
thinking_output = metadata.get("thinking", "")
|
|
1396
|
+
# Store thinking content if available
|
|
1397
|
+
if thinking_output:
|
|
1398
|
+
if "thinking_content" not in token_details[(engine_name, model_name)]:
|
|
1399
|
+
token_details[(engine_name, model_name)]["thinking_content"] = []
|
|
1400
|
+
token_details[(engine_name, model_name)]["thinking_content"].append(
|
|
1401
|
+
thinking_output
|
|
1402
|
+
)
|
|
1403
|
+
# Note: Anthropic doesn't break down reasoning tokens separately in usage,
|
|
1404
|
+
# but extended thinking is included in output_tokens
|
|
1405
|
+
token_details[(engine_name, model_name)]["completion_breakdown"][
|
|
1406
|
+
"reasoning_tokens"
|
|
1407
|
+
] += 0
|
|
1408
|
+
elif engine_name == "GeminiXReasoningEngine":
|
|
1409
|
+
usage = metadata["raw_output"].usage_metadata
|
|
1410
|
+
token_details[(engine_name, model_name)]["usage"]["prompt_tokens"] += (
|
|
1411
|
+
usage.prompt_token_count
|
|
1412
|
+
)
|
|
1413
|
+
token_details[(engine_name, model_name)]["usage"]["completion_tokens"] += (
|
|
1414
|
+
usage.candidates_token_count
|
|
1415
|
+
)
|
|
1416
|
+
token_details[(engine_name, model_name)]["usage"]["total_tokens"] += (
|
|
1417
|
+
usage.total_token_count
|
|
1418
|
+
)
|
|
1419
|
+
token_details[(engine_name, model_name)]["usage"]["total_calls"] += 1
|
|
1420
|
+
# Track cache tokens if available
|
|
1421
|
+
cache_read = getattr(usage, "cached_content_token_count", 0) or 0
|
|
1422
|
+
token_details[(engine_name, model_name)]["prompt_breakdown"][
|
|
1423
|
+
"cached_tokens"
|
|
1424
|
+
] += cache_read
|
|
1425
|
+
# Track thinking content if available
|
|
1426
|
+
thinking_output = metadata.get("thinking", "")
|
|
1427
|
+
if thinking_output:
|
|
1428
|
+
if "thinking_content" not in token_details[(engine_name, model_name)]:
|
|
1429
|
+
token_details[(engine_name, model_name)]["thinking_content"] = []
|
|
1430
|
+
token_details[(engine_name, model_name)]["thinking_content"].append(
|
|
1431
|
+
thinking_output
|
|
1432
|
+
)
|
|
1433
|
+
# Note: Gemini reasoning tokens are part of candidates_token_count
|
|
1434
|
+
token_details[(engine_name, model_name)]["completion_breakdown"][
|
|
1435
|
+
"reasoning_tokens"
|
|
1436
|
+
] += 0
|
|
1437
|
+
elif engine_name == "DeepSeekXReasoningEngine":
|
|
1438
|
+
usage = metadata["raw_output"].usage
|
|
1439
|
+
token_details[(engine_name, model_name)]["usage"]["completion_tokens"] += (
|
|
1440
|
+
usage.completion_tokens
|
|
1441
|
+
)
|
|
1442
|
+
token_details[(engine_name, model_name)]["usage"]["prompt_tokens"] += (
|
|
1443
|
+
usage.prompt_tokens
|
|
1444
|
+
)
|
|
1445
|
+
token_details[(engine_name, model_name)]["usage"]["total_tokens"] += (
|
|
1446
|
+
usage.total_tokens
|
|
1447
|
+
)
|
|
1448
|
+
token_details[(engine_name, model_name)]["usage"]["total_calls"] += 1
|
|
1449
|
+
# Track thinking content if available
|
|
1450
|
+
thinking_output = metadata.get("thinking", "")
|
|
1451
|
+
if thinking_output:
|
|
1452
|
+
if "thinking_content" not in token_details[(engine_name, model_name)]:
|
|
1453
|
+
token_details[(engine_name, model_name)]["thinking_content"] = []
|
|
1454
|
+
token_details[(engine_name, model_name)]["thinking_content"].append(
|
|
1455
|
+
thinking_output
|
|
1456
|
+
)
|
|
1457
|
+
# Note: DeepSeek reasoning tokens might be in completion_tokens_details
|
|
1458
|
+
reasoning_tokens = 0
|
|
1459
|
+
if hasattr(usage, "completion_tokens_details") and usage.completion_tokens_details:
|
|
1460
|
+
reasoning_tokens = getattr(usage.completion_tokens_details, "reasoning_tokens", 0) or 0
|
|
1461
|
+
token_details[(engine_name, model_name)]["completion_breakdown"][
|
|
1462
|
+
"reasoning_tokens"
|
|
1463
|
+
] += reasoning_tokens
|
|
1353
1464
|
else:
|
|
1354
1465
|
logger.warning(f"Tracking {engine_name} is not supported.")
|
|
1355
1466
|
continue
|
|
@@ -1361,8 +1472,60 @@ class MetadataTracker(Expression):
|
|
|
1361
1472
|
# Convert to normal dict
|
|
1362
1473
|
return {**token_details}
|
|
1363
1474
|
|
|
1475
|
+
def _extract_claude_usage(self, raw_output):
|
|
1476
|
+
"""Extract usage information from Claude response (handles both streaming and non-streaming).
|
|
1477
|
+
|
|
1478
|
+
For non-streaming responses, raw_output is a Message object with a .usage attribute.
|
|
1479
|
+
For streaming responses, raw_output is a list of stream events. Usage info is in:
|
|
1480
|
+
- RawMessageStartEvent.message.usage (input_tokens)
|
|
1481
|
+
- RawMessageDeltaEvent.usage (output_tokens)
|
|
1482
|
+
"""
|
|
1483
|
+
# Non-streaming: raw_output is a Message with .usage
|
|
1484
|
+
if hasattr(raw_output, "usage"):
|
|
1485
|
+
return raw_output.usage
|
|
1486
|
+
|
|
1487
|
+
# Streaming: raw_output is a list of events
|
|
1488
|
+
if isinstance(raw_output, list):
|
|
1489
|
+
# Accumulate usage from stream events
|
|
1490
|
+
input_tokens = 0
|
|
1491
|
+
output_tokens = 0
|
|
1492
|
+
cache_creation = 0
|
|
1493
|
+
cache_read = 0
|
|
1494
|
+
|
|
1495
|
+
for event in raw_output:
|
|
1496
|
+
# RawMessageStartEvent contains initial usage with input_tokens
|
|
1497
|
+
if hasattr(event, "message") and hasattr(event.message, "usage"):
|
|
1498
|
+
msg_usage = event.message.usage
|
|
1499
|
+
input_tokens += getattr(msg_usage, "input_tokens", 0) or 0
|
|
1500
|
+
cache_creation += getattr(msg_usage, "cache_creation_input_tokens", 0) or 0
|
|
1501
|
+
cache_read += getattr(msg_usage, "cache_read_input_tokens", 0) or 0
|
|
1502
|
+
# RawMessageDeltaEvent contains usage with output_tokens
|
|
1503
|
+
elif hasattr(event, "usage") and event.usage is not None:
|
|
1504
|
+
evt_usage = event.usage
|
|
1505
|
+
output_tokens += getattr(evt_usage, "output_tokens", 0) or 0
|
|
1506
|
+
|
|
1507
|
+
# Create a simple object-like dict to hold usage (using Box for attribute access)
|
|
1508
|
+
return Box({
|
|
1509
|
+
"input_tokens": input_tokens,
|
|
1510
|
+
"output_tokens": output_tokens,
|
|
1511
|
+
"cache_creation_input_tokens": cache_creation,
|
|
1512
|
+
"cache_read_input_tokens": cache_read,
|
|
1513
|
+
})
|
|
1514
|
+
|
|
1515
|
+
return None
|
|
1516
|
+
|
|
1364
1517
|
def _can_accumulate_engine(self, engine_name: str) -> bool:
|
|
1365
|
-
supported_engines = (
|
|
1518
|
+
supported_engines = (
|
|
1519
|
+
"GPTXChatEngine",
|
|
1520
|
+
"GPTXReasoningEngine",
|
|
1521
|
+
"GPTXSearchEngine",
|
|
1522
|
+
"ClaudeXChatEngine",
|
|
1523
|
+
"ClaudeXReasoningEngine",
|
|
1524
|
+
"GeminiXReasoningEngine",
|
|
1525
|
+
"DeepSeekXReasoningEngine",
|
|
1526
|
+
"GroqEngine",
|
|
1527
|
+
"CerebrasEngine",
|
|
1528
|
+
)
|
|
1366
1529
|
return engine_name in supported_engines
|
|
1367
1530
|
|
|
1368
1531
|
def _track_parallel_usage_items(self, token_details, engine_name, metadata):
|
|
@@ -1388,21 +1551,48 @@ class MetadataTracker(Expression):
|
|
|
1388
1551
|
|
|
1389
1552
|
metadata_raw_output = metadata["raw_output"]
|
|
1390
1553
|
accumulated_raw_output = accumulated["raw_output"]
|
|
1391
|
-
if not hasattr(metadata_raw_output, "usage") or not hasattr(
|
|
1392
|
-
accumulated_raw_output, "usage"
|
|
1393
|
-
):
|
|
1394
|
-
return
|
|
1395
1554
|
|
|
1396
|
-
|
|
1397
|
-
|
|
1555
|
+
# Handle both OpenAI/Anthropic-style (usage) and Gemini-style (usage_metadata)
|
|
1556
|
+
current_usage = getattr(metadata_raw_output, "usage", None) or getattr(
|
|
1557
|
+
metadata_raw_output, "usage_metadata", None
|
|
1558
|
+
)
|
|
1559
|
+
accumulated_usage = getattr(accumulated_raw_output, "usage", None) or getattr(
|
|
1560
|
+
accumulated_raw_output, "usage_metadata", None
|
|
1561
|
+
)
|
|
1398
1562
|
|
|
1399
|
-
|
|
1563
|
+
if not current_usage or not accumulated_usage:
|
|
1564
|
+
return
|
|
1565
|
+
|
|
1566
|
+
# Handle both OpenAI-style (completion_tokens, prompt_tokens),
|
|
1567
|
+
# Anthropic-style (output_tokens, input_tokens),
|
|
1568
|
+
# and Gemini-style (candidates_token_count, prompt_token_count) fields
|
|
1569
|
+
token_attrs = [
|
|
1570
|
+
"completion_tokens",
|
|
1571
|
+
"prompt_tokens",
|
|
1572
|
+
"total_tokens",
|
|
1573
|
+
"input_tokens",
|
|
1574
|
+
"output_tokens",
|
|
1575
|
+
"candidates_token_count",
|
|
1576
|
+
"prompt_token_count",
|
|
1577
|
+
"total_token_count",
|
|
1578
|
+
]
|
|
1579
|
+
for attr in token_attrs:
|
|
1400
1580
|
if hasattr(current_usage, attr) and hasattr(accumulated_usage, attr):
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1581
|
+
current_val = getattr(current_usage, attr) or 0
|
|
1582
|
+
accumulated_val = getattr(accumulated_usage, attr) or 0
|
|
1583
|
+
setattr(accumulated_usage, attr, accumulated_val + current_val)
|
|
1584
|
+
|
|
1585
|
+
# Handle Anthropic cache tokens and Gemini cached tokens
|
|
1586
|
+
cache_attrs = [
|
|
1587
|
+
"cache_creation_input_tokens",
|
|
1588
|
+
"cache_read_input_tokens",
|
|
1589
|
+
"cached_content_token_count",
|
|
1590
|
+
]
|
|
1591
|
+
for attr in cache_attrs:
|
|
1592
|
+
if hasattr(current_usage, attr) and hasattr(accumulated_usage, attr):
|
|
1593
|
+
current_val = getattr(current_usage, attr) or 0
|
|
1594
|
+
accumulated_val = getattr(accumulated_usage, attr) or 0
|
|
1595
|
+
setattr(accumulated_usage, attr, accumulated_val + current_val)
|
|
1406
1596
|
|
|
1407
1597
|
for detail_attr in ["completion_tokens_details", "prompt_tokens_details"]:
|
|
1408
1598
|
if not hasattr(current_usage, detail_attr) or not hasattr(
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from ... import core
|
|
2
|
+
from ...backend.engines.drawing.engine_gemini_image import GeminiImageResult
|
|
3
|
+
from ...symbol import Expression
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class nanobanana(Expression):
|
|
7
|
+
def __init__(self, *args, **kwargs):
|
|
8
|
+
super().__init__(*args, **kwargs)
|
|
9
|
+
self.name = self.__class__.__name__
|
|
10
|
+
|
|
11
|
+
def __call__(
|
|
12
|
+
self,
|
|
13
|
+
prompt: str | None = None,
|
|
14
|
+
operation: str = "create",
|
|
15
|
+
**kwargs,
|
|
16
|
+
) -> list:
|
|
17
|
+
prompt = self._to_symbol(prompt)
|
|
18
|
+
|
|
19
|
+
@core.draw(operation=operation, **kwargs)
|
|
20
|
+
def _func(_) -> GeminiImageResult:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
return _func(prompt).value
|
symai/interfaces.py
CHANGED
|
@@ -42,6 +42,8 @@ def _add_symbolic_interface(mapping):
|
|
|
42
42
|
def _resolve_drawing_interface_name(drawing_engine_model):
|
|
43
43
|
if drawing_engine_model.startswith("flux"):
|
|
44
44
|
return "flux"
|
|
45
|
+
if drawing_engine_model.startswith(("gemini-2.5-flash-image", "gemini-3-pro-image-preview")):
|
|
46
|
+
return "nanobanana"
|
|
45
47
|
if drawing_engine_model.startswith("dall-e-"):
|
|
46
48
|
return "dall_e"
|
|
47
49
|
if drawing_engine_model.startswith("gpt-image-"):
|
symai/ops/primitives.py
CHANGED
|
@@ -745,12 +745,6 @@ class OperatorPrimitives(Primitive):
|
|
|
745
745
|
Returns:
|
|
746
746
|
Symbol: A new symbol with the result of the OR operation.
|
|
747
747
|
"""
|
|
748
|
-
# Exclude the evaluation for the Aggregator class; keep import local to avoid ops.primitives <-> collect.stats cycle.
|
|
749
|
-
from ..collect.stats import Aggregator # noqa
|
|
750
|
-
|
|
751
|
-
if isinstance(other, Aggregator):
|
|
752
|
-
return NotImplemented
|
|
753
|
-
|
|
754
748
|
result = self.__try_type_specific_func(
|
|
755
749
|
other, lambda self, other: self.value | other.value, op="|"
|
|
756
750
|
)
|
|
@@ -777,12 +771,6 @@ class OperatorPrimitives(Primitive):
|
|
|
777
771
|
Returns:
|
|
778
772
|
Symbol: A new Symbol object with the concatenated value.
|
|
779
773
|
"""
|
|
780
|
-
# Exclude the evaluation for the Aggregator class; keep import local to avoid ops.primitives <-> collect.stats cycle.
|
|
781
|
-
from ..collect.stats import Aggregator # noqa
|
|
782
|
-
|
|
783
|
-
if isinstance(other, Aggregator):
|
|
784
|
-
return NotImplemented
|
|
785
|
-
|
|
786
774
|
result = self.__try_type_specific_func(
|
|
787
775
|
other, lambda self, other: self.value | other.value, op="|"
|
|
788
776
|
)
|
|
@@ -811,12 +799,6 @@ class OperatorPrimitives(Primitive):
|
|
|
811
799
|
Returns:
|
|
812
800
|
Symbol: A new Symbol object with the concatenated value.
|
|
813
801
|
"""
|
|
814
|
-
# Exclude the evaluation for the Aggregator class; keep import local to avoid ops.primitives <-> collect.stats cycle.
|
|
815
|
-
from ..collect.stats import Aggregator # noqa
|
|
816
|
-
|
|
817
|
-
if isinstance(other, Aggregator):
|
|
818
|
-
return NotImplemented
|
|
819
|
-
|
|
820
802
|
result = self.__try_type_specific_func(
|
|
821
803
|
other, lambda self, other: self.value | other.value, op="|="
|
|
822
804
|
)
|
symai/shellsv.py
CHANGED
|
@@ -1070,14 +1070,9 @@ def run(auto_query_on_error=False, conversation_style=None, verbose=False):
|
|
|
1070
1070
|
) = styles_
|
|
1071
1071
|
state.use_styles = True
|
|
1072
1072
|
|
|
1073
|
-
if SYMSH_CONFIG
|
|
1073
|
+
if SYMSH_CONFIG.get("show-splash-screen", True):
|
|
1074
1074
|
show_intro_menu()
|
|
1075
|
-
|
|
1076
|
-
SYMSH_CONFIG["show-splash-screen"] = False
|
|
1077
|
-
# save config
|
|
1078
|
-
_config_path = HOME_PATH / "symsh.config.json"
|
|
1079
|
-
with _config_path.open("w") as f:
|
|
1080
|
-
json.dump(SYMSH_CONFIG, f, indent=4)
|
|
1075
|
+
|
|
1081
1076
|
if "plugin_prefix" not in SYMSH_CONFIG:
|
|
1082
1077
|
SYMSH_CONFIG["plugin_prefix"] = None
|
|
1083
1078
|
|