langroid 0.1.85__py3-none-any.whl → 0.1.219__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/__init__.py +95 -0
- langroid/agent/__init__.py +40 -0
- langroid/agent/base.py +222 -91
- langroid/agent/batch.py +264 -0
- langroid/agent/callbacks/chainlit.py +608 -0
- langroid/agent/chat_agent.py +247 -101
- langroid/agent/chat_document.py +41 -4
- langroid/agent/openai_assistant.py +842 -0
- langroid/agent/special/__init__.py +50 -0
- langroid/agent/special/doc_chat_agent.py +837 -141
- langroid/agent/special/lance_doc_chat_agent.py +258 -0
- langroid/agent/special/lance_rag/__init__.py +9 -0
- langroid/agent/special/lance_rag/critic_agent.py +136 -0
- langroid/agent/special/lance_rag/lance_rag_task.py +80 -0
- langroid/agent/special/lance_rag/query_planner_agent.py +180 -0
- langroid/agent/special/lance_tools.py +44 -0
- langroid/agent/special/neo4j/__init__.py +0 -0
- langroid/agent/special/neo4j/csv_kg_chat.py +174 -0
- langroid/agent/special/neo4j/neo4j_chat_agent.py +370 -0
- langroid/agent/special/neo4j/utils/__init__.py +0 -0
- langroid/agent/special/neo4j/utils/system_message.py +46 -0
- langroid/agent/special/relevance_extractor_agent.py +127 -0
- langroid/agent/special/retriever_agent.py +32 -198
- langroid/agent/special/sql/__init__.py +11 -0
- langroid/agent/special/sql/sql_chat_agent.py +47 -23
- langroid/agent/special/sql/utils/__init__.py +22 -0
- langroid/agent/special/sql/utils/description_extractors.py +95 -46
- langroid/agent/special/sql/utils/populate_metadata.py +28 -21
- langroid/agent/special/table_chat_agent.py +43 -9
- langroid/agent/task.py +475 -122
- langroid/agent/tool_message.py +75 -13
- langroid/agent/tools/__init__.py +13 -0
- langroid/agent/tools/duckduckgo_search_tool.py +66 -0
- langroid/agent/tools/google_search_tool.py +11 -0
- langroid/agent/tools/metaphor_search_tool.py +67 -0
- langroid/agent/tools/recipient_tool.py +16 -29
- langroid/agent/tools/run_python_code.py +60 -0
- langroid/agent/tools/sciphi_search_rag_tool.py +79 -0
- langroid/agent/tools/segment_extract_tool.py +36 -0
- langroid/cachedb/__init__.py +9 -0
- langroid/cachedb/base.py +22 -2
- langroid/cachedb/momento_cachedb.py +26 -2
- langroid/cachedb/redis_cachedb.py +78 -11
- langroid/embedding_models/__init__.py +34 -0
- langroid/embedding_models/base.py +21 -2
- langroid/embedding_models/models.py +120 -18
- langroid/embedding_models/protoc/embeddings.proto +19 -0
- langroid/embedding_models/protoc/embeddings_pb2.py +33 -0
- langroid/embedding_models/protoc/embeddings_pb2.pyi +50 -0
- langroid/embedding_models/protoc/embeddings_pb2_grpc.py +79 -0
- langroid/embedding_models/remote_embeds.py +153 -0
- langroid/language_models/__init__.py +45 -0
- langroid/language_models/azure_openai.py +80 -27
- langroid/language_models/base.py +117 -12
- langroid/language_models/config.py +5 -0
- langroid/language_models/openai_assistants.py +3 -0
- langroid/language_models/openai_gpt.py +558 -174
- langroid/language_models/prompt_formatter/__init__.py +15 -0
- langroid/language_models/prompt_formatter/base.py +4 -6
- langroid/language_models/prompt_formatter/hf_formatter.py +135 -0
- langroid/language_models/utils.py +18 -21
- langroid/mytypes.py +25 -8
- langroid/parsing/__init__.py +46 -0
- langroid/parsing/document_parser.py +260 -63
- langroid/parsing/image_text.py +32 -0
- langroid/parsing/parse_json.py +143 -0
- langroid/parsing/parser.py +122 -59
- langroid/parsing/repo_loader.py +114 -52
- langroid/parsing/search.py +68 -63
- langroid/parsing/spider.py +3 -2
- langroid/parsing/table_loader.py +44 -0
- langroid/parsing/url_loader.py +59 -11
- langroid/parsing/urls.py +85 -37
- langroid/parsing/utils.py +298 -4
- langroid/parsing/web_search.py +73 -0
- langroid/prompts/__init__.py +11 -0
- langroid/prompts/chat-gpt4-system-prompt.md +68 -0
- langroid/prompts/prompts_config.py +1 -1
- langroid/utils/__init__.py +17 -0
- langroid/utils/algorithms/__init__.py +3 -0
- langroid/utils/algorithms/graph.py +103 -0
- langroid/utils/configuration.py +36 -5
- langroid/utils/constants.py +4 -0
- langroid/utils/globals.py +2 -2
- langroid/utils/logging.py +2 -5
- langroid/utils/output/__init__.py +21 -0
- langroid/utils/output/printing.py +47 -1
- langroid/utils/output/status.py +33 -0
- langroid/utils/pandas_utils.py +30 -0
- langroid/utils/pydantic_utils.py +616 -2
- langroid/utils/system.py +98 -0
- langroid/vector_store/__init__.py +40 -0
- langroid/vector_store/base.py +203 -6
- langroid/vector_store/chromadb.py +59 -32
- langroid/vector_store/lancedb.py +463 -0
- langroid/vector_store/meilisearch.py +10 -7
- langroid/vector_store/momento.py +262 -0
- langroid/vector_store/qdrantdb.py +104 -22
- {langroid-0.1.85.dist-info → langroid-0.1.219.dist-info}/METADATA +329 -149
- langroid-0.1.219.dist-info/RECORD +127 -0
- {langroid-0.1.85.dist-info → langroid-0.1.219.dist-info}/WHEEL +1 -1
- langroid/agent/special/recipient_validator_agent.py +0 -157
- langroid/parsing/json.py +0 -64
- langroid/utils/web/selenium_login.py +0 -36
- langroid-0.1.85.dist-info/RECORD +0 -94
- /langroid/{scripts → agent/callbacks}/__init__.py +0 -0
- {langroid-0.1.85.dist-info → langroid-0.1.219.dist-info}/LICENSE +0 -0
langroid/agent/task.py
CHANGED
@@ -1,9 +1,25 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
import copy
|
3
4
|
import logging
|
4
|
-
|
5
|
+
import re
|
6
|
+
from collections import Counter
|
7
|
+
from types import SimpleNamespace
|
8
|
+
from typing import (
|
9
|
+
Any,
|
10
|
+
Callable,
|
11
|
+
Coroutine,
|
12
|
+
Dict,
|
13
|
+
List,
|
14
|
+
Optional,
|
15
|
+
Set,
|
16
|
+
Tuple,
|
17
|
+
Type,
|
18
|
+
cast,
|
19
|
+
)
|
5
20
|
|
6
21
|
from rich import print
|
22
|
+
from rich.markup import escape
|
7
23
|
|
8
24
|
from langroid.agent.base import Agent
|
9
25
|
from langroid.agent.chat_agent import ChatAgent
|
@@ -13,8 +29,9 @@ from langroid.agent.chat_document import (
|
|
13
29
|
ChatDocument,
|
14
30
|
)
|
15
31
|
from langroid.mytypes import Entity
|
32
|
+
from langroid.parsing.parse_json import extract_top_level_json
|
16
33
|
from langroid.utils.configuration import settings
|
17
|
-
from langroid.utils.constants import DONE, NO_ANSWER, USER_QUIT
|
34
|
+
from langroid.utils.constants import DONE, NO_ANSWER, PASS, PASS_TO, SEND_TO, USER_QUIT
|
18
35
|
from langroid.utils.logging import RichFileLogger, setup_file_logger
|
19
36
|
|
20
37
|
logger = logging.getLogger(__name__)
|
@@ -22,6 +39,10 @@ logger = logging.getLogger(__name__)
|
|
22
39
|
Responder = Entity | Type["Task"]
|
23
40
|
|
24
41
|
|
42
|
+
def noop_fn(*args: List[Any], **kwargs: Dict[str, Any]) -> None:
|
43
|
+
pass
|
44
|
+
|
45
|
+
|
25
46
|
class Task:
|
26
47
|
"""
|
27
48
|
A `Task` wraps an `Agent` object, and sets up the `Agent`'s goals and instructions.
|
@@ -54,17 +75,21 @@ class Task:
|
|
54
75
|
|
55
76
|
def __init__(
|
56
77
|
self,
|
57
|
-
agent: Agent,
|
78
|
+
agent: Optional[Agent] = None,
|
58
79
|
name: str = "",
|
59
80
|
llm_delegate: bool = False,
|
60
81
|
single_round: bool = False,
|
61
82
|
system_message: str = "",
|
62
|
-
user_message: str = "",
|
63
|
-
restart: bool =
|
83
|
+
user_message: str | None = "",
|
84
|
+
restart: bool = True,
|
64
85
|
default_human_response: Optional[str] = None,
|
65
86
|
interactive: bool = True,
|
66
|
-
only_user_quits_root: bool =
|
87
|
+
only_user_quits_root: bool = False,
|
67
88
|
erase_substeps: bool = False,
|
89
|
+
allow_null_result: bool = True,
|
90
|
+
max_stalled_steps: int = 5,
|
91
|
+
done_if_no_response: List[Responder] = [],
|
92
|
+
done_if_response: List[Responder] = [],
|
68
93
|
):
|
69
94
|
"""
|
70
95
|
A task to be performed by an agent.
|
@@ -72,12 +97,17 @@ class Task:
|
|
72
97
|
Args:
|
73
98
|
agent (Agent): agent associated with the task
|
74
99
|
name (str): name of the task
|
75
|
-
llm_delegate (bool):
|
100
|
+
llm_delegate (bool):
|
101
|
+
[Deprecated, not used; use `done_if_response`, `done_if_no_response`
|
102
|
+
instead]
|
103
|
+
Whether to delegate control to LLM; conceptually,
|
76
104
|
the "controlling entity" is the one "seeking" responses to its queries,
|
77
105
|
and has a goal it is aiming to achieve. The "controlling entity" is
|
78
106
|
either the LLM or the USER. (Note within a Task there is just one
|
79
107
|
LLM, and all other entities are proxies of the "User" entity).
|
80
|
-
single_round (bool):
|
108
|
+
single_round (bool):
|
109
|
+
[Deprecated: Use `done_if_response`, `done_if_no_response` instead].
|
110
|
+
If true, task runs until one message by controller,
|
81
111
|
and subsequent response by non-controller. If false, runs for the
|
82
112
|
specified number of turns in `run`, or until `done()` is true.
|
83
113
|
One run of step() is considered a "turn".
|
@@ -86,19 +116,54 @@ class Task:
|
|
86
116
|
restart (bool): if true, resets the agent's message history
|
87
117
|
default_human_response (str): default response from user; useful for
|
88
118
|
testing, to avoid interactive input from user.
|
119
|
+
[Instead of this, setting `interactive` usually suffices]
|
89
120
|
interactive (bool): if true, wait for human input after each non-human
|
90
121
|
response (prevents infinite loop of non-human responses).
|
91
122
|
Default is true. If false, then `default_human_response` is set to ""
|
92
123
|
only_user_quits_root (bool): if true, only user can quit the root task.
|
124
|
+
[This param is ignored & deprecated; Keeping for backward compatibility.
|
125
|
+
Instead of this, setting `interactive` suffices]
|
93
126
|
erase_substeps (bool): if true, when task completes, erase intermediate
|
94
127
|
conversation with subtasks from this agent's `message_history`, and also
|
95
128
|
erase all subtask agents' `message_history`.
|
96
129
|
Note: erasing can reduce prompt sizes, but results in repetitive
|
97
130
|
sub-task delegation.
|
131
|
+
allow_null_result (bool): [Deprecated, may be removed in future.]
|
132
|
+
If true, allow null (empty or NO_ANSWER)
|
133
|
+
as the result of a step or overall task result.
|
134
|
+
Optional, default is True.
|
135
|
+
max_stalled_steps (int): task considered done after this many consecutive
|
136
|
+
steps with no progress. Default is 3.
|
137
|
+
done_if_no_response (List[Responder]): consider task done if NULL
|
138
|
+
response from any of these responders. Default is empty list.
|
139
|
+
done_if_response (List[Responder]): consider task done if NON-NULL
|
140
|
+
response from any of these responders. Default is empty list.
|
98
141
|
"""
|
142
|
+
if agent is None:
|
143
|
+
agent = ChatAgent()
|
144
|
+
|
145
|
+
self.callbacks = SimpleNamespace(
|
146
|
+
show_subtask_response=noop_fn,
|
147
|
+
set_parent_agent=noop_fn,
|
148
|
+
)
|
149
|
+
# copy the agent's config, so that we don't modify the original agent's config,
|
150
|
+
# which may be shared by other agents.
|
151
|
+
try:
|
152
|
+
config_copy = copy.deepcopy(agent.config)
|
153
|
+
agent.config = config_copy
|
154
|
+
except Exception:
|
155
|
+
logger.warning(
|
156
|
+
"""
|
157
|
+
Failed to deep-copy Agent config during task creation,
|
158
|
+
proceeding with original config. Be aware that changes to
|
159
|
+
the config may affect other agents using the same config.
|
160
|
+
"""
|
161
|
+
)
|
162
|
+
|
99
163
|
if isinstance(agent, ChatAgent) and len(agent.message_history) == 0 or restart:
|
100
164
|
agent = cast(ChatAgent, agent)
|
101
165
|
agent.clear_history(0)
|
166
|
+
agent.clear_dialog()
|
102
167
|
# possibly change the system and user messages
|
103
168
|
if system_message:
|
104
169
|
# we always have at least 1 task_message
|
@@ -108,18 +173,41 @@ class Task:
|
|
108
173
|
|
109
174
|
self.logger: None | RichFileLogger = None
|
110
175
|
self.tsv_logger: None | logging.Logger = None
|
111
|
-
self.color_log: bool = True
|
176
|
+
self.color_log: bool = False if settings.notebook else True
|
112
177
|
self.agent = agent
|
178
|
+
self.step_progress = False # progress in current step?
|
179
|
+
self.n_stalled_steps = 0 # how many consecutive steps with no progress?
|
180
|
+
self.max_stalled_steps = max_stalled_steps
|
181
|
+
self.done_if_response = [r.value for r in done_if_response]
|
182
|
+
self.done_if_no_response = [r.value for r in done_if_no_response]
|
183
|
+
self.is_done = False # is task done (based on response)?
|
184
|
+
self.is_pass_thru = False # is current response a pass-thru?
|
185
|
+
self.task_progress = False # progress in current task (since run or run_async)?
|
186
|
+
if name:
|
187
|
+
# task name overrides name in agent config
|
188
|
+
agent.config.name = name
|
113
189
|
self.name = name or agent.config.name
|
190
|
+
self.value: str = self.name
|
114
191
|
self.default_human_response = default_human_response
|
192
|
+
if default_human_response is not None and default_human_response == "":
|
193
|
+
interactive = False
|
115
194
|
self.interactive = interactive
|
116
195
|
self.message_history_idx = -1
|
117
|
-
if
|
118
|
-
|
196
|
+
if interactive:
|
197
|
+
only_user_quits_root = True
|
198
|
+
else:
|
199
|
+
default_human_response = default_human_response or ""
|
200
|
+
only_user_quits_root = False
|
119
201
|
if default_human_response is not None:
|
120
202
|
self.agent.default_human_response = default_human_response
|
203
|
+
if self.interactive:
|
204
|
+
self.agent.default_human_response = None
|
121
205
|
self.only_user_quits_root = only_user_quits_root
|
206
|
+
# set to True if we want to collapse multi-turn conversation with sub-tasks into
|
207
|
+
# just the first outgoing message and last incoming message.
|
208
|
+
# Note this also completely erases sub-task agents' message_history.
|
122
209
|
self.erase_substeps = erase_substeps
|
210
|
+
self.allow_null_result = allow_null_result
|
123
211
|
|
124
212
|
agent_entity_responders = agent.entity_responders()
|
125
213
|
agent_entity_responders_async = agent.entity_responders_async()
|
@@ -149,6 +237,7 @@ class Task:
|
|
149
237
|
self.pending_sender: Responder = Entity.USER
|
150
238
|
self.single_round = single_round
|
151
239
|
self.turns = -1 # no limit
|
240
|
+
self.llm_delegate = llm_delegate
|
152
241
|
if llm_delegate:
|
153
242
|
self.controller = Entity.LLM
|
154
243
|
if self.single_round:
|
@@ -166,6 +255,29 @@ class Task:
|
|
166
255
|
self.parent_task: Set[Task] = set()
|
167
256
|
self.caller: Task | None = None # which task called this task's `run` method
|
168
257
|
|
258
|
+
def clone(self, i: int) -> "Task":
|
259
|
+
"""
|
260
|
+
Returns a copy of this task, with a new agent.
|
261
|
+
"""
|
262
|
+
assert isinstance(self.agent, ChatAgent), "Task clone only works for ChatAgent"
|
263
|
+
agent: ChatAgent = self.agent.clone(i)
|
264
|
+
return Task(
|
265
|
+
agent,
|
266
|
+
name=self.name + f"-{i}",
|
267
|
+
llm_delegate=self.llm_delegate,
|
268
|
+
single_round=self.single_round,
|
269
|
+
system_message=self.agent.system_message,
|
270
|
+
user_message=self.agent.user_message,
|
271
|
+
restart=False,
|
272
|
+
default_human_response=self.default_human_response,
|
273
|
+
interactive=self.interactive,
|
274
|
+
erase_substeps=self.erase_substeps,
|
275
|
+
allow_null_result=self.allow_null_result,
|
276
|
+
max_stalled_steps=self.max_stalled_steps,
|
277
|
+
done_if_no_response=[Entity(s) for s in self.done_if_no_response],
|
278
|
+
done_if_response=[Entity(s) for s in self.done_if_response],
|
279
|
+
)
|
280
|
+
|
169
281
|
def __repr__(self) -> str:
|
170
282
|
return f"{self.name}"
|
171
283
|
|
@@ -266,6 +378,11 @@ class Task:
|
|
266
378
|
) -> Optional[ChatDocument]:
|
267
379
|
"""Synchronous version of `run_async()`.
|
268
380
|
See `run_async()` for details."""
|
381
|
+
self.task_progress = False
|
382
|
+
self.n_stalled_steps = 0
|
383
|
+
assert (
|
384
|
+
msg is None or isinstance(msg, str) or isinstance(msg, ChatDocument)
|
385
|
+
), f"msg arg in Task.run() must be None, str, or ChatDocument, not {type(msg)}"
|
269
386
|
|
270
387
|
if (
|
271
388
|
isinstance(msg, ChatDocument)
|
@@ -285,7 +402,7 @@ class Task:
|
|
285
402
|
while True:
|
286
403
|
self.step()
|
287
404
|
if self.done():
|
288
|
-
if self._level == 0:
|
405
|
+
if self._level == 0 and not settings.quiet:
|
289
406
|
print("[magenta]Bye, hope this was useful!")
|
290
407
|
break
|
291
408
|
i += 1
|
@@ -326,7 +443,7 @@ class Task:
|
|
326
443
|
# have come from another LLM), as far as this agent is concerned, the initial
|
327
444
|
# message can be considered to be from the USER
|
328
445
|
# (from the POV of this agent's LLM).
|
329
|
-
|
446
|
+
self.task_progress = False
|
330
447
|
if (
|
331
448
|
isinstance(msg, ChatDocument)
|
332
449
|
and msg.metadata.recipient != ""
|
@@ -345,7 +462,7 @@ class Task:
|
|
345
462
|
while True:
|
346
463
|
await self.step_async()
|
347
464
|
if self.done():
|
348
|
-
if self._level == 0:
|
465
|
+
if self._level == 0 and not settings.quiet:
|
349
466
|
print("[magenta]Bye, hope this was useful!")
|
350
467
|
break
|
351
468
|
i += 1
|
@@ -386,10 +503,12 @@ class Task:
|
|
386
503
|
if self.agent.config.llm is None
|
387
504
|
else self.agent.config.llm.chat_model
|
388
505
|
)
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
506
|
+
if not settings.quiet:
|
507
|
+
print(
|
508
|
+
f"[bold magenta]{self._enter} Starting Agent "
|
509
|
+
f"{self.name} ({self.message_history_idx+1}) "
|
510
|
+
f"{llm_model} [/bold magenta]"
|
511
|
+
)
|
393
512
|
|
394
513
|
def _post_run_loop(self) -> None:
|
395
514
|
# delete all messages from our agent's history, AFTER the first incoming
|
@@ -412,16 +531,20 @@ class Task:
|
|
412
531
|
# ONLY talking to the current agent.
|
413
532
|
if isinstance(t.agent, ChatAgent):
|
414
533
|
t.agent.clear_history(0)
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
534
|
+
if not settings.quiet:
|
535
|
+
print(
|
536
|
+
f"[bold magenta]{self._leave} Finished Agent "
|
537
|
+
f"{self.name} ({n_messages}) [/bold magenta]"
|
538
|
+
)
|
419
539
|
|
420
540
|
def step(self, turns: int = -1) -> ChatDocument | None:
|
421
541
|
"""
|
422
542
|
Synchronous version of `step_async()`. See `step_async()` for details.
|
543
|
+
TODO: Except for the self.response() calls, this fn should be identical to
|
544
|
+
`step_async()`. Consider refactoring to avoid duplication.
|
423
545
|
"""
|
424
|
-
|
546
|
+
self.is_done = False
|
547
|
+
self.step_progress = False
|
425
548
|
parent = self.pending_message
|
426
549
|
recipient = (
|
427
550
|
""
|
@@ -437,16 +560,30 @@ class Task:
|
|
437
560
|
sender_name=Entity.AGENT,
|
438
561
|
),
|
439
562
|
)
|
440
|
-
self.
|
563
|
+
self._process_valid_responder_result(Entity.AGENT, parent, error_doc)
|
441
564
|
return error_doc
|
442
565
|
|
443
566
|
responders: List[Responder] = self.non_human_responders.copy()
|
444
|
-
|
445
|
-
|
446
|
-
|
567
|
+
|
568
|
+
if (
|
569
|
+
Entity.USER in self.responders
|
570
|
+
and not self.human_tried
|
571
|
+
and not self.agent.has_tool_message_attempt(self.pending_message)
|
572
|
+
):
|
573
|
+
# Give human first chance if they haven't been tried in last step,
|
574
|
+
# and the msg is not a tool-call attempt;
|
575
|
+
# This ensures human gets a chance to respond,
|
576
|
+
# other than to a LLM tool-call.
|
577
|
+
# When there's a tool msg attempt we want the
|
578
|
+
# Agent to be the next responder; this only makes a difference in an
|
579
|
+
# interactive setting: LLM generates tool, then we don't want user to
|
580
|
+
# have to respond, and instead let the agent_response handle the tool.
|
581
|
+
|
447
582
|
responders.insert(0, Entity.USER)
|
448
583
|
|
584
|
+
found_response = False
|
449
585
|
for r in responders:
|
586
|
+
self.is_pass_thru = False
|
450
587
|
if not self._can_respond(r):
|
451
588
|
# create dummy msg for logging
|
452
589
|
log_doc = ChatDocument(
|
@@ -462,10 +599,19 @@ class Task:
|
|
462
599
|
continue
|
463
600
|
self.human_tried = r == Entity.USER
|
464
601
|
result = self.response(r, turns)
|
465
|
-
|
466
|
-
if
|
602
|
+
self.is_done = self._is_done_response(result, r)
|
603
|
+
self.is_pass_thru = PASS in result.content if result else False
|
604
|
+
if self.valid(result, r):
|
605
|
+
found_response = True
|
606
|
+
assert result is not None
|
607
|
+
self._process_valid_responder_result(r, parent, result)
|
608
|
+
break
|
609
|
+
else:
|
610
|
+
self.log_message(r, result)
|
611
|
+
if self.is_done:
|
612
|
+
# skip trying other responders in this step
|
467
613
|
break
|
468
|
-
if not
|
614
|
+
if not found_response:
|
469
615
|
self._process_invalid_step_result(parent)
|
470
616
|
self._show_pending_message_if_debug()
|
471
617
|
return self.pending_message
|
@@ -491,7 +637,8 @@ class Task:
|
|
491
637
|
other use-cases, e.g. where we want to run a task step by step in a
|
492
638
|
different context.
|
493
639
|
"""
|
494
|
-
|
640
|
+
self.is_done = False
|
641
|
+
self.step_progress = False
|
495
642
|
parent = self.pending_message
|
496
643
|
recipient = (
|
497
644
|
""
|
@@ -507,15 +654,27 @@ class Task:
|
|
507
654
|
sender_name=Entity.AGENT,
|
508
655
|
),
|
509
656
|
)
|
510
|
-
self.
|
657
|
+
self._process_valid_responder_result(Entity.AGENT, parent, error_doc)
|
511
658
|
return error_doc
|
512
659
|
|
513
660
|
responders: List[Responder] = self.non_human_responders_async.copy()
|
514
|
-
|
515
|
-
|
516
|
-
|
661
|
+
|
662
|
+
if (
|
663
|
+
Entity.USER in self.responders
|
664
|
+
and not self.human_tried
|
665
|
+
and not self.agent.has_tool_message_attempt(self.pending_message)
|
666
|
+
):
|
667
|
+
# Give human first chance if they haven't been tried in last step,
|
668
|
+
# and the msg is not a tool-call attempt;
|
669
|
+
# This ensures human gets a chance to respond,
|
670
|
+
# other than to a LLM tool-call.
|
671
|
+
# When there's a tool msg attempt we want the
|
672
|
+
# Agent to be the next responder; this only makes a difference in an
|
673
|
+
# interactive setting: LLM generates tool, then we don't want user to
|
674
|
+
# have to respond, and instead let the agent_response handle the tool.
|
517
675
|
responders.insert(0, Entity.USER)
|
518
676
|
|
677
|
+
found_response = False
|
519
678
|
for r in responders:
|
520
679
|
if not self._can_respond(r):
|
521
680
|
# create dummy msg for logging
|
@@ -532,67 +691,117 @@ class Task:
|
|
532
691
|
continue
|
533
692
|
self.human_tried = r == Entity.USER
|
534
693
|
result = await self.response_async(r, turns)
|
535
|
-
|
536
|
-
if
|
694
|
+
self.is_done = self._is_done_response(result, r)
|
695
|
+
self.is_pass_thru = PASS in result.content if result else False
|
696
|
+
if self.valid(result, r):
|
697
|
+
found_response = True
|
698
|
+
assert result is not None
|
699
|
+
self._process_valid_responder_result(r, parent, result)
|
700
|
+
break
|
701
|
+
else:
|
702
|
+
self.log_message(r, result)
|
703
|
+
if self.is_done:
|
704
|
+
# skip trying other responders in this step
|
537
705
|
break
|
538
|
-
if not
|
706
|
+
if not found_response:
|
539
707
|
self._process_invalid_step_result(parent)
|
540
708
|
self._show_pending_message_if_debug()
|
541
709
|
return self.pending_message
|
542
710
|
|
543
|
-
def
|
711
|
+
def _process_valid_responder_result(
|
544
712
|
self,
|
545
713
|
r: Responder,
|
546
714
|
parent: ChatDocument | None,
|
547
|
-
result: ChatDocument
|
548
|
-
) ->
|
549
|
-
"""Processes result
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
715
|
+
result: ChatDocument,
|
716
|
+
) -> None:
|
717
|
+
"""Processes valid result from a responder, during a step"""
|
718
|
+
|
719
|
+
# pending_sender is of type Responder,
|
720
|
+
# i.e. it is either one of the agent's entities
|
721
|
+
# OR a sub-task, that has produced a valid response.
|
722
|
+
# Contrast this with self.pending_message.metadata.sender, which is an ENTITY
|
723
|
+
# of this agent, or a sub-task's agent.
|
724
|
+
if not self.is_pass_thru:
|
554
725
|
self.pending_sender = r
|
555
|
-
|
556
|
-
|
557
|
-
):
|
558
|
-
# When result is from a sub-task, and `result.metadata` contains
|
559
|
-
# a non-null `parent_responder`, pretend this result was
|
560
|
-
# from the parent_responder, by setting `self.pending_sender`.
|
561
|
-
self.pending_sender = result.metadata.parent_responder
|
562
|
-
# Since we've just used the "pretend responder",
|
563
|
-
# clear out the pretend responder in metadata
|
564
|
-
# (so that it doesn't get used again)
|
565
|
-
result.metadata.parent_responder = None
|
566
|
-
result.metadata.parent = parent
|
567
|
-
old_attachment = (
|
568
|
-
self.pending_message.attachment if self.pending_message else None
|
569
|
-
)
|
726
|
+
result.metadata.parent = parent
|
727
|
+
if not self.is_pass_thru:
|
570
728
|
self.pending_message = result
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
729
|
+
self.log_message(self.pending_sender, result, mark=True)
|
730
|
+
self.step_progress = True
|
731
|
+
self.task_progress = True
|
732
|
+
if self.is_pass_thru:
|
733
|
+
self.n_stalled_steps += 1
|
576
734
|
else:
|
577
|
-
|
578
|
-
|
735
|
+
# reset stuck counter since we made progress
|
736
|
+
self.n_stalled_steps = 0
|
579
737
|
|
580
738
|
def _process_invalid_step_result(self, parent: ChatDocument | None) -> None:
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
739
|
+
"""
|
740
|
+
Since step had no valid result from any responder, decide whether to update the
|
741
|
+
self.pending_message to a NO_ANSWER message from the opposite entity,
|
742
|
+
or leave it as is.
|
743
|
+
Args:
|
744
|
+
parent (ChatDocument|None): parent message of the current message
|
745
|
+
"""
|
746
|
+
self.n_stalled_steps += 1
|
747
|
+
if (not self.task_progress or self.allow_null_result) and not self.is_pass_thru:
|
748
|
+
# There has been no progress at all in this task, so we
|
749
|
+
# update the pending_message to a dummy NO_ANSWER msg
|
750
|
+
# from the entity 'opposite' to the current pending_sender,
|
751
|
+
# so we show "progress" and avoid getting stuck in an infinite loop.
|
752
|
+
responder = (
|
753
|
+
Entity.LLM if self.pending_sender == Entity.USER else Entity.USER
|
754
|
+
)
|
755
|
+
self.pending_message = ChatDocument(
|
756
|
+
content=NO_ANSWER,
|
757
|
+
metadata=ChatDocMetaData(sender=responder, parent=parent),
|
758
|
+
)
|
759
|
+
self.pending_sender = responder
|
587
760
|
self.log_message(self.pending_sender, self.pending_message, mark=True)
|
588
761
|
|
589
762
|
def _show_pending_message_if_debug(self) -> None:
|
590
763
|
if self.pending_message is None:
|
591
764
|
return
|
592
765
|
if settings.debug:
|
593
|
-
sender_str = str(self.pending_sender)
|
594
|
-
msg_str = str(self.pending_message)
|
595
|
-
print(f"[
|
766
|
+
sender_str = escape(str(self.pending_sender))
|
767
|
+
msg_str = escape(str(self.pending_message))
|
768
|
+
print(f"[grey37][{sender_str}]{msg_str}[/grey37]")
|
769
|
+
|
770
|
+
def _parse_routing(self, msg: ChatDocument | str) -> Tuple[bool | None, str | None]:
|
771
|
+
"""
|
772
|
+
Parse routing instruction if any, of the form:
|
773
|
+
PASS:<recipient> (pass current pending msg to recipient)
|
774
|
+
SEND:<recipient> <content> (send content to recipient)
|
775
|
+
Args:
|
776
|
+
msg (ChatDocument|str|None): message to parse
|
777
|
+
Returns:
|
778
|
+
Tuple[bool,str|None]:
|
779
|
+
bool: true=PASS, false=SEND, or None if neither
|
780
|
+
str: recipient, or None
|
781
|
+
"""
|
782
|
+
# handle routing instruction in result if any,
|
783
|
+
# of the form PASS=<recipient>
|
784
|
+
content = msg.content if isinstance(msg, ChatDocument) else msg
|
785
|
+
content = content.strip()
|
786
|
+
if PASS in content and PASS_TO not in content:
|
787
|
+
return True, None
|
788
|
+
if PASS_TO in content and content.split(":")[1] != "":
|
789
|
+
return True, content.split(":")[1]
|
790
|
+
if SEND_TO in content and (send_parts := re.split(r"[,: ]", content))[1] != "":
|
791
|
+
# assume syntax is SEND_TO:<recipient> <content>
|
792
|
+
# or SEND_TO:<recipient>,<content> or SEND_TO:<recipient>:<content>
|
793
|
+
recipient = send_parts[1].strip()
|
794
|
+
# get content to send, clean out routing instruction, and
|
795
|
+
# start from 1 char after SEND_TO:<recipient>,
|
796
|
+
# because we expect there is either a blank or some other separator
|
797
|
+
# after the recipient
|
798
|
+
content_to_send = content.replace(f"{SEND_TO}{recipient}", "").strip()[1:]
|
799
|
+
# if no content then treat same as PASS_TO
|
800
|
+
if content_to_send == "":
|
801
|
+
return True, recipient
|
802
|
+
else:
|
803
|
+
return False, recipient
|
804
|
+
return None, None
|
596
805
|
|
597
806
|
def response(
|
598
807
|
self,
|
@@ -604,17 +813,57 @@ class Task:
|
|
604
813
|
"""
|
605
814
|
if isinstance(e, Task):
|
606
815
|
actual_turns = e.turns if e.turns > 0 else turns
|
816
|
+
e.agent.callbacks.set_parent_agent(self.agent)
|
817
|
+
# e.callbacks.set_parent_agent(self.agent)
|
607
818
|
result = e.run(
|
608
819
|
self.pending_message,
|
609
820
|
turns=actual_turns,
|
610
821
|
caller=self,
|
611
822
|
)
|
612
|
-
|
823
|
+
result_str = str(ChatDocument.to_LLMMessage(result))
|
824
|
+
maybe_tool = len(extract_top_level_json(result_str)) > 0
|
825
|
+
self.callbacks.show_subtask_response(
|
826
|
+
task=e,
|
827
|
+
content=result_str,
|
828
|
+
is_tool=maybe_tool,
|
829
|
+
)
|
613
830
|
else:
|
614
|
-
# Note we always use async responders, even though
|
615
|
-
# ultimately a synch endpoint is used.
|
616
831
|
response_fn = self._entity_responder_map[cast(Entity, e)]
|
617
832
|
result = response_fn(self.pending_message)
|
833
|
+
return self._process_result_routing(result)
|
834
|
+
|
835
|
+
def _process_result_routing(
|
836
|
+
self, result: ChatDocument | None
|
837
|
+
) -> ChatDocument | None:
|
838
|
+
# process result in case there is a routing instruction
|
839
|
+
if result is None:
|
840
|
+
return None
|
841
|
+
is_pass, recipient = self._parse_routing(result)
|
842
|
+
if is_pass is None: # no routing, i.e. neither PASS nor SEND
|
843
|
+
return result
|
844
|
+
if is_pass:
|
845
|
+
if recipient is None or self.pending_message is None:
|
846
|
+
# Just PASS, no recipient
|
847
|
+
# This means pass on self.pending_message to the next responder
|
848
|
+
# in the default sequence of responders.
|
849
|
+
# So leave result intact since we handle "PASS" in step()
|
850
|
+
return result
|
851
|
+
# set recipient in self.pending_message
|
852
|
+
self.pending_message.metadata.recipient = recipient
|
853
|
+
# clear out recipient, replace with just PASS
|
854
|
+
result.content = result.content.replace(
|
855
|
+
f"{PASS_TO}:{recipient}", PASS
|
856
|
+
).strip()
|
857
|
+
return result
|
858
|
+
elif recipient is not None:
|
859
|
+
# we are sending non-empty content to non-null recipient
|
860
|
+
# clean up result.content, set metadata.recipient and return
|
861
|
+
result.content = result.content.replace(
|
862
|
+
f"{SEND_TO}:{recipient}", ""
|
863
|
+
).strip()
|
864
|
+
result.metadata.recipient = recipient
|
865
|
+
return result
|
866
|
+
else:
|
618
867
|
return result
|
619
868
|
|
620
869
|
async def response_async(
|
@@ -639,16 +888,24 @@ class Task:
|
|
639
888
|
"""
|
640
889
|
if isinstance(e, Task):
|
641
890
|
actual_turns = e.turns if e.turns > 0 else turns
|
891
|
+
e.agent.callbacks.set_parent_agent(self.agent)
|
892
|
+
# e.callbacks.set_parent_agent(self.agent)
|
642
893
|
result = await e.run_async(
|
643
894
|
self.pending_message,
|
644
895
|
turns=actual_turns,
|
645
896
|
caller=self,
|
646
897
|
)
|
647
|
-
|
898
|
+
result_str = str(ChatDocument.to_LLMMessage(result))
|
899
|
+
maybe_tool = len(extract_top_level_json(result_str)) > 0
|
900
|
+
self.callbacks.show_subtask_response(
|
901
|
+
task=e,
|
902
|
+
content=result_str,
|
903
|
+
is_tool=maybe_tool,
|
904
|
+
)
|
648
905
|
else:
|
649
906
|
response_fn = self._entity_responder_async_map[cast(Entity, e)]
|
650
907
|
result = await response_fn(self.pending_message)
|
651
|
-
|
908
|
+
return self._process_result_routing(result)
|
652
909
|
|
653
910
|
def result(self) -> ChatDocument:
|
654
911
|
"""
|
@@ -664,10 +921,11 @@ class Task:
|
|
664
921
|
# assuming it is of the form "DONE: <content>"
|
665
922
|
content = content.replace(DONE, "").strip()
|
666
923
|
fun_call = result_msg.function_call if result_msg else None
|
667
|
-
|
924
|
+
tool_messages = result_msg.tool_messages if result_msg else []
|
668
925
|
block = result_msg.metadata.block if result_msg else None
|
669
926
|
recipient = result_msg.metadata.recipient if result_msg else None
|
670
927
|
responder = result_msg.metadata.parent_responder if result_msg else None
|
928
|
+
tool_ids = result_msg.metadata.tool_ids if result_msg else []
|
671
929
|
|
672
930
|
# regardless of which entity actually produced the result,
|
673
931
|
# when we return the result, we set entity to USER
|
@@ -675,7 +933,7 @@ class Task:
|
|
675
933
|
return ChatDocument(
|
676
934
|
content=content,
|
677
935
|
function_call=fun_call,
|
678
|
-
|
936
|
+
tool_messages=tool_messages,
|
679
937
|
metadata=ChatDocMetaData(
|
680
938
|
source=Entity.USER,
|
681
939
|
sender=Entity.USER,
|
@@ -683,60 +941,156 @@ class Task:
|
|
683
941
|
parent_responder=responder,
|
684
942
|
sender_name=self.name,
|
685
943
|
recipient=recipient,
|
944
|
+
tool_ids=tool_ids,
|
686
945
|
),
|
687
946
|
)
|
688
947
|
|
689
|
-
def
|
948
|
+
def _is_empty_message(self, msg: str | ChatDocument | None) -> bool:
|
949
|
+
"""
|
950
|
+
Check if msg is empty or None
|
951
|
+
Args:
|
952
|
+
msg (str|ChatDocument|None): message to check
|
953
|
+
Returns:
|
954
|
+
bool: True if msg is (equivalent to) empty or None, False otherwise
|
955
|
+
"""
|
956
|
+
return (
|
957
|
+
msg is None
|
958
|
+
or (isinstance(msg, str) and msg.strip() in [PASS, ""])
|
959
|
+
or (
|
960
|
+
isinstance(msg, ChatDocument)
|
961
|
+
and msg.content.strip() in [PASS, ""]
|
962
|
+
and msg.function_call is None
|
963
|
+
and msg.tool_messages == []
|
964
|
+
)
|
965
|
+
)
|
966
|
+
|
967
|
+
def _is_done_response(
|
968
|
+
self, result: str | None | ChatDocument, responder: Responder
|
969
|
+
) -> bool:
|
970
|
+
"""Is the task done based on the response from the given responder?"""
|
971
|
+
|
972
|
+
response_says_done = result is not None and (
|
973
|
+
(isinstance(result, str) and DONE in result)
|
974
|
+
or (isinstance(result, ChatDocument) and DONE in result.content)
|
975
|
+
)
|
976
|
+
return (
|
977
|
+
(
|
978
|
+
responder.value in self.done_if_response
|
979
|
+
and not self._is_empty_message(result)
|
980
|
+
)
|
981
|
+
or (
|
982
|
+
responder.value in self.done_if_no_response
|
983
|
+
and self._is_empty_message(result)
|
984
|
+
)
|
985
|
+
or (not self._is_empty_message(result) and response_says_done)
|
986
|
+
)
|
987
|
+
|
988
|
+
def _maybe_infinite_loop(self, history: int = 10) -> bool:
|
989
|
+
"""
|
990
|
+
TODO Not currently used, until we figure out best way.
|
991
|
+
Check if {NO_ANSWER}, empty answer, or a specific non-LLM msg occurs too
|
992
|
+
often in history of pending messages -- this can be an indicator of a possible
|
993
|
+
multi-step infinite loop that we should exit.
|
994
|
+
(A single-step infinite loop is where individual steps don't show progress
|
995
|
+
and are easy to detect via n_stalled_steps, but a multi-step infinite loop
|
996
|
+
could show "progress" at each step, but can still be an infinite loop, e.g.
|
997
|
+
if the steps are just alternating between two messages).
|
998
|
+
"""
|
999
|
+
p = self.pending_message
|
1000
|
+
n_no_answers = 0
|
1001
|
+
n_empty_answers = 0
|
1002
|
+
counter: Counter[str] = Counter()
|
1003
|
+
# count number of NO_ANSWER and empty answers in last up to 10 messages
|
1004
|
+
# in ancestors of self.pending_message
|
1005
|
+
for _ in range(history):
|
1006
|
+
if p is None:
|
1007
|
+
break
|
1008
|
+
n_no_answers += p.content.strip() == NO_ANSWER
|
1009
|
+
n_empty_answers += p.content.strip() == "" and p.function_call is None
|
1010
|
+
if p.metadata.sender != Entity.LLM and PASS not in p.content:
|
1011
|
+
counter.update([p.metadata.sender + ":" + p.content])
|
1012
|
+
p = p.metadata.parent
|
1013
|
+
|
1014
|
+
# freq of most common message in history
|
1015
|
+
high_freq = (counter.most_common(1) or [("", 0)])[0][1]
|
1016
|
+
# We deem this a potential infinite loop if:
|
1017
|
+
# - a specific non-LLM msg occurs too often, or
|
1018
|
+
# - a NO_ANSWER or empty answer occurs too often
|
1019
|
+
return max(high_freq, n_no_answers) > self.max_stalled_steps
|
1020
|
+
|
1021
|
+
def done(
|
1022
|
+
self, result: ChatDocument | None = None, r: Responder | None = None
|
1023
|
+
) -> bool:
|
690
1024
|
"""
|
691
1025
|
Check if task is done. This is the default behavior.
|
692
1026
|
Derived classes can override this.
|
1027
|
+
Args:
|
1028
|
+
result (ChatDocument|None): result from a responder
|
1029
|
+
r (Responder|None): responder that produced the result
|
1030
|
+
Not used here, but could be used by derived classes.
|
693
1031
|
Returns:
|
694
1032
|
bool: True if task is done, False otherwise
|
695
1033
|
"""
|
1034
|
+
result = result or self.pending_message
|
696
1035
|
user_quit = (
|
697
|
-
|
698
|
-
and
|
699
|
-
and
|
1036
|
+
result is not None
|
1037
|
+
and result.content in USER_QUIT
|
1038
|
+
and result.metadata.sender == Entity.USER
|
700
1039
|
)
|
701
1040
|
if self._level == 0 and self.only_user_quits_root:
|
702
1041
|
# for top-level task, only user can quit out
|
703
1042
|
return user_quit
|
704
1043
|
|
1044
|
+
if self.is_done:
|
1045
|
+
return True
|
1046
|
+
|
1047
|
+
if self.n_stalled_steps >= self.max_stalled_steps:
|
1048
|
+
# we are stuck, so bail to avoid infinite loop
|
1049
|
+
logger.warning(
|
1050
|
+
f"Task {self.name} stuck for {self.max_stalled_steps} steps; exiting."
|
1051
|
+
)
|
1052
|
+
return True
|
1053
|
+
|
705
1054
|
return (
|
706
1055
|
# no valid response from any entity/agent in current turn
|
707
|
-
|
708
|
-
#
|
709
|
-
or DONE in
|
1056
|
+
result is None
|
1057
|
+
# An entity decided task is done
|
1058
|
+
or DONE in result.content
|
710
1059
|
or ( # current task is addressing message to caller task
|
711
1060
|
self.caller is not None
|
712
1061
|
and self.caller.name != ""
|
713
|
-
and
|
714
|
-
)
|
715
|
-
or (
|
716
|
-
# Task controller is "stuck", has nothing to say
|
717
|
-
NO_ANSWER in self.pending_message.content
|
718
|
-
and self.pending_message.metadata.sender == self.controller
|
1062
|
+
and result.metadata.recipient == self.caller.name
|
719
1063
|
)
|
1064
|
+
# or (
|
1065
|
+
# # Task controller is "stuck", has nothing to say
|
1066
|
+
# NO_ANSWER in result.content
|
1067
|
+
# and result.metadata.sender == self.controller
|
1068
|
+
# )
|
720
1069
|
or user_quit
|
721
1070
|
)
|
722
1071
|
|
723
|
-
def valid(
|
1072
|
+
def valid(
|
1073
|
+
self,
|
1074
|
+
result: Optional[ChatDocument],
|
1075
|
+
r: Responder,
|
1076
|
+
) -> bool:
|
724
1077
|
"""
|
725
|
-
Is the result from an entity or sub-task
|
726
|
-
for responses
|
1078
|
+
Is the result from a Responder (i.e. an entity or sub-task)
|
1079
|
+
such that we can stop searching for responses in this step?
|
727
1080
|
"""
|
728
1081
|
# TODO caution we should ensure that no handler method (tool) returns simply
|
729
1082
|
# an empty string (e.g when showing contents of an empty file), since that
|
730
1083
|
# would be considered an invalid response, and other responders will wrongly
|
731
1084
|
# be given a chance to respond.
|
1085
|
+
|
1086
|
+
# if task would be considered done given responder r's `result`,
|
1087
|
+
# then consider the result valid.
|
1088
|
+
if result is not None and self.done(result, r):
|
1089
|
+
return True
|
732
1090
|
return (
|
733
1091
|
result is not None
|
734
|
-
and (result
|
735
|
-
and (
|
736
|
-
# controller is stuck and we are done with task loop
|
737
|
-
NO_ANSWER not in result.content
|
738
|
-
or result.metadata.sender == self.controller
|
739
|
-
)
|
1092
|
+
and not self._is_empty_message(result)
|
1093
|
+
and result.content.strip() != NO_ANSWER
|
740
1094
|
)
|
741
1095
|
|
742
1096
|
def log_message(
|
@@ -802,35 +1156,34 @@ class Task:
|
|
802
1156
|
"""
|
803
1157
|
if recipient == "":
|
804
1158
|
return True
|
805
|
-
|
806
|
-
|
1159
|
+
# native responders names are USER, LLM, AGENT,
|
1160
|
+
# and the names of subtasks are from Task.name attribute
|
1161
|
+
responder_names = [self.name.lower()] + [
|
1162
|
+
r.name.lower() for r in self.responders
|
1163
|
+
]
|
1164
|
+
return recipient.lower() in responder_names
|
807
1165
|
|
808
1166
|
def _recipient_mismatch(self, e: Responder) -> bool:
|
809
1167
|
"""
|
810
1168
|
Is the recipient explicitly specified and does not match responder "e" ?
|
811
1169
|
"""
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
# or the name of another task.
|
819
|
-
return recipient not in (e.name, self.name)
|
1170
|
+
# Note that recipient could be specified as an Entity or a Task name
|
1171
|
+
return (
|
1172
|
+
self.pending_message is not None
|
1173
|
+
and (recipient := self.pending_message.metadata.recipient) != ""
|
1174
|
+
and recipient not in (e.name, self.name)
|
1175
|
+
)
|
820
1176
|
|
821
1177
|
def _can_respond(self, e: Responder) -> bool:
|
822
1178
|
if self.pending_sender == e:
|
1179
|
+
# Responder cannot respond to its own message
|
823
1180
|
return False
|
824
1181
|
if self.pending_message is None:
|
825
1182
|
return True
|
826
|
-
if self.pending_message.metadata.block == e:
|
827
|
-
# the entity should only be blocked at the first try;
|
828
|
-
# Remove the block so it does not block the entity forever
|
829
|
-
self.pending_message.metadata.block = None
|
830
|
-
return False
|
831
1183
|
if self._recipient_mismatch(e):
|
1184
|
+
# Cannot respond if not addressed to this entity
|
832
1185
|
return False
|
833
|
-
return self.pending_message
|
1186
|
+
return self.pending_message.metadata.block != e
|
834
1187
|
|
835
1188
|
def set_color_log(self, enable: bool = True) -> None:
|
836
1189
|
"""
|