Skip to content

Commit 57c1ea3

Browse files
authored
Merge branch 'master' into fix-reset-bug_a7m
2 parents 7aad8df + 7af55a3 commit 57c1ea3

30 files changed

+1483
-857
lines changed

.github/ISSUE_TEMPLATE/bug_report.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ body:
2626
attributes:
2727
label: What version of camel are you using?
2828
description: Run command `python3 -c 'print(__import__("camel").__version__)'` in your shell and paste the output here.
29-
placeholder: E.g., 0.2.80a3
29+
placeholder: E.g., 0.2.80
3030
validations:
3131
required: true
3232

.github/workflows/build_package.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ jobs:
148148
CRYNUX_API_KEY: "${{ secrets.CRYNUX_API_KEY }}"
149149
NEBIUS_API_KEY: "${{ secrets.NEBIUS_API_KEY }}"
150150
COMETAPI_KEY: "${{ secrets.COMETAPI_KEY }}"
151+
CEREBRAS_API_KEY: "${{ secrets.CEREBRAS_API_KEY }}"
151152
run: |
152153
source test_venv/bin/activate
153154
pytest --fast-test-mode -m "not heavy_dependency" \

.github/workflows/pytest_package.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ jobs:
9191
NEBIUS_API_KEY: "${{ secrets.NEBIUS_API_KEY }}"
9292
COMETAPI_KEY: "${{ secrets.COMETAPI_KEY }}"
9393
MINIMAX_API_KEY: "${{ secrets.MINIMAX_API_KEY }}"
94+
CEREBRAS_API_KEY: "${{ secrets.CEREBRAS_API_KEY }}"
9495
run: |
9596
source .venv/bin/activate
9697
uv pip install -e ".[all, dev, docs]"
@@ -179,6 +180,7 @@ jobs:
179180
AMD_API_KEY: "${{ secrets.AMD_API_KEY }}"
180181
COMETAPI_KEY: "${{ secrets.COMETAPI_KEY }}"
181182
MINIMAX_API_KEY: "${{ secrets.MINIMAX_API_KEY }}"
183+
CEREBRAS_API_KEY: "${{ secrets.CEREBRAS_API_KEY }}"
182184
run: |
183185
source .venv/bin/activate
184186
uv pip install -e ".[all, dev, docs]"
@@ -265,6 +267,7 @@ jobs:
265267
AMD_API_KEY: "${{ secrets.AMD_API_KEY }}"
266268
COMETAPI_KEY: "${{ secrets.COMETAPI_KEY }}"
267269
MINIMAX_API_KEY: "${{ secrets.MINIMAX_API_KEY }}"
270+
CEREBRAS_API_KEY: "${{ secrets.CEREBRAS_API_KEY }}"
268271
run: |
269272
source .venv/bin/activate
270273
uv pip install -e ".[all, dev, docs]"

camel/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from camel.logger import disable_logging, enable_logging, set_log_level
1616

17-
__version__ = '0.2.80a3'
17+
__version__ = '0.2.80'
1818

