Skip to content

Commit a0e531d

Browse files
refactored function target logic, new FunctionTargetResult and FunctionTargetMessage classes
1 parent dc55045 commit a0e531d

File tree

5 files changed

+96
-125
lines changed

5 files changed

+96
-125
lines changed

autogen/agentchat/group/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from .on_condition import OnCondition
1515
from .on_context_condition import OnContextCondition
1616
from .reply_result import ReplyResult
17+
from .function_target_result import FunctionTargetResult
1718
from .speaker_selection_result import SpeakerSelectionResult
1819
from .targets.group_chat_target import GroupChatConfig, GroupChatTarget
1920

@@ -55,6 +56,7 @@
5556
"OnCondition",
5657
"OnContextCondition",
5758
"ReplyResult",
59+
"FunctionTargetResult",
5860
"RevertToUserTarget",
5961
"SpeakerSelectionResult",
6062
"StayTarget",
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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+
__all__ = ["FunctionTargetResult"]
6+
7+
8+
from pydantic import BaseModel
9+
from typing import TYPE_CHECKING, Any
10+
from .context_variables import ContextVariables
11+
from .targets.transition_target import TransitionTarget
12+
13+
if TYPE_CHECKING:
14+
from ..conversable_agent import ConversableAgent
15+
class FunctionTargetMessage(BaseModel):
16+
content: str
17+
msg_target: "ConversableAgent"
18+
else:
19+
class FunctionTargetMessage(BaseModel):
20+
content: str
21+
msg_target: Any
22+
23+
class FunctionTargetResult(BaseModel):
24+
"""Result of a function handoff that is used to provide the return message and the target to transition to."""
25+
messages: list[FunctionTargetMessage] | str | None = None
26+
target: TransitionTarget
27+
context_variables: ContextVariables | None = None
28+
29+
def __str__(self) -> str:
30+
"""The string representation for FunctionTargetResult will be """
31+
# not implemented yet
32+
return ""

autogen/agentchat/group/group_utils.py

Lines changed: 7 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
from .targets.transition_target import (
1717
AgentNameTarget,
1818
AgentTarget,
19-
FunctionTarget,
2019
TransitionTarget,
2120
)
2221

