langroid 0.6.7__py3-none-any.whl → 0.9.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.
- langroid/agent/base.py +499 -55
- langroid/agent/callbacks/chainlit.py +1 -1
- langroid/agent/chat_agent.py +191 -37
- langroid/agent/chat_document.py +142 -29
- langroid/agent/openai_assistant.py +20 -4
- 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/special/sql/sql_chat_agent.py +69 -13
- langroid/agent/task.py +179 -43
- langroid/agent/tool_message.py +19 -7
- langroid/agent/tools/__init__.py +5 -0
- langroid/agent/tools/orchestration.py +216 -0
- langroid/agent/tools/recipient_tool.py +6 -11
- langroid/agent/tools/rewind_tool.py +1 -1
- langroid/agent/typed_task.py +19 -0
- langroid/language_models/.chainlit/config.toml +121 -0
- langroid/language_models/.chainlit/translations/en-US.json +231 -0
- langroid/language_models/base.py +114 -12
- langroid/language_models/mock_lm.py +10 -1
- langroid/language_models/openai_gpt.py +260 -36
- 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.6.7.dist-info → langroid-0.9.0.dist-info}/METADATA +4 -1
- {langroid-0.6.7.dist-info → langroid-0.9.0.dist-info}/RECORD +32 -33
- 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.6.7.dist-info → langroid-0.9.0.dist-info}/LICENSE +0 -0
- {langroid-0.6.7.dist-info → langroid-0.9.0.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,
|
@@ -923,7 +934,6 @@ class Task:
|
|
923
934
|
# create dummy msg for logging
|
924
935
|
log_doc = ChatDocument(
|
925
936
|
content="[CANNOT RESPOND]",
|
926
|
-
function_call=None,
|
927
937
|
metadata=ChatDocMetaData(
|
928
938
|
sender=r if isinstance(r, Entity) else Entity.USER,
|
929
939
|
sender_name=str(r),
|
@@ -1023,11 +1033,11 @@ class Task:
|
|
1023
1033
|
# (responder, result) from a responder who explicitly said NO_ANSWER
|
1024
1034
|
no_answer_response: None | Tuple[Responder, ChatDocument] = None
|
1025
1035
|
for r in responders:
|
1036
|
+
self.is_pass_thru = False
|
1026
1037
|
if not self._can_respond(r):
|
1027
1038
|
# create dummy msg for logging
|
1028
1039
|
log_doc = ChatDocument(
|
1029
1040
|
content="[CANNOT RESPOND]",
|
1030
|
-
function_call=None,
|
1031
1041
|
metadata=ChatDocMetaData(
|
1032
1042
|
sender=r if isinstance(r, Entity) else Entity.USER,
|
1033
1043
|
sender_name=str(r),
|
@@ -1097,10 +1107,7 @@ class Task:
|
|
1097
1107
|
# Contrast this with self.pending_message.metadata.sender, which is an ENTITY
|
1098
1108
|
# of this agent, or a sub-task's agent.
|
1099
1109
|
if not self.is_pass_thru:
|
1100
|
-
if (
|
1101
|
-
self.pending_message is not None
|
1102
|
-
and self.pending_message.metadata.agent_id == self.agent.id
|
1103
|
-
):
|
1110
|
+
if self.pending_message is not None and not isinstance(r, Task):
|
1104
1111
|
# when pending msg is from our own agent, respect the sender set there,
|
1105
1112
|
# since sometimes a response may "mock" as if the response is from
|
1106
1113
|
# another entity (e.g when using RewindTool, the agent handler
|
@@ -1111,7 +1118,7 @@ class Task:
|
|
1111
1118
|
self.pending_sender = r
|
1112
1119
|
self.pending_message = result
|
1113
1120
|
# set the parent/child links ONLY if not already set by agent internally,
|
1114
|
-
# which may happen when using the RewindTool
|
1121
|
+
# which may happen when using the RewindTool, or in other scenarios.
|
1115
1122
|
if parent is not None and not result.metadata.parent_id:
|
1116
1123
|
result.metadata.parent_id = parent.id()
|
1117
1124
|
if parent is not None and not parent.metadata.child_id:
|
@@ -1166,6 +1173,26 @@ class Task:
|
|
1166
1173
|
msg_str = escape(str(self.pending_message))
|
1167
1174
|
print(f"[grey37][{sender_str}]{msg_str}[/grey37]")
|
1168
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
|
+
|
1169
1196
|
def response(
|
1170
1197
|
self,
|
1171
1198
|
e: Responder,
|
@@ -1178,15 +1205,40 @@ class Task:
|
|
1178
1205
|
actual_turns = e.turns if e.turns > 0 else turns
|
1179
1206
|
e.agent.callbacks.set_parent_agent(self.agent)
|
1180
1207
|
# e.callbacks.set_parent_agent(self.agent)
|
1181
|
-
|
1182
|
-
|
1183
|
-
|
1184
|
-
|
1185
|
-
|
1186
|
-
|
1187
|
-
)
|
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,
|
1223
|
+
)
|
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
|
1237
|
+
|
1188
1238
|
result_str = ( # only used by callback to display content and possible tool
|
1189
|
-
"NONE"
|
1239
|
+
"NONE"
|
1240
|
+
if result is None
|
1241
|
+
else "\n\n".join(str(m) for m in ChatDocument.to_LLMMessage(result))
|
1190
1242
|
)
|
1191
1243
|
maybe_tool = len(extract_top_level_json(result_str)) > 0
|
1192
1244
|
self.callbacks.show_subtask_response(
|
@@ -1197,14 +1249,26 @@ class Task:
|
|
1197
1249
|
else:
|
1198
1250
|
response_fn = self._entity_responder_map[cast(Entity, e)]
|
1199
1251
|
result = response_fn(self.pending_message)
|
1200
|
-
return self._process_result_routing(result)
|
1252
|
+
return self._process_result_routing(result, e)
|
1201
1253
|
|
1202
1254
|
def _process_result_routing(
|
1203
|
-
self, result: ChatDocument | None
|
1255
|
+
self, result: ChatDocument | None, e: Responder
|
1204
1256
|
) -> ChatDocument | None:
|
1205
1257
|
# process result in case there is a routing instruction
|
1206
1258
|
if result is None:
|
1207
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])
|
1208
1272
|
# if result content starts with @name, set recipient to name
|
1209
1273
|
is_pass, recipient, content = parse_routing(
|
1210
1274
|
result,
|
@@ -1258,15 +1322,42 @@ class Task:
|
|
1258
1322
|
if isinstance(e, Task):
|
1259
1323
|
actual_turns = e.turns if e.turns > 0 else turns
|
1260
1324
|
e.agent.callbacks.set_parent_agent(self.agent)
|
1261
|
-
|
1262
|
-
|
1263
|
-
|
1264
|
-
|
1265
|
-
|
1266
|
-
|
1267
|
-
|
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
|
+
|
1356
|
+
result_str = ( # only used by callback to display content and possible tool
|
1357
|
+
"NONE"
|
1358
|
+
if result is None
|
1359
|
+
else "\n\n".join(str(m) for m in ChatDocument.to_LLMMessage(result))
|
1268
1360
|
)
|
1269
|
-
result_str = str(ChatDocument.to_LLMMessage(result))
|
1270
1361
|
maybe_tool = len(extract_top_level_json(result_str)) > 0
|
1271
1362
|
self.callbacks.show_subtask_response(
|
1272
1363
|
task=e,
|
@@ -1276,7 +1367,7 @@ class Task:
|
|
1276
1367
|
else:
|
1277
1368
|
response_fn = self._entity_responder_async_map[cast(Entity, e)]
|
1278
1369
|
result = await response_fn(self.pending_message)
|
1279
|
-
return self._process_result_routing(result)
|
1370
|
+
return self._process_result_routing(result, e)
|
1280
1371
|
|
1281
1372
|
def result(self, status: StatusCode | None = None) -> ChatDocument | None:
|
1282
1373
|
"""
|
@@ -1301,8 +1392,24 @@ class Task:
|
|
1301
1392
|
if DONE in content:
|
1302
1393
|
# assuming it is of the form "DONE: <content>"
|
1303
1394
|
content = content.replace(DONE, "").strip()
|
1395
|
+
oai_tool_calls = result_msg.oai_tool_calls if result_msg else None
|
1396
|
+
oai_tool_id2result = result_msg.oai_tool_id2result if result_msg else None
|
1304
1397
|
fun_call = result_msg.function_call if result_msg else None
|
1305
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
|
+
]
|
1306
1413
|
block = result_msg.metadata.block if result_msg else None
|
1307
1414
|
recipient = result_msg.metadata.recipient if result_msg else ""
|
1308
1415
|
tool_ids = result_msg.metadata.tool_ids if result_msg else []
|
@@ -1312,6 +1419,8 @@ class Task:
|
|
1312
1419
|
# since to the "parent" task, this result is equivalent to a response from USER
|
1313
1420
|
result_doc = ChatDocument(
|
1314
1421
|
content=content,
|
1422
|
+
oai_tool_calls=oai_tool_calls,
|
1423
|
+
oai_tool_id2result=oai_tool_id2result,
|
1315
1424
|
function_call=fun_call,
|
1316
1425
|
tool_messages=tool_messages,
|
1317
1426
|
metadata=ChatDocMetaData(
|
@@ -1346,6 +1455,8 @@ class Task:
|
|
1346
1455
|
isinstance(msg, ChatDocument)
|
1347
1456
|
and msg.content.strip() in [PASS, ""]
|
1348
1457
|
and msg.function_call is None
|
1458
|
+
and msg.oai_tool_calls is None
|
1459
|
+
and msg.oai_tool_id2result is None
|
1349
1460
|
and msg.tool_messages == []
|
1350
1461
|
)
|
1351
1462
|
)
|
@@ -1357,7 +1468,16 @@ class Task:
|
|
1357
1468
|
|
1358
1469
|
response_says_done = result is not None and (
|
1359
1470
|
(isinstance(result, str) and DONE in result)
|
1360
|
-
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
|
+
)
|
1361
1481
|
)
|
1362
1482
|
return (
|
1363
1483
|
(
|
@@ -1469,13 +1589,22 @@ class Task:
|
|
1469
1589
|
if self._is_kill():
|
1470
1590
|
return (True, StatusCode.KILL)
|
1471
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
|
+
|
1472
1601
|
user_quit = (
|
1473
1602
|
result is not None
|
1474
|
-
and (result.content in USER_QUIT_STRINGS or
|
1603
|
+
and (result.content in USER_QUIT_STRINGS or done_result)
|
1475
1604
|
and result.metadata.sender == Entity.USER
|
1476
1605
|
)
|
1477
|
-
if self._level == 0 and self.
|
1478
|
-
# for top-level task,
|
1606
|
+
if self._level == 0 and self._user_can_respond() and self.only_user_quits_root:
|
1607
|
+
# for top-level task, only user can quit out
|
1479
1608
|
return (user_quit, StatusCode.USER_QUIT if user_quit else StatusCode.OK)
|
1480
1609
|
|
1481
1610
|
if self.is_done:
|
@@ -1510,8 +1639,7 @@ class Task:
|
|
1510
1639
|
final = (
|
1511
1640
|
# no valid response from any entity/agent in current turn
|
1512
1641
|
result is None
|
1513
|
-
|
1514
|
-
or DONE in result.content
|
1642
|
+
or done_result
|
1515
1643
|
or ( # current task is addressing message to caller task
|
1516
1644
|
self.caller is not None
|
1517
1645
|
and self.caller.name != ""
|
@@ -1630,14 +1758,17 @@ class Task:
|
|
1630
1758
|
and recipient != self.name # case sensitive
|
1631
1759
|
)
|
1632
1760
|
|
1633
|
-
def
|
1634
|
-
|
1761
|
+
def _user_can_respond(self) -> bool:
|
1762
|
+
return self.interactive or (
|
1635
1763
|
# regardless of self.interactive, if a msg is explicitly addressed to
|
1636
1764
|
# user, then wait for user response
|
1637
1765
|
self.pending_message is not None
|
1638
1766
|
and self.pending_message.metadata.recipient == Entity.USER
|
1639
1767
|
)
|
1640
1768
|
|
1769
|
+
def _can_respond(self, e: Responder) -> bool:
|
1770
|
+
user_can_respond = self._user_can_respond()
|
1771
|
+
|
1641
1772
|
if self.pending_sender == e or (e == Entity.USER and not user_can_respond):
|
1642
1773
|
# sender is same as e (an entity cannot respond to its own msg),
|
1643
1774
|
# or user cannot respond
|
@@ -1645,6 +1776,11 @@ class Task:
|
|
1645
1776
|
|
1646
1777
|
if self.pending_message is None:
|
1647
1778
|
return True
|
1779
|
+
if isinstance(e, Task) and e.agent.has_only_unhandled_tools(
|
1780
|
+
self.pending_message
|
1781
|
+
):
|
1782
|
+
return False
|
1783
|
+
|
1648
1784
|
if self._recipient_mismatch(e):
|
1649
1785
|
# Cannot respond if not addressed to this entity
|
1650
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,
|
@@ -39,14 +39,21 @@ class ToolMessage(ABC, BaseModel):
|
|
39
39
|
|
40
40
|
request: str
|
41
41
|
purpose: str
|
42
|
+
id: str = "" # placeholder for OpenAI-API tool_call_id
|
43
|
+
|
44
|
+
model_config = ConfigDict(extra=Extra.allow)
|
45
|
+
|
46
|
+
_handle_only: bool = False # only allow handling, but not use (LLM-generation)?
|
42
47
|
|
43
48
|
class Config:
|
49
|
+
# only HANDLING allowed, NOT "use" (i.e LLM generation)
|
50
|
+
handle_only: bool = False
|
44
51
|
arbitrary_types_allowed = False
|
45
52
|
validate_all = True
|
46
53
|
validate_assignment = True
|
47
54
|
# do not include these fields in the generated schema
|
48
55
|
# since we don't require the LLM to specify them
|
49
|
-
schema_extra = {"exclude": {"purpose"}}
|
56
|
+
schema_extra = {"exclude": {"purpose", "id", "model_config"}}
|
50
57
|
|
51
58
|
@classmethod
|
52
59
|
def instructions(cls) -> str:
|
@@ -108,13 +115,13 @@ class ToolMessage(ABC, BaseModel):
|
|
108
115
|
return "\n\n".join(examples_jsons)
|
109
116
|
|
110
117
|
def to_json(self) -> str:
|
111
|
-
return self.json(indent=4, exclude=
|
118
|
+
return self.json(indent=4, exclude=self.Config.schema_extra["exclude"])
|
112
119
|
|
113
120
|
def json_example(self) -> str:
|
114
|
-
return self.json(indent=4, exclude=
|
121
|
+
return self.json(indent=4, exclude=self.Config.schema_extra["exclude"])
|
115
122
|
|
116
123
|
def dict_example(self) -> Dict[str, Any]:
|
117
|
-
return self.dict(exclude=
|
124
|
+
return self.dict(exclude=self.Config.schema_extra["exclude"])
|
118
125
|
|
119
126
|
@classmethod
|
120
127
|
def default_value(cls, f: str) -> Any:
|
@@ -218,7 +225,9 @@ class ToolMessage(ABC, BaseModel):
|
|
218
225
|
if "description" not in parameters["properties"][name]:
|
219
226
|
parameters["properties"][name]["description"] = description
|
220
227
|
|
221
|
-
excludes = ["
|
228
|
+
excludes = cls.Config.schema_extra["exclude"]
|
229
|
+
if not request:
|
230
|
+
excludes = excludes.union({"request"})
|
222
231
|
# exclude 'excludes' from parameters["properties"]:
|
223
232
|
parameters["properties"] = {
|
224
233
|
field: details
|
@@ -259,5 +268,8 @@ class ToolMessage(ABC, BaseModel):
|
|
259
268
|
Returns:
|
260
269
|
Dict[str, Any]: simplified schema
|
261
270
|
"""
|
262
|
-
schema = generate_simple_schema(
|
271
|
+
schema = generate_simple_schema(
|
272
|
+
cls,
|
273
|
+
exclude=list(cls.Config.schema_extra["exclude"]),
|
274
|
+
)
|
263
275
|
return schema
|
langroid/agent/tools/__init__.py
CHANGED
@@ -1,9 +1,11 @@
|
|
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 AgentDoneTool, DoneTool
|
7
9
|
|
8
10
|
__all__ = [
|
9
11
|
"GoogleSearchTool",
|
@@ -13,4 +15,7 @@ __all__ = [
|
|
13
15
|
"recipient_tool",
|
14
16
|
"rewind_tool",
|
15
17
|
"RewindTool",
|
18
|
+
"orchestration",
|
19
|
+
"AgentDoneTool",
|
20
|
+
"DoneTool",
|
16
21
|
]
|