langroid 0.10.1__py3-none-any.whl → 0.11.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/task.py CHANGED
@@ -18,7 +18,9 @@ from typing import (
18
18
  Optional,
19
19
  Tuple,
20
20
  Type,
21
+ TypeVar,
21
22
  cast,
23
+ overload,
22
24
  )
23
25
 
24
26
  import numpy as np
@@ -33,8 +35,8 @@ from langroid.agent.chat_document import (
33
35
  ChatDocument,
34
36
  StatusCode,
35
37
  )
36
- from langroid.agent.tool_message import FinalResultTool, ToolMessage
37
- from langroid.agent.tools.orchestration import AgentDoneTool, DoneTool
38
+ from langroid.agent.tool_message import ToolMessage
39
+ from langroid.agent.tools.orchestration import AgentDoneTool, DoneTool, FinalResultTool
38
40
  from langroid.cachedb.redis_cachedb import RedisCache, RedisCacheConfig
39
41
  from langroid.exceptions import InfiniteLoopException
40
42
  from langroid.mytypes import Entity
@@ -53,11 +55,14 @@ from langroid.utils.constants import (
53
55
  from langroid.utils.logging import RichFileLogger, setup_file_logger
54
56
  from langroid.utils.object_registry import scheduled_cleanup
55
57
  from langroid.utils.system import hash
58
+ from langroid.utils.types import to_string
56
59
 
57
60
  logger = logging.getLogger(__name__)
58
61
 
59
62
  Responder = Entity | Type["Task"]
60
63
 
64
+ T = TypeVar("T")
65
+
61
66
 
62
67
  def noop_fn(*args: List[Any], **kwargs: Dict[str, Any]) -> None:
63
68
  pass
@@ -153,6 +158,7 @@ class Task:
153
158
  erase_substeps: bool = False,
154
159
  allow_null_result: bool = False,
155
160
  max_stalled_steps: int = 5,
161
+ default_return_type: Optional[type] = None,
156
162
  done_if_no_response: List[Responder] = [],
157
163
  done_if_response: List[Responder] = [],
158
164
  config: TaskConfig = TaskConfig(),
@@ -190,6 +196,8 @@ class Task:
190
196
  default_human_response (str|None): default response from user; useful for
191
197
  testing, to avoid interactive input from user.
192
198
  [Instead of this, setting `interactive` usually suffices]
199
+ default_return_type: if not None, extracts a value of this type from the
200
+ result of self.run()
193
201
  interactive (bool): if true, wait for human input after each non-human
194
202
  response (prevents infinite loop of non-human responses).
195
203
  Default is true. If false, then `default_human_response` is set to ""
@@ -259,8 +267,7 @@ class Task:
259
267
  agent = cast(ChatAgent, agent)
260
268
  self.agent: ChatAgent = agent
261
269
  if isinstance(agent, ChatAgent) and len(agent.message_history) == 0 or restart:
262
- self.agent.clear_history(0)
263
- self.agent.clear_dialog()
270
+ self.agent.init_state()
264
271
  # possibly change the system and user messages
265
272
  if system_message:
266
273
  # we always have at least 1 task_message
@@ -299,6 +306,7 @@ class Task:
299
306
  self.agent.interactive = interactive
300
307
  self.only_user_quits_root = only_user_quits_root
301
308
  self.message_history_idx = -1
309
+ self.default_return_type = default_return_type
302
310
 
303
311
  # set to True if we want to collapse multi-turn conversation with sub-tasks into
304
312
  # just the first outgoing message and last incoming message.
@@ -579,22 +587,54 @@ class Task:
579
587
  Recursively reset message history & state of own agent and
580
588
  those of all sub-tasks.
581
589
  """
582
- self.agent.clear_history(0)
583
- self.agent.clear_dialog()
584
590
  self.agent.init_state()
585
591
  for t in self.sub_tasks:
586
592
  t.reset_all_sub_tasks()
587
593
 
594
+ def __getitem__(self, return_type: type) -> Task:
595
+ """Returns a (shallow) copy of `self` with a default return type."""
596
+ clone = copy.copy(self)
597
+ clone.default_return_type = return_type
598
+ return clone
599
+
600
+ @overload
601
+ def run( # noqa
602
+ self,
603
+ msg: Any = None,
604
+ *,
605
+ turns: int = -1,
606
+ caller: None | Task = None,
607
+ max_cost: float = 0,
608
+ max_tokens: int = 0,
609
+ session_id: str = "",
610
+ allow_restart: bool = True,
611
+ ) -> Optional[ChatDocument]: ... # noqa
612
+
613
+ @overload
614
+ def run( # noqa
615
+ self,
616
+ msg: Any = None,
617
+ *,
618
+ turns: int = -1,
619
+ caller: None | Task = None,
620
+ max_cost: float = 0,
621
+ max_tokens: int = 0,
622
+ session_id: str = "",
623
+ allow_restart: bool = True,
624
+ return_type: Type[T],
625
+ ) -> Optional[T]: ... # noqa
626
+
588
627
  def run(
589
628
  self,
590
- msg: Optional[str | ChatDocument] = None,
629
+ msg: Any = None,
591
630
  turns: int = -1,
592
631
  caller: None | Task = None,
593
632
  max_cost: float = 0,
594
633
  max_tokens: int = 0,
595
634
  session_id: str = "",
596
635
  allow_restart: bool = True,
597
- ) -> Optional[ChatDocument]:
636
+ return_type: Optional[Type[T]] = None,
637
+ ) -> Optional[ChatDocument | T]:
598
638
  """Synchronous version of `run_async()`.
599
639
  See `run_async()` for details."""
600
640
  if allow_restart and (
@@ -617,19 +657,18 @@ class Task:
617
657
  self._init_message_counter()
618
658
  self.history.clear()
619
659
 
620
- assert (
621
- msg is None or isinstance(msg, str) or isinstance(msg, ChatDocument)
622
- ), f"msg arg in Task.run() must be None, str, or ChatDocument, not {type(msg)}"
660
+ msg_input = self.agent.to_ChatDocument(msg, author_entity=Entity.USER)
623
661
 
624
662
  if (
625
- isinstance(msg, ChatDocument)
626
- and msg.metadata.recipient != ""
627
- and msg.metadata.recipient != self.name
663
+ isinstance(msg_input, ChatDocument)
664
+ and msg_input.metadata.recipient != ""
665
+ and msg_input.metadata.recipient != self.name
628
666
  ):
629
667
  # this task is not the intended recipient so return None
630
668
  return None
669
+
631
670
  self._pre_run_loop(
632
- msg=msg,
671
+ msg=msg_input,
633
672
  caller=caller,
634
673
  is_async=False,
635
674
  )
@@ -680,24 +719,60 @@ class Task:
680
719
 
681
720
  final_result = self.result(status)
682
721
  self._post_run_loop()
722
+ if final_result is None:
723
+ return None
724
+
725
+ if return_type is None:
726
+ return_type = self.default_return_type
727
+
728
+ if return_type is not None and return_type != ChatDocument:
729
+ return self.agent.from_ChatDocument(final_result, return_type)
683
730
  return final_result
684
731
 
732
+ @overload
733
+ async def run_async( # noqa
734
+ self,
735
+ msg: Any = None,
736
+ *,
737
+ turns: int = -1,
738
+ caller: None | Task = None,
739
+ max_cost: float = 0,
740
+ max_tokens: int = 0,
741
+ session_id: str = "",
742
+ allow_restart: bool = True,
743
+ ) -> Optional[ChatDocument]: ... # noqa
744
+
745
+ @overload
746
+ async def run_async( # noqa
747
+ self,
748
+ msg: Any = None,
749
+ *,
750
+ turns: int = -1,
751
+ caller: None | Task = None,
752
+ max_cost: float = 0,
753
+ max_tokens: int = 0,
754
+ session_id: str = "",
755
+ allow_restart: bool = True,
756
+ return_type: Type[T],
757
+ ) -> Optional[T]: ... # noqa
758
+
685
759
  async def run_async(
686
760
  self,
687
- msg: Optional[str | ChatDocument] = None,
761
+ msg: Any = None,
688
762
  turns: int = -1,
689
763
  caller: None | Task = None,
690
764
  max_cost: float = 0,
691
765
  max_tokens: int = 0,
692
766
  session_id: str = "",
693
767
  allow_restart: bool = True,
694
- ) -> Optional[ChatDocument]:
768
+ return_type: Optional[Type[T]] = None,
769
+ ) -> Optional[ChatDocument | T]:
695
770
  """
696
771
  Loop over `step()` until task is considered done or `turns` is reached.
697
772
  Runs asynchronously.
698
773
 
699
774
  Args:
700
- msg (str|ChatDocument): initial *user-role* message to process; if None,
775
+ msg (Any): initial *user-role* message to process; if None,
701
776
  the LLM will respond to its initial `self.task_messages`
702
777
  which set up and kick off the overall task.
703
778
  The agent tries to achieve this goal by looping
@@ -713,6 +788,7 @@ class Task:
713
788
  max_tokens (int): max tokens allowed for the task (default 0 -> no limit)
714
789
  session_id (str): session id for the task
715
790
  allow_restart (bool): whether to allow restarting the task
791
+ return_type (Optional[Type[T]]): desired final result type
716
792
 
717
793
  Returns:
718
794
  Optional[ChatDocument]: valid result of the task.
@@ -743,17 +819,20 @@ class Task:
743
819
  self._init_message_counter()
744
820
  self.history.clear()
745
821
 
822
+ msg_input = self.agent.to_ChatDocument(msg, author_entity=Entity.USER)
823
+
746
824
  if (
747
- isinstance(msg, ChatDocument)
748
- and msg.metadata.recipient != ""
749
- and msg.metadata.recipient != self.name
825
+ isinstance(msg_input, ChatDocument)
826
+ and msg_input.metadata.recipient != ""
827
+ and msg_input.metadata.recipient != self.name
750
828
  ):
751
829
  # this task is not the intended recipient so return None
752
830
  return None
831
+
753
832
  self._pre_run_loop(
754
- msg=msg,
833
+ msg=msg_input,
755
834
  caller=caller,
756
- is_async=True,
835
+ is_async=False,
757
836
  )
758
837
  # self.turns overrides if it is > 0 and turns not set (i.e. = -1)
759
838
  turns = self.turns if turns < 0 else turns
@@ -803,6 +882,14 @@ class Task:
803
882
 
804
883
  final_result = self.result(status)
805
884
  self._post_run_loop()
885
+ if final_result is None:
886
+ return None
887
+
888
+ if return_type is None:
889
+ return_type = self.default_return_type
890
+
891
+ if return_type is not None and return_type != ChatDocument:
892
+ return self.agent.from_ChatDocument(final_result, return_type)
806
893
  return final_result
807
894
 
808
895
  def _pre_run_loop(
@@ -1249,7 +1336,13 @@ class Task:
1249
1336
  else:
1250
1337
  response_fn = self._entity_responder_map[cast(Entity, e)]
1251
1338
  result = response_fn(self.pending_message)
1252
- return self._process_result_routing(result, e)
1339
+
1340
+ result_chat_doc = self.agent.to_ChatDocument(
1341
+ result,
1342
+ chat_doc=self.pending_message,
1343
+ author_entity=e if isinstance(e, Entity) else Entity.USER,
1344
+ )
1345
+ return self._process_result_routing(result_chat_doc, e)
1253
1346
 
1254
1347
  def _process_result_routing(
1255
1348
  self, result: ChatDocument | None, e: Responder
@@ -1367,7 +1460,13 @@ class Task:
1367
1460
  else:
1368
1461
  response_fn = self._entity_responder_async_map[cast(Entity, e)]
1369
1462
  result = await response_fn(self.pending_message)
1370
- return self._process_result_routing(result, e)
1463
+
1464
+ result_chat_doc = self.agent.to_ChatDocument(
1465
+ result,
1466
+ chat_doc=self.pending_message,
1467
+ author_entity=e if isinstance(e, Entity) else Entity.USER,
1468
+ )
1469
+ return self._process_result_routing(result_chat_doc, e)
1371
1470
 
1372
1471
  def result(self, status: StatusCode | None = None) -> ChatDocument | None:
1373
1472
  """
@@ -1389,6 +1488,7 @@ class Task:
1389
1488
  result_msg = self.pending_message
1390
1489
 
1391
1490
  content = result_msg.content if result_msg else ""
1491
+ content_any = result_msg.content_any if result_msg else None
1392
1492
  if DONE in content:
1393
1493
  # assuming it is of the form "DONE: <content>"
1394
1494
  content = content.replace(DONE, "").strip()
@@ -1401,11 +1501,13 @@ class Task:
1401
1501
  for t in tool_messages:
1402
1502
  if isinstance(t, FinalResultTool):
1403
1503
  content = ""
1504
+ content_any = None
1404
1505
  tool_messages = [t] # pass it on to parent so it also quits
1405
1506
  break
1406
1507
  elif isinstance(t, (AgentDoneTool, DoneTool)):
1407
1508
  # there shouldn't be multiple tools like this; just take the first
1408
- content = t.content
1509
+ content = to_string(t.content)
1510
+ content_any = t.content
1409
1511
  if isinstance(t, AgentDoneTool):
1410
1512
  tool_messages = t.tools
1411
1513
  break
@@ -1423,6 +1525,7 @@ class Task:
1423
1525
  # since to the "parent" task, this result is equivalent to a response from USER
1424
1526
  result_doc = ChatDocument(
1425
1527
  content=content,
1528
+ content_any=content_any,
1426
1529
  oai_tool_calls=oai_tool_calls,
1427
1530
  oai_tool_id2result=oai_tool_id2result,
1428
1531
  function_call=fun_call,
@@ -1781,9 +1884,7 @@ class Task:
1781
1884
 
1782
1885
  if self.pending_message is None:
1783
1886
  return True
1784
- if isinstance(e, Task) and e.agent.has_only_unhandled_tools(
1785
- self.pending_message
1786
- ):
1887
+ if isinstance(e, Task) and not e.agent.can_respond(self.pending_message):
1787
1888
  return False
1788
1889
 
1789
1890
  if self._recipient_mismatch(e):
@@ -15,11 +15,12 @@ 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, ConfigDict, Extra
18
+ from langroid.pydantic_v1 import BaseModel, Extra
19
19
  from langroid.utils.pydantic_utils import (
20
20
  _recursive_purge_dict_key,
21
21
  generate_simple_schema,
22
22
  )
23
+ from langroid.utils.types import is_instance_of
23
24
 
24
25
 
25
26
  class ToolMessage(ABC, BaseModel):
@@ -41,19 +42,18 @@ class ToolMessage(ABC, BaseModel):
41
42
  purpose: str
42
43
  id: str = "" # placeholder for OpenAI-API tool_call_id
43
44
 
44
- model_config = ConfigDict(extra=Extra.allow)
45
+ _allow_llm_use: bool = True # allow an LLM to use (i.e. generate) this tool?
45
46
 
46
- _handle_only: bool = False # only allow handling, but not use (LLM-generation)?
47
+ # model_config = ConfigDict(extra=Extra.allow)
47
48
 
48
49
  class Config:
49
- # only HANDLING allowed, NOT "use" (i.e LLM generation)
50
- handle_only: bool = False
50
+ extra = Extra.allow
51
51
  arbitrary_types_allowed = False
52
52
  validate_all = True
53
53
  validate_assignment = True
54
54
  # do not include these fields in the generated schema
55
55
  # since we don't require the LLM to specify them
56
- schema_extra = {"exclude": {"purpose", "id", "model_config"}}
56
+ schema_extra = {"exclude": {"purpose", "id"}}
57
57
 
58
58
  @classmethod
59
59
  def instructions(cls) -> str:
@@ -123,6 +123,15 @@ class ToolMessage(ABC, BaseModel):
123
123
  def dict_example(self) -> Dict[str, Any]:
124
124
  return self.dict(exclude=self.Config.schema_extra["exclude"])
125
125
 
126
+ def get_value_of_type(self, target_type: Type[Any]) -> Any:
127
+ """Try to find a value of a desired type in the fields of the ToolMessage."""
128
+ ignore_fields = self.Config.schema_extra["exclude"].union(["request"])
129
+ for field_name in set(self.dict().keys()) - ignore_fields:
130
+ value = getattr(self, field_name)
131
+ if is_instance_of(value, target_type):
132
+ return value
133
+ return None
134
+
126
135
  @classmethod
127
136
  def default_value(cls, f: str) -> Any:
128
137
  """
@@ -273,40 +282,3 @@ class ToolMessage(ABC, BaseModel):
273
282
  exclude=list(cls.Config.schema_extra["exclude"]),
274
283
  )
275
284
  return schema
276
-
277
-
278
- class FinalResultTool(ToolMessage):
279
- """Class to use as a wrapper for sending arbitrary results from an Agent's
280
- agent_response or tool handlers, to:
281
- (a) trigger completion of the current task as well as all parent tasks, and
282
- (b) be returned as the final result of the root task, i.e. this tool would appear
283
- in the final ChatDocument's `tool_messages` list.
284
- See test_tool_handlers_and_results in test_tool_messages.py, and
285
- examples/basic/tool-extract-short-example.py.
286
-
287
- Note:
288
- - when defining a tool handler or agent_response, you can directly return
289
- FinalResultTool(field1 = val1, ...),
290
- where the values can be aribitrary data structures, including nested
291
- Pydantic objs, or you can define a subclass of FinalResultTool with the
292
- fields you want to return.
293
- - This is a special ToolMessage that is NOT meant to be used or handled
294
- by an agent.
295
- """
296
-
297
- request: str = ""
298
- purpose: str = "Ignored; Wrapper for a structured message"
299
- id: str = "" # placeholder for OpenAI-API tool_call_id
300
-
301
- _handle_only: bool = False # only allow handling, but not use (LLM-generation)?
302
-
303
- class Config:
304
- extra = Extra.allow
305
- # only HANDLING allowed, NOT "use" (i.e LLM generation)
306
- handle_only: bool = False
307
- arbitrary_types_allowed = False
308
- validate_all = True
309
- validate_assignment = True
310
- # do not include these fields in the generated schema
311
- # since we don't require the LLM to specify them
312
- schema_extra = {"exclude": {"purpose", "id"}}
@@ -13,6 +13,8 @@ from .orchestration import (
13
13
  SendTool,
14
14
  AgentSendTool,
15
15
  DonePassTool,
16
+ ResultTool,
17
+ FinalResultTool,
16
18
  )
17
19
 
18
20
  __all__ = [
@@ -31,4 +33,6 @@ __all__ = [
31
33
  "PassTool",
32
34
  "SendTool",
33
35
  "AgentSendTool",
36
+ "ResultTool",
37
+ "FinalResultTool",
34
38
  ]
@@ -3,12 +3,14 @@ Various tools to for agents to be able to control flow of Task, e.g.
3
3
  termination, routing to another agent, etc.
4
4
  """
5
5
 
6
- from typing import List, Tuple
6
+ from typing import Any, List, Tuple
7
7
 
8
8
  from langroid.agent.chat_agent import ChatAgent
9
9
  from langroid.agent.chat_document import ChatDocument
10
10
  from langroid.agent.tool_message import ToolMessage
11
11
  from langroid.mytypes import Entity
12
+ from langroid.pydantic_v1 import Extra
13
+ from langroid.utils.types import to_string
12
14
 
13
15
 
14
16
  class AgentDoneTool(ToolMessage):
@@ -17,16 +19,20 @@ class AgentDoneTool(ToolMessage):
17
19
 
18
20
  purpose: str = """
19
21
  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).
22
+ of arbitrary type (default None) and an
23
+ optional list of <tools> (default empty list).
21
24
  """
22
25
  request: str = "agent_done_tool"
23
- content: str = ""
26
+ content: Any = None
24
27
  tools: List[ToolMessage] = []
25
- _handle_only: bool = True
28
+ # only meant for agent_response or tool-handlers, not for LLM generation:
29
+ _allow_llm_use: bool = False
26
30
 
27
31
  def response(self, agent: ChatAgent) -> ChatDocument:
32
+ content_str = "" if self.content is None else to_string(self.content)
28
33
  return agent.create_agent_response(
29
- self.content,
34
+ content=content_str,
35
+ content_any=self.content,
30
36
  tool_messages=[self] + self.tools,
31
37
  )
32
38
 
@@ -37,14 +43,15 @@ class DoneTool(ToolMessage):
37
43
 
38
44
  purpose = """
39
45
  To signal the current task is done, along with an optional message <content>
40
- (default empty string).
46
+ of arbitrary type (default None).
41
47
  """
42
48
  request = "done_tool"
43
49
  content: str = ""
44
50
 
45
51
  def response(self, agent: ChatAgent) -> ChatDocument:
46
52
  return agent.create_agent_response(
47
- self.content,
53
+ content=self.content,
54
+ content_any=self.content,
48
55
  tool_messages=[self],
49
56
  )
50
57
 
@@ -58,6 +65,78 @@ class DoneTool(ToolMessage):
58
65
  """
59
66
 
60
67
 
68
+ class ResultTool(ToolMessage):
69
+ """Class to use as a wrapper for sending arbitrary results from an Agent's
70
+ agent_response or tool handlers, to:
71
+ (a) trigger completion of the current task (similar to (Agent)DoneTool), and
72
+ (b) be returned as the result of the current task, i.e. this tool would appear
73
+ in the resulting ChatDocument's `tool_messages` list.
74
+ See test_tool_handlers_and_results in test_tool_messages.py, and
75
+ examples/basic/tool-extract-short-example.py.
76
+
77
+ Note:
78
+ - when defining a tool handler or agent_response, you can directly return
79
+ ResultTool(field1 = val1, ...),
80
+ where the values can be aribitrary data structures, including nested
81
+ Pydantic objs, or you can define a subclass of ResultTool with the
82
+ fields you want to return.
83
+ - This is a special ToolMessage that is NOT meant to be used or handled
84
+ by an agent.
85
+ - AgentDoneTool is more restrictive in that you can only send a `content`
86
+ or `tools` in the result.
87
+ """
88
+
89
+ request: str = "result_tool"
90
+ purpose: str = "Ignored; Wrapper for a structured message"
91
+ id: str = "" # placeholder for OpenAI-API tool_call_id
92
+
93
+ class Config:
94
+ extra = Extra.allow
95
+ arbitrary_types_allowed = False
96
+ validate_all = True
97
+ validate_assignment = True
98
+ # do not include these fields in the generated schema
99
+ # since we don't require the LLM to specify them
100
+ schema_extra = {"exclude": {"purpose", "id"}}
101
+
102
+ def handle(self) -> AgentDoneTool:
103
+ return AgentDoneTool(tools=[self])
104
+
105
+
106
+ class FinalResultTool(ToolMessage):
107
+ """Class to use as a wrapper for sending arbitrary results from an Agent's
108
+ agent_response or tool handlers, to:
109
+ (a) trigger completion of the current task as well as all parent tasks, and
110
+ (b) be returned as the final result of the root task, i.e. this tool would appear
111
+ in the final ChatDocument's `tool_messages` list.
112
+ See test_tool_handlers_and_results in test_tool_messages.py, and
113
+ examples/basic/tool-extract-short-example.py.
114
+
115
+ Note:
116
+ - when defining a tool handler or agent_response, you can directly return
117
+ FinalResultTool(field1 = val1, ...),
118
+ where the values can be aribitrary data structures, including nested
119
+ Pydantic objs, or you can define a subclass of FinalResultTool with the
120
+ fields you want to return.
121
+ - This is a special ToolMessage that is NOT meant to be used or handled
122
+ by an agent.
123
+ """
124
+
125
+ request: str = ""
126
+ purpose: str = "Ignored; Wrapper for a structured message"
127
+ id: str = "" # placeholder for OpenAI-API tool_call_id
128
+ _allow_llm_use: bool = False
129
+
130
+ class Config:
131
+ extra = Extra.allow
132
+ arbitrary_types_allowed = False
133
+ validate_all = True
134
+ validate_assignment = True
135
+ # do not include these fields in the generated schema
136
+ # since we don't require the LLM to specify them
137
+ schema_extra = {"exclude": {"purpose", "id"}}
138
+
139
+
61
140
  class PassTool(ToolMessage):
62
141
  """Tool for "passing" on the received msg (ChatDocument),
63
142
  so that an as-yet-unspecified agent can handle it.
@@ -206,7 +285,7 @@ class AgentSendTool(ToolMessage):
206
285
  to: str
207
286
  content: str = ""
208
287
  tools: List[ToolMessage] = []
209
- _handle_only: bool = True
288
+ _allow_llm_use: bool = False
210
289
 
211
290
  def response(self, agent: ChatAgent) -> ChatDocument:
212
291
  return agent.create_agent_response(
@@ -10,6 +10,7 @@ from langroid.language_models.base import (
10
10
  OpenAIToolSpec,
11
11
  ToolChoiceTypes,
12
12
  )
13
+ from langroid.utils.types import to_string
13
14
 
14
15
 
15
16
  def none_fn(x: str) -> None | str:
@@ -43,11 +44,11 @@ class MockLM(LanguageModel):
43
44
  # - response_dict
44
45
  # - response_fn
45
46
  # - default_response
47
+ mapped_response = self.config.response_dict.get(
48
+ msg, self.config.response_fn(msg) or self.config.default_response
49
+ )
46
50
  return lm.LLMResponse(
47
- message=self.config.response_dict.get(
48
- msg,
49
- self.config.response_fn(msg) or self.config.default_response,
50
- ),
51
+ message=to_string(mapped_response),
51
52
  cached=False,
52
53
  )
53
54
 
@@ -48,10 +48,13 @@ class WebSearchResult:
48
48
  return self.full_content[: self.max_summary_length]
49
49
 
50
50
  def get_full_content(self) -> str:
51
- response: Response = requests.get(self.link)
52
- soup: BeautifulSoup = BeautifulSoup(response.text, "lxml")
53
- text = " ".join(soup.stripped_strings)
54
- return text[: self.max_content_length]
51
+ try:
52
+ response: Response = requests.get(self.link)
53
+ soup: BeautifulSoup = BeautifulSoup(response.text, "lxml")
54
+ text = " ".join(soup.stripped_strings)
55
+ return text[: self.max_content_length]
56
+ except Exception as e:
57
+ return f"Error fetching content from {self.link}: {e}"
55
58
 
56
59
  def __str__(self) -> str:
57
60
  return f"Title: {self.title}\nLink: {self.link}\nSummary: {self.summary}"