1919
__all__ = [
2020
'__version__',

camel/agents/chat_agent.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3914,7 +3914,8 @@ async def _aexecute_tool(
39143914

39153915
else:
39163916
# Fallback: synchronous call
3917-
result = tool(**args)
3917+
loop = asyncio.get_running_loop()
3918+
result = await loop.run_in_executor(None, lambda: tool(**args))
39183919

39193920
except Exception as e:
39203921
# Capture the error message to prevent framework crash
@@ -4741,7 +4742,10 @@ async def _aexecute_tool_from_stream_data(
47414742

47424743
else:
47434744
# Fallback: synchronous call
4744-
result = tool(**args)
4745+
loop = asyncio.get_running_loop()
4746+
result = await loop.run_in_executor(
4747+
None, lambda: tool(**args)
4748+
)
47454749

47464750
# Create the tool response message
47474751
func_msg = FunctionCallingMessage(
@@ -4916,7 +4920,12 @@ async def _astream_response(
49164920
return
49174921

49184922
# Handle streaming response
4919-
if isinstance(response, AsyncStream):
4923+
# Note: Also check for async generators since some model backends
4924+
# (e.g., GeminiModel) wrap AsyncStream in async generators for
4925+
# additional processing
4926+
if isinstance(response, AsyncStream) or inspect.isasyncgen(
4927+
response
4928+
):
49204929
stream_completed = False
49214930
tool_calls_complete = False
49224931

@@ -5121,7 +5130,10 @@ def _record_assistant_tool_calls_message(
51215130

51225131
async def _aprocess_stream_chunks_with_accumulator(
51235132
self,
5124-
stream: AsyncStream[ChatCompletionChunk],
5133+
stream: Union[
5134+
AsyncStream[ChatCompletionChunk],
5135+
AsyncGenerator[ChatCompletionChunk, None],
5136+
],
51255137
content_accumulator: StreamContentAccumulator,
51265138
accumulated_tool_calls: Dict[str, Any],
51275139
tool_call_records: List[ToolCallingRecord],

camel/logger.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ def _configure_library_logging():
3636
f"CAMEL library logging has been configured "
3737
f"(level: {_logger.getEffectiveLevel()}). "
3838
f"To change level, use set_log_level() or "
39-
"set CAMEL_LOGGING_LEVEL env var. To disable logging, "
40-
"set CAMEL_LOGGING_DISABLED=true or use disable_logging()"
39+
f"set CAMEL_LOGGING_LEVEL env var. To disable logging, "
40+
f"set CAMEL_LOGGING_DISABLED=true or use disable_logging()"
4141
)
4242
else:
4343
_logger.debug("Existing logger configuration found, using that.")

camel/models/gemini_model.py

Lines changed: 103 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,19 +122,116 @@ def __init__(
122122
def _process_messages(self, messages) -> List[OpenAIMessage]:
123123
r"""Process the messages for Gemini API to ensure no empty content,
124124
which is not accepted by Gemini. Also preserves thought signatures
125-
required for Gemini 3 Pro function calling and adds fallback signatures
126-
when they are missing.
125+
required for Gemini 3 Pro function calling.
126+
127+
This method also merges consecutive assistant messages with single
128+
tool calls into a single assistant message with multiple tool calls,
129+
as required by Gemini's OpenAI-compatible API for parallel function
130+
calling.
127131
"""
128132
import copy
129133

130-
processed_messages = []
131-
for msg in messages:
132-
# Use deep copy to preserve all nested structures including
133-
# thought signatures in extra_content
134+
processed_messages: List[OpenAIMessage] = []
135+
i = 0
136+
n = len(messages)
137+
138+
while i < n:
139+
msg = messages[i]
140+
141+
# Check if this is an assistant message with a single tool_call
142+
# that might need to be merged with subsequent ones
143+
if (
144+
msg.get('role') == 'assistant'
145+
and 'tool_calls' in msg
146+
and isinstance(msg['tool_calls'], list)
147+
and len(msg['tool_calls']) == 1
148+
):
149+
# Look ahead to check if there are more assistant messages
150+
# with single tool calls (interleaved with their tool results)
151+
j = i + 1
152+
has_more_tool_calls = False
153+
154+
# Collect tool_call_ids we've seen so far
155+
first_tool_call_id = msg['tool_calls'][0].get('id')
156+
seen_tool_call_ids = (
157+
{first_tool_call_id} if first_tool_call_id else set()
158+
)
159+
160+
# Scan ahead to find pattern: tool_result, assistant,
161+
# tool_result, ...
162+
while j < n:
163+
next_msg = messages[j]
164+
next_role = next_msg.get('role')
165+
166+
if next_role == 'tool':
167+
# Tool result - check if it belongs to our batch
168+
if next_msg.get('tool_call_id') in seen_tool_call_ids:
169+
j += 1
170+
continue
171+
else:
172+
# Tool result for unknown call, stop scanning
173+
break
174+
elif (
175+
next_role == 'assistant'
176+
and 'tool_calls' in next_msg
177+
and isinstance(next_msg['tool_calls'], list)
178+
and len(next_msg['tool_calls']) == 1
179+
):
180+
# Another single tool call - mark for merging
181+
has_more_tool_calls = True
182+
tc_id = next_msg['tool_calls'][0].get('id')
183+
if tc_id:
184+
seen_tool_call_ids.add(tc_id)
185+
j += 1
186+
continue
187+
else:
188+
# Something else, stop scanning
189+
break
190+
191+
if has_more_tool_calls:
192+
# Need to merge: collect all tool calls and results
193+
merged_tool_calls = []
194+
tool_results = []
195+
is_first = True
196+
197+
for k in range(i, j):
198+
m = messages[k]
199+
if m.get('role') == 'assistant' and 'tool_calls' in m:
200+
tc = m['tool_calls'][0]
201+
if is_first:
202+
# Keep extra_content only on first tool call
203+
merged_tool_calls.append(copy.deepcopy(tc))
204+
is_first = False
205+
else:
206+
# Remove extra_content from subsequent tool
207+
# calls
208+
tc_copy = {
209+
k: v
210+
for k, v in tc.items()
211+
if k != 'extra_content'
212+
}
213+
merged_tool_calls.append(tc_copy)
214+
elif m.get('role') == 'tool':
215+
tool_results.append(copy.deepcopy(m))
216+
217+
# Build merged assistant message
218+
merged_msg = copy.deepcopy(msg)
219+
merged_msg['tool_calls'] = merged_tool_calls
220+
if 'content' in merged_msg and merged_msg['content'] == '':
221+
merged_msg['content'] = 'null'
222+
223+
processed_messages.append(merged_msg)
224+
processed_messages.extend(tool_results)
225+
i = j
226+
continue
227+
228+
# Regular message processing (no merging needed)
134229
msg_copy = copy.deepcopy(msg)
135230
if 'content' in msg_copy and msg_copy['content'] == '':
136231
msg_copy['content'] = 'null'
137232
processed_messages.append(msg_copy)
233+
i += 1
234+
138235
return processed_messages
139236

140237
def _preserve_thought_signatures(

camel/toolkits/function_tool.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212
# limitations under the License.
1313
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
1414
import ast
15+
import asyncio
1516
import inspect
1617
import logging
1718
import textwrap
1819
import warnings
20+
from concurrent.futures import ThreadPoolExecutor
1921
from inspect import Parameter, getsource, signature
2022
from typing import Any, Callable, Dict, Mapping, Optional, Tuple, Type
2123

@@ -31,6 +33,9 @@
3133

3234
logger = logging.getLogger(__name__)
3335

36+
# Shared thread pool for running sync tools without blocking the event loop
37+
_SYNC_TOOL_EXECUTOR = ThreadPoolExecutor(max_workers=64)
38+
3439

3540
def _remove_a_key(d: Dict, remove_key: Any) -> None:
3641
r"""Remove a key from a dictionary recursively."""
@@ -500,7 +505,11 @@ async def async_call(self, *args: Any, **kwargs: Any) -> Any:
500505
if self.is_async:
501506
return await self.func(*args, **kwargs)
502507
else:
503-
return self.func(*args, **kwargs)
508+
# Run sync function in executor to avoid blocking event loop
509+
loop = asyncio.get_running_loop()
510+
return await loop.run_in_executor(
511+
_SYNC_TOOL_EXECUTOR, lambda: self.func(*args, **kwargs)
512+
)
504513

505514
@property
506515
def is_async(self) -> bool:

camel/toolkits/hybrid_browser_toolkit/config_loader.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,14 +150,14 @@ def to_ws_config(self) -> Dict[str, Any]:
150150
"networkIdleTimeout": self.browser_config.network_idle_timeout,
151151
"screenshotTimeout": self.browser_config.screenshot_timeout,
152152
"pageStabilityTimeout": self.browser_config.page_stability_timeout,
153-
"browser_log_to_file": self.toolkit_config.browser_log_to_file,
154-
"log_dir": self.toolkit_config.log_dir,
155-
"session_id": self.toolkit_config.session_id,
156153
"viewport_limit": self.browser_config.viewport_limit,
157154
"connectOverCdp": self.browser_config.connect_over_cdp,
158155
"cdpUrl": self.browser_config.cdp_url,
159156
"cdpKeepCurrentPage": self.browser_config.cdp_keep_current_page,
160157
"fullVisualMode": self.browser_config.full_visual_mode,
158+
"browser_log_to_file": self.toolkit_config.browser_log_to_file,
159+
"log_dir": self.toolkit_config.log_dir,
160+
"session_id": self.toolkit_config.session_id,
161161
}
162162

163163
def get_timeout_config(self) -> Dict[str, Optional[int]]:

0 commit comments

Comments
 (0)