@@ -114,101 +113,13 @@ def _evaluate_after_works_conditions(
114113
if is_available and (
115114
after_work_condition.condition is None or after_work_condition.condition.evaluate(agent.context_variables)
116115
):
117-
tgt = after_work_condition.target
118-
119-
# NEW: Inline execution path for FunctionTarget
120-
if isinstance(tgt, FunctionTarget):
121-
# extract the agent output and context variables to pass in to the FunctionTarget
122-
first_input = groupchat.messages[-1]["content"] if groupchat.messages else ""
123-
ctx = agent.context_variables
124-
125-
try:
126-
rr = tgt.fn(first_input, ctx) # rr: ReplyResult
127-
except Exception as e:
128-
from types import SimpleNamespace
129-
rr = SimpleNamespace(message=f"[{tgt.fn_name}] error: {e}", target=None, context_variables=None)
130-
131-
# 1) merge returned context vars (if your ReplyResult carries them)
132-
if getattr(rr, "context_variables", None):
133-
if isinstance(ctx, dict) and isinstance(rr.context_variables, dict):
134-
ctx.update(rr.context_variables)
135-
136-
# 2) append/broadcast the tool output
137-
fn_content = rr.message if getattr(rr, "message", None) is not None else str(rr)
138-
139-
# send to manager itself for visibility
140-
agent._group_manager.send(
141-
{
142-
"role": "assistant",
143-
"name": tgt.fn_name,
144-
"content": f"[{tgt.fn_name}] {fn_content}",
145-
},
146-
agent._group_manager,
147-
request_reply=False,
148-
silent=True,
149-
)
150-
# this may not be ideal but system gives more priority to the msg than tool
151-
broadcast = {
152-
"role": "system",
153-
"name": tgt.fn_name,
154-
"content": f"""{fn_content}"""
155-
}
156-
# 3) choose next agent and broadcast based on rr.target or broadcast_recipients parameter if provided
157-
# TODO: fix for StayTarget and TerminateTarget
158-
next_target = getattr(rr, "target", None)
159-
if hasattr(next_target, "agent_name"):
160-
next_ag_name = next_target.agent_name
161-
else:
162-
next_ag_name = None
163-
print("No next target. next_target: ", next_target)
164-
for ag in groupchat.agents:
165-
if (
166-
# ag != tool_executor
167-
ag != agent._group_manager
168-
and (
169-
# 1. broadcast_recipients overrides everything
170-
(tgt.broadcast_recipients == "all")
171-
or (tgt.broadcast_recipients is not None and ag.name in tgt.broadcast_recipients)
172-
# 2. otherwise, send only to next agent if present
173-
or (tgt.broadcast_recipients is None and next_ag_name is not None and ag.name == next_ag_name)
174-
# 3. otherwise, send to all
175-
or (tgt.broadcast_recipients is None and next_ag_name is None)
176-
)
177-
):
178-
# sending from group manager seems to give higher priority than from tool executor.
179-
agent._group_manager.send(
180-
broadcast,
181-
ag,
182-
request_reply=False,
183-
silent=False,
184-
)
185-
186-
# add some error handling
187-
if next_target is None:
188-
# no explicit next target: delegate to the manager's auto selection
189-
from .targets.group_manager_target import GroupManagerTarget
190-
return GroupManagerTarget().resolve(groupchat, agent, user_agent).get_speaker_selection_result(groupchat)
191-
192-
# explicit target provided
193-
if isinstance(next_target, TransitionTarget):
194-
if next_target.can_resolve_for_speaker_selection():
195-
return next_target.resolve(groupchat, agent, user_agent).get_speaker_selection_result(groupchat)
196-
else:
197-
# If someone tries to return another FunctionTarget here, you can decide what to do.
198-
# For now, delegate to manager to avoid loops.
199-
from .targets.group_manager_target import GroupManagerTarget
200-
return GroupManagerTarget().resolve(groupchat, agent, user_agent).get_speaker_selection_result(groupchat)
201-
202-
# fallback if target type isn't recognized
203-
from .targets.group_manager_target import GroupManagerTarget
204-
return GroupManagerTarget().resolve(groupchat, agent, user_agent).get_speaker_selection_result(groupchat)
205-
else:
206-
# proceed as before if not a FunctionTarget
207-
return after_work_condition.target.resolve(
208-
groupchat,
209-
agent,
210-
user_agent,
211-
).get_speaker_selection_result(groupchat)
116+
# Condition matched, resolve and return
117+
return after_work_condition.target.resolve(
118+
groupchat,
119+
agent,
120+
user_agent,
121+
).get_speaker_selection_result(groupchat)
122+
212123
return None
213124

214125

autogen/agentchat/group/targets/transition_target.py

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
import random
66
from collections.abc import Callable
7-
from typing import TYPE_CHECKING, Any, Optional
7+
from types import SimpleNamespace
8+
from typing import TYPE_CHECKING, Any, List, Optional
89

910
from pydantic import BaseModel, Field
1011

@@ -423,19 +424,44 @@ def create_wrapper_agent(self, parent_agent: "ConversableAgent", index: int) ->
423424

424425

425426
if TYPE_CHECKING:
426-
from ..reply_result import ReplyResult
427-
428-
AfterworkFn = Callable[[str, Any], "ReplyResult"]
427+
from ..function_target_result import FunctionTargetResult
428+
from ..function_target_result import FunctionTargetMessage
429+
AfterworkFn = Callable[..., "FunctionTargetResult"]
429430
else:
430-
AfterworkFn = Callable[[str, Any], Any]
431-
431+
AfterworkFn = Callable[..., Any]
432+
433+
def broadcast(messages: List["FunctionTargetMessage"] | str, group_chat, current_agent, fn_name, target, user_agent, *args) -> None:
434+
"""Broadcast a message to a specific agent."""
435+
if isinstance(messages, str):
436+
if hasattr(target, "agent_name"):
437+
next_target = target.agent_name
438+
for agent in group_chat.agents:
439+
if agent.name == next_target:
440+
messages = [SimpleNamespace(content=messages, msg_target=agent)]
441+
break
442+
elif isinstance(target, RevertToUserTarget):
443+
messages = [SimpleNamespace(content=messages, msg_target=user_agent)]
444+
elif isinstance(target, StayTarget):
445+
messages = [SimpleNamespace(content=messages, msg_target=current_agent)]
446+
for message in messages:
447+
content = message.content
448+
broadcast = {
449+
"role": "system",
450+
"name": f"{fn_name}",
451+
"content": f"[FUNCTION_HANDOFF] - Reply from function {fn_name}: \n\n {content}"
452+
}
453+
current_agent._group_manager.send(
454+
broadcast,
455+
message.msg_target,
456+
request_reply=False,
457+
silent=False,
458+
)
432459

433460
class FunctionTarget(TransitionTarget):
434461
"""Transition target that invokes a tool function with (prev_output, context)."""
435462

436463
fn_name: str = Field(...)
437464
fn: AfterworkFn = Field(..., repr=False)
438-
broadcast_recipients: list[str] | None = None
439465

440466
def __init__(self, incoming_fn, **kwargs):
441467
# If the first arg is callable, auto-populate fn and fn_name
@@ -446,7 +472,6 @@ def __init__(self, incoming_fn, **kwargs):
446472
raise ValueError(
447473
"FunctionTarget must be initialized with a callable function as the first argument or 'fn' keyword argument."
448474
)
449-
# --- TransitionTarget API ---
450475
def can_resolve_for_speaker_selection(self) -> bool:
451476
return False
452477

@@ -455,17 +480,13 @@ def resolve(self, *args, **kwargs) -> SpeakerSelectionResult:
455480
last_message = group_chat.messages[-1]["content"] if group_chat.messages else ""
456481
current_agent: ConversableAgent = args[1]
457482
ctx = current_agent.context_variables
458-
459-
# Define the signature of the function that can be called (parameters and return type)
460-
# Create a message class and object for adding a message to the conversation
461-
function_target_result = self.fn(group_chat, current_agent, last_message, ctx)
462-
463-
# resolve_next_target = function_target_result.target.resolve(group_chat, args[1], args[2])
464-
465-
# if function_target_result.message:
466-
# broadcast(function_target_result.message.content, function_target_result.message.target_agent)
467-
468-
return function_target_result.target.resolve(group_chat, args[1], args[2])
483+
user_agent = args[2]
484+
function_target_result = self.fn(last_message, ctx, group_chat, current_agent)
485+
if function_target_result.context_variables:
486+
ctx.update(function_target_result.context_variables)
487+
if function_target_result.messages:
488+
broadcast(function_target_result.messages, group_chat, current_agent, self.fn_name, function_target_result.target, user_agent)
489+
return function_target_result.target.resolve(group_chat, current_agent, user_agent)
469490

470491
def display_name(self) -> str:
471492
return self.fn_name

test/agentchat/test_function_targets.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
from autogen import LLMConfig, ConversableAgent
1212
from autogen.agentchat import initiate_group_chat
1313
from autogen.agentchat.group import ContextVariables, AgentTarget, FunctionTarget, ReplyResult
14+
from autogen.agentchat.group.function_target_result import FunctionTargetMessage, FunctionTargetResult
1415
from autogen.agentchat.group.patterns import DefaultPattern
16+
from autogen.agentchat.group.targets.transition_target import RevertToUserTarget, StayTarget, TerminateTarget
1517

1618
load_dotenv()
1719

@@ -21,7 +23,7 @@ def main(session_id: Optional[str] = None) -> dict:
2123
cfg = LLMConfig(api_type="openai", model="gpt-4o", api_key=os.environ["OPENAI_API_KEY"])
2224

2325
# Shared context
24-
ctx = ContextVariables(data={"variable": "value1"})
26+
ctx = ContextVariables(data={"application": "<empty>"})
2527

2628
# Agents
2729
first_agent = ConversableAgent(
@@ -43,20 +45,23 @@ def main(session_id: Optional[str] = None) -> dict:
4345
)
4446

4547
# After-work hook
46-
def afterwork_function(output: str, context_variables: Any) -> ReplyResult:
48+
def afterwork_function(output: str, context_variables: Any, *args) -> FunctionTargetResult:
4749
"""
4850
Switches a context variable and routes the next turn.
4951
"""
50-
if context_variables.get("variable") == "value1":
51-
context_variables["variable"] = "value2"
52-
return ReplyResult(
53-
message="The job you are applying to is specifically in GPU optimization",
54-
target=AgentTarget(first_agent),
52+
if context_variables.get("application") == "<empty>":
53+
context_variables["application"] = output
54+
return FunctionTargetResult(
55+
messages="apply for a job in gpu optimization",
56+
target=StayTarget(),
5557
context_variables=context_variables,
5658
)
5759

58-
return ReplyResult(
59-
message="The job you are applying to is specifically in agentic open source development",
60+
return FunctionTargetResult(
61+
messages=[FunctionTargetMessage(
62+
content=f"Revise the draft written by the first agent: {output}",
63+
msg_target=second_agent
64+
)],
6065
target=AgentTarget(second_agent),
6166
context_variables=context_variables,
6267
)
@@ -77,7 +82,7 @@ def afterwork_function(output: str, context_variables: Any) -> ReplyResult:
7782
initiate_group_chat(
7883
pattern=pattern,
7984
messages="the job you are applying to is specifically in machine learning",
80-
max_rounds=60,
85+
max_rounds=20,
8186
)
8287

8388
return {"session_id": session_id}

0 commit comments

Comments
 (0)