ag2 0.9.3__py3-none-any.whl → 0.9.5__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.
Potentially problematic release.
This version of ag2 might be problematic. Click here for more details.
- {ag2-0.9.3.dist-info → ag2-0.9.5.dist-info}/METADATA +1 -1
- {ag2-0.9.3.dist-info → ag2-0.9.5.dist-info}/RECORD +32 -25
- autogen/agentchat/contrib/agent_optimizer.py +6 -3
- autogen/agentchat/conversable_agent.py +51 -5
- autogen/agentchat/group/group_utils.py +16 -7
- autogen/agentchat/group/guardrails.py +171 -0
- autogen/agentchat/group/targets/transition_target.py +10 -0
- autogen/agentchat/groupchat.py +93 -6
- autogen/agentchat/realtime/experimental/realtime_swarm.py +2 -0
- autogen/agents/experimental/websurfer/websurfer.py +9 -1
- autogen/code_utils.py +8 -6
- autogen/events/agent_events.py +6 -0
- autogen/events/helpers.py +8 -0
- autogen/mcp/helpers.py +45 -0
- autogen/mcp/mcp_proxy/mcp_proxy.py +2 -3
- autogen/messages/agent_messages.py +1 -1
- autogen/oai/client.py +44 -1
- autogen/oai/gemini.py +39 -24
- autogen/oai/gemini_types.py +1 -1
- autogen/oai/openai_responses.py +426 -0
- autogen/tools/experimental/__init__.py +4 -0
- autogen/tools/experimental/browser_use/browser_use.py +4 -11
- autogen/tools/experimental/firecrawl/__init__.py +7 -0
- autogen/tools/experimental/firecrawl/firecrawl_tool.py +853 -0
- autogen/tools/experimental/searxng/__init__.py +7 -0
- autogen/tools/experimental/searxng/searxng_search.py +141 -0
- autogen/version.py +1 -1
- templates/client_template/main.jinja2 +5 -2
- templates/main.jinja2 +1 -1
- {ag2-0.9.3.dist-info → ag2-0.9.5.dist-info}/WHEEL +0 -0
- {ag2-0.9.3.dist-info → ag2-0.9.5.dist-info}/licenses/LICENSE +0 -0
- {ag2-0.9.3.dist-info → ag2-0.9.5.dist-info}/licenses/NOTICE.md +0 -0
autogen/agentchat/groupchat.py
CHANGED
|
@@ -1023,6 +1023,38 @@ class GroupChat:
|
|
|
1023
1023
|
mentions[agent.name] = count
|
|
1024
1024
|
return mentions
|
|
1025
1025
|
|
|
1026
|
+
def _run_input_guardrails(
|
|
1027
|
+
self,
|
|
1028
|
+
agent: "ConversableAgent",
|
|
1029
|
+
messages: Optional[list[dict[str, Any]]] = None,
|
|
1030
|
+
) -> Optional[str]:
|
|
1031
|
+
"""Run input guardrails for an agent before the reply is generated.
|
|
1032
|
+
Args:
|
|
1033
|
+
agent (ConversableAgent): The agent whose input guardrails to run.
|
|
1034
|
+
messages (Optional[list[dict[str, Any]]]): The messages to check against the guardrails.
|
|
1035
|
+
"""
|
|
1036
|
+
for guardrail in agent.input_guardrails:
|
|
1037
|
+
guardrail_result = guardrail.check(context=messages)
|
|
1038
|
+
|
|
1039
|
+
if guardrail_result.activated:
|
|
1040
|
+
guardrail.target.activate_target(self)
|
|
1041
|
+
return f"{guardrail.activation_message}\nJustification: {guardrail_result.justification}"
|
|
1042
|
+
return None
|
|
1043
|
+
|
|
1044
|
+
def _run_output_guardrails(self, agent: "ConversableAgent", reply: str) -> None:
|
|
1045
|
+
"""Run output guardrails for an agent after the reply is generated.
|
|
1046
|
+
Args:
|
|
1047
|
+
agent (ConversableAgent): The agent whose output guardrails to run.
|
|
1048
|
+
reply (str): The reply generated by the agent.
|
|
1049
|
+
"""
|
|
1050
|
+
for guardrail in agent.output_guardrails:
|
|
1051
|
+
guardrail_result = guardrail.check(context=reply)
|
|
1052
|
+
|
|
1053
|
+
if guardrail_result.activated:
|
|
1054
|
+
guardrail.target.activate_target(self)
|
|
1055
|
+
return f"{guardrail.activation_message}\nJustification: {guardrail_result.justification}"
|
|
1056
|
+
return None
|
|
1057
|
+
|
|
1026
1058
|
|
|
1027
1059
|
@export_module("autogen")
|
|
1028
1060
|
class GroupChatManager(ConversableAgent):
|
|
@@ -1194,8 +1226,17 @@ class GroupChatManager(ConversableAgent):
|
|
|
1194
1226
|
if not silent:
|
|
1195
1227
|
iostream = IOStream.get_default()
|
|
1196
1228
|
iostream.send(GroupChatRunChatEvent(speaker=speaker, silent=silent))
|
|
1197
|
-
|
|
1198
|
-
|
|
1229
|
+
|
|
1230
|
+
guardrails_activated = False
|
|
1231
|
+
guardrails_reply = groupchat._run_input_guardrails(speaker, speaker._oai_messages[self])
|
|
1232
|
+
|
|
1233
|
+
if guardrails_reply is not None:
|
|
1234
|
+
# if a guardrail has been activated, then the next target has been set and the guardrail reply will be sent
|
|
1235
|
+
guardrails_activated = True
|
|
1236
|
+
reply = guardrails_reply
|
|
1237
|
+
else:
|
|
1238
|
+
# let the speaker speak
|
|
1239
|
+
reply = speaker.generate_reply(sender=self)
|
|
1199
1240
|
except KeyboardInterrupt:
|
|
1200
1241
|
# let the admin agent speak if interrupted
|
|
1201
1242
|
if groupchat.admin_name in groupchat.agent_names:
|
|
@@ -1215,6 +1256,15 @@ class GroupChatManager(ConversableAgent):
|
|
|
1215
1256
|
termination_reason = "No reply generated"
|
|
1216
1257
|
break
|
|
1217
1258
|
|
|
1259
|
+
if not guardrails_activated:
|
|
1260
|
+
# if the input guardrails were not activated, and the agent returned a reply
|
|
1261
|
+
guardrails_reply = groupchat._run_output_guardrails(speaker, reply)
|
|
1262
|
+
|
|
1263
|
+
if guardrails_reply is not None:
|
|
1264
|
+
# if a guardrail has been activated, then the next target has been set and the guardrail reply will be sent
|
|
1265
|
+
guardrails_activated = True
|
|
1266
|
+
reply = guardrails_reply
|
|
1267
|
+
|
|
1218
1268
|
# check for "clear history" phrase in reply and activate clear history function if found
|
|
1219
1269
|
if (
|
|
1220
1270
|
groupchat.enable_clear_history
|
|
@@ -1233,7 +1283,11 @@ class GroupChatManager(ConversableAgent):
|
|
|
1233
1283
|
a.previous_cache = None
|
|
1234
1284
|
|
|
1235
1285
|
if termination_reason:
|
|
1236
|
-
iostream.send(
|
|
1286
|
+
iostream.send(
|
|
1287
|
+
TerminationEvent(
|
|
1288
|
+
termination_reason=termination_reason, sender=self, recipient=speaker if speaker else None
|
|
1289
|
+
)
|
|
1290
|
+
)
|
|
1237
1291
|
|
|
1238
1292
|
return True, None
|
|
1239
1293
|
|
|
@@ -1287,8 +1341,19 @@ class GroupChatManager(ConversableAgent):
|
|
|
1287
1341
|
try:
|
|
1288
1342
|
# select the next speaker
|
|
1289
1343
|
speaker = await groupchat.a_select_speaker(speaker, self)
|
|
1290
|
-
|
|
1291
|
-
|
|
1344
|
+
if not silent:
|
|
1345
|
+
iostream.send(GroupChatRunChatEvent(speaker=speaker, silent=silent))
|
|
1346
|
+
|
|
1347
|
+
guardrails_activated = False
|
|
1348
|
+
guardrails_reply = groupchat._run_input_guardrails(speaker, speaker._oai_messages[self])
|
|
1349
|
+
|
|
1350
|
+
if guardrails_reply is not None:
|
|
1351
|
+
# if a guardrail has been activated, then the next target has been set and the guardrail reply will be sent
|
|
1352
|
+
guardrails_activated = True
|
|
1353
|
+
reply = guardrails_reply
|
|
1354
|
+
else:
|
|
1355
|
+
# let the speaker speak
|
|
1356
|
+
reply = await speaker.a_generate_reply(sender=self)
|
|
1292
1357
|
except KeyboardInterrupt:
|
|
1293
1358
|
# let the admin agent speak if interrupted
|
|
1294
1359
|
if groupchat.admin_name in groupchat.agent_names:
|
|
@@ -1308,6 +1373,24 @@ class GroupChatManager(ConversableAgent):
|
|
|
1308
1373
|
termination_reason = "No reply generated"
|
|
1309
1374
|
break
|
|
1310
1375
|
|
|
1376
|
+
if not guardrails_activated:
|
|
1377
|
+
# if the input guardrails were not activated, and the agent returned a reply
|
|
1378
|
+
guardrails_reply = groupchat._run_output_guardrails(speaker, reply)
|
|
1379
|
+
|
|
1380
|
+
if guardrails_reply is not None:
|
|
1381
|
+
# if a guardrail has been activated, then the next target has been set and the guardrail reply will be sent
|
|
1382
|
+
guardrails_activated = True
|
|
1383
|
+
reply = guardrails_reply
|
|
1384
|
+
|
|
1385
|
+
# check for "clear history" phrase in reply and activate clear history function if found
|
|
1386
|
+
if (
|
|
1387
|
+
groupchat.enable_clear_history
|
|
1388
|
+
and isinstance(reply, dict)
|
|
1389
|
+
and reply["content"]
|
|
1390
|
+
and "CLEAR HISTORY" in reply["content"].upper()
|
|
1391
|
+
):
|
|
1392
|
+
reply["content"] = self.clear_agents_history(reply, groupchat)
|
|
1393
|
+
|
|
1311
1394
|
# The speaker sends the message without requesting a reply
|
|
1312
1395
|
await speaker.a_send(reply, self, request_reply=False, silent=silent)
|
|
1313
1396
|
message = self.last_message(speaker)
|
|
@@ -1317,7 +1400,11 @@ class GroupChatManager(ConversableAgent):
|
|
|
1317
1400
|
a.previous_cache = None
|
|
1318
1401
|
|
|
1319
1402
|
if termination_reason:
|
|
1320
|
-
iostream.send(
|
|
1403
|
+
iostream.send(
|
|
1404
|
+
TerminationEvent(
|
|
1405
|
+
termination_reason=termination_reason, sender=self, recipient=speaker if speaker else None
|
|
1406
|
+
)
|
|
1407
|
+
)
|
|
1321
1408
|
|
|
1322
1409
|
return True, None
|
|
1323
1410
|
|
|
@@ -12,7 +12,9 @@ from ....tools.experimental import (
|
|
|
12
12
|
BrowserUseTool,
|
|
13
13
|
Crawl4AITool,
|
|
14
14
|
DuckDuckGoSearchTool,
|
|
15
|
+
FirecrawlTool,
|
|
15
16
|
PerplexitySearchTool,
|
|
17
|
+
SearxngSearchTool,
|
|
16
18
|
TavilySearchTool,
|
|
17
19
|
)
|
|
18
20
|
|
|
@@ -28,7 +30,9 @@ class WebSurferAgent(ConversableAgent):
|
|
|
28
30
|
*,
|
|
29
31
|
llm_config: Optional[Union[LLMConfig, dict[str, Any]]] = None,
|
|
30
32
|
web_tool_llm_config: Optional[Union[LLMConfig, dict[str, Any]]] = None,
|
|
31
|
-
web_tool: Literal[
|
|
33
|
+
web_tool: Literal[
|
|
34
|
+
"browser_use", "crawl4ai", "duckduckgo", "firecrawl", "perplexity", "tavily", "searxng"
|
|
35
|
+
] = "browser_use",
|
|
32
36
|
web_tool_kwargs: Optional[dict[str, Any]] = None,
|
|
33
37
|
**kwargs: Any,
|
|
34
38
|
) -> None:
|
|
@@ -48,12 +52,16 @@ class WebSurferAgent(ConversableAgent):
|
|
|
48
52
|
self.tool: Tool = BrowserUseTool(llm_config=web_tool_llm_config, **web_tool_kwargs) # type: ignore[arg-type]
|
|
49
53
|
elif web_tool == "crawl4ai":
|
|
50
54
|
self.tool = Crawl4AITool(llm_config=web_tool_llm_config, **web_tool_kwargs)
|
|
55
|
+
elif web_tool == "firecrawl":
|
|
56
|
+
self.tool = FirecrawlTool(llm_config=web_tool_llm_config, **web_tool_kwargs)
|
|
51
57
|
elif web_tool == "perplexity":
|
|
52
58
|
self.tool = PerplexitySearchTool(**web_tool_kwargs)
|
|
53
59
|
elif web_tool == "tavily":
|
|
54
60
|
self.tool = TavilySearchTool(llm_config=web_tool_llm_config, **web_tool_kwargs)
|
|
55
61
|
elif web_tool == "duckduckgo":
|
|
56
62
|
self.tool = DuckDuckGoSearchTool(**web_tool_kwargs)
|
|
63
|
+
elif web_tool == "searxng":
|
|
64
|
+
self.tool = SearxngSearchTool(**web_tool_kwargs)
|
|
57
65
|
else:
|
|
58
66
|
raise ValueError(f"Unsupported {web_tool=}.")
|
|
59
67
|
|
autogen/code_utils.py
CHANGED
|
@@ -72,18 +72,20 @@ def content_str(content: Union[str, list[Union[UserMessageTextContentPart, UserM
|
|
|
72
72
|
if not isinstance(content, list):
|
|
73
73
|
raise TypeError(f"content must be None, str, or list, but got {type(content)}")
|
|
74
74
|
|
|
75
|
-
rst =
|
|
75
|
+
rst = []
|
|
76
76
|
for item in content:
|
|
77
77
|
if not isinstance(item, dict):
|
|
78
78
|
raise TypeError("Wrong content format: every element should be dict if the content is a list.")
|
|
79
79
|
assert "type" in item, "Wrong content format. Missing 'type' key in content's dict."
|
|
80
|
-
if item["type"]
|
|
81
|
-
rst
|
|
82
|
-
elif item["type"]
|
|
83
|
-
rst
|
|
80
|
+
if item["type"] in ["text", "input_text"]:
|
|
81
|
+
rst.append(item["text"])
|
|
82
|
+
elif item["type"] in ["image_url", "input_image"]:
|
|
83
|
+
rst.append("<image>")
|
|
84
|
+
elif item["type"] in ["function", "tool_call", "tool_calls"]:
|
|
85
|
+
rst.append("<function>" if "name" not in item else f"<function: {item['name']}>")
|
|
84
86
|
else:
|
|
85
87
|
raise ValueError(f"Wrong content format: unknown type {item['type']} within the content")
|
|
86
|
-
return rst
|
|
88
|
+
return "\n".join(rst)
|
|
87
89
|
|
|
88
90
|
|
|
89
91
|
def infer_lang(code: str) -> str:
|
autogen/events/agent_events.py
CHANGED
|
@@ -669,16 +669,22 @@ class TerminationEvent(BaseEvent):
|
|
|
669
669
|
"""When a workflow termination condition is met"""
|
|
670
670
|
|
|
671
671
|
termination_reason: str
|
|
672
|
+
sender: str
|
|
673
|
+
recipient: Optional[str] = None
|
|
672
674
|
|
|
673
675
|
def __init__(
|
|
674
676
|
self,
|
|
675
677
|
*,
|
|
676
678
|
uuid: Optional[UUID] = None,
|
|
679
|
+
sender: Union["Agent", str],
|
|
680
|
+
recipient: Optional[Union["Agent", str]] = None,
|
|
677
681
|
termination_reason: str,
|
|
678
682
|
):
|
|
679
683
|
super().__init__(
|
|
680
684
|
uuid=uuid,
|
|
681
685
|
termination_reason=termination_reason,
|
|
686
|
+
sender=sender.name if hasattr(sender, "name") else sender,
|
|
687
|
+
recipient=recipient.name if hasattr(recipient, "name") else recipient if recipient else None,
|
|
682
688
|
)
|
|
683
689
|
|
|
684
690
|
def print(self, f: Optional[Callable[..., Any]] = None) -> None:
|
autogen/events/helpers.py
CHANGED
|
@@ -13,12 +13,15 @@ logger = logging.getLogger(__name__)
|
|
|
13
13
|
def deprecated_by(
|
|
14
14
|
new_class: type[BaseModel],
|
|
15
15
|
param_mapping: dict[str, str] = None,
|
|
16
|
+
default_params: dict[str, any] = None,
|
|
16
17
|
) -> Callable[[type[BaseModel]], Callable[..., BaseModel]]:
|
|
17
18
|
param_mapping = param_mapping or {}
|
|
19
|
+
default_params = default_params or {}
|
|
18
20
|
|
|
19
21
|
def decorator(
|
|
20
22
|
old_class: type[BaseModel],
|
|
21
23
|
param_mapping: dict[str, str] = param_mapping,
|
|
24
|
+
default_params: dict[str, any] = default_params,
|
|
22
25
|
) -> Callable[..., BaseModel]:
|
|
23
26
|
@wraps(old_class)
|
|
24
27
|
def wrapper(*args, **kwargs) -> BaseModel:
|
|
@@ -28,6 +31,11 @@ def deprecated_by(
|
|
|
28
31
|
# Translate old parameters to new parameters
|
|
29
32
|
new_kwargs = {param_mapping.get(k, k): v for k, v in kwargs.items()}
|
|
30
33
|
|
|
34
|
+
# Add default parameters if not already present
|
|
35
|
+
for key, value in default_params.items():
|
|
36
|
+
if key not in new_kwargs:
|
|
37
|
+
new_kwargs[key] = value
|
|
38
|
+
|
|
31
39
|
# Pass the translated parameters to the new class
|
|
32
40
|
return new_class(*args, **new_kwargs)
|
|
33
41
|
|
autogen/mcp/helpers.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import asyncio
|
|
5
|
+
import os
|
|
6
|
+
import signal
|
|
7
|
+
from asyncio.subprocess import PIPE, Process, create_subprocess_exec
|
|
8
|
+
from contextlib import asynccontextmanager
|
|
9
|
+
from typing import AsyncGenerator, Dict, Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@asynccontextmanager
|
|
13
|
+
async def run_streamable_http_client(
|
|
14
|
+
*, mcp_server_path: str, env_vars: Optional[Dict[str, str]] = None, startup_wait_secs: float = 5.0
|
|
15
|
+
) -> AsyncGenerator[Process, None]:
|
|
16
|
+
"""
|
|
17
|
+
Async context manager to run a Python subprocess for streamable-http with custom env vars.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
mcp_server_path: Path to the Python script to run.
|
|
21
|
+
env_vars: Environment variables to export to the subprocess.
|
|
22
|
+
startup_wait_secs: Time to wait for the server to start (in seconds).
|
|
23
|
+
Yields:
|
|
24
|
+
An asyncio.subprocess.Process object.
|
|
25
|
+
"""
|
|
26
|
+
env = os.environ.copy()
|
|
27
|
+
if env_vars:
|
|
28
|
+
env.update(env_vars)
|
|
29
|
+
|
|
30
|
+
process = await create_subprocess_exec(
|
|
31
|
+
"python", mcp_server_path, "streamable-http", env=env, stdout=PIPE, stderr=PIPE
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Optional startup delay to let the server initialize
|
|
35
|
+
await asyncio.sleep(startup_wait_secs)
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
yield process
|
|
39
|
+
finally:
|
|
40
|
+
if process.returncode is None:
|
|
41
|
+
process.send_signal(signal.SIGINT)
|
|
42
|
+
try:
|
|
43
|
+
await asyncio.wait_for(process.wait(), timeout=5.0)
|
|
44
|
+
except asyncio.TimeoutError:
|
|
45
|
+
process.kill()
|
|
@@ -117,9 +117,8 @@ class MCPProxy:
|
|
|
117
117
|
|
|
118
118
|
return q_params, path_params, body, security
|
|
119
119
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
mcp = FastMCP(title=self._title)
|
|
120
|
+
def get_mcp(self, **settings: Any) -> "FastMCP":
|
|
121
|
+
mcp = FastMCP(title=self._title, **settings)
|
|
123
122
|
|
|
124
123
|
for func in self._registered_funcs:
|
|
125
124
|
try:
|
|
@@ -647,7 +647,7 @@ class UsingAutoReplyMessage(BaseMessage):
|
|
|
647
647
|
f(colored("\n>>>>>>>> USING AUTO REPLY...", "red"), flush=True)
|
|
648
648
|
|
|
649
649
|
|
|
650
|
-
@deprecated_by(TerminationEvent)
|
|
650
|
+
@deprecated_by(TerminationEvent, default_params={"sender": "system"})
|
|
651
651
|
@wrap_message
|
|
652
652
|
class TerminationMessage(BaseMessage):
|
|
653
653
|
"""When a workflow termination condition is met"""
|
autogen/oai/client.py
CHANGED
|
@@ -51,6 +51,8 @@ if openai_result.is_successful:
|
|
|
51
51
|
from openai.types.completion import Completion
|
|
52
52
|
from openai.types.completion_usage import CompletionUsage
|
|
53
53
|
|
|
54
|
+
from autogen.oai.openai_responses import OpenAIResponsesClient
|
|
55
|
+
|
|
54
56
|
if openai.__version__ >= "1.1.0":
|
|
55
57
|
TOOL_ENABLED = True
|
|
56
58
|
ERROR = None
|
|
@@ -314,7 +316,7 @@ class ModelClient(Protocol):
|
|
|
314
316
|
class ModelClientResponseProtocol(Protocol):
|
|
315
317
|
class Choice(Protocol):
|
|
316
318
|
class Message(Protocol):
|
|
317
|
-
content: Optional[str]
|
|
319
|
+
content: Optional[str] | Optional[dict[str, Any]]
|
|
318
320
|
|
|
319
321
|
message: Message
|
|
320
322
|
|
|
@@ -952,6 +954,15 @@ class OpenAIWrapper:
|
|
|
952
954
|
raise ImportError("Please install `boto3` to use the Amazon Bedrock API.")
|
|
953
955
|
client = BedrockClient(response_format=response_format, **openai_config)
|
|
954
956
|
self._clients.append(client)
|
|
957
|
+
elif api_type is not None and api_type.startswith("responses"):
|
|
958
|
+
# OpenAI Responses API (stateful). Reuse the same OpenAI SDK but call the `/responses` endpoint via the new client.
|
|
959
|
+
@require_optional_import("openai>=1.66.2", "openai")
|
|
960
|
+
def create_responses_client() -> "OpenAI":
|
|
961
|
+
client = OpenAI(**openai_config)
|
|
962
|
+
self._clients.append(OpenAIResponsesClient(client, response_format=response_format))
|
|
963
|
+
return client
|
|
964
|
+
|
|
965
|
+
client = create_responses_client()
|
|
955
966
|
else:
|
|
956
967
|
|
|
957
968
|
@require_optional_import("openai>=1.66.2", "openai")
|
|
@@ -1442,3 +1453,35 @@ class OpenAIWrapper:
|
|
|
1442
1453
|
A list of text, or a list of ChatCompletion objects if function_call/tool_calls are present.
|
|
1443
1454
|
"""
|
|
1444
1455
|
return response.message_retrieval_function(response)
|
|
1456
|
+
|
|
1457
|
+
|
|
1458
|
+
# -----------------------------------------------------------------------------
|
|
1459
|
+
# New: Responses API config entry (OpenAI-hosted preview endpoint)
|
|
1460
|
+
# -----------------------------------------------------------------------------
|
|
1461
|
+
|
|
1462
|
+
|
|
1463
|
+
@register_llm_config
|
|
1464
|
+
class OpenAIResponsesLLMConfigEntry(OpenAILLMConfigEntry):
|
|
1465
|
+
"""LLMConfig entry for the OpenAI Responses API (stateful, tool-enabled).
|
|
1466
|
+
|
|
1467
|
+
This reuses all the OpenAI fields but changes *api_type* so the wrapper can
|
|
1468
|
+
route traffic to the `client.responses` endpoint instead of
|
|
1469
|
+
`chat.completions`. It inherits everything else – including reasoning
|
|
1470
|
+
fields – from *OpenAILLMConfigEntry* so users can simply set
|
|
1471
|
+
|
|
1472
|
+
```python
|
|
1473
|
+
{
|
|
1474
|
+
"api_type": "responses", # <-- key differentiator
|
|
1475
|
+
"model": "o3", # reasoning model
|
|
1476
|
+
"reasoning_effort": "medium", # low / medium / high
|
|
1477
|
+
"stream": True,
|
|
1478
|
+
}
|
|
1479
|
+
```
|
|
1480
|
+
"""
|
|
1481
|
+
|
|
1482
|
+
api_type: Literal["responses"] = "responses"
|
|
1483
|
+
tool_choice: Optional[Literal["none", "auto", "required"]] = "auto"
|
|
1484
|
+
built_in_tools: Optional[list[str]] = None
|
|
1485
|
+
|
|
1486
|
+
def create_client(self) -> "ModelClient": # pragma: no cover
|
|
1487
|
+
raise NotImplementedError("Handled via OpenAIWrapper._register_default_client")
|
autogen/oai/gemini.py
CHANGED
|
@@ -54,7 +54,6 @@ from io import BytesIO
|
|
|
54
54
|
from typing import Any, Literal, Optional, Type, Union
|
|
55
55
|
|
|
56
56
|
import requests
|
|
57
|
-
from packaging import version
|
|
58
57
|
from pydantic import BaseModel, Field
|
|
59
58
|
|
|
60
59
|
from ..import_utils import optional_import_block, require_optional_import
|
|
@@ -332,6 +331,12 @@ class GeminiClient:
|
|
|
332
331
|
recitation_part = Part(text="Unsuccessful Finish Reason: RECITATION")
|
|
333
332
|
parts = [recitation_part]
|
|
334
333
|
error_finish_reason = "content_filter" # As per available finish_reason in Choice
|
|
334
|
+
elif not response.candidates[0].content or not response.candidates[0].content.parts:
|
|
335
|
+
error_part = Part(
|
|
336
|
+
text=f"Unsuccessful Finish Reason: ({str(response.candidates[0].finish_reason)}) NO CONTENT RETURNED"
|
|
337
|
+
)
|
|
338
|
+
parts = [error_part]
|
|
339
|
+
error_finish_reason = "content_filter" # No other option in Choice in chat_completion.py
|
|
335
340
|
else:
|
|
336
341
|
parts = response.candidates[0].content.parts
|
|
337
342
|
elif isinstance(response, VertexAIGenerationResponse): # or hasattr(response, "candidates"):
|
|
@@ -570,25 +575,19 @@ class GeminiClient:
|
|
|
570
575
|
|
|
571
576
|
if part_type == "text":
|
|
572
577
|
rst.append(
|
|
573
|
-
VertexAIContent(parts=parts, role=role)
|
|
574
|
-
if self.use_vertexai
|
|
575
|
-
else rst.append(Content(parts=parts, role=role))
|
|
578
|
+
VertexAIContent(parts=parts, role=role) if self.use_vertexai else Content(parts=parts, role=role)
|
|
576
579
|
)
|
|
577
|
-
elif part_type == "
|
|
578
|
-
# Function
|
|
579
|
-
role = "
|
|
580
|
+
elif part_type == "tool_call":
|
|
581
|
+
# Function calls should be from the model/assistant
|
|
582
|
+
role = "model"
|
|
580
583
|
rst.append(
|
|
581
|
-
VertexAIContent(parts=parts, role=role)
|
|
582
|
-
if self.use_vertexai
|
|
583
|
-
else rst.append(Content(parts=parts, role=role))
|
|
584
|
+
VertexAIContent(parts=parts, role=role) if self.use_vertexai else Content(parts=parts, role=role)
|
|
584
585
|
)
|
|
585
|
-
elif part_type == "
|
|
586
|
-
# Function
|
|
587
|
-
role = "
|
|
586
|
+
elif part_type == "tool":
|
|
587
|
+
# Function responses should be from the user
|
|
588
|
+
role = "user"
|
|
588
589
|
rst.append(
|
|
589
|
-
VertexAIContent(parts=parts, role=role)
|
|
590
|
-
if self.use_vertexai
|
|
591
|
-
else rst.append(Content(parts=parts, role=role))
|
|
590
|
+
VertexAIContent(parts=parts, role=role) if self.use_vertexai else Content(parts=parts, role=role)
|
|
592
591
|
)
|
|
593
592
|
elif part_type == "image":
|
|
594
593
|
# Image has multiple parts, some can be text and some can be image based
|
|
@@ -608,14 +607,14 @@ class GeminiClient:
|
|
|
608
607
|
rst.append(
|
|
609
608
|
VertexAIContent(parts=text_parts, role=role)
|
|
610
609
|
if self.use_vertexai
|
|
611
|
-
else
|
|
610
|
+
else Content(parts=text_parts, role=role)
|
|
612
611
|
)
|
|
613
612
|
|
|
614
613
|
if len(image_parts) > 0:
|
|
615
614
|
rst.append(
|
|
616
615
|
VertexAIContent(parts=image_parts, role=role)
|
|
617
616
|
if self.use_vertexai
|
|
618
|
-
else
|
|
617
|
+
else Content(parts=image_parts, role=role)
|
|
619
618
|
)
|
|
620
619
|
|
|
621
620
|
if len(rst) != 0 and rst[-1] is None:
|
|
@@ -908,18 +907,25 @@ def calculate_gemini_cost(use_vertexai: bool, input_tokens: int, output_tokens:
|
|
|
908
907
|
# https://cloud.google.com/vertex-ai/generative-ai/pricing#vertex-ai-pricing
|
|
909
908
|
|
|
910
909
|
if (
|
|
911
|
-
"gemini-2.5-pro
|
|
912
|
-
or "gemini-2.5-pro-
|
|
910
|
+
model_name == "gemini-2.5-pro"
|
|
911
|
+
or "gemini-2.5-pro-preview-06-05" in model_name
|
|
913
912
|
or "gemini-2.5-pro-preview-05-06" in model_name
|
|
913
|
+
or "gemini-2.5-pro-preview-03-25" in model_name
|
|
914
914
|
):
|
|
915
915
|
if up_to_200k:
|
|
916
916
|
return total_cost_mil(1.25, 10)
|
|
917
917
|
else:
|
|
918
918
|
return total_cost_mil(2.5, 15)
|
|
919
919
|
|
|
920
|
-
elif "gemini-2.5-flash
|
|
920
|
+
elif "gemini-2.5-flash" in model_name:
|
|
921
|
+
return total_cost_mil(0.3, 2.5)
|
|
922
|
+
|
|
923
|
+
elif "gemini-2.5-flash-preview-04-17" in model_name or "gemini-2.5-flash-preview-05-20" in model_name:
|
|
921
924
|
return total_cost_mil(0.15, 0.6) # NON-THINKING OUTPUT PRICE, $3 FOR THINKING!
|
|
922
925
|
|
|
926
|
+
elif "gemini-2.5-flash-lite-preview-06-17" in model_name:
|
|
927
|
+
return total_cost_mil(0.1, 0.4)
|
|
928
|
+
|
|
923
929
|
elif "gemini-2.0-flash-lite" in model_name:
|
|
924
930
|
return total_cost_mil(0.075, 0.3)
|
|
925
931
|
|
|
@@ -952,9 +958,10 @@ def calculate_gemini_cost(use_vertexai: bool, input_tokens: int, output_tokens:
|
|
|
952
958
|
# Non-Vertex AI pricing
|
|
953
959
|
|
|
954
960
|
if (
|
|
955
|
-
"gemini-2.5-pro
|
|
956
|
-
or "gemini-2.5-pro-
|
|
961
|
+
model_name == "gemini-2.5-pro"
|
|
962
|
+
or "gemini-2.5-pro-preview-06-05" in model_name
|
|
957
963
|
or "gemini-2.5-pro-preview-05-06" in model_name
|
|
964
|
+
or "gemini-2.5-pro-preview-03-25" in model_name
|
|
958
965
|
):
|
|
959
966
|
# https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-pro-preview
|
|
960
967
|
if up_to_200k:
|
|
@@ -962,10 +969,18 @@ def calculate_gemini_cost(use_vertexai: bool, input_tokens: int, output_tokens:
|
|
|
962
969
|
else:
|
|
963
970
|
return total_cost_mil(2.5, 15)
|
|
964
971
|
|
|
965
|
-
elif "gemini-2.5-flash
|
|
972
|
+
elif "gemini-2.5-flash" in model_name:
|
|
973
|
+
# https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-flash
|
|
974
|
+
return total_cost_mil(0.3, 2.5)
|
|
975
|
+
|
|
976
|
+
elif "gemini-2.5-flash-preview-04-17" in model_name or "gemini-2.5-flash-preview-05-20" in model_name:
|
|
966
977
|
# https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-flash
|
|
967
978
|
return total_cost_mil(0.15, 0.6)
|
|
968
979
|
|
|
980
|
+
elif "gemini-2.5-flash-lite-preview-06-17" in model_name:
|
|
981
|
+
# https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-flash-lite
|
|
982
|
+
return total_cost_mil(0.1, 0.4)
|
|
983
|
+
|
|
969
984
|
elif "gemini-2.0-flash-lite" in model_name:
|
|
970
985
|
# https://ai.google.dev/gemini-api/docs/pricing#gemini-2.0-flash-lite
|
|
971
986
|
return total_cost_mil(0.075, 0.3)
|
autogen/oai/gemini_types.py
CHANGED
|
@@ -91,7 +91,7 @@ class CaseInSensitiveEnum(str, enum.Enum):
|
|
|
91
91
|
try:
|
|
92
92
|
# Creating a enum instance based on the value
|
|
93
93
|
# We need to use super() to avoid infinite recursion.
|
|
94
|
-
unknown_enum_val = super().__new__(cls, value)
|
|
94
|
+
unknown_enum_val = super(CaseInSensitiveEnum, cls).__new__(cls, value)
|
|
95
95
|
unknown_enum_val._name_ = str(value) # pylint: disable=protected-access
|
|
96
96
|
unknown_enum_val._value_ = value # pylint: disable=protected-access
|
|
97
97
|
return unknown_enum_val
|