langroid 0.8.0__py3-none-any.whl → 0.9.1__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.
langroid/agent/task.py CHANGED
@@ -5,7 +5,7 @@ import copy
5
5
  import logging
6
6
  import re
7
7
  import threading
8
- from collections import Counter, deque
8
+ from collections import Counter, OrderedDict, deque
9
9
  from pathlib import Path
10
10
  from types import SimpleNamespace
11
11
  from typing import (
@@ -33,6 +33,8 @@ from langroid.agent.chat_document import (
33
33
  ChatDocument,
34
34
  StatusCode,
35
35
  )
36
+ from langroid.agent.tool_message import ToolMessage
37
+ from langroid.agent.tools.orchestration import AgentDoneTool, DoneTool
36
38
  from langroid.cachedb.redis_cachedb import RedisCache, RedisCacheConfig
37
39
  from langroid.exceptions import InfiniteLoopException
38
40
  from langroid.mytypes import Entity
@@ -89,6 +91,8 @@ class TaskConfig(BaseModel):
89
91
  this is always enabled since this is a critical way for responders to
90
92
  indicate that the message should be sent to a specific entity/agent.
91
93
  (Search for "SEND_TO" in the examples/ dir to see how this is used.)
94
+ allow_subtask_multi_oai_tools (bool): whether to allow multiple OpenAI
95
+ tool-calls to be sent to a sub-task.
92
96
  """
93
97
 
94
98
  inf_loop_cycle_len: int = 10
@@ -97,6 +101,7 @@ class TaskConfig(BaseModel):
97
101
  restart_as_subtask: bool = False
98
102
  logs_dir: str = "logs"
99
103
  addressing_prefix: str = ""
104
+ allow_subtask_multi_oai_tools: bool = True
100
105
 
101
106
 
102
107
  class Task:
@@ -570,9 +575,13 @@ class Task:
570
575
  return self.pending_message
571
576
 
572
577
  def reset_all_sub_tasks(self) -> None:
573
- """Recursively reset message history of own agent and all sub-tasks"""
578
+ """
579
+ Recursively reset message history & state of own agent and
580
+ those of all sub-tasks.
581
+ """
574
582
  self.agent.clear_history(0)
575
583
  self.agent.clear_dialog()
584
+ self.agent.init_state()
576
585
  for t in self.sub_tasks:
577
586
  t.reset_all_sub_tasks()
578
587
 
@@ -584,11 +593,13 @@ class Task:
584
593
  max_cost: float = 0,
585
594
  max_tokens: int = 0,
586
595
  session_id: str = "",
596
+ allow_restart: bool = True,
587
597
  ) -> Optional[ChatDocument]:
588
598
  """Synchronous version of `run_async()`.
589
599
  See `run_async()` for details."""
590
- if (self.restart and caller is None) or (
591
- self.config_sub_task.restart_as_subtask and caller is not None
600
+ if allow_restart and (
601
+ (self.restart and caller is None)
602
+ or (self.config_sub_task.restart_as_subtask and caller is not None)
592
603
  ):
593
604
  # We are either at top level, with restart = True, OR
594
605
  # we are a sub-task with restart_as_subtask = True,
@@ -679,6 +690,7 @@ class Task:
679
690
  max_cost: float = 0,
680
691
  max_tokens: int = 0,
681
692
  session_id: str = "",
693
+ allow_restart: bool = True,
682
694
  ) -> Optional[ChatDocument]:
683
695
  """
684
696
  Loop over `step()` until task is considered done or `turns` is reached.
@@ -700,6 +712,7 @@ class Task:
700
712
  max_cost (float): max cost allowed for the task (default 0 -> no limit)
701
713
  max_tokens (int): max tokens allowed for the task (default 0 -> no limit)
702
714
  session_id (str): session id for the task
715
+ allow_restart (bool): whether to allow restarting the task
703
716
 
704
717
  Returns:
705
718
  Optional[ChatDocument]: valid result of the task.
@@ -710,11 +723,9 @@ class Task:
710
723
  # message can be considered to be from the USER
711
724
  # (from the POV of this agent's LLM).
712
725
 
713
- if (
714
- self.restart
715
- and caller is None
716
- or self.config_sub_task.restart_as_subtask
717
- and caller is not None
726
+ if allow_restart and (
727
+ (self.restart and caller is None)
728
+ or (self.config_sub_task.restart_as_subtask and caller is not None)
718
729
  ):
719
730
  # We are either at top level, with restart = True, OR
720
731
  # we are a sub-task with restart_as_subtask = True,
@@ -1022,6 +1033,7 @@ class Task:
1022
1033
  # (responder, result) from a responder who explicitly said NO_ANSWER
1023
1034
  no_answer_response: None | Tuple[Responder, ChatDocument] = None
1024
1035
  for r in responders:
1036
+ self.is_pass_thru = False
1025
1037
  if not self._can_respond(r):
1026
1038
  # create dummy msg for logging
1027
1039
  log_doc = ChatDocument(
@@ -1095,10 +1107,7 @@ class Task:
1095
1107
  # Contrast this with self.pending_message.metadata.sender, which is an ENTITY
1096
1108
  # of this agent, or a sub-task's agent.
1097
1109
  if not self.is_pass_thru:
1098
- if (
1099
- self.pending_message is not None
1100
- and self.pending_message.metadata.agent_id == self.agent.id
1101
- ):
1110
+ if self.pending_message is not None and not isinstance(r, Task):
1102
1111
  # when pending msg is from our own agent, respect the sender set there,
1103
1112
  # since sometimes a response may "mock" as if the response is from
1104
1113
  # another entity (e.g when using RewindTool, the agent handler
@@ -1164,6 +1173,26 @@ class Task:
1164
1173
  msg_str = escape(str(self.pending_message))
1165
1174
  print(f"[grey37][{sender_str}]{msg_str}[/grey37]")
1166
1175
 
1176
+ def _forbid_multi_oai_tools(self, e: Responder) -> ChatDocument:
1177
+ # Passing multiple OpenAI Tools to be handled by another agent
1178
+ # is not supported yet (we need to carefully establish correspondence
1179
+ # between the original tool-calls of agent A, and the returned results,
1180
+ # which may involve recursive-called tools by agent B).
1181
+ # So we set an error result corresponding to each tool-call.
1182
+ assert isinstance(
1183
+ e, Task
1184
+ ), "Forbidding multiple OAI tools only applies to a responder of type Task"
1185
+ err_str = """
1186
+ ERROR: cannot pass multiple tools to another agent!
1187
+ Please use ONE tool at a time!
1188
+ """
1189
+ id2result = OrderedDict((tc.id, err_str) for tc in self.agent.oai_tool_calls)
1190
+ result = e.agent.create_user_response(
1191
+ content="",
1192
+ oai_tool_id2result=id2result,
1193
+ )
1194
+ return result
1195
+
1167
1196
  def response(
1168
1197
  self,
1169
1198
  e: Responder,
@@ -1176,26 +1205,35 @@ class Task:
1176
1205
  actual_turns = e.turns if e.turns > 0 else turns
1177
1206
  e.agent.callbacks.set_parent_agent(self.agent)
1178
1207
  # e.callbacks.set_parent_agent(self.agent)
1179
- result = e.run(
1180
- self.pending_message,
1181
- turns=actual_turns,
1182
- caller=self,
1183
- max_cost=self.max_cost,
1184
- max_tokens=self.max_tokens,
1185
- )
1186
- if result is not None:
1187
- content, id2result, oai_tool_id = self.agent._process_tool_results(
1188
- result.content,
1189
- result.oai_tool_id2result,
1190
- (
1191
- self.pending_message.oai_tool_calls
1192
- if isinstance(self.pending_message, ChatDocument)
1193
- else None
1194
- ),
1208
+ pending_tools = self.agent.get_tool_messages(self.pending_message)
1209
+ # TODO disable this
1210
+ if (
1211
+ len(pending_tools) > 1
1212
+ and len(self.agent.oai_tool_calls) > 1
1213
+ and not self.config.allow_subtask_multi_oai_tools
1214
+ ):
1215
+ result = self._forbid_multi_oai_tools(e)
1216
+ else:
1217
+ result = e.run(
1218
+ self.pending_message,
1219
+ turns=actual_turns,
1220
+ caller=self,
1221
+ max_cost=self.max_cost,
1222
+ max_tokens=self.max_tokens,
1195
1223
  )
1196
- result.content = content
1197
- result.oai_tool_id2result = id2result
1198
- result.metadata.oai_tool_id = oai_tool_id
1224
+ if result is not None:
1225
+ content, id2result, oai_tool_id = self.agent.process_tool_results(
1226
+ result.content,
1227
+ result.oai_tool_id2result,
1228
+ (
1229
+ self.pending_message.oai_tool_calls
1230
+ if isinstance(self.pending_message, ChatDocument)
1231
+ else None
1232
+ ),
1233
+ )
1234
+ result.content = content
1235
+ result.oai_tool_id2result = id2result
1236
+ result.metadata.oai_tool_id = oai_tool_id
1199
1237
 
1200
1238
  result_str = ( # only used by callback to display content and possible tool
1201
1239
  "NONE"
@@ -1211,14 +1249,26 @@ class Task:
1211
1249
  else:
1212
1250
  response_fn = self._entity_responder_map[cast(Entity, e)]
1213
1251
  result = response_fn(self.pending_message)
1214
- return self._process_result_routing(result)
1252
+ return self._process_result_routing(result, e)
1215
1253
 
1216
1254
  def _process_result_routing(
1217
- self, result: ChatDocument | None
1255
+ self, result: ChatDocument | None, e: Responder
1218
1256
  ) -> ChatDocument | None:
1219
1257
  # process result in case there is a routing instruction
1220
1258
  if result is None:
1221
1259
  return None
1260
+ if isinstance(result, ToolMessage):
1261
+ # this supports Agent responders and Task.run() to
1262
+ # return a ToolMessage, in addition str, ChatDocument
1263
+ if isinstance(e, Task):
1264
+ # With the curr defn of Task.result(),
1265
+ # Task.run() can't return a ToolMessage, so this case doesn't occur,
1266
+ # but we leave it here in case a
1267
+ # Task subclass overrides default behavior
1268
+ return e.agent.create_user_response(tool_messages=[result])
1269
+ else:
1270
+ # e must be this agent's Entity (LLM, AGENT or USER)
1271
+ return self.agent.response_template(e=e, tool_messages=[result])
1222
1272
  # if result content starts with @name, set recipient to name
1223
1273
  is_pass, recipient, content = parse_routing(
1224
1274
  result,
@@ -1272,14 +1322,37 @@ class Task:
1272
1322
  if isinstance(e, Task):
1273
1323
  actual_turns = e.turns if e.turns > 0 else turns
1274
1324
  e.agent.callbacks.set_parent_agent(self.agent)
1275
- # e.callbacks.set_parent_agent(self.agent)
1276
- result = await e.run_async(
1277
- self.pending_message,
1278
- turns=actual_turns,
1279
- caller=self,
1280
- max_cost=self.max_cost,
1281
- max_tokens=self.max_tokens,
1282
- )
1325
+ pending_tools = self.agent.get_tool_messages(self.pending_message)
1326
+ # TODO disable this
1327
+ if (
1328
+ len(pending_tools) > 1
1329
+ and len(self.agent.oai_tool_calls) > 1
1330
+ and not self.config.allow_subtask_multi_oai_tools
1331
+ ):
1332
+ result = self._forbid_multi_oai_tools(e)
1333
+ else:
1334
+ # e.callbacks.set_parent_agent(self.agent)
1335
+ result = await e.run_async(
1336
+ self.pending_message,
1337
+ turns=actual_turns,
1338
+ caller=self,
1339
+ max_cost=self.max_cost,
1340
+ max_tokens=self.max_tokens,
1341
+ )
1342
+ if result is not None:
1343
+ content, id2result, oai_tool_id = self.agent.process_tool_results(
1344
+ result.content,
1345
+ result.oai_tool_id2result,
1346
+ (
1347
+ self.pending_message.oai_tool_calls
1348
+ if isinstance(self.pending_message, ChatDocument)
1349
+ else None
1350
+ ),
1351
+ )
1352
+ result.content = content
1353
+ result.oai_tool_id2result = id2result
1354
+ result.metadata.oai_tool_id = oai_tool_id
1355
+
1283
1356
  result_str = ( # only used by callback to display content and possible tool
1284
1357
  "NONE"
1285
1358
  if result is None
@@ -1294,7 +1367,7 @@ class Task:
1294
1367
  else:
1295
1368
  response_fn = self._entity_responder_async_map[cast(Entity, e)]
1296
1369
  result = await response_fn(self.pending_message)
1297
- return self._process_result_routing(result)
1370
+ return self._process_result_routing(result, e)
1298
1371
 
1299
1372
  def result(self, status: StatusCode | None = None) -> ChatDocument | None:
1300
1373
  """
@@ -1323,6 +1396,20 @@ class Task:
1323
1396
  oai_tool_id2result = result_msg.oai_tool_id2result if result_msg else None
1324
1397
  fun_call = result_msg.function_call if result_msg else None
1325
1398
  tool_messages = result_msg.tool_messages if result_msg else []
1399
+ # if there is an LLMDoneTool or AgentDoneTool among these,
1400
+ # we extract content and tools from here, and ignore all others
1401
+ for t in tool_messages:
1402
+ if isinstance(t, (AgentDoneTool, DoneTool)):
1403
+ # there shouldn't be multiple tools like this; just take the first
1404
+ content = t.content
1405
+ if isinstance(t, AgentDoneTool):
1406
+ tool_messages = t.tools
1407
+ break
1408
+ # drop the "Done" tools since they should not be part of the task result,
1409
+ # or else they would cause the parent task to get done!
1410
+ tool_messages = [
1411
+ t for t in tool_messages if not isinstance(t, (DoneTool, AgentDoneTool))
1412
+ ]
1326
1413
  block = result_msg.metadata.block if result_msg else None
1327
1414
  recipient = result_msg.metadata.recipient if result_msg else ""
1328
1415
  tool_ids = result_msg.metadata.tool_ids if result_msg else []
@@ -1381,7 +1468,16 @@ class Task:
1381
1468
 
1382
1469
  response_says_done = result is not None and (
1383
1470
  (isinstance(result, str) and DONE in result)
1384
- or (isinstance(result, ChatDocument) and DONE in result.content)
1471
+ or (
1472
+ isinstance(result, ChatDocument)
1473
+ and (
1474
+ DONE in result.content
1475
+ or any(
1476
+ isinstance(t, (DoneTool, AgentDoneTool))
1477
+ for t in result.tool_messages
1478
+ )
1479
+ )
1480
+ )
1385
1481
  )
1386
1482
  return (
1387
1483
  (
@@ -1493,9 +1589,18 @@ class Task:
1493
1589
  if self._is_kill():
1494
1590
  return (True, StatusCode.KILL)
1495
1591
  result = result or self.pending_message
1592
+ # An entity decided task is done, either via DoneTool,
1593
+ # or by explicitly saying DONE
1594
+ done_result = result is not None and (
1595
+ DONE in (result.content if isinstance(result, str) else result.content)
1596
+ or any(
1597
+ isinstance(t, (DoneTool, AgentDoneTool)) for t in result.tool_messages
1598
+ )
1599
+ )
1600
+
1496
1601
  user_quit = (
1497
1602
  result is not None
1498
- and (result.content in USER_QUIT_STRINGS or DONE in result.content)
1603
+ and (result.content in USER_QUIT_STRINGS or done_result)
1499
1604
  and result.metadata.sender == Entity.USER
1500
1605
  )
1501
1606
  if self._level == 0 and self._user_can_respond() and self.only_user_quits_root:
@@ -1534,8 +1639,7 @@ class Task:
1534
1639
  final = (
1535
1640
  # no valid response from any entity/agent in current turn
1536
1641
  result is None
1537
- # An entity decided task is done
1538
- or DONE in result.content
1642
+ or done_result
1539
1643
  or ( # current task is addressing message to caller task
1540
1644
  self.caller is not None
1541
1645
  and self.caller.name != ""
@@ -1672,6 +1776,11 @@ class Task:
1672
1776
 
1673
1777
  if self.pending_message is None:
1674
1778
  return True
1779
+ if isinstance(e, Task) and e.agent.has_only_unhandled_tools(
1780
+ self.pending_message
1781
+ ):
1782
+ return False
1783
+
1675
1784
  if self._recipient_mismatch(e):
1676
1785
  # Cannot respond if not addressed to this entity
1677
1786
  return False
@@ -15,7 +15,7 @@ from typing import Any, Dict, List, Tuple, Type
15
15
  from docstring_parser import parse
16
16
 
17
17
  from langroid.language_models.base import LLMFunctionSpec
18
- from langroid.pydantic_v1 import BaseModel
18
+ from langroid.pydantic_v1 import BaseModel, ConfigDict, Extra
19
19
  from langroid.utils.pydantic_utils import (
20
20
  _recursive_purge_dict_key,
21
21
  generate_simple_schema,
@@ -41,13 +41,19 @@ class ToolMessage(ABC, BaseModel):
41
41
  purpose: str
42
42
  id: str = "" # placeholder for OpenAI-API tool_call_id
43
43
 
44
+ model_config = ConfigDict(extra=Extra.allow)
45
+
46
+ _handle_only: bool = False # only allow handling, but not use (LLM-generation)?
47
+
44
48
  class Config:
49
+ # only HANDLING allowed, NOT "use" (i.e LLM generation)
50
+ handle_only: bool = False
45
51
  arbitrary_types_allowed = False
46
52
  validate_all = True
47
53
  validate_assignment = True
48
54
  # do not include these fields in the generated schema
49
55
  # since we don't require the LLM to specify them
50
- schema_extra = {"exclude": {"purpose", "id"}}
56
+ schema_extra = {"exclude": {"purpose", "id", "model_config"}}
51
57
 
52
58
  @classmethod
53
59
  def instructions(cls) -> str:
@@ -262,5 +268,8 @@ class ToolMessage(ABC, BaseModel):
262
268
  Returns:
263
269
  Dict[str, Any]: simplified schema
264
270
  """
265
- schema = generate_simple_schema(cls, exclude=["purpose"])
271
+ schema = generate_simple_schema(
272
+ cls,
273
+ exclude=list(cls.Config.schema_extra["exclude"]),
274
+ )
266
275
  return schema
@@ -1,9 +1,19 @@
1
1
  from . import google_search_tool
2
2
  from . import recipient_tool
3
3
  from . import rewind_tool
4
+ from . import orchestration
4
5
  from .google_search_tool import GoogleSearchTool
5
6
  from .recipient_tool import AddRecipientTool, RecipientTool
6
7
  from .rewind_tool import RewindTool
8
+ from .orchestration import (
9
+ AgentDoneTool,
10
+ DoneTool,
11
+ ForwardTool,
12
+ PassTool,
13
+ SendTool,
14
+ AgentSendTool,
15
+ DonePassTool,
16
+ )
7
17
 
8
18
  __all__ = [
9
19
  "GoogleSearchTool",
@@ -13,4 +23,12 @@ __all__ = [
13
23
  "recipient_tool",
14
24
  "rewind_tool",
15
25
  "RewindTool",
26
+ "orchestration",
27
+ "AgentDoneTool",
28
+ "DoneTool",
29
+ "DonePassTool",
30
+ "ForwardTool",
31
+ "PassTool",
32
+ "SendTool",
33
+ "AgentSendTool",
16
34
  ]
@@ -0,0 +1,216 @@
1
+ """
2
+ Various tools to for agents to be able to control flow of Task, e.g.
3
+ termination, routing to another agent, etc.
4
+ """
5
+
6
+ from typing import List, Tuple
7
+
8
+ from langroid.agent.chat_agent import ChatAgent
9
+ from langroid.agent.chat_document import ChatDocument
10
+ from langroid.agent.tool_message import ToolMessage
11
+ from langroid.mytypes import Entity
12
+
13
+
14
+ class AgentDoneTool(ToolMessage):
15
+ """Tool for AGENT entity (i.e. agent_response or downstream tool handling fns) to
16
+ signal the current task is done."""
17
+
18
+ purpose: str = """
19
+ To signal the current task is done, along with an optional message <content>
20
+ (default empty string) and an optional list of <tools> (default empty list).
21
+ """
22
+ request: str = "agent_done_tool"
23
+ content: str = ""
24
+ tools: List[ToolMessage] = []
25
+ _handle_only: bool = True
26
+
27
+ def response(self, agent: ChatAgent) -> ChatDocument:
28
+ return agent.create_agent_response(
29
+ self.content,
30
+ tool_messages=[self] + self.tools,
31
+ )
32
+
33
+
34
+ class DoneTool(ToolMessage):
35
+ """Tool for Agent Entity (i.e. agent_response) or LLM entity (i.e. llm_response) to
36
+ signal the current task is done, with some content as the result."""
37
+
38
+ purpose = """
39
+ To signal the current task is done, along with an optional message <content>
40
+ (default empty string).
41
+ """
42
+ request = "done_tool"
43
+ content: str = ""
44
+
45
+ def response(self, agent: ChatAgent) -> ChatDocument:
46
+ return agent.create_agent_response(
47
+ self.content,
48
+ tool_messages=[self],
49
+ )
50
+
51
+ @classmethod
52
+ def instructions(cls) -> str:
53
+ tool_name = cls.default_value("request")
54
+ return f"""
55
+ When you determine your task is finished,
56
+ use the tool `{tool_name}` to signal this,
57
+ along with any message or result, in the `content` field.
58
+ """
59
+
60
+
61
+ class PassTool(ToolMessage):
62
+ """Tool for "passing" on the received msg (ChatDocument),
63
+ so that an as-yet-unspecified agent can handle it.
64
+ Similar to ForwardTool, but without specifying the recipient agent.
65
+ """
66
+
67
+ purpose = """
68
+ To pass the current message so that other agents can handle it.
69
+ """
70
+ request = "pass_tool"
71
+
72
+ def response(self, agent: ChatAgent, chat_doc: ChatDocument) -> ChatDocument:
73
+ """When this tool is enabled for an Agent, this will result in a method
74
+ added to the Agent with signature:
75
+ `forward_tool(self, tool: PassTool, chat_doc: ChatDocument) -> ChatDocument:`
76
+ """
77
+ # if PassTool is in chat_doc, pass its parent, else pass chat_doc itself
78
+ tools = agent.get_tool_messages(chat_doc)
79
+ doc = (
80
+ chat_doc.parent
81
+ if any(isinstance(t, type(self)) for t in tools)
82
+ else chat_doc
83
+ )
84
+ assert doc is not None, "PassTool: parent of chat_doc must not be None"
85
+ new_doc = ChatDocument.deepcopy(doc)
86
+ new_doc.metadata.sender = Entity.AGENT
87
+ return new_doc
88
+
89
+ @classmethod
90
+ def instructions(cls) -> str:
91
+ return """
92
+ Use the `pass_tool` to PASS the current message
93
+ so that another agent can handle it.
94
+ """
95
+
96
+
97
+ class DonePassTool(PassTool):
98
+ """Tool to signal DONE, AND Pass incoming/current msg as result.
99
+ Similar to PassTool, except we append a DoneTool to the result tool_messages.
100
+ """
101
+
102
+ purpose = """
103
+ To signal the current task is done, with results set to the current/incoming msg.
104
+ """
105
+ request = "done_pass_tool"
106
+
107
+ def response(self, agent: ChatAgent, chat_doc: ChatDocument) -> ChatDocument:
108
+ # use PassTool to get the right ChatDocument to pass...
109
+ new_doc = PassTool.response(self, agent, chat_doc)
110
+ tools = agent.get_tool_messages(new_doc)
111
+ # ...then return an AgentDoneTool with content, tools from this ChatDocument
112
+ return AgentDoneTool(content=new_doc.content, tools=tools) # type: ignore
113
+
114
+ @classmethod
115
+ def instructions(cls) -> str:
116
+ return """
117
+ When you determine your task is finished,
118
+ and want to pass the current message as the result of the task,
119
+ use the `done_pass_tool` to signal this.
120
+ """
121
+
122
+
123
+ class ForwardTool(PassTool):
124
+ """Tool for forwarding the received msg (ChatDocument) to another agent.
125
+ Similar to PassTool, but with a specified recipient agent.
126
+ """
127
+
128
+ purpose: str = """
129
+ To forward the current message to an <agent>.
130
+ """
131
+ request: str = "forward_tool"
132
+ agent: str
133
+
134
+ def response(self, agent: ChatAgent, chat_doc: ChatDocument) -> ChatDocument:
135
+ """When this tool is enabled for an Agent, this will result in a method
136
+ added to the Agent with signature:
137
+ `forward_tool(self, tool: ForwardTool, chat_doc: ChatDocument) -> ChatDocument:`
138
+ """
139
+ # if chat_doc contains ForwardTool, then we forward its parent ChatDocument;
140
+ # else forward chat_doc itself
141
+ new_doc = PassTool.response(self, agent, chat_doc)
142
+ new_doc.metadata.recipient = self.agent
143
+ return new_doc
144
+
145
+ @classmethod
146
+ def instructions(cls) -> str:
147
+ return """
148
+ If you need to forward the current message to another agent,
149
+ use the `forward_tool` to do so,
150
+ setting the `recipient` field to the name of the recipient agent.
151
+ """
152
+
153
+
154
+ class SendTool(ToolMessage):
155
+ """Tool for agent or LLM to send content to a specified agent.
156
+ Similar to RecipientTool.
157
+ """
158
+
159
+ purpose: str = """
160
+ To send message <content> to agent specified in <to> field.
161
+ """
162
+ request: str = "send_tool"
163
+ to: str
164
+ content: str = ""
165
+
166
+ def response(self, agent: ChatAgent) -> ChatDocument:
167
+ return agent.create_agent_response(
168
+ self.content,
169
+ recipient=self.to,
170
+ )
171
+
172
+ @classmethod
173
+ def instructions(cls) -> str:
174
+ return """
175
+ If you need to send a message to another agent,
176
+ use the `send_tool` to do so, with these field values:
177
+ - `to` field = name of the recipient agent,
178
+ - `content` field = the message to send.
179
+ """
180
+
181
+ @classmethod
182
+ def examples(cls) -> List["ToolMessage" | Tuple[str, "ToolMessage"]]:
183
+ return [
184
+ cls(to="agent1", content="Hello, agent1!"),
185
+ (
186
+ """
187
+ I need to send the content 'Who built the Gemini model?',
188
+ to the 'Searcher' agent.
189
+ """,
190
+ cls(to="Searcher", content="Who built the Gemini model?"),
191
+ ),
192
+ ]
193
+
194
+
195
+ class AgentSendTool(ToolMessage):
196
+ """Tool for Agent (i.e. agent_response) to send content or tool_messages
197
+ to a specified agent. Similar to SendTool except that AgentSendTool is only
198
+ usable by agent_response (or handler of another tool), to send content or
199
+ tools to another agent. SendTool does not allow sending tools.
200
+ """
201
+
202
+ purpose: str = """
203
+ To send message <content> and <tools> to agent specified in <to> field.
204
+ """
205
+ request: str = "agent_send_tool"
206
+ to: str
207
+ content: str = ""
208
+ tools: List[ToolMessage] = []
209
+ _handle_only: bool = True
210
+
211
+ def response(self, agent: ChatAgent) -> ChatDocument:
212
+ return agent.create_agent_response(
213
+ self.content,
214
+ tool_messages=self.tools,
215
+ recipient=self.to,
216
+ )