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/base.py +353 -94
- langroid/agent/chat_agent.py +68 -9
- langroid/agent/chat_document.py +16 -7
- langroid/agent/openai_assistant.py +12 -1
- langroid/agent/special/lance_doc_chat_agent.py +25 -18
- langroid/agent/special/lance_rag/critic_agent.py +37 -5
- langroid/agent/special/lance_rag/query_planner_agent.py +102 -63
- langroid/agent/special/lance_tools.py +10 -2
- langroid/agent/task.py +156 -47
- langroid/agent/tool_message.py +12 -3
- langroid/agent/tools/__init__.py +18 -0
- langroid/agent/tools/orchestration.py +216 -0
- langroid/agent/tools/recipient_tool.py +6 -11
- langroid/agent/typed_task.py +19 -0
- langroid/language_models/base.py +3 -2
- langroid/mytypes.py +0 -1
- langroid/parsing/parse_json.py +19 -2
- langroid/utils/pydantic_utils.py +19 -0
- langroid/vector_store/base.py +3 -1
- langroid/vector_store/lancedb.py +2 -0
- {langroid-0.8.0.dist-info → langroid-0.9.1.dist-info}/METADATA +4 -2
- {langroid-0.8.0.dist-info → langroid-0.9.1.dist-info}/RECORD +25 -28
- pyproject.toml +2 -1
- langroid/agent/special/lance_rag_new/__init__.py +0 -9
- langroid/agent/special/lance_rag_new/critic_agent.py +0 -171
- langroid/agent/special/lance_rag_new/lance_rag_task.py +0 -144
- langroid/agent/special/lance_rag_new/query_planner_agent.py +0 -222
- langroid/agent/team.py +0 -1758
- {langroid-0.8.0.dist-info → langroid-0.9.1.dist-info}/LICENSE +0 -0
- {langroid-0.8.0.dist-info → langroid-0.9.1.dist-info}/WHEEL +0 -0
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
|
-
"""
|
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
|
591
|
-
self.
|
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
|
-
|
1180
|
-
|
1181
|
-
|
1182
|
-
|
1183
|
-
|
1184
|
-
|
1185
|
-
)
|
1186
|
-
|
1187
|
-
|
1188
|
-
|
1189
|
-
|
1190
|
-
|
1191
|
-
|
1192
|
-
|
1193
|
-
|
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
|
1197
|
-
|
1198
|
-
|
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
|
-
|
1276
|
-
|
1277
|
-
|
1278
|
-
|
1279
|
-
|
1280
|
-
|
1281
|
-
|
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 (
|
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
|
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
|
-
|
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
|
langroid/agent/tool_message.py
CHANGED
@@ -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(
|
271
|
+
schema = generate_simple_schema(
|
272
|
+
cls,
|
273
|
+
exclude=list(cls.Config.schema_extra["exclude"]),
|
274
|
+
)
|
266
275
|
return schema
|
langroid/agent/tools/__init__.py
CHANGED
@@ -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
|
+
)
|