ag2 0.9.10__py3-none-any.whl → 0.10.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.
Potentially problematic release.
This version of ag2 might be problematic. Click here for more details.
- {ag2-0.9.10.dist-info → ag2-0.10.0.dist-info}/METADATA +14 -7
- {ag2-0.9.10.dist-info → ag2-0.10.0.dist-info}/RECORD +42 -24
- autogen/a2a/__init__.py +36 -0
- autogen/a2a/agent_executor.py +105 -0
- autogen/a2a/client.py +280 -0
- autogen/a2a/errors.py +18 -0
- autogen/a2a/httpx_client_factory.py +79 -0
- autogen/a2a/server.py +221 -0
- autogen/a2a/utils.py +165 -0
- autogen/agentchat/__init__.py +3 -0
- autogen/agentchat/agent.py +0 -2
- autogen/agentchat/chat.py +5 -1
- autogen/agentchat/contrib/llava_agent.py +1 -13
- autogen/agentchat/conversable_agent.py +178 -73
- autogen/agentchat/group/group_tool_executor.py +46 -15
- autogen/agentchat/group/guardrails.py +41 -33
- autogen/agentchat/group/multi_agent_chat.py +53 -0
- autogen/agentchat/group/safeguards/api.py +19 -2
- autogen/agentchat/group/safeguards/enforcer.py +134 -40
- autogen/agentchat/groupchat.py +45 -33
- autogen/agentchat/realtime/experimental/realtime_swarm.py +1 -3
- autogen/interop/pydantic_ai/pydantic_ai.py +1 -1
- autogen/llm_config/client.py +3 -2
- autogen/oai/bedrock.py +0 -13
- autogen/oai/client.py +15 -8
- autogen/oai/client_utils.py +30 -0
- autogen/oai/cohere.py +0 -10
- autogen/remote/__init__.py +18 -0
- autogen/remote/agent.py +199 -0
- autogen/remote/agent_service.py +142 -0
- autogen/remote/errors.py +17 -0
- autogen/remote/httpx_client_factory.py +131 -0
- autogen/remote/protocol.py +37 -0
- autogen/remote/retry.py +102 -0
- autogen/remote/runtime.py +96 -0
- autogen/testing/__init__.py +12 -0
- autogen/testing/messages.py +45 -0
- autogen/testing/test_agent.py +111 -0
- autogen/version.py +1 -1
- {ag2-0.9.10.dist-info → ag2-0.10.0.dist-info}/WHEEL +0 -0
- {ag2-0.9.10.dist-info → ag2-0.10.0.dist-info}/licenses/LICENSE +0 -0
- {ag2-0.9.10.dist-info → ag2-0.10.0.dist-info}/licenses/NOTICE.md +0 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
from typing import Any, Literal, cast
|
|
7
|
+
|
|
8
|
+
from autogen.agentchat import ConversableAgent
|
|
9
|
+
from autogen.agentchat.conversable_agent import normilize_message_to_oai
|
|
10
|
+
from autogen.agentchat.group.context_variables import ContextVariables
|
|
11
|
+
from autogen.agentchat.group.group_tool_executor import GroupToolExecutor
|
|
12
|
+
from autogen.agentchat.group.reply_result import ReplyResult
|
|
13
|
+
from autogen.agentchat.group.targets.transition_target import TransitionTarget
|
|
14
|
+
|
|
15
|
+
from .protocol import RemoteService, RequestMessage, ResponseMessage, get_tool_names
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AgentService(RemoteService):
|
|
19
|
+
def __init__(self, agent: ConversableAgent) -> None:
|
|
20
|
+
self.name = agent.name
|
|
21
|
+
self.agent = agent
|
|
22
|
+
|
|
23
|
+
async def __call__(self, state: RequestMessage) -> ResponseMessage | None:
|
|
24
|
+
out_message: dict[str, Any] | None
|
|
25
|
+
if guardrail_result := self.agent.run_input_guardrails(state.messages):
|
|
26
|
+
# input guardrail activated by initial messages
|
|
27
|
+
_, out_message = normilize_message_to_oai(guardrail_result.reply, self.agent.name, role="assistant")
|
|
28
|
+
return ResponseMessage(messages=[out_message], context=state.context)
|
|
29
|
+
|
|
30
|
+
context_variables = ContextVariables(state.context)
|
|
31
|
+
tool_executor = self._make_tool_executor(context_variables)
|
|
32
|
+
|
|
33
|
+
local_history: list[dict[str, Any]] = []
|
|
34
|
+
while True:
|
|
35
|
+
messages = state.messages + local_history
|
|
36
|
+
|
|
37
|
+
# TODO: catch ask user input event
|
|
38
|
+
is_final, _ = await self.agent.a_check_termination_and_human_reply(messages)
|
|
39
|
+
if is_final:
|
|
40
|
+
break
|
|
41
|
+
|
|
42
|
+
reply = await self.agent.a_generate_reply(
|
|
43
|
+
messages,
|
|
44
|
+
exclude=(
|
|
45
|
+
ConversableAgent.check_termination_and_human_reply,
|
|
46
|
+
ConversableAgent.a_check_termination_and_human_reply,
|
|
47
|
+
ConversableAgent.generate_oai_reply,
|
|
48
|
+
ConversableAgent.a_generate_oai_reply,
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if not reply:
|
|
53
|
+
_, reply = await self.agent.a_generate_oai_reply(
|
|
54
|
+
messages,
|
|
55
|
+
tools=state.client_tools,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
should_continue, out_message = self._add_message_to_local_history(reply, role="assistant")
|
|
59
|
+
if out_message:
|
|
60
|
+
local_history.append(out_message)
|
|
61
|
+
if not should_continue:
|
|
62
|
+
break
|
|
63
|
+
out_message = cast(dict[str, Any], out_message)
|
|
64
|
+
|
|
65
|
+
called_tools = get_tool_names(out_message.get("tool_calls", []))
|
|
66
|
+
if state.client_tool_names.intersection(called_tools):
|
|
67
|
+
break # return client tool execution command back to client
|
|
68
|
+
|
|
69
|
+
tool_result, updated_context_variables = self._try_execute_local_tool(tool_executor, out_message)
|
|
70
|
+
|
|
71
|
+
if updated_context_variables:
|
|
72
|
+
context_variables.update(updated_context_variables.to_dict())
|
|
73
|
+
|
|
74
|
+
should_continue, out_message = self._add_message_to_local_history(tool_result, role="tool")
|
|
75
|
+
if out_message:
|
|
76
|
+
local_history.append(out_message)
|
|
77
|
+
if not should_continue:
|
|
78
|
+
break
|
|
79
|
+
|
|
80
|
+
if not local_history:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
return ResponseMessage(messages=local_history, context=context_variables.data or None)
|
|
84
|
+
|
|
85
|
+
def _add_message_to_local_history(
|
|
86
|
+
self, message: str | dict[str, Any] | None, role: str
|
|
87
|
+
) -> tuple[Literal[True], dict[str, Any]] | tuple[Literal[False], dict[str, Any] | None]:
|
|
88
|
+
if message is None:
|
|
89
|
+
return False, None # output message is empty, interrupt the loop
|
|
90
|
+
|
|
91
|
+
if guardrail_result := self.agent.run_output_guardrails(message):
|
|
92
|
+
_, out_message = normilize_message_to_oai(guardrail_result.reply, self.agent.name, role=role)
|
|
93
|
+
return False, out_message # output guardrail activated, interrupt the loop
|
|
94
|
+
|
|
95
|
+
valid, out_message = normilize_message_to_oai(message, self.agent.name, role=role)
|
|
96
|
+
if not valid:
|
|
97
|
+
return False, None # tool result is not valid OAI message, interrupt the loop
|
|
98
|
+
|
|
99
|
+
return True, out_message
|
|
100
|
+
|
|
101
|
+
def _make_tool_executor(self, context_variables: ContextVariables) -> GroupToolExecutor:
|
|
102
|
+
tool_executor = GroupToolExecutor()
|
|
103
|
+
for tool in self.agent.tools:
|
|
104
|
+
# TODO: inject ChatContext to tool
|
|
105
|
+
new_tool = tool_executor.make_tool_copy_with_context_variables(tool, context_variables) or tool
|
|
106
|
+
tool_executor.register_for_execution(serialize=False, silent_override=True)(new_tool)
|
|
107
|
+
return tool_executor
|
|
108
|
+
|
|
109
|
+
def _try_execute_local_tool(
|
|
110
|
+
self,
|
|
111
|
+
tool_executor: GroupToolExecutor,
|
|
112
|
+
tool_message: dict[str, Any],
|
|
113
|
+
) -> tuple[dict[str, Any] | None, ContextVariables | None]:
|
|
114
|
+
tool_result: dict[str, Any] | None = None
|
|
115
|
+
updated_context_variables: ContextVariables | None = None
|
|
116
|
+
|
|
117
|
+
if "tool_calls" in tool_message:
|
|
118
|
+
_, tool_result = tool_executor.generate_tool_calls_reply([tool_message])
|
|
119
|
+
if tool_result is None:
|
|
120
|
+
return tool_result, updated_context_variables
|
|
121
|
+
|
|
122
|
+
if "tool_responses" in tool_result:
|
|
123
|
+
# TODO: catch handoffs
|
|
124
|
+
for tool_response in tool_result["tool_responses"]:
|
|
125
|
+
content = tool_response["content"]
|
|
126
|
+
|
|
127
|
+
if isinstance(content, TransitionTarget):
|
|
128
|
+
warnings.warn(
|
|
129
|
+
f"Tool {self.agent.name} returned a target, which is not supported in remote mode"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
elif isinstance(content, ReplyResult):
|
|
133
|
+
if content.target:
|
|
134
|
+
warnings.warn(
|
|
135
|
+
f"Tool {self.agent.name} returned a target, which is not supported in remote mode"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if content.context_variables:
|
|
139
|
+
updated_context_variables = content.context_variables
|
|
140
|
+
tool_response["content"] = content.message
|
|
141
|
+
|
|
142
|
+
return tool_result, updated_context_variables
|
autogen/remote/errors.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RemoteAgentError(Exception):
|
|
7
|
+
"""Base class for remote agent errors"""
|
|
8
|
+
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RemoteAgentNotFoundError(RemoteAgentError):
|
|
13
|
+
"""Raised when a remote agent is not found"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, agent_name: str) -> None:
|
|
16
|
+
self.agent_name = agent_name
|
|
17
|
+
super().__init__(f"Remote agent `{agent_name}` not found")
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
import ssl
|
|
6
|
+
import typing
|
|
7
|
+
from typing import Protocol
|
|
8
|
+
|
|
9
|
+
from httpx._client import AsyncClient, Client, EventHook
|
|
10
|
+
from httpx._config import DEFAULT_LIMITS, DEFAULT_MAX_REDIRECTS, DEFAULT_TIMEOUT_CONFIG, Limits
|
|
11
|
+
from httpx._transports.base import AsyncBaseTransport
|
|
12
|
+
from httpx._types import AuthTypes, CertTypes, CookieTypes, HeaderTypes, ProxyTypes, QueryParamTypes, TimeoutTypes
|
|
13
|
+
from httpx._urls import URL
|
|
14
|
+
|
|
15
|
+
from autogen.doc_utils import export_module
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ClientFactory(Protocol):
|
|
19
|
+
def __call__(self) -> AsyncClient: ...
|
|
20
|
+
|
|
21
|
+
def make_sync(self) -> Client: ...
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@export_module("autogen.a2a")
|
|
25
|
+
class HttpxClientFactory(ClientFactory):
|
|
26
|
+
"""
|
|
27
|
+
An asynchronous HTTP client factory, with connection pooling, HTTP/2, redirects,
|
|
28
|
+
cookie persistence, etc.
|
|
29
|
+
|
|
30
|
+
It can be shared between tasks.
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
>>> factory = HttpxClientFactory()
|
|
36
|
+
>>> async with factory() as client:
|
|
37
|
+
>>> response = await client.get('https://example.org')
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Parameters:**
|
|
41
|
+
|
|
42
|
+
* **auth** - *(optional)* An authentication class to use when sending
|
|
43
|
+
requests.
|
|
44
|
+
* **params** - *(optional)* Query parameters to include in request URLs, as
|
|
45
|
+
a string, dictionary, or sequence of two-tuples.
|
|
46
|
+
* **headers** - *(optional)* Dictionary of HTTP headers to include when
|
|
47
|
+
sending requests.
|
|
48
|
+
* **cookies** - *(optional)* Dictionary of Cookie items to include when
|
|
49
|
+
sending requests.
|
|
50
|
+
* **verify** - *(optional)* Either `True` to use an SSL context with the
|
|
51
|
+
default CA bundle, `False` to disable verification, or an instance of
|
|
52
|
+
`ssl.SSLContext` to use a custom context.
|
|
53
|
+
* **http2** - *(optional)* A boolean indicating if HTTP/2 support should be
|
|
54
|
+
enabled. Defaults to `False`.
|
|
55
|
+
* **proxy** - *(optional)* A proxy URL where all the traffic should be routed.
|
|
56
|
+
* **timeout** - *(optional)* The timeout configuration to use when sending
|
|
57
|
+
requests.
|
|
58
|
+
* **limits** - *(optional)* The limits configuration to use.
|
|
59
|
+
* **max_redirects** - *(optional)* The maximum number of redirect responses
|
|
60
|
+
that should be followed.
|
|
61
|
+
* **base_url** - *(optional)* A URL to use as the base when building
|
|
62
|
+
request URLs.
|
|
63
|
+
* **transport** - *(optional)* A transport class to use for sending requests
|
|
64
|
+
over the network.
|
|
65
|
+
* **trust_env** - *(optional)* Enables or disables usage of environment
|
|
66
|
+
variables for configuration.
|
|
67
|
+
* **default_encoding** - *(optional)* The default encoding to use for decoding
|
|
68
|
+
response text, if no charset information is included in a response Content-Type
|
|
69
|
+
header. Set to a callable for automatic character set detection. Default: "utf-8".
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
*,
|
|
75
|
+
auth: AuthTypes | None = None,
|
|
76
|
+
params: QueryParamTypes | None = None,
|
|
77
|
+
headers: HeaderTypes | None = None,
|
|
78
|
+
cookies: CookieTypes | None = None,
|
|
79
|
+
verify: ssl.SSLContext | str | bool = True,
|
|
80
|
+
cert: CertTypes | None = None,
|
|
81
|
+
http1: bool = True,
|
|
82
|
+
http2: bool = False,
|
|
83
|
+
proxy: ProxyTypes | None = None,
|
|
84
|
+
mounts: None | (typing.Mapping[str, AsyncBaseTransport | None]) = None,
|
|
85
|
+
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
|
|
86
|
+
follow_redirects: bool = False,
|
|
87
|
+
limits: Limits = DEFAULT_LIMITS,
|
|
88
|
+
max_redirects: int = DEFAULT_MAX_REDIRECTS,
|
|
89
|
+
event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None,
|
|
90
|
+
base_url: URL | str = "",
|
|
91
|
+
transport: AsyncBaseTransport | None = None,
|
|
92
|
+
trust_env: bool = True,
|
|
93
|
+
default_encoding: str | typing.Callable[[bytes], str] = "utf-8",
|
|
94
|
+
**kwargs: typing.Any,
|
|
95
|
+
) -> None:
|
|
96
|
+
self.options = {
|
|
97
|
+
"auth": auth,
|
|
98
|
+
"params": params,
|
|
99
|
+
"headers": headers,
|
|
100
|
+
"cookies": cookies,
|
|
101
|
+
"verify": verify,
|
|
102
|
+
"cert": cert,
|
|
103
|
+
"http1": http1,
|
|
104
|
+
"http2": http2,
|
|
105
|
+
"proxy": proxy,
|
|
106
|
+
"mounts": mounts,
|
|
107
|
+
"timeout": timeout,
|
|
108
|
+
"follow_redirects": follow_redirects,
|
|
109
|
+
"limits": limits,
|
|
110
|
+
"max_redirects": max_redirects,
|
|
111
|
+
"event_hooks": event_hooks,
|
|
112
|
+
"base_url": base_url,
|
|
113
|
+
"transport": transport,
|
|
114
|
+
"trust_env": trust_env,
|
|
115
|
+
"default_encoding": default_encoding,
|
|
116
|
+
**kwargs,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
def __call__(self) -> AsyncClient:
|
|
120
|
+
return AsyncClient(**self.options)
|
|
121
|
+
|
|
122
|
+
def make_sync(self) -> Client:
|
|
123
|
+
return Client(**self.options)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class EmptyClientFactory(ClientFactory):
|
|
127
|
+
def __call__(self) -> AsyncClient:
|
|
128
|
+
return AsyncClient(timeout=30.0)
|
|
129
|
+
|
|
130
|
+
def make_sync(self) -> Client:
|
|
131
|
+
return Client(timeout=30.0)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
from typing import Any, Protocol
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AgentBusMessage(BaseModel):
|
|
10
|
+
messages: list[dict[str, Any]]
|
|
11
|
+
context: dict[str, Any] | None = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RequestMessage(AgentBusMessage):
|
|
15
|
+
client_tools: list[dict[str, Any]] = Field(default_factory=list)
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def client_tool_names(self) -> set[str]:
|
|
19
|
+
return get_tool_names(self.client_tools)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ResponseMessage(AgentBusMessage):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RemoteService(Protocol):
|
|
27
|
+
"""Interface to make AgentBus compatible with non AG2 systems."""
|
|
28
|
+
|
|
29
|
+
name: str
|
|
30
|
+
|
|
31
|
+
async def __call__(self, state: RequestMessage) -> ResponseMessage | None:
|
|
32
|
+
"""Executable that consumes Conversation State and returns a new state."""
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_tool_names(tools: list[dict[str, Any]]) -> set[str]:
|
|
37
|
+
return set(filter(bool, (tool.get("function", {}).get("name", "") for tool in tools)))
|
autogen/remote/retry.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
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 time
|
|
5
|
+
from types import TracebackType
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
|
|
8
|
+
import anyio
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RetryPolicyManager(Protocol):
|
|
12
|
+
def __enter__(self) -> None:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
async def __aenter__(self) -> None:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
def __exit__(
|
|
19
|
+
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None
|
|
20
|
+
) -> None | bool:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
async def __aexit__(
|
|
24
|
+
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None
|
|
25
|
+
) -> None | bool:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RetryPolicy(Protocol):
|
|
30
|
+
def __call__(self) -> RetryPolicyManager: ...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SleepRetryPolicy(RetryPolicy):
|
|
34
|
+
def __init__(self, retry_interval: float = 10.0, retry_count: int = 3) -> None:
|
|
35
|
+
self.retry_interval = retry_interval
|
|
36
|
+
self.retry_count = retry_count
|
|
37
|
+
|
|
38
|
+
def __call__(self) -> RetryPolicyManager:
|
|
39
|
+
return _SleepRetryPolicy(self.retry_interval, self.retry_count)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class _SleepRetryPolicy(RetryPolicyManager):
|
|
43
|
+
def __init__(self, retry_interval: float = 10.0, retry_count: int = 3) -> None:
|
|
44
|
+
self.retry_interval = retry_interval
|
|
45
|
+
self.retry_count = retry_count
|
|
46
|
+
self.errors_count = 0
|
|
47
|
+
|
|
48
|
+
def __enter__(self) -> None:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
async def __aenter__(self) -> None:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
def __exit__(
|
|
55
|
+
self,
|
|
56
|
+
exc_type: type[BaseException] | None,
|
|
57
|
+
exc_value: BaseException | None,
|
|
58
|
+
traceback: TracebackType | None,
|
|
59
|
+
) -> None | bool:
|
|
60
|
+
if exc_type is not None:
|
|
61
|
+
self.errors_count += 1
|
|
62
|
+
should_suppress = self.errors_count < self.retry_count
|
|
63
|
+
time.sleep(self.retry_interval)
|
|
64
|
+
return should_suppress
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
async def __aexit__(
|
|
68
|
+
self,
|
|
69
|
+
exc_type: type[BaseException] | None,
|
|
70
|
+
exc_value: BaseException | None,
|
|
71
|
+
traceback: TracebackType | None,
|
|
72
|
+
) -> None | bool:
|
|
73
|
+
if exc_type is not None:
|
|
74
|
+
self.errors_count += 1
|
|
75
|
+
should_suppress = self.errors_count < self.retry_count
|
|
76
|
+
await anyio.sleep(self.retry_interval)
|
|
77
|
+
return should_suppress
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class NoRetryPolicy(RetryPolicyManager):
|
|
82
|
+
def __enter__(self) -> None:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
async def __aenter__(self) -> None:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
async def __aexit__(
|
|
89
|
+
self,
|
|
90
|
+
exc_type: type[BaseException] | None,
|
|
91
|
+
exc_value: BaseException | None,
|
|
92
|
+
traceback: TracebackType | None,
|
|
93
|
+
) -> None | bool:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
def __exit__(
|
|
97
|
+
self,
|
|
98
|
+
exc_type: type[BaseException] | None,
|
|
99
|
+
exc_value: BaseException | None,
|
|
100
|
+
traceback: TracebackType | None,
|
|
101
|
+
) -> None | bool:
|
|
102
|
+
pass
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import Awaitable, Callable, Iterable, MutableMapping
|
|
7
|
+
from itertools import chain
|
|
8
|
+
from typing import Any
|
|
9
|
+
from uuid import UUID, uuid4
|
|
10
|
+
|
|
11
|
+
from fastapi import FastAPI, HTTPException, Response, status
|
|
12
|
+
|
|
13
|
+
from autogen.agentchat import ConversableAgent
|
|
14
|
+
|
|
15
|
+
from .agent_service import AgentService
|
|
16
|
+
from .protocol import RemoteService, RequestMessage, ResponseMessage
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class HTTPAgentBus:
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
agents: Iterable[ConversableAgent] = (),
|
|
23
|
+
*,
|
|
24
|
+
long_polling_interval: float = 10.0,
|
|
25
|
+
additional_services: Iterable[RemoteService] = (),
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Create HTTPAgentBus runtime.
|
|
28
|
+
|
|
29
|
+
Makes the passed agents capable of processing remote calls.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
agents: Agents to register as remote services.
|
|
33
|
+
long_polling_interval: Timeout to respond on task status calls for long-living executions.
|
|
34
|
+
Should be less than clients' HTTP request timeout.
|
|
35
|
+
additional_services: Additional services to register.
|
|
36
|
+
"""
|
|
37
|
+
self.app = FastAPI()
|
|
38
|
+
|
|
39
|
+
for service in chain(map(AgentService, agents), additional_services):
|
|
40
|
+
register_agent_endpoints(
|
|
41
|
+
app=self.app,
|
|
42
|
+
service=service,
|
|
43
|
+
long_polling_interval=long_polling_interval,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
async def __call__(
|
|
47
|
+
self,
|
|
48
|
+
scope: MutableMapping[str, Any],
|
|
49
|
+
receive: Callable[[], Awaitable[MutableMapping[str, Any]]],
|
|
50
|
+
send: Callable[[MutableMapping[str, Any]], Awaitable[None]],
|
|
51
|
+
) -> None:
|
|
52
|
+
"""ASGI interface."""
|
|
53
|
+
await self.app(scope, receive, send)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def register_agent_endpoints(
|
|
57
|
+
app: FastAPI,
|
|
58
|
+
service: RemoteService,
|
|
59
|
+
long_polling_interval: float,
|
|
60
|
+
) -> None:
|
|
61
|
+
tasks: dict[UUID, asyncio.Task[ResponseMessage | None]] = {}
|
|
62
|
+
|
|
63
|
+
@app.get(f"/{service.name}" + "/{task_id}", response_model=ResponseMessage | None)
|
|
64
|
+
async def remote_call_result(task_id: UUID) -> Response | ResponseMessage | None:
|
|
65
|
+
if task_id not in tasks:
|
|
66
|
+
raise HTTPException(
|
|
67
|
+
detail=f"`{task_id}` task not found",
|
|
68
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
task = tasks[task_id]
|
|
72
|
+
|
|
73
|
+
await asyncio.wait(
|
|
74
|
+
(task, asyncio.create_task(asyncio.sleep(long_polling_interval))),
|
|
75
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if not task.done():
|
|
79
|
+
return Response(status_code=status.HTTP_425_TOO_EARLY)
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
reply = task.result() # Task inner errors raising here
|
|
83
|
+
finally:
|
|
84
|
+
# TODO: how to clear hanged tasks?
|
|
85
|
+
tasks.pop(task_id, None)
|
|
86
|
+
|
|
87
|
+
if reply is None:
|
|
88
|
+
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
89
|
+
|
|
90
|
+
return reply
|
|
91
|
+
|
|
92
|
+
@app.post(f"/{service.name}", status_code=status.HTTP_202_ACCEPTED)
|
|
93
|
+
async def remote_call_starter(state: RequestMessage) -> UUID:
|
|
94
|
+
task, task_id = asyncio.create_task(service(state)), uuid4()
|
|
95
|
+
tasks[task_id] = task
|
|
96
|
+
return task_id
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from .messages import ToolCall, tools_message
|
|
6
|
+
from .test_agent import TestAgent
|
|
7
|
+
|
|
8
|
+
__all__ = (
|
|
9
|
+
"TestAgent",
|
|
10
|
+
"ToolCall",
|
|
11
|
+
"tools_message",
|
|
12
|
+
)
|
|
@@ -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
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
from pydantic_core import to_json
|
|
9
|
+
|
|
10
|
+
from autogen.events.agent_events import FunctionCall
|
|
11
|
+
from autogen.events.agent_events import ToolCall as RawToolCall
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ToolCall:
|
|
15
|
+
"""Represents a tool call with a specified tool name and arguments.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
tool_name: Tool name to call. Tool should be rigestered in Agent you send message.
|
|
19
|
+
arguments: keyword arguments to pass to the tool.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, tool_name: str, /, **arguments: Any) -> None:
|
|
23
|
+
self.tool_message = RawToolCall(
|
|
24
|
+
id=f"call_{uuid4()}",
|
|
25
|
+
type="function",
|
|
26
|
+
function=FunctionCall(name=tool_name, arguments=to_json(arguments).decode()),
|
|
27
|
+
).model_dump()
|
|
28
|
+
|
|
29
|
+
def to_message(self) -> dict[str, Any]:
|
|
30
|
+
"""Convert the tool call to a message format suitable for API calls.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
A dictionary containing the tool call in message format,
|
|
34
|
+
ready to be used in API requests or message queues.
|
|
35
|
+
"""
|
|
36
|
+
return tools_message(self)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def tools_message(*tool_calls: ToolCall) -> dict[str, Any]:
|
|
40
|
+
"""Convert multiple tool calls into a message format suitable for API calls.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
*tool_calls: One or more ToolCall objects to convert.
|
|
44
|
+
"""
|
|
45
|
+
return {"content": None, "tool_calls": [c.tool_message for c in tool_calls]}
|