langroid 0.23.3__py3-none-any.whl → 0.25.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 +379 -130
- langroid/agent/chat_agent.py +686 -16
- langroid/agent/chat_document.py +8 -3
- langroid/agent/openai_assistant.py +1 -1
- langroid/agent/special/sql/sql_chat_agent.py +20 -6
- langroid/agent/task.py +62 -3
- langroid/agent/tool_message.py +82 -2
- langroid/agent/tools/orchestration.py +10 -5
- langroid/agent/xml_tool_message.py +43 -28
- langroid/language_models/azure_openai.py +18 -5
- langroid/language_models/base.py +22 -0
- langroid/language_models/mock_lm.py +3 -0
- langroid/language_models/openai_gpt.py +81 -4
- langroid/utils/pydantic_utils.py +11 -0
- {langroid-0.23.3.dist-info → langroid-0.25.0.dist-info}/METADATA +3 -1
- {langroid-0.23.3.dist-info → langroid-0.25.0.dist-info}/RECORD +19 -19
- pyproject.toml +2 -2
- {langroid-0.23.3.dist-info → langroid-0.25.0.dist-info}/LICENSE +0 -0
- {langroid-0.23.3.dist-info → langroid-0.25.0.dist-info}/WHEEL +0 -0
langroid/agent/base.py
CHANGED
@@ -142,12 +142,19 @@ class Agent(ABC):
|
|
142
142
|
self.llm_tools_handled: Set[str] = set()
|
143
143
|
self.llm_tools_usable: Set[str] = set()
|
144
144
|
self.llm_tools_known: Set[str] = set() # all known tools, handled/used or not
|
145
|
+
# Indicates which tool-names are allowed to be inferred when
|
146
|
+
# the LLM "forgets" to include the request field in its
|
147
|
+
# tool-call.
|
148
|
+
self.enabled_requests_for_inference: Optional[Set[str]] = (
|
149
|
+
None # If None, we allow all
|
150
|
+
)
|
145
151
|
self.interactive: bool = True # may be modified by Task wrapper
|
146
152
|
self.token_stats_str = ""
|
147
153
|
self.default_human_response: Optional[str] = None
|
148
154
|
self._indent = ""
|
149
155
|
self.llm = LanguageModel.create(config.llm)
|
150
156
|
self.vecdb = VectorStore.create(config.vecdb) if config.vecdb else None
|
157
|
+
self.tool_error = False
|
151
158
|
if config.parsing is not None and self.config.llm is not None:
|
152
159
|
# token_encoding_model is used to obtain the tokenizer,
|
153
160
|
# so in case it's an OpenAI model, we ensure that the tokenizer
|
@@ -168,6 +175,7 @@ class Agent(ABC):
|
|
168
175
|
show_llm_response=noop_fn,
|
169
176
|
show_agent_response=noop_fn,
|
170
177
|
get_user_response=None,
|
178
|
+
get_user_response_async=None,
|
171
179
|
get_last_step=noop_fn,
|
172
180
|
set_parent_agent=noop_fn,
|
173
181
|
show_error_message=noop_fn,
|
@@ -315,6 +323,52 @@ class Agent(ABC):
|
|
315
323
|
lambda msg: message_class.handle_message_fallback(self, msg),
|
316
324
|
)
|
317
325
|
|
326
|
+
async_tool_name = f"{tool}_async"
|
327
|
+
if (
|
328
|
+
hasattr(message_class, "handle_async")
|
329
|
+
and inspect.isfunction(message_class.handle_async)
|
330
|
+
and not hasattr(self, async_tool_name)
|
331
|
+
):
|
332
|
+
has_chat_doc_arg = (
|
333
|
+
len(inspect.signature(message_class.handle_async).parameters) > 1
|
334
|
+
)
|
335
|
+
|
336
|
+
if has_chat_doc_arg:
|
337
|
+
|
338
|
+
@no_type_check
|
339
|
+
async def handler(obj, chat_doc):
|
340
|
+
return await obj.handle_async(chat_doc)
|
341
|
+
|
342
|
+
else:
|
343
|
+
|
344
|
+
@no_type_check
|
345
|
+
async def handler(obj):
|
346
|
+
return await obj.handle_async()
|
347
|
+
|
348
|
+
setattr(self, async_tool_name, handler)
|
349
|
+
elif (
|
350
|
+
hasattr(message_class, "response_async")
|
351
|
+
and inspect.isfunction(message_class.response_async)
|
352
|
+
and not hasattr(self, async_tool_name)
|
353
|
+
):
|
354
|
+
has_chat_doc_arg = (
|
355
|
+
len(inspect.signature(message_class.response_async).parameters) > 2
|
356
|
+
)
|
357
|
+
|
358
|
+
if has_chat_doc_arg:
|
359
|
+
|
360
|
+
@no_type_check
|
361
|
+
async def handler(obj, chat_doc):
|
362
|
+
return await obj.response_async(self, chat_doc)
|
363
|
+
|
364
|
+
else:
|
365
|
+
|
366
|
+
@no_type_check
|
367
|
+
async def handler(obj):
|
368
|
+
return await obj.response_async(self)
|
369
|
+
|
370
|
+
setattr(self, async_tool_name, handler)
|
371
|
+
|
318
372
|
return [tool]
|
319
373
|
|
320
374
|
def enable_message_handling(
|
@@ -386,32 +440,14 @@ class Agent(ABC):
|
|
386
440
|
recipient=recipient,
|
387
441
|
)
|
388
442
|
|
389
|
-
|
443
|
+
def _agent_response_final(
|
390
444
|
self,
|
391
|
-
msg: Optional[str | ChatDocument]
|
392
|
-
|
393
|
-
return self.agent_response(msg)
|
394
|
-
|
395
|
-
def agent_response(
|
396
|
-
self,
|
397
|
-
msg: Optional[str | ChatDocument] = None,
|
445
|
+
msg: Optional[str | ChatDocument],
|
446
|
+
results: Optional[str | OrderedDict[str, str] | ChatDocument],
|
398
447
|
) -> Optional[ChatDocument]:
|
399
448
|
"""
|
400
|
-
|
401
|
-
used to handle LLM's "tool message" or `function_call`
|
402
|
-
(e.g. OpenAI `function_call`).
|
403
|
-
Args:
|
404
|
-
msg (str|ChatDocument): the input to respond to: if msg is a string,
|
405
|
-
and it contains a valid JSON-structured "tool message", or
|
406
|
-
if msg is a ChatDocument, and it contains a `function_call`.
|
407
|
-
Returns:
|
408
|
-
Optional[ChatDocument]: the response, packaged as a ChatDocument
|
409
|
-
|
449
|
+
Convert results to final response.
|
410
450
|
"""
|
411
|
-
if msg is None:
|
412
|
-
return None
|
413
|
-
|
414
|
-
results = self.handle_message(msg)
|
415
451
|
if results is None:
|
416
452
|
return None
|
417
453
|
if not settings.quiet:
|
@@ -431,7 +467,7 @@ class Agent(ABC):
|
|
431
467
|
if isinstance(results, ChatDocument):
|
432
468
|
# Preserve trail of tool_ids for OpenAI Assistant fn-calls
|
433
469
|
results.metadata.tool_ids = (
|
434
|
-
[] if isinstance(msg, str) else msg.metadata.tool_ids
|
470
|
+
[] if msg is None or isinstance(msg, str) else msg.metadata.tool_ids
|
435
471
|
)
|
436
472
|
return results
|
437
473
|
sender_name = self.config.name
|
@@ -454,10 +490,49 @@ class Agent(ABC):
|
|
454
490
|
sender_name=sender_name,
|
455
491
|
oai_tool_id=oai_tool_id,
|
456
492
|
# preserve trail of tool_ids for OpenAI Assistant fn-calls
|
457
|
-
tool_ids=
|
493
|
+
tool_ids=(
|
494
|
+
[] if msg is None or isinstance(msg, str) else msg.metadata.tool_ids
|
495
|
+
),
|
458
496
|
),
|
459
497
|
)
|
460
498
|
|
499
|
+
async def agent_response_async(
|
500
|
+
self,
|
501
|
+
msg: Optional[str | ChatDocument] = None,
|
502
|
+
) -> Optional[ChatDocument]:
|
503
|
+
"""
|
504
|
+
Asynch version of `agent_response`. See there for details.
|
505
|
+
"""
|
506
|
+
if msg is None:
|
507
|
+
return None
|
508
|
+
|
509
|
+
results = await self.handle_message_async(msg)
|
510
|
+
|
511
|
+
return self._agent_response_final(msg, results)
|
512
|
+
|
513
|
+
def agent_response(
|
514
|
+
self,
|
515
|
+
msg: Optional[str | ChatDocument] = None,
|
516
|
+
) -> Optional[ChatDocument]:
|
517
|
+
"""
|
518
|
+
Response from the "agent itself", typically (but not only)
|
519
|
+
used to handle LLM's "tool message" or `function_call`
|
520
|
+
(e.g. OpenAI `function_call`).
|
521
|
+
Args:
|
522
|
+
msg (str|ChatDocument): the input to respond to: if msg is a string,
|
523
|
+
and it contains a valid JSON-structured "tool message", or
|
524
|
+
if msg is a ChatDocument, and it contains a `function_call`.
|
525
|
+
Returns:
|
526
|
+
Optional[ChatDocument]: the response, packaged as a ChatDocument
|
527
|
+
|
528
|
+
"""
|
529
|
+
if msg is None:
|
530
|
+
return None
|
531
|
+
|
532
|
+
results = self.handle_message(msg)
|
533
|
+
|
534
|
+
return self._agent_response_final(msg, results)
|
535
|
+
|
461
536
|
def process_tool_results(
|
462
537
|
self,
|
463
538
|
results: str,
|
@@ -618,60 +693,46 @@ class Agent(ABC):
|
|
618
693
|
recipient=recipient,
|
619
694
|
)
|
620
695
|
|
621
|
-
|
622
|
-
self,
|
623
|
-
msg: Optional[str | ChatDocument] = None,
|
624
|
-
) -> Optional[ChatDocument]:
|
625
|
-
return self.user_response(msg)
|
626
|
-
|
627
|
-
def user_response(
|
628
|
-
self,
|
629
|
-
msg: Optional[str | ChatDocument] = None,
|
630
|
-
) -> Optional[ChatDocument]:
|
696
|
+
def user_can_respond(self, msg: Optional[str | ChatDocument] = None) -> bool:
|
631
697
|
"""
|
632
|
-
|
633
|
-
with an actual answer, or quit using "q" or "x"
|
698
|
+
Whether the user can respond to a message.
|
634
699
|
|
635
700
|
Args:
|
636
701
|
msg (str|ChatDocument): the string to respond to.
|
637
702
|
|
638
703
|
Returns:
|
639
|
-
(str) User response, packaged as a ChatDocument
|
640
704
|
|
641
705
|
"""
|
642
|
-
|
643
706
|
# When msg explicitly addressed to user, this means an actual human response
|
644
707
|
# is being sought.
|
645
708
|
need_human_response = (
|
646
709
|
isinstance(msg, ChatDocument) and msg.metadata.recipient == Entity.USER
|
647
710
|
)
|
648
|
-
default_user_msg = (
|
649
|
-
(self.default_human_response or "null") if need_human_response else ""
|
650
|
-
)
|
651
711
|
|
652
712
|
if not self.interactive and not need_human_response:
|
653
|
-
return
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
713
|
+
return False
|
714
|
+
|
715
|
+
return True
|
716
|
+
|
717
|
+
def _user_response_final(
|
718
|
+
self, msg: Optional[str | ChatDocument], user_msg: str
|
719
|
+
) -> Optional[ChatDocument]:
|
720
|
+
"""
|
721
|
+
Convert user_msg to final response.
|
722
|
+
"""
|
723
|
+
if not user_msg:
|
724
|
+
need_human_response = (
|
725
|
+
isinstance(msg, ChatDocument) and msg.metadata.recipient == Entity.USER
|
726
|
+
)
|
727
|
+
user_msg = (
|
728
|
+
(self.default_human_response or "null") if need_human_response else ""
|
729
|
+
)
|
730
|
+
user_msg = user_msg.strip()
|
669
731
|
|
670
732
|
tool_ids = []
|
671
733
|
if msg is not None and isinstance(msg, ChatDocument):
|
672
734
|
tool_ids = msg.metadata.tool_ids
|
673
735
|
|
674
|
-
user_msg = user_msg.strip() or default_user_msg.strip()
|
675
736
|
# only return non-None result if user_msg not empty
|
676
737
|
if not user_msg:
|
677
738
|
return None
|
@@ -693,6 +754,72 @@ class Agent(ABC):
|
|
693
754
|
),
|
694
755
|
)
|
695
756
|
|
757
|
+
async def user_response_async(
|
758
|
+
self,
|
759
|
+
msg: Optional[str | ChatDocument] = None,
|
760
|
+
) -> Optional[ChatDocument]:
|
761
|
+
"""
|
762
|
+
Asynch version of `user_response`. See there for details.
|
763
|
+
"""
|
764
|
+
if not self.user_can_respond(msg):
|
765
|
+
return None
|
766
|
+
|
767
|
+
if self.default_human_response is not None:
|
768
|
+
user_msg = self.default_human_response
|
769
|
+
else:
|
770
|
+
if (
|
771
|
+
self.callbacks.get_user_response_async is not None
|
772
|
+
and self.callbacks.get_user_response_async is not async_noop_fn
|
773
|
+
):
|
774
|
+
user_msg = await self.callbacks.get_user_response_async(prompt="")
|
775
|
+
elif self.callbacks.get_user_response is not None:
|
776
|
+
user_msg = self.callbacks.get_user_response(prompt="")
|
777
|
+
else:
|
778
|
+
user_msg = Prompt.ask(
|
779
|
+
f"[blue]{self.indent}"
|
780
|
+
+ self.config.human_prompt
|
781
|
+
+ f"\n{self.indent}"
|
782
|
+
)
|
783
|
+
|
784
|
+
return self._user_response_final(msg, user_msg)
|
785
|
+
|
786
|
+
def user_response(
|
787
|
+
self,
|
788
|
+
msg: Optional[str | ChatDocument] = None,
|
789
|
+
) -> Optional[ChatDocument]:
|
790
|
+
"""
|
791
|
+
Get user response to current message. Could allow (human) user to intervene
|
792
|
+
with an actual answer, or quit using "q" or "x"
|
793
|
+
|
794
|
+
Args:
|
795
|
+
msg (str|ChatDocument): the string to respond to.
|
796
|
+
|
797
|
+
Returns:
|
798
|
+
(str) User response, packaged as a ChatDocument
|
799
|
+
|
800
|
+
"""
|
801
|
+
|
802
|
+
if not self.user_can_respond(msg):
|
803
|
+
return None
|
804
|
+
|
805
|
+
if self.default_human_response is not None:
|
806
|
+
user_msg = self.default_human_response
|
807
|
+
else:
|
808
|
+
if self.callbacks.get_user_response is not None:
|
809
|
+
# ask user with empty prompt: no need for prompt
|
810
|
+
# since user has seen the conversation so far.
|
811
|
+
# But non-empty prompt can be useful when Agent
|
812
|
+
# uses a tool that requires user input, or in other scenarios.
|
813
|
+
user_msg = self.callbacks.get_user_response(prompt="")
|
814
|
+
else:
|
815
|
+
user_msg = Prompt.ask(
|
816
|
+
f"[blue]{self.indent}"
|
817
|
+
+ self.config.human_prompt
|
818
|
+
+ f"\n{self.indent}"
|
819
|
+
)
|
820
|
+
|
821
|
+
return self._user_response_final(msg, user_msg)
|
822
|
+
|
696
823
|
@no_type_check
|
697
824
|
def llm_can_respond(self, message: Optional[str | ChatDocument] = None) -> bool:
|
698
825
|
"""
|
@@ -784,7 +911,7 @@ class Agent(ABC):
|
|
784
911
|
f"""
|
785
912
|
Requested output length has been shortened to {output_len}
|
786
913
|
so that the total length of Prompt + Output is less than
|
787
|
-
the completion context length of the LLM.
|
914
|
+
the completion context length of the LLM.
|
788
915
|
"""
|
789
916
|
)
|
790
917
|
|
@@ -858,7 +985,7 @@ class Agent(ABC):
|
|
858
985
|
f"""
|
859
986
|
Requested output length has been shortened to {output_len}
|
860
987
|
so that the total length of Prompt + Output is less than
|
861
|
-
the completion context length of the LLM.
|
988
|
+
the completion context length of the LLM.
|
862
989
|
"""
|
863
990
|
)
|
864
991
|
if self.llm.get_stream() and not settings.quiet:
|
@@ -1040,6 +1167,7 @@ class Agent(ABC):
|
|
1040
1167
|
Returns:
|
1041
1168
|
List[ToolMessage]: list of ToolMessage objects
|
1042
1169
|
"""
|
1170
|
+
self.tool_error = False
|
1043
1171
|
substrings = XMLToolMessage.find_candidates(input_str)
|
1044
1172
|
is_json = False
|
1045
1173
|
if len(substrings) == 0:
|
@@ -1049,7 +1177,11 @@ class Agent(ABC):
|
|
1049
1177
|
return []
|
1050
1178
|
|
1051
1179
|
results = [self._get_one_tool_message(j, is_json) for j in substrings]
|
1052
|
-
|
1180
|
+
valid_results = [r for r in results if r is not None]
|
1181
|
+
# If any tool is correctly formed we do not set the flag
|
1182
|
+
if len(valid_results) > 0:
|
1183
|
+
self.tool_error = False
|
1184
|
+
return valid_results
|
1053
1185
|
|
1054
1186
|
def get_function_call_class(self, msg: ChatDocument) -> Optional[ToolMessage]:
|
1055
1187
|
"""
|
@@ -1063,7 +1195,7 @@ class Agent(ABC):
|
|
1063
1195
|
if tool_name not in self.llm_tools_handled:
|
1064
1196
|
logger.warning(
|
1065
1197
|
f"""
|
1066
|
-
The function_call '{tool_name}' is not handled
|
1198
|
+
The function_call '{tool_name}' is not handled
|
1067
1199
|
by the agent named '{self.config.name}'!
|
1068
1200
|
If you intended this agent to handle this function_call,
|
1069
1201
|
either the fn-call name is incorrectly generated by the LLM,
|
@@ -1071,7 +1203,10 @@ class Agent(ABC):
|
|
1071
1203
|
or you need to enable this agent to handle this fn-call.
|
1072
1204
|
"""
|
1073
1205
|
)
|
1206
|
+
if tool_name not in self.all_llm_tools_known:
|
1207
|
+
self.tool_error = True
|
1074
1208
|
return None
|
1209
|
+
self.tool_error = False
|
1075
1210
|
tool_class = self.llm_tools_map[tool_name]
|
1076
1211
|
tool_msg.update(dict(request=tool_name))
|
1077
1212
|
tool = tool_class.parse_obj(tool_msg)
|
@@ -1086,6 +1221,7 @@ class Agent(ABC):
|
|
1086
1221
|
if msg.oai_tool_calls is None:
|
1087
1222
|
return []
|
1088
1223
|
tools = []
|
1224
|
+
all_errors = True
|
1089
1225
|
for tc in msg.oai_tool_calls:
|
1090
1226
|
if tc.function is None:
|
1091
1227
|
continue
|
@@ -1094,7 +1230,7 @@ class Agent(ABC):
|
|
1094
1230
|
if tool_name not in self.llm_tools_handled:
|
1095
1231
|
logger.warning(
|
1096
1232
|
f"""
|
1097
|
-
The tool_call '{tool_name}' is not handled
|
1233
|
+
The tool_call '{tool_name}' is not handled
|
1098
1234
|
by the agent named '{self.config.name}'!
|
1099
1235
|
If you intended this agent to handle this function_call,
|
1100
1236
|
either the fn-call name is incorrectly generated by the LLM,
|
@@ -1103,11 +1239,14 @@ class Agent(ABC):
|
|
1103
1239
|
"""
|
1104
1240
|
)
|
1105
1241
|
continue
|
1242
|
+
all_errors = False
|
1106
1243
|
tool_class = self.llm_tools_map[tool_name]
|
1107
1244
|
tool_msg.update(dict(request=tool_name))
|
1108
1245
|
tool = tool_class.parse_obj(tool_msg)
|
1109
1246
|
tool.id = tc.id or ""
|
1110
1247
|
tools.append(tool)
|
1248
|
+
# When no tool is valid, set the recovery flag
|
1249
|
+
self.tool_error = all_errors
|
1111
1250
|
return tools
|
1112
1251
|
|
1113
1252
|
def tool_validation_error(self, ve: ValidationError) -> str:
|
@@ -1126,65 +1265,18 @@ class Agent(ABC):
|
|
1126
1265
|
[f"{e['loc']}: {e['msg']}" for e in ve.errors() if "loc" in e]
|
1127
1266
|
)
|
1128
1267
|
return f"""
|
1129
|
-
There were one or more errors in your attempt to use the
|
1130
|
-
TOOL or function_call named '{tool_name}':
|
1268
|
+
There were one or more errors in your attempt to use the
|
1269
|
+
TOOL or function_call named '{tool_name}':
|
1131
1270
|
{bad_field_errors}
|
1132
1271
|
Please write your message again, correcting the errors.
|
1133
1272
|
"""
|
1134
1273
|
|
1135
|
-
def
|
1136
|
-
self,
|
1137
|
-
) ->
|
1274
|
+
def _get_multiple_orch_tool_errs(
|
1275
|
+
self, tools: List[ToolMessage]
|
1276
|
+
) -> List[str | ChatDocument | None]:
|
1138
1277
|
"""
|
1139
|
-
|
1140
|
-
valid "tool" JSON substrings, or a
|
1141
|
-
ChatDocument containing a `function_call` attribute.
|
1142
|
-
Handle with the corresponding handler method, and return
|
1143
|
-
the results as a combined string.
|
1144
|
-
|
1145
|
-
Args:
|
1146
|
-
msg (str | ChatDocument): The string or ChatDocument to handle
|
1147
|
-
|
1148
|
-
Returns:
|
1149
|
-
The result of the handler method can be:
|
1150
|
-
- None if no tools successfully handled, or no tools present
|
1151
|
-
- str if langroid-native JSON tools were handled, and results concatenated,
|
1152
|
-
OR there's a SINGLE OpenAI tool-call.
|
1153
|
-
(We do this so the common scenario of a single tool/fn-call
|
1154
|
-
has a simple behavior).
|
1155
|
-
- Dict[str, str] if multiple OpenAI tool-calls were handled
|
1156
|
-
(dict is an id->result map)
|
1157
|
-
- ChatDocument if a handler returned a ChatDocument, intended to be the
|
1158
|
-
final response of the `agent_response` method.
|
1278
|
+
Return error document if the message contains multiple orchestration tools
|
1159
1279
|
"""
|
1160
|
-
try:
|
1161
|
-
tools = self.get_tool_messages(msg)
|
1162
|
-
tools = [t for t in tools if self._tool_recipient_match(t)]
|
1163
|
-
except ValidationError as ve:
|
1164
|
-
# correct tool name but bad fields
|
1165
|
-
return self.tool_validation_error(ve)
|
1166
|
-
except XMLException as xe: # from XMLToolMessage parsing
|
1167
|
-
return str(xe)
|
1168
|
-
except ValueError:
|
1169
|
-
# invalid tool name
|
1170
|
-
# We return None since returning "invalid tool name" would
|
1171
|
-
# be considered a valid result in task loop, and would be treated
|
1172
|
-
# as a response to the tool message even though the tool was not intended
|
1173
|
-
# for this agent.
|
1174
|
-
return None
|
1175
|
-
if len(tools) > 1 and not self.config.allow_multiple_tools:
|
1176
|
-
return self.to_ChatDocument("ERROR: Use ONE tool at a time!")
|
1177
|
-
if len(tools) == 0:
|
1178
|
-
fallback_result = self.handle_message_fallback(msg)
|
1179
|
-
if fallback_result is None:
|
1180
|
-
return None
|
1181
|
-
return self.to_ChatDocument(
|
1182
|
-
fallback_result,
|
1183
|
-
chat_doc=msg if isinstance(msg, ChatDocument) else None,
|
1184
|
-
)
|
1185
|
-
has_ids = all([t.id != "" for t in tools])
|
1186
|
-
chat_doc = msg if isinstance(msg, ChatDocument) else None
|
1187
|
-
|
1188
1280
|
# check whether there are multiple orchestration-tools (e.g. DoneTool etc),
|
1189
1281
|
# in which case set result to error-string since we don't yet support
|
1190
1282
|
# multi-tools with one or more orch tools.
|
@@ -1211,20 +1303,24 @@ class Agent(ABC):
|
|
1211
1303
|
)
|
1212
1304
|
|
1213
1305
|
has_orch = any(isinstance(t, ORCHESTRATION_TOOLS) for t in tools)
|
1214
|
-
results: List[str | ChatDocument | None]
|
1215
1306
|
if has_orch and len(tools) > 1:
|
1216
1307
|
err_str = "ERROR: Use ONE tool at a time!"
|
1217
|
-
|
1218
|
-
|
1219
|
-
|
1220
|
-
|
1221
|
-
|
1222
|
-
|
1223
|
-
|
1224
|
-
|
1308
|
+
return [err_str for _ in tools]
|
1309
|
+
|
1310
|
+
return []
|
1311
|
+
|
1312
|
+
def _handle_message_final(
|
1313
|
+
self, tools: List[ToolMessage], results: List[str | ChatDocument | None]
|
1314
|
+
) -> None | str | OrderedDict[str, str] | ChatDocument:
|
1315
|
+
"""
|
1316
|
+
Convert results to final response
|
1317
|
+
"""
|
1318
|
+
# extract content from ChatDocument results so we have all str|None
|
1319
|
+
results = [r.content if isinstance(r, ChatDocument) else r for r in results]
|
1225
1320
|
|
1226
|
-
# now all results are str|None
|
1227
1321
|
tool_names = [t.default_value("request") for t in tools]
|
1322
|
+
|
1323
|
+
has_ids = all([t.id != "" for t in tools])
|
1228
1324
|
if has_ids:
|
1229
1325
|
id2result = OrderedDict(
|
1230
1326
|
(t.id, r)
|
@@ -1259,6 +1355,117 @@ class Agent(ABC):
|
|
1259
1355
|
final = "\n\n".join(str_results)
|
1260
1356
|
return final
|
1261
1357
|
|
1358
|
+
async def handle_message_async(
|
1359
|
+
self, msg: str | ChatDocument
|
1360
|
+
) -> None | str | OrderedDict[str, str] | ChatDocument:
|
1361
|
+
"""
|
1362
|
+
Asynch version of `handle_message`. See there for details.
|
1363
|
+
"""
|
1364
|
+
try:
|
1365
|
+
tools = self.get_tool_messages(msg)
|
1366
|
+
tools = [t for t in tools if self._tool_recipient_match(t)]
|
1367
|
+
except ValidationError as ve:
|
1368
|
+
# correct tool name but bad fields
|
1369
|
+
return self.tool_validation_error(ve)
|
1370
|
+
except XMLException as xe: # from XMLToolMessage parsing
|
1371
|
+
return str(xe)
|
1372
|
+
except ValueError:
|
1373
|
+
# invalid tool name
|
1374
|
+
# We return None since returning "invalid tool name" would
|
1375
|
+
# be considered a valid result in task loop, and would be treated
|
1376
|
+
# as a response to the tool message even though the tool was not intended
|
1377
|
+
# for this agent.
|
1378
|
+
return None
|
1379
|
+
if len(tools) > 1 and not self.config.allow_multiple_tools:
|
1380
|
+
return self.to_ChatDocument("ERROR: Use ONE tool at a time!")
|
1381
|
+
if len(tools) == 0:
|
1382
|
+
fallback_result = self.handle_message_fallback(msg)
|
1383
|
+
if fallback_result is None:
|
1384
|
+
return None
|
1385
|
+
return self.to_ChatDocument(
|
1386
|
+
fallback_result,
|
1387
|
+
chat_doc=msg if isinstance(msg, ChatDocument) else None,
|
1388
|
+
)
|
1389
|
+
chat_doc = msg if isinstance(msg, ChatDocument) else None
|
1390
|
+
|
1391
|
+
results = self._get_multiple_orch_tool_errs(tools)
|
1392
|
+
if not results:
|
1393
|
+
results = [
|
1394
|
+
await self.handle_tool_message_async(t, chat_doc=chat_doc)
|
1395
|
+
for t in tools
|
1396
|
+
]
|
1397
|
+
# if there's a solitary ChatDocument|str result, return it as is
|
1398
|
+
if len(results) == 1 and isinstance(results[0], (str, ChatDocument)):
|
1399
|
+
return results[0]
|
1400
|
+
|
1401
|
+
return self._handle_message_final(tools, results)
|
1402
|
+
|
1403
|
+
def handle_message(
|
1404
|
+
self, msg: str | ChatDocument
|
1405
|
+
) -> None | str | OrderedDict[str, str] | ChatDocument:
|
1406
|
+
"""
|
1407
|
+
Handle a "tool" message either a string containing one or more
|
1408
|
+
valid "tool" JSON substrings, or a
|
1409
|
+
ChatDocument containing a `function_call` attribute.
|
1410
|
+
Handle with the corresponding handler method, and return
|
1411
|
+
the results as a combined string.
|
1412
|
+
|
1413
|
+
Args:
|
1414
|
+
msg (str | ChatDocument): The string or ChatDocument to handle
|
1415
|
+
|
1416
|
+
Returns:
|
1417
|
+
The result of the handler method can be:
|
1418
|
+
- None if no tools successfully handled, or no tools present
|
1419
|
+
- str if langroid-native JSON tools were handled, and results concatenated,
|
1420
|
+
OR there's a SINGLE OpenAI tool-call.
|
1421
|
+
(We do this so the common scenario of a single tool/fn-call
|
1422
|
+
has a simple behavior).
|
1423
|
+
- Dict[str, str] if multiple OpenAI tool-calls were handled
|
1424
|
+
(dict is an id->result map)
|
1425
|
+
- ChatDocument if a handler returned a ChatDocument, intended to be the
|
1426
|
+
final response of the `agent_response` method.
|
1427
|
+
"""
|
1428
|
+
try:
|
1429
|
+
tools = self.get_tool_messages(msg)
|
1430
|
+
tools = [t for t in tools if self._tool_recipient_match(t)]
|
1431
|
+
except ValidationError as ve:
|
1432
|
+
# correct tool name but bad fields
|
1433
|
+
return self.tool_validation_error(ve)
|
1434
|
+
except XMLException as xe: # from XMLToolMessage parsing
|
1435
|
+
return str(xe)
|
1436
|
+
except ValueError:
|
1437
|
+
# invalid tool name
|
1438
|
+
# We return None since returning "invalid tool name" would
|
1439
|
+
# be considered a valid result in task loop, and would be treated
|
1440
|
+
# as a response to the tool message even though the tool was not intended
|
1441
|
+
# for this agent.
|
1442
|
+
return None
|
1443
|
+
if len(tools) > 1 and not self.config.allow_multiple_tools:
|
1444
|
+
return self.to_ChatDocument("ERROR: Use ONE tool at a time!")
|
1445
|
+
if len(tools) == 0:
|
1446
|
+
fallback_result = self.handle_message_fallback(msg)
|
1447
|
+
if fallback_result is None:
|
1448
|
+
return None
|
1449
|
+
return self.to_ChatDocument(
|
1450
|
+
fallback_result,
|
1451
|
+
chat_doc=msg if isinstance(msg, ChatDocument) else None,
|
1452
|
+
)
|
1453
|
+
chat_doc = msg if isinstance(msg, ChatDocument) else None
|
1454
|
+
|
1455
|
+
results = self._get_multiple_orch_tool_errs(tools)
|
1456
|
+
if not results:
|
1457
|
+
results = [self.handle_tool_message(t, chat_doc=chat_doc) for t in tools]
|
1458
|
+
# if there's a solitary ChatDocument|str result, return it as is
|
1459
|
+
if len(results) == 1 and isinstance(results[0], (str, ChatDocument)):
|
1460
|
+
return results[0]
|
1461
|
+
|
1462
|
+
return self._handle_message_final(tools, results)
|
1463
|
+
|
1464
|
+
@property
|
1465
|
+
def all_llm_tools_known(self) -> set[str]:
|
1466
|
+
"""All known tools; this may extend self.llm_tools_known."""
|
1467
|
+
return self.llm_tools_known
|
1468
|
+
|
1262
1469
|
def handle_message_fallback(self, msg: str | ChatDocument) -> Any:
|
1263
1470
|
"""
|
1264
1471
|
Fallback method for the "no-tools" scenario.
|
@@ -1278,7 +1485,7 @@ class Agent(ABC):
|
|
1278
1485
|
) -> Optional[ToolMessage]:
|
1279
1486
|
"""
|
1280
1487
|
Parse the tool_candidate_str into ANY ToolMessage KNOWN to agent --
|
1281
|
-
This includes non-used/handled tools, i.e. any tool in self.
|
1488
|
+
This includes non-used/handled tools, i.e. any tool in self.all_llm_tools_known.
|
1282
1489
|
The exception to this is below where we try our best to infer the tool
|
1283
1490
|
when the LLM has "forgotten" to include the "request" field in the tool str ---
|
1284
1491
|
in this case we ONLY look at the possible set of HANDLED tools, i.e.
|
@@ -1311,6 +1518,7 @@ class Agent(ABC):
|
|
1311
1518
|
# }
|
1312
1519
|
|
1313
1520
|
if not isinstance(maybe_tool_dict, dict):
|
1521
|
+
self.tool_error = True
|
1314
1522
|
return None
|
1315
1523
|
|
1316
1524
|
properties = maybe_tool_dict.get("properties")
|
@@ -1318,7 +1526,14 @@ class Agent(ABC):
|
|
1318
1526
|
maybe_tool_dict = properties
|
1319
1527
|
request = maybe_tool_dict.get("request")
|
1320
1528
|
if request is None:
|
1321
|
-
|
1529
|
+
if self.enabled_requests_for_inference is None:
|
1530
|
+
possible = [self.llm_tools_map[r] for r in self.llm_tools_handled]
|
1531
|
+
else:
|
1532
|
+
allowable = self.enabled_requests_for_inference.intersection(
|
1533
|
+
self.llm_tools_handled
|
1534
|
+
)
|
1535
|
+
possible = [self.llm_tools_map[r] for r in allowable]
|
1536
|
+
|
1322
1537
|
default_keys = set(ToolMessage.__fields__.keys())
|
1323
1538
|
request_keys = set(maybe_tool_dict.keys())
|
1324
1539
|
|
@@ -1351,19 +1566,23 @@ class Agent(ABC):
|
|
1351
1566
|
if len(candidate_tools) == 1:
|
1352
1567
|
return candidate_tools[0]
|
1353
1568
|
else:
|
1569
|
+
self.tool_error = True
|
1354
1570
|
return None
|
1355
1571
|
|
1356
|
-
if not isinstance(request, str) or request not in self.
|
1572
|
+
if not isinstance(request, str) or request not in self.all_llm_tools_known:
|
1573
|
+
self.tool_error = True
|
1357
1574
|
return None
|
1358
1575
|
|
1359
1576
|
message_class = self.llm_tools_map.get(request)
|
1360
1577
|
if message_class is None:
|
1361
1578
|
logger.warning(f"No message class found for request '{request}'")
|
1579
|
+
self.tool_error = True
|
1362
1580
|
return None
|
1363
1581
|
|
1364
1582
|
try:
|
1365
1583
|
message = message_class.parse_obj(maybe_tool_dict)
|
1366
1584
|
except ValidationError as ve:
|
1585
|
+
self.tool_error = True
|
1367
1586
|
raise ve
|
1368
1587
|
return message
|
1369
1588
|
|
@@ -1474,7 +1693,6 @@ class Agent(ABC):
|
|
1474
1693
|
value = tool.get_value_of_type(output_type)
|
1475
1694
|
if value is not None:
|
1476
1695
|
return cast(T, value)
|
1477
|
-
|
1478
1696
|
return None
|
1479
1697
|
|
1480
1698
|
def _maybe_truncate_result(
|
@@ -1511,6 +1729,37 @@ class Agent(ABC):
|
|
1511
1729
|
) + truncate_warning
|
1512
1730
|
return result
|
1513
1731
|
|
1732
|
+
async def handle_tool_message_async(
|
1733
|
+
self,
|
1734
|
+
tool: ToolMessage,
|
1735
|
+
chat_doc: Optional[ChatDocument] = None,
|
1736
|
+
) -> None | str | ChatDocument:
|
1737
|
+
"""
|
1738
|
+
Asynch version of `handle_tool_message`. See there for details.
|
1739
|
+
"""
|
1740
|
+
tool_name = tool.default_value("request")
|
1741
|
+
handler_method = getattr(self, tool_name + "_async", None)
|
1742
|
+
if handler_method is None:
|
1743
|
+
return self.handle_tool_message(tool, chat_doc=chat_doc)
|
1744
|
+
has_chat_doc_arg = (
|
1745
|
+
chat_doc is not None
|
1746
|
+
and "chat_doc" in inspect.signature(handler_method).parameters
|
1747
|
+
)
|
1748
|
+
try:
|
1749
|
+
if has_chat_doc_arg:
|
1750
|
+
maybe_result = await handler_method(tool, chat_doc=chat_doc)
|
1751
|
+
else:
|
1752
|
+
maybe_result = await handler_method(tool)
|
1753
|
+
result = self.to_ChatDocument(maybe_result, tool_name, chat_doc)
|
1754
|
+
except Exception as e:
|
1755
|
+
# raise the error here since we are sure it's
|
1756
|
+
# not a pydantic validation error,
|
1757
|
+
# which we check in `handle_message`
|
1758
|
+
raise e
|
1759
|
+
return self._maybe_truncate_result(
|
1760
|
+
result, tool._max_result_tokens
|
1761
|
+
) # type: ignore
|
1762
|
+
|
1514
1763
|
def handle_tool_message(
|
1515
1764
|
self,
|
1516
1765
|
tool: ToolMessage,
|