langroid 0.2.0__tar.gz → 0.2.3__tar.gz
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-0.2.0 → langroid-0.2.3}/PKG-INFO +6 -2
- {langroid-0.2.0 → langroid-0.2.3}/README.md +4 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/base.py +6 -8
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/chat_agent.py +1 -1
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/chat_document.py +2 -1
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/task.py +169 -81
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/tools/rewind_tool.py +3 -2
- {langroid-0.2.0 → langroid-0.2.3}/langroid/language_models/mock_lm.py +24 -29
- {langroid-0.2.0 → langroid-0.2.3}/langroid/utils/constants.py +2 -1
- {langroid-0.2.0 → langroid-0.2.3}/pyproject.toml +2 -2
- {langroid-0.2.0 → langroid-0.2.3}/LICENSE +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/__init__.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/__init__.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/batch.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/callbacks/__init__.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/callbacks/chainlit.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/helpers.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/junk +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/openai_assistant.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/special/__init__.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/special/doc_chat_agent.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/special/lance_doc_chat_agent.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/special/lance_rag/__init__.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/special/lance_rag/critic_agent.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/special/lance_rag/lance_rag_task.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/special/lance_rag/query_planner_agent.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/special/lance_tools.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/special/neo4j/__init__.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/special/neo4j/csv_kg_chat.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/special/neo4j/neo4j_chat_agent.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/special/neo4j/utils/__init__.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/special/neo4j/utils/system_message.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/special/relevance_extractor_agent.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/special/retriever_agent.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/special/sql/__init__.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/special/sql/sql_chat_agent.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/special/sql/utils/__init__.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/special/sql/utils/description_extractors.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/special/sql/utils/populate_metadata.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/special/sql/utils/system_message.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/special/sql/utils/tools.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/special/table_chat_agent.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/tool_message.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/tools/__init__.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/tools/duckduckgo_search_tool.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/tools/extract_tool.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/tools/generator_tool.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/tools/google_search_tool.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/tools/metaphor_search_tool.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/tools/recipient_tool.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/tools/retrieval_tool.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/tools/run_python_code.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent/tools/segment_extract_tool.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/agent_config.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/cachedb/__init__.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/cachedb/base.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/cachedb/momento_cachedb.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/cachedb/redis_cachedb.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/embedding_models/__init__.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/embedding_models/base.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/embedding_models/clustering.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/embedding_models/models.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/embedding_models/protoc/__init__.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/embedding_models/protoc/embeddings.proto +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/embedding_models/protoc/embeddings_pb2.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/embedding_models/protoc/embeddings_pb2.pyi +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/embedding_models/protoc/embeddings_pb2_grpc.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/embedding_models/remote_embeds.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/exceptions.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/language_models/__init__.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/language_models/azure_openai.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/language_models/base.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/language_models/config.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/language_models/openai_gpt.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/language_models/prompt_formatter/__init__.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/language_models/prompt_formatter/base.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/language_models/prompt_formatter/hf_formatter.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/language_models/prompt_formatter/llama2_formatter.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/language_models/utils.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/mytypes.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/parsing/__init__.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/parsing/agent_chats.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/parsing/code-parsing.md +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/parsing/code_parser.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/parsing/config.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/parsing/document_parser.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/parsing/image_text.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/parsing/para_sentence_split.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/parsing/parse_json.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/parsing/parser.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/parsing/repo_loader.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/parsing/routing.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/parsing/search.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/parsing/spider.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/parsing/table_loader.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/parsing/url_loader.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/parsing/url_loader_cookies.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/parsing/urls.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/parsing/utils.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/parsing/web_search.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/prompts/__init__.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/prompts/chat-gpt4-system-prompt.md +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/prompts/dialog.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/prompts/prompts_config.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/prompts/templates.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/pydantic_v1/__init__.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/pydantic_v1/main.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/utils/__init__.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/utils/algorithms/__init__.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/utils/algorithms/graph.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/utils/configuration.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/utils/docker.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/utils/globals.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/utils/llms/__init__.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/utils/llms/strings.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/utils/logging.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/utils/object_registry.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/utils/output/__init__.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/utils/output/citations.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/utils/output/printing.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/utils/output/status.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/utils/pandas_utils.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/utils/pydantic_utils.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/utils/system.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/utils/web/__init__.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/utils/web/login.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/vector_store/__init__.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/vector_store/base.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/vector_store/chromadb.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/vector_store/lancedb.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/vector_store/meilisearch.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/vector_store/momento.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/vector_store/qdrant_cloud.py +0 -0
- {langroid-0.2.0 → langroid-0.2.3}/langroid/vector_store/qdrantdb.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: langroid
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.3
|
4
4
|
Summary: Harness LLMs with Multi-Agent Programming
|
5
5
|
License: MIT
|
6
6
|
Author: Prasad Chalasani
|
@@ -36,7 +36,7 @@ Provides-Extra: vecdbs
|
|
36
36
|
Requires-Dist: aiohttp (>=3.9.1,<4.0.0)
|
37
37
|
Requires-Dist: async-generator (>=1.10,<2.0)
|
38
38
|
Requires-Dist: bs4 (>=0.0.1,<0.0.2)
|
39
|
-
Requires-Dist: chainlit (
|
39
|
+
Requires-Dist: chainlit (==1.1.202) ; extra == "all" or extra == "chainlit"
|
40
40
|
Requires-Dist: chromadb (>=0.4.21,<=0.4.23) ; extra == "vecdbs" or extra == "all" or extra == "chromadb"
|
41
41
|
Requires-Dist: colorlog (>=6.7.0,<7.0.0)
|
42
42
|
Requires-Dist: docstring-parser (>=0.15,<0.16)
|
@@ -226,6 +226,10 @@ teacher_task.run()
|
|
226
226
|
<details>
|
227
227
|
<summary> <b>Click to expand</b></summary>
|
228
228
|
|
229
|
+
- **Jun 2024:**
|
230
|
+
- **0.2.0:** Improved lineage tracking, granular sub-task configs, and a new tool, `RewindTool`,
|
231
|
+
that lets an agent "rewind and redo" a past message (and all dependent messages are cleared out
|
232
|
+
thanks to the lineage tracking). Read notes [here](https://github.com/langroid/langroid/releases/tag/0.2.0).
|
229
233
|
- **May 2024:**
|
230
234
|
- **Slimmer langroid**: All document-parsers (i.e. pdf, doc, docx) and most
|
231
235
|
vector-databases (except qdrant)
|
@@ -123,6 +123,10 @@ teacher_task.run()
|
|
123
123
|
<details>
|
124
124
|
<summary> <b>Click to expand</b></summary>
|
125
125
|
|
126
|
+
- **Jun 2024:**
|
127
|
+
- **0.2.0:** Improved lineage tracking, granular sub-task configs, and a new tool, `RewindTool`,
|
128
|
+
that lets an agent "rewind and redo" a past message (and all dependent messages are cleared out
|
129
|
+
thanks to the lineage tracking). Read notes [here](https://github.com/langroid/langroid/releases/tag/0.2.0).
|
126
130
|
- **May 2024:**
|
127
131
|
- **Slimmer langroid**: All document-parsers (i.e. pdf, doc, docx) and most
|
128
132
|
vector-databases (except qdrant)
|
@@ -344,7 +344,7 @@ class Agent(ABC):
|
|
344
344
|
return results
|
345
345
|
if not settings.quiet:
|
346
346
|
console.print(f"[red]{self.indent}", end="")
|
347
|
-
print(f"[red]Agent: {results}")
|
347
|
+
print(f"[red]Agent: {escape(results)}")
|
348
348
|
maybe_json = len(extract_top_level_json(results)) > 0
|
349
349
|
self.callbacks.show_agent_response(
|
350
350
|
content=results,
|
@@ -409,14 +409,12 @@ class Agent(ABC):
|
|
409
409
|
isinstance(msg, ChatDocument) and msg.metadata.recipient == Entity.USER
|
410
410
|
)
|
411
411
|
|
412
|
-
interactive =
|
413
|
-
|
414
|
-
|
415
|
-
if self.default_human_response is not None and not need_human_response:
|
416
|
-
# useful for automated testing
|
417
|
-
user_msg = self.default_human_response
|
418
|
-
elif not interactive and not need_human_response:
|
412
|
+
interactive = self.interactive or settings.interactive
|
413
|
+
|
414
|
+
if not interactive and not need_human_response:
|
419
415
|
return None
|
416
|
+
elif self.default_human_response is not None:
|
417
|
+
user_msg = self.default_human_response
|
420
418
|
else:
|
421
419
|
if self.callbacks.get_user_response is not None:
|
422
420
|
# ask user with empty prompt: no need for prompt
|
@@ -857,7 +857,7 @@ class ChatAgent(Agent):
|
|
857
857
|
# we won't have citations yet, so we're done
|
858
858
|
return
|
859
859
|
if response.metadata.has_citation and not settings.quiet:
|
860
|
-
print("[grey37]SOURCES:\n" + response.metadata.source + "[/grey37]")
|
860
|
+
print("[grey37]SOURCES:\n" + escape(response.metadata.source) + "[/grey37]")
|
861
861
|
self.callbacks.show_llm_response(
|
862
862
|
content=str(response.metadata.source),
|
863
863
|
is_tool=False,
|
@@ -36,7 +36,8 @@ class StatusCode(str, Enum):
|
|
36
36
|
STALLED = "STALLED"
|
37
37
|
INF_LOOP = "INF_LOOP"
|
38
38
|
KILL = "KILL"
|
39
|
-
|
39
|
+
FIXED_TURNS = "FIXED_TURNS" # reached intended number of turns
|
40
|
+
MAX_TURNS = "MAX_TURNS" # hit max-turns limit
|
40
41
|
MAX_COST = "MAX_COST"
|
41
42
|
MAX_TOKENS = "MAX_TOKENS"
|
42
43
|
TIMEOUT = "TIMEOUT"
|
@@ -5,6 +5,7 @@ import copy
|
|
5
5
|
import logging
|
6
6
|
import threading
|
7
7
|
from collections import Counter, deque
|
8
|
+
from pathlib import Path
|
8
9
|
from types import SimpleNamespace
|
9
10
|
from typing import (
|
10
11
|
Any,
|
@@ -39,6 +40,7 @@ from langroid.parsing.routing import parse_addressed_message
|
|
39
40
|
from langroid.pydantic_v1 import BaseModel
|
40
41
|
from langroid.utils.configuration import settings
|
41
42
|
from langroid.utils.constants import (
|
43
|
+
AT, # regex for start of an addressed recipient e.g. "@"
|
42
44
|
DONE,
|
43
45
|
NO_ANSWER,
|
44
46
|
PASS,
|
@@ -77,6 +79,7 @@ class TaskConfig(BaseModel):
|
|
77
79
|
inf_loop_dominance_factor: float = 1.5
|
78
80
|
inf_loop_wait_factor: int = 5
|
79
81
|
restart_as_subtask: bool = False
|
82
|
+
logs_dir: str = "logs"
|
80
83
|
|
81
84
|
|
82
85
|
class Task:
|
@@ -124,13 +127,14 @@ class Task:
|
|
124
127
|
restart: bool = True,
|
125
128
|
default_human_response: Optional[str] = None,
|
126
129
|
interactive: bool = True,
|
127
|
-
only_user_quits_root: bool =
|
130
|
+
only_user_quits_root: bool = True,
|
128
131
|
erase_substeps: bool = False,
|
129
|
-
allow_null_result: bool =
|
132
|
+
allow_null_result: bool = False,
|
130
133
|
max_stalled_steps: int = 5,
|
131
134
|
done_if_no_response: List[Responder] = [],
|
132
135
|
done_if_response: List[Responder] = [],
|
133
136
|
config: TaskConfig = TaskConfig(),
|
137
|
+
**kwargs: Any, # catch-all for any legacy params, for backwards compatibility
|
134
138
|
):
|
135
139
|
"""
|
136
140
|
A task to be performed by an agent.
|
@@ -139,23 +143,29 @@ class Task:
|
|
139
143
|
agent (Agent): agent associated with the task
|
140
144
|
name (str): name of the task
|
141
145
|
llm_delegate (bool):
|
142
|
-
|
143
|
-
instead]
|
144
|
-
Whether to delegate control to LLM; conceptually,
|
146
|
+
Whether to delegate "control" to LLM; conceptually,
|
145
147
|
the "controlling entity" is the one "seeking" responses to its queries,
|
146
|
-
and has a goal it is aiming to achieve
|
147
|
-
either the LLM or the USER.
|
148
|
+
and has a goal it is aiming to achieve, and decides when a task is done.
|
149
|
+
The "controlling entity" is either the LLM or the USER.
|
150
|
+
(Note within a Task there is just one
|
148
151
|
LLM, and all other entities are proxies of the "User" entity).
|
152
|
+
See also: `done_if_response`, `done_if_no_response` for more granular
|
153
|
+
control of task termination.
|
149
154
|
single_round (bool):
|
150
|
-
|
151
|
-
|
152
|
-
and subsequent response by non-controller
|
153
|
-
|
155
|
+
If true, task runs until one message by "controller"
|
156
|
+
(i.e. LLM if `llm_delegate` is true, otherwise USER)
|
157
|
+
and subsequent response by non-controller [When a tool is involved,
|
158
|
+
this will not give intended results. See `done_if_response`,
|
159
|
+
`done_if_no_response` below].
|
160
|
+
termination]. If false, runs for the specified number of turns in
|
161
|
+
`run`, or until `done()` is true.
|
154
162
|
One run of step() is considered a "turn".
|
163
|
+
See also: `done_if_response`, `done_if_no_response` for more granular
|
164
|
+
control of task termination.
|
155
165
|
system_message (str): if not empty, overrides agent's system_message
|
156
166
|
user_message (str): if not empty, overrides agent's user_message
|
157
167
|
restart (bool): if true, resets the agent's message history *at every run*.
|
158
|
-
default_human_response (str): default response from user; useful for
|
168
|
+
default_human_response (str|None): default response from user; useful for
|
159
169
|
testing, to avoid interactive input from user.
|
160
170
|
[Instead of this, setting `interactive` usually suffices]
|
161
171
|
interactive (bool): if true, wait for human input after each non-human
|
@@ -166,18 +176,26 @@ class Task:
|
|
166
176
|
case the system will wait for a user response. In other words, use
|
167
177
|
`interactive=False` when you want a "largely non-interactive"
|
168
178
|
run, with the exception of explicit user addressing.
|
169
|
-
only_user_quits_root (bool): if true, only user can
|
170
|
-
|
171
|
-
Instead of this, setting `interactive` suffices]
|
179
|
+
only_user_quits_root (bool): if true, when interactive=True, only user can
|
180
|
+
quit the root task (Ignored when interactive=False).
|
172
181
|
erase_substeps (bool): if true, when task completes, erase intermediate
|
173
182
|
conversation with subtasks from this agent's `message_history`, and also
|
174
183
|
erase all subtask agents' `message_history`.
|
175
184
|
Note: erasing can reduce prompt sizes, but results in repetitive
|
176
185
|
sub-task delegation.
|
177
|
-
allow_null_result (bool):
|
178
|
-
If true,
|
179
|
-
|
180
|
-
Optional, default is
|
186
|
+
allow_null_result (bool):
|
187
|
+
If true, create dummy NO_ANSWER response when no valid response is found
|
188
|
+
in a step.
|
189
|
+
Optional, default is False.
|
190
|
+
*Note:* In non-interactive mode, when this is set to True,
|
191
|
+
you can have a situation where an LLM generates (non-tool) text,
|
192
|
+
and no other responders have valid responses, and a "Null result"
|
193
|
+
is inserted as a dummy response from the User entity, so the LLM
|
194
|
+
will now respond to this Null result, and this will continue
|
195
|
+
until the LLM emits a DONE signal (if instructed to do so),
|
196
|
+
otherwise langroid detects a potential infinite loop after
|
197
|
+
a certain number of such steps (= `TaskConfig.inf_loop_wait_factor`)
|
198
|
+
and will raise an InfiniteLoopException.
|
181
199
|
max_stalled_steps (int): task considered done after this many consecutive
|
182
200
|
steps with no progress. Default is 3.
|
183
201
|
done_if_no_response (List[Responder]): consider task done if NULL
|
@@ -234,36 +252,32 @@ class Task:
|
|
234
252
|
self.tsv_logger: None | logging.Logger = None
|
235
253
|
self.color_log: bool = False if settings.notebook else True
|
236
254
|
|
237
|
-
self.step_progress = False # progress in current step?
|
238
255
|
self.n_stalled_steps = 0 # how many consecutive steps with no progress?
|
256
|
+
# how many 2-step-apart alternations of no_answer step-result have we had,
|
257
|
+
# i.e. x1, N/A, x2, N/A, x3, N/A ...
|
258
|
+
self.n_no_answer_alternations = 0
|
259
|
+
self._no_answer_step: int = -1
|
260
|
+
self._step_idx = -1 # current step index
|
239
261
|
self.max_stalled_steps = max_stalled_steps
|
240
262
|
self.done_if_response = [r.value for r in done_if_response]
|
241
263
|
self.done_if_no_response = [r.value for r in done_if_no_response]
|
242
264
|
self.is_done = False # is task done (based on response)?
|
243
265
|
self.is_pass_thru = False # is current response a pass-thru?
|
244
|
-
self.task_progress = False # progress in current task (since run or run_async)?
|
245
266
|
if name:
|
246
267
|
# task name overrides name in agent config
|
247
268
|
agent.config.name = name
|
248
269
|
self.name = name or agent.config.name
|
249
270
|
self.value: str = self.name
|
250
271
|
|
251
|
-
|
252
|
-
interactive = False
|
253
|
-
self.interactive = interactive
|
254
|
-
self.agent.interactive = interactive
|
255
|
-
self.message_history_idx = -1
|
256
|
-
if interactive:
|
257
|
-
only_user_quits_root = True
|
258
|
-
else:
|
259
|
-
default_human_response = default_human_response or ""
|
260
|
-
only_user_quits_root = False
|
272
|
+
self.default_human_response = default_human_response
|
261
273
|
if default_human_response is not None:
|
274
|
+
# only override agent's default_human_response if it is explicitly set
|
262
275
|
self.agent.default_human_response = default_human_response
|
263
|
-
self.
|
264
|
-
|
265
|
-
self.agent.default_human_response = None
|
276
|
+
self.interactive = interactive
|
277
|
+
self.agent.interactive = interactive
|
266
278
|
self.only_user_quits_root = only_user_quits_root
|
279
|
+
self.message_history_idx = -1
|
280
|
+
|
267
281
|
# set to True if we want to collapse multi-turn conversation with sub-tasks into
|
268
282
|
# just the first outgoing message and last incoming message.
|
269
283
|
# Note this also completely erases sub-task agents' message_history.
|
@@ -300,17 +314,16 @@ class Task:
|
|
300
314
|
self.turns = -1 # no limit
|
301
315
|
self.llm_delegate = llm_delegate
|
302
316
|
if llm_delegate:
|
303
|
-
self.controller = Entity.LLM
|
304
317
|
if self.single_round:
|
305
318
|
# 0: User instructs (delegating to LLM);
|
306
|
-
# 1: LLM asks;
|
319
|
+
# 1: LLM (as the Controller) asks;
|
307
320
|
# 2: user replies.
|
308
321
|
self.turns = 2
|
309
322
|
else:
|
310
|
-
self.controller = Entity.USER
|
311
323
|
if self.single_round:
|
312
|
-
|
313
|
-
|
324
|
+
# 0: User (as Controller) asks,
|
325
|
+
# 1: LLM replies.
|
326
|
+
self.turns = 1
|
314
327
|
# other sub_tasks this task can delegate to
|
315
328
|
self.sub_tasks: List[Task] = []
|
316
329
|
self.caller: Task | None = None # which task called this task's `run` method
|
@@ -521,12 +534,18 @@ class Task:
|
|
521
534
|
if self.caller is not None and self.caller.logger is not None:
|
522
535
|
self.logger = self.caller.logger
|
523
536
|
else:
|
524
|
-
self.logger = RichFileLogger(
|
537
|
+
self.logger = RichFileLogger(
|
538
|
+
str(Path(self.config.logs_dir) / f"{self.name}.log"),
|
539
|
+
color=self.color_log,
|
540
|
+
)
|
525
541
|
|
526
542
|
if self.caller is not None and self.caller.tsv_logger is not None:
|
527
543
|
self.tsv_logger = self.caller.tsv_logger
|
528
544
|
else:
|
529
|
-
self.tsv_logger = setup_file_logger(
|
545
|
+
self.tsv_logger = setup_file_logger(
|
546
|
+
"tsv_logger",
|
547
|
+
str(Path(self.config.logs_dir) / f"{self.name}.tsv"),
|
548
|
+
)
|
530
549
|
header = ChatDocLoggerFields().tsv_header()
|
531
550
|
self.tsv_logger.info(f" \tTask\tResponder\t{header}")
|
532
551
|
|
@@ -559,8 +578,10 @@ class Task:
|
|
559
578
|
# so reset own agent and recursively for all sub-tasks
|
560
579
|
self.reset_all_sub_tasks()
|
561
580
|
|
562
|
-
self.task_progress = False
|
563
581
|
self.n_stalled_steps = 0
|
582
|
+
self._no_answer_step = -1 # last step where the best explicit response was N/A
|
583
|
+
# how many N/A alternations have we had so far? (for Inf loop detection)
|
584
|
+
self.n_no_answer_alternations = 0
|
564
585
|
self.max_cost = max_cost
|
565
586
|
self.max_tokens = max_tokens
|
566
587
|
self.session_id = session_id
|
@@ -588,6 +609,7 @@ class Task:
|
|
588
609
|
turns = self.turns if turns < 0 else turns
|
589
610
|
i = 0
|
590
611
|
while True:
|
612
|
+
self._step_idx = i # used in step() below
|
591
613
|
self.step()
|
592
614
|
done, status = self.done()
|
593
615
|
if done:
|
@@ -601,7 +623,17 @@ class Task:
|
|
601
623
|
else max(turns, settings.max_turns)
|
602
624
|
)
|
603
625
|
if max_turns > 0 and i >= max_turns:
|
604
|
-
|
626
|
+
# Important to distinguish between:
|
627
|
+
# (a) intentional run for a
|
628
|
+
# fixed number of turns, where we expect the pending message
|
629
|
+
# at that stage to be the desired result, and
|
630
|
+
# (b) hitting max_turns limit, which is not intentional, and is an
|
631
|
+
# exception, resulting in a None task result
|
632
|
+
status = (
|
633
|
+
StatusCode.MAX_TURNS
|
634
|
+
if i == settings.max_turns
|
635
|
+
else StatusCode.FIXED_TURNS
|
636
|
+
)
|
605
637
|
break
|
606
638
|
if (
|
607
639
|
self.config.inf_loop_cycle_len > 0
|
@@ -617,9 +649,7 @@ class Task:
|
|
617
649
|
"""
|
618
650
|
)
|
619
651
|
|
620
|
-
final_result = self.result()
|
621
|
-
if final_result is not None:
|
622
|
-
final_result.metadata.status = status
|
652
|
+
final_result = self.result(status)
|
623
653
|
self._post_run_loop()
|
624
654
|
return final_result
|
625
655
|
|
@@ -673,8 +703,10 @@ class Task:
|
|
673
703
|
# so reset own agent and recursively for all sub-tasks
|
674
704
|
self.reset_all_sub_tasks()
|
675
705
|
|
676
|
-
self.task_progress = False
|
677
706
|
self.n_stalled_steps = 0
|
707
|
+
self._no_answer_step = -1 # last step where the best explicit response was N/A
|
708
|
+
# how many N/A alternations have we had so far? (for Inf loop detection)
|
709
|
+
self.n_no_answer_alternations = 0
|
678
710
|
self.max_cost = max_cost
|
679
711
|
self.max_tokens = max_tokens
|
680
712
|
self.session_id = session_id
|
@@ -698,6 +730,7 @@ class Task:
|
|
698
730
|
turns = self.turns if turns < 0 else turns
|
699
731
|
i = 0
|
700
732
|
while True:
|
733
|
+
self._step_idx = i # used in step() below
|
701
734
|
await self.step_async()
|
702
735
|
await asyncio.sleep(0.01) # temp yield to avoid blocking
|
703
736
|
done, status = self.done()
|
@@ -712,7 +745,17 @@ class Task:
|
|
712
745
|
else max(turns, settings.max_turns)
|
713
746
|
)
|
714
747
|
if max_turns > 0 and i >= max_turns:
|
715
|
-
|
748
|
+
# Important to distinguish between:
|
749
|
+
# (a) intentional run for a
|
750
|
+
# fixed number of turns, where we expect the pending message
|
751
|
+
# at that stage to be the desired result, and
|
752
|
+
# (b) hitting max_turns limit, which is not intentional, and is an
|
753
|
+
# exception, resulting in a None task result
|
754
|
+
status = (
|
755
|
+
StatusCode.MAX_TURNS
|
756
|
+
if i == settings.max_turns
|
757
|
+
else StatusCode.FIXED_TURNS
|
758
|
+
)
|
716
759
|
break
|
717
760
|
if (
|
718
761
|
self.config.inf_loop_cycle_len > 0
|
@@ -728,9 +771,7 @@ class Task:
|
|
728
771
|
"""
|
729
772
|
)
|
730
773
|
|
731
|
-
final_result = self.result()
|
732
|
-
if final_result is not None:
|
733
|
-
final_result.metadata.status = status
|
774
|
+
final_result = self.result(status)
|
734
775
|
self._post_run_loop()
|
735
776
|
return final_result
|
736
777
|
|
@@ -744,9 +785,6 @@ class Task:
|
|
744
785
|
self.init(msg)
|
745
786
|
# sets indentation to be printed prior to any output from agent
|
746
787
|
self.agent.indent = self._indent
|
747
|
-
if self.default_human_response is not None:
|
748
|
-
self.agent.default_human_response = self.default_human_response
|
749
|
-
|
750
788
|
self.message_history_idx = -1
|
751
789
|
if isinstance(self.agent, ChatAgent):
|
752
790
|
# mark where we are in the message history, so we can reset to this when
|
@@ -820,7 +858,6 @@ class Task:
|
|
820
858
|
`step_async()`. Consider refactoring to avoid duplication.
|
821
859
|
"""
|
822
860
|
self.is_done = False
|
823
|
-
self.step_progress = False
|
824
861
|
parent = self.pending_message
|
825
862
|
recipient = (
|
826
863
|
""
|
@@ -860,6 +897,8 @@ class Task:
|
|
860
897
|
responders.insert(0, Entity.USER)
|
861
898
|
|
862
899
|
found_response = False
|
900
|
+
# (responder, result) from a responder who explicitly said NO_ANSWER
|
901
|
+
no_answer_response: None | Tuple[Responder, ChatDocument] = None
|
863
902
|
for r in responders:
|
864
903
|
self.is_pass_thru = False
|
865
904
|
if not self._can_respond(r):
|
@@ -879,6 +918,8 @@ class Task:
|
|
879
918
|
continue
|
880
919
|
self.human_tried = r == Entity.USER
|
881
920
|
result = self.response(r, turns)
|
921
|
+
if result and NO_ANSWER in result.content:
|
922
|
+
no_answer_response = (r, result)
|
882
923
|
self.is_done = self._is_done_response(result, r)
|
883
924
|
self.is_pass_thru = PASS in result.content if result else False
|
884
925
|
if self.valid(result, r):
|
@@ -891,8 +932,15 @@ class Task:
|
|
891
932
|
if self.is_done:
|
892
933
|
# skip trying other responders in this step
|
893
934
|
break
|
894
|
-
if not found_response:
|
895
|
-
|
935
|
+
if not found_response: # did not find a Non-NO_ANSWER response
|
936
|
+
if no_answer_response:
|
937
|
+
# even though there was no valid response from anyone in this step,
|
938
|
+
# if there was at least one who EXPLICITLY said NO_ANSWER, then
|
939
|
+
# we process that as a valid response.
|
940
|
+
r, result = no_answer_response
|
941
|
+
self._process_valid_responder_result(r, parent, result)
|
942
|
+
else:
|
943
|
+
self._process_invalid_step_result(parent)
|
896
944
|
self._show_pending_message_if_debug()
|
897
945
|
return self.pending_message
|
898
946
|
|
@@ -918,7 +966,6 @@ class Task:
|
|
918
966
|
different context.
|
919
967
|
"""
|
920
968
|
self.is_done = False
|
921
|
-
self.step_progress = False
|
922
969
|
parent = self.pending_message
|
923
970
|
recipient = (
|
924
971
|
""
|
@@ -956,6 +1003,8 @@ class Task:
|
|
956
1003
|
responders.insert(0, Entity.USER)
|
957
1004
|
|
958
1005
|
found_response = False
|
1006
|
+
# (responder, result) from a responder who explicitly said NO_ANSWER
|
1007
|
+
no_answer_response: None | Tuple[Responder, ChatDocument] = None
|
959
1008
|
for r in responders:
|
960
1009
|
if not self._can_respond(r):
|
961
1010
|
# create dummy msg for logging
|
@@ -974,6 +1023,8 @@ class Task:
|
|
974
1023
|
continue
|
975
1024
|
self.human_tried = r == Entity.USER
|
976
1025
|
result = await self.response_async(r, turns)
|
1026
|
+
if result and NO_ANSWER in result.content:
|
1027
|
+
no_answer_response = (r, result)
|
977
1028
|
self.is_done = self._is_done_response(result, r)
|
978
1029
|
self.is_pass_thru = PASS in result.content if result else False
|
979
1030
|
if self.valid(result, r):
|
@@ -987,10 +1038,32 @@ class Task:
|
|
987
1038
|
# skip trying other responders in this step
|
988
1039
|
break
|
989
1040
|
if not found_response:
|
990
|
-
|
1041
|
+
if no_answer_response:
|
1042
|
+
# even though there was no valid response from anyone in this step,
|
1043
|
+
# if there was at least one who EXPLICITLY said NO_ANSWER, then
|
1044
|
+
# we process that as a valid response.
|
1045
|
+
r, result = no_answer_response
|
1046
|
+
self._process_valid_responder_result(r, parent, result)
|
1047
|
+
else:
|
1048
|
+
self._process_invalid_step_result(parent)
|
991
1049
|
self._show_pending_message_if_debug()
|
992
1050
|
return self.pending_message
|
993
1051
|
|
1052
|
+
def _update_no_answer_vars(self, result: ChatDocument) -> None:
|
1053
|
+
"""Update variables related to NO_ANSWER responses, to aid
|
1054
|
+
in alternating NO_ANSWER infinite-loop detection."""
|
1055
|
+
|
1056
|
+
if NO_ANSWER in result.content:
|
1057
|
+
if self._no_answer_step == self._step_idx - 2:
|
1058
|
+
# N/A two steps ago
|
1059
|
+
self.n_no_answer_alternations += 1
|
1060
|
+
else:
|
1061
|
+
# reset alternations counter
|
1062
|
+
self.n_no_answer_alternations = 0
|
1063
|
+
|
1064
|
+
# record the last step where the best explicit response was N/A
|
1065
|
+
self._no_answer_step = self._step_idx
|
1066
|
+
|
994
1067
|
def _process_valid_responder_result(
|
995
1068
|
self,
|
996
1069
|
r: Responder,
|
@@ -999,6 +1072,8 @@ class Task:
|
|
999
1072
|
) -> None:
|
1000
1073
|
"""Processes valid result from a responder, during a step"""
|
1001
1074
|
|
1075
|
+
self._update_no_answer_vars(result)
|
1076
|
+
|
1002
1077
|
# pending_sender is of type Responder,
|
1003
1078
|
# i.e. it is either one of the agent's entities
|
1004
1079
|
# OR a sub-task, that has produced a valid response.
|
@@ -1026,8 +1101,6 @@ class Task:
|
|
1026
1101
|
parent.metadata.child_id = result.id()
|
1027
1102
|
|
1028
1103
|
self.log_message(self.pending_sender, result, mark=True)
|
1029
|
-
self.step_progress = True
|
1030
|
-
self.task_progress = True
|
1031
1104
|
if self.is_pass_thru:
|
1032
1105
|
self.n_stalled_steps += 1
|
1033
1106
|
else:
|
@@ -1049,11 +1122,13 @@ class Task:
|
|
1049
1122
|
parent (ChatDocument|None): parent message of the current message
|
1050
1123
|
"""
|
1051
1124
|
self.n_stalled_steps += 1
|
1052
|
-
if
|
1053
|
-
#
|
1054
|
-
# update the pending_message to a dummy NO_ANSWER msg
|
1125
|
+
if self.allow_null_result and not self.is_pass_thru:
|
1126
|
+
# Null step-result is allowed, and we're not in a "pass-thru" situation,
|
1127
|
+
# so we update the pending_message to a dummy NO_ANSWER msg
|
1055
1128
|
# from the entity 'opposite' to the current pending_sender,
|
1056
|
-
# so
|
1129
|
+
# so that the task can continue.
|
1130
|
+
# CAUTION: unless the LLM is instructed to signal DONE at an appropriate
|
1131
|
+
# time, this can result in an infinite loop.
|
1057
1132
|
responder = (
|
1058
1133
|
Entity.LLM if self.pending_sender == Entity.USER else Entity.USER
|
1059
1134
|
)
|
@@ -1063,6 +1138,7 @@ class Task:
|
|
1063
1138
|
metadata=ChatDocMetaData(sender=responder, parent_id=parent_id),
|
1064
1139
|
)
|
1065
1140
|
self.pending_sender = responder
|
1141
|
+
self._update_no_answer_vars(self.pending_message)
|
1066
1142
|
self.log_message(self.pending_sender, self.pending_message, mark=True)
|
1067
1143
|
|
1068
1144
|
def _show_pending_message_if_debug(self) -> None:
|
@@ -1092,7 +1168,9 @@ class Task:
|
|
1092
1168
|
max_cost=self.max_cost,
|
1093
1169
|
max_tokens=self.max_tokens,
|
1094
1170
|
)
|
1095
|
-
result_str =
|
1171
|
+
result_str = ( # only used by callback to display content and possible tool
|
1172
|
+
"NONE" if result is None else str(ChatDocument.to_LLMMessage(result))
|
1173
|
+
)
|
1096
1174
|
maybe_tool = len(extract_top_level_json(result_str)) > 0
|
1097
1175
|
self.callbacks.show_subtask_response(
|
1098
1176
|
task=e,
|
@@ -1180,16 +1258,23 @@ class Task:
|
|
1180
1258
|
result = await response_fn(self.pending_message)
|
1181
1259
|
return self._process_result_routing(result)
|
1182
1260
|
|
1183
|
-
def result(self) -> ChatDocument:
|
1261
|
+
def result(self, status: StatusCode | None = None) -> ChatDocument | None:
|
1184
1262
|
"""
|
1185
1263
|
Get result of task. This is the default behavior.
|
1186
1264
|
Derived classes can override this.
|
1187
1265
|
|
1188
1266
|
Note the result of a task is returned as if it is from the User entity.
|
1189
1267
|
|
1268
|
+
Args:
|
1269
|
+
status (StatusCode): status of the task when it ended
|
1190
1270
|
Returns:
|
1191
1271
|
ChatDocument: result of task
|
1192
1272
|
"""
|
1273
|
+
if status in [StatusCode.STALLED, StatusCode.MAX_TURNS, StatusCode.INF_LOOP]:
|
1274
|
+
# In these case we don't know (and don't want to try to guess)
|
1275
|
+
# what the task result should be, so we return None
|
1276
|
+
return None
|
1277
|
+
|
1193
1278
|
result_msg = self.pending_message
|
1194
1279
|
|
1195
1280
|
content = result_msg.content if result_msg else ""
|
@@ -1201,7 +1286,6 @@ class Task:
|
|
1201
1286
|
block = result_msg.metadata.block if result_msg else None
|
1202
1287
|
recipient = result_msg.metadata.recipient if result_msg else ""
|
1203
1288
|
tool_ids = result_msg.metadata.tool_ids if result_msg else []
|
1204
|
-
status = result_msg.metadata.status if result_msg else None
|
1205
1289
|
|
1206
1290
|
# regardless of which entity actually produced the result,
|
1207
1291
|
# when we return the result, we set entity to USER
|
@@ -1214,7 +1298,7 @@ class Task:
|
|
1214
1298
|
source=Entity.USER,
|
1215
1299
|
sender=Entity.USER,
|
1216
1300
|
block=block,
|
1217
|
-
status=status,
|
1301
|
+
status=status or (result_msg.metadata.status if result_msg else None),
|
1218
1302
|
sender_name=self.name,
|
1219
1303
|
recipient=recipient,
|
1220
1304
|
tool_ids=tool_ids,
|
@@ -1270,13 +1354,20 @@ class Task:
|
|
1270
1354
|
def _maybe_infinite_loop(self) -> bool:
|
1271
1355
|
"""
|
1272
1356
|
Detect possible infinite loop based on message frequencies.
|
1273
|
-
NOTE: This
|
1274
|
-
|
1357
|
+
NOTE: This detects two types of loops:
|
1358
|
+
- Alternating NO_ANSWER loops, specifically of the form
|
1359
|
+
x1 NO_ANSWER x2 NO_ANSWER x3 NO_ANSWER...
|
1360
|
+
(e.g. an LLM repeatedly saying something different, and another responder
|
1361
|
+
or sub-task saying NO_ANSWER -- i.e. "DO-NOT-KNOW")
|
1362
|
+
|
1363
|
+
- "exact" loops, i.e. a cycle of messages that repeats exactly, e.g.
|
1275
1364
|
a r b i t r a t e r a t e r a t e r a t e ...
|
1276
1365
|
|
1277
|
-
[It does not detect "approximate" loops, where
|
1278
|
-
|
1366
|
+
[It does not detect more general "approximate" loops, where two entities are
|
1367
|
+
responding to each other potentially forever, with (slightly) different
|
1368
|
+
messages each time]
|
1279
1369
|
|
1370
|
+
Here is the logic for the exact-loop detection:
|
1280
1371
|
Intuition: when you look at a sufficiently long sequence with an m-message
|
1281
1372
|
loop, then the frequencies of these m messages will "dominate" those
|
1282
1373
|
of all other messages.
|
@@ -1294,6 +1385,9 @@ class Task:
|
|
1294
1385
|
If the set of last (W * m) messages are the same as the
|
1295
1386
|
set of m dominant messages, then we are likely in a loop.
|
1296
1387
|
"""
|
1388
|
+
if self.n_no_answer_alternations > self.config.inf_loop_wait_factor:
|
1389
|
+
return True
|
1390
|
+
|
1297
1391
|
max_cycle_len = self.config.inf_loop_cycle_len
|
1298
1392
|
if max_cycle_len <= 0:
|
1299
1393
|
# no loop detection
|
@@ -1362,8 +1456,8 @@ class Task:
|
|
1362
1456
|
and result.content in USER_QUIT_STRINGS
|
1363
1457
|
and result.metadata.sender == Entity.USER
|
1364
1458
|
)
|
1365
|
-
if self._level == 0 and self.only_user_quits_root:
|
1366
|
-
# for top-level task, only user can quit out
|
1459
|
+
if self._level == 0 and self.interactive and self.only_user_quits_root:
|
1460
|
+
# for top-level task, in interactive mode, only user can quit out
|
1367
1461
|
return (user_quit, StatusCode.USER_QUIT if user_quit else StatusCode.OK)
|
1368
1462
|
|
1369
1463
|
if self.is_done:
|
@@ -1405,11 +1499,6 @@ class Task:
|
|
1405
1499
|
and self.caller.name != ""
|
1406
1500
|
and result.metadata.recipient == self.caller.name
|
1407
1501
|
)
|
1408
|
-
# or (
|
1409
|
-
# # Task controller is "stuck", has nothing to say
|
1410
|
-
# NO_ANSWER in result.content
|
1411
|
-
# and result.metadata.sender == self.controller
|
1412
|
-
# )
|
1413
1502
|
or user_quit
|
1414
1503
|
)
|
1415
1504
|
return (final, StatusCode.OK)
|
@@ -1591,7 +1680,6 @@ def parse_routing(
|
|
1591
1680
|
return True, addressee, None
|
1592
1681
|
else:
|
1593
1682
|
return False, addressee, content_to_send
|
1594
|
-
AT = "@"
|
1595
1683
|
if (
|
1596
1684
|
AT in content
|
1597
1685
|
and (addressee_content := parse_addressed_message(content, AT))[0] is not None
|