langroid 0.1.250__tar.gz → 0.1.251__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.1.250 → langroid-0.1.251}/PKG-INFO +1 -1
- {langroid-0.1.250 → langroid-0.1.251}/langroid/__init__.py +6 -1
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/base.py +42 -13
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/chat_agent.py +9 -11
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/special/lance_rag/critic_agent.py +7 -1
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/special/lance_rag/query_planner_agent.py +1 -1
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/task.py +187 -76
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/tool_message.py +4 -1
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/tools/recipient_tool.py +17 -9
- langroid-0.1.251/langroid/exceptions.py +3 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/mytypes.py +12 -0
- langroid-0.1.251/langroid/parsing/routing.py +27 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/utils/configuration.py +1 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/utils/system.py +20 -0
- {langroid-0.1.250 → langroid-0.1.251}/pyproject.toml +1 -1
- {langroid-0.1.250 → langroid-0.1.251}/LICENSE +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/README.md +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/__init__.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/batch.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/callbacks/__init__.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/callbacks/chainlit.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/chat_document.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/helpers.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/junk +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/openai_assistant.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/special/__init__.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/special/doc_chat_agent.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/special/lance_doc_chat_agent.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/special/lance_rag/__init__.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/special/lance_rag/lance_rag_task.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/special/lance_tools.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/special/neo4j/__init__.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/special/neo4j/csv_kg_chat.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/special/neo4j/neo4j_chat_agent.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/special/neo4j/utils/__init__.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/special/neo4j/utils/system_message.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/special/relevance_extractor_agent.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/special/retriever_agent.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/special/sql/__init__.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/special/sql/sql_chat_agent.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/special/sql/utils/__init__.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/special/sql/utils/description_extractors.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/special/sql/utils/populate_metadata.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/special/sql/utils/system_message.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/special/sql/utils/tools.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/special/table_chat_agent.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/tools/__init__.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/tools/duckduckgo_search_tool.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/tools/extract_tool.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/tools/generator_tool.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/tools/google_search_tool.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/tools/metaphor_search_tool.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/tools/retrieval_tool.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/tools/run_python_code.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent/tools/segment_extract_tool.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/agent_config.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/cachedb/__init__.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/cachedb/base.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/cachedb/momento_cachedb.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/cachedb/redis_cachedb.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/embedding_models/__init__.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/embedding_models/base.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/embedding_models/clustering.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/embedding_models/models.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/embedding_models/protoc/__init__.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/embedding_models/protoc/embeddings.proto +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/embedding_models/protoc/embeddings_pb2.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/embedding_models/protoc/embeddings_pb2.pyi +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/embedding_models/protoc/embeddings_pb2_grpc.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/embedding_models/remote_embeds.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/language_models/__init__.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/language_models/azure_openai.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/language_models/base.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/language_models/config.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/language_models/openai_assistants.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/language_models/openai_gpt.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/language_models/prompt_formatter/__init__.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/language_models/prompt_formatter/base.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/language_models/prompt_formatter/hf_formatter.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/language_models/prompt_formatter/llama2_formatter.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/language_models/utils.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/parsing/__init__.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/parsing/agent_chats.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/parsing/code-parsing.md +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/parsing/code_parser.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/parsing/config.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/parsing/document_parser.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/parsing/image_text.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/parsing/para_sentence_split.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/parsing/parse_json.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/parsing/parser.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/parsing/repo_loader.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/parsing/search.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/parsing/spider.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/parsing/table_loader.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/parsing/url_loader.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/parsing/url_loader_cookies.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/parsing/urls.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/parsing/utils.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/parsing/web_search.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/prompts/__init__.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/prompts/chat-gpt4-system-prompt.md +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/prompts/dialog.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/prompts/prompts_config.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/prompts/templates.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/prompts/transforms.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/utils/__init__.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/utils/algorithms/__init__.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/utils/algorithms/graph.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/utils/constants.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/utils/docker.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/utils/globals.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/utils/llms/__init__.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/utils/llms/strings.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/utils/logging.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/utils/output/__init__.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/utils/output/printing.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/utils/output/status.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/utils/pandas_utils.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/utils/pydantic_utils.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/utils/web/__init__.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/utils/web/login.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/vector_store/__init__.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/vector_store/base.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/vector_store/chromadb.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/vector_store/lancedb.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/vector_store/meilisearch.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/vector_store/momento.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/vector_store/qdrant_cloud.py +0 -0
- {langroid-0.1.250 → langroid-0.1.251}/langroid/vector_store/qdrantdb.py +0 -0
@@ -41,7 +41,7 @@ from .agent.chat_agent import (
|
|
41
41
|
ChatAgentConfig,
|
42
42
|
)
|
43
43
|
|
44
|
-
from .agent.task import Task
|
44
|
+
from .agent.task import Task, TaskConfig
|
45
45
|
|
46
46
|
try:
|
47
47
|
from .agent.callbacks.chainlit import (
|
@@ -64,8 +64,11 @@ from .mytypes import (
|
|
64
64
|
Entity,
|
65
65
|
)
|
66
66
|
|
67
|
+
from .exceptions import InfiniteLoopException
|
68
|
+
|
67
69
|
__all__ = [
|
68
70
|
"mytypes",
|
71
|
+
"exceptions",
|
69
72
|
"utils",
|
70
73
|
"parsing",
|
71
74
|
"prompts",
|
@@ -82,6 +85,7 @@ __all__ = [
|
|
82
85
|
"ChatDocument",
|
83
86
|
"ChatDocMetaData",
|
84
87
|
"Task",
|
88
|
+
"TaskConfig",
|
85
89
|
"DocMetaData",
|
86
90
|
"Document",
|
87
91
|
"Entity",
|
@@ -89,6 +93,7 @@ __all__ = [
|
|
89
93
|
"run_batch_tasks",
|
90
94
|
"llm_response_batch",
|
91
95
|
"agent_response_batch",
|
96
|
+
"InfiniteLoopException",
|
92
97
|
]
|
93
98
|
if chainlit_available:
|
94
99
|
__all__.extend(
|
@@ -87,6 +87,7 @@ class Agent(ABC):
|
|
87
87
|
self.llm_tools_map: Dict[str, Type[ToolMessage]] = {}
|
88
88
|
self.llm_tools_handled: Set[str] = set()
|
89
89
|
self.llm_tools_usable: Set[str] = set()
|
90
|
+
self.interactive: bool | None = None
|
90
91
|
self.total_llm_token_cost = 0.0
|
91
92
|
self.total_llm_token_usage = 0
|
92
93
|
self.token_stats_str = ""
|
@@ -223,8 +224,8 @@ class Agent(ABC):
|
|
223
224
|
):
|
224
225
|
setattr(self, tool, lambda obj: obj.response(self))
|
225
226
|
|
226
|
-
if hasattr(message_class, "handle_message_fallback") and
|
227
|
-
message_class.handle_message_fallback
|
227
|
+
if hasattr(message_class, "handle_message_fallback") and (
|
228
|
+
inspect.isfunction(message_class.handle_message_fallback)
|
228
229
|
):
|
229
230
|
setattr(
|
230
231
|
self,
|
@@ -279,9 +280,9 @@ class Agent(ABC):
|
|
279
280
|
]
|
280
281
|
return "\n\n".join(sample_convo)
|
281
282
|
|
282
|
-
def
|
283
|
+
def create_agent_response(self, content: str | None = None) -> ChatDocument:
|
283
284
|
"""Template for agent_response."""
|
284
|
-
return self._response_template(Entity.AGENT)
|
285
|
+
return self._response_template(Entity.AGENT, content)
|
285
286
|
|
286
287
|
async def agent_response_async(
|
287
288
|
self,
|
@@ -342,19 +343,19 @@ class Agent(ABC):
|
|
342
343
|
),
|
343
344
|
)
|
344
345
|
|
345
|
-
def _response_template(self, e: Entity) -> ChatDocument:
|
346
|
+
def _response_template(self, e: Entity, content: str | None = None) -> ChatDocument:
|
346
347
|
"""Template for response from entity `e`."""
|
347
348
|
return ChatDocument(
|
348
|
-
content="",
|
349
|
+
content=content or "",
|
349
350
|
tool_messages=[],
|
350
351
|
metadata=ChatDocMetaData(
|
351
352
|
source=e, sender=e, sender_name=self.config.name, tool_ids=[]
|
352
353
|
),
|
353
354
|
)
|
354
355
|
|
355
|
-
def
|
356
|
+
def create_user_response(self, content: str | None = None) -> ChatDocument:
|
356
357
|
"""Template for user_response."""
|
357
|
-
return self._response_template(Entity.USER)
|
358
|
+
return self._response_template(Entity.USER, content)
|
358
359
|
|
359
360
|
async def user_response_async(
|
360
361
|
self,
|
@@ -377,11 +378,21 @@ class Agent(ABC):
|
|
377
378
|
(str) User response, packaged as a ChatDocument
|
378
379
|
|
379
380
|
"""
|
380
|
-
|
381
|
+
|
382
|
+
# When msg explicitly addressed to user, this means an actual human response
|
383
|
+
# is being sought.
|
384
|
+
need_human_response = (
|
385
|
+
isinstance(msg, ChatDocument) and msg.metadata.recipient == Entity.USER
|
386
|
+
)
|
387
|
+
|
388
|
+
interactive = (
|
389
|
+
self.interactive if self.interactive is not None else settings.interactive
|
390
|
+
)
|
391
|
+
if self.default_human_response is not None and not need_human_response:
|
381
392
|
# useful for automated testing
|
382
393
|
user_msg = self.default_human_response
|
383
|
-
elif not
|
384
|
-
|
394
|
+
elif not interactive and not need_human_response:
|
395
|
+
return None
|
385
396
|
else:
|
386
397
|
if self.callbacks.get_user_response is not None:
|
387
398
|
# ask user with empty prompt: no need for prompt
|
@@ -440,9 +451,9 @@ class Agent(ABC):
|
|
440
451
|
|
441
452
|
return True
|
442
453
|
|
443
|
-
def
|
454
|
+
def create_llm_response(self, content: str | None = None) -> ChatDocument:
|
444
455
|
"""Template for llm_response."""
|
445
|
-
return self._response_template(Entity.LLM)
|
456
|
+
return self._response_template(Entity.LLM, content)
|
446
457
|
|
447
458
|
@no_type_check
|
448
459
|
async def llm_response_async(
|
@@ -736,6 +747,24 @@ class Agent(ABC):
|
|
736
747
|
|
737
748
|
def _get_one_tool_message(self, json_str: str) -> Optional[ToolMessage]:
|
738
749
|
json_data = json.loads(json_str)
|
750
|
+
# check if the json_data contains a "properties" field
|
751
|
+
# which further contains the actual tool-call
|
752
|
+
# (some weak LLMs do this). E.g. gpt-4o sometimes generates this:
|
753
|
+
# TOOL: {
|
754
|
+
# "type": "object",
|
755
|
+
# "properties": {
|
756
|
+
# "request": "square",
|
757
|
+
# "number": 9
|
758
|
+
# },
|
759
|
+
# "required": [
|
760
|
+
# "number",
|
761
|
+
# "request"
|
762
|
+
# ]
|
763
|
+
# }
|
764
|
+
|
765
|
+
properties = json_data.get("properties")
|
766
|
+
if properties is not None:
|
767
|
+
json_data = properties
|
739
768
|
request = json_data.get("request")
|
740
769
|
if (
|
741
770
|
request is None
|
@@ -273,10 +273,11 @@ class ChatAgent(Agent):
|
|
273
273
|
example = "" if self.config.use_tools else (msg_cls.usage_example())
|
274
274
|
if example != "":
|
275
275
|
example = "EXAMPLE: " + example
|
276
|
+
class_instructions = msg_cls.instructions()
|
276
277
|
guidance = (
|
277
278
|
""
|
278
|
-
if
|
279
|
-
else ("GUIDANCE: " +
|
279
|
+
if class_instructions == ""
|
280
|
+
else ("GUIDANCE: " + class_instructions)
|
280
281
|
)
|
281
282
|
if guidance == "" and example == "":
|
282
283
|
continue
|
@@ -783,23 +784,20 @@ class ChatAgent(Agent):
|
|
783
784
|
if self.llm is None:
|
784
785
|
return
|
785
786
|
if not citation_only and (not self.llm.get_stream() or is_cached):
|
786
|
-
# We expect response to be LLMResponse in this context
|
787
|
-
if not isinstance(response, LLMResponse):
|
788
|
-
raise ValueError(
|
789
|
-
"Expected response to be LLMResponse, but got "
|
790
|
-
f"{type(response)} instead."
|
791
|
-
)
|
792
787
|
# We would have already displayed the msg "live" ONLY if
|
793
788
|
# streaming was enabled, AND we did not find a cached response.
|
794
789
|
# If we are here, it means the response has not yet been displayed.
|
795
790
|
cached = f"[red]{self.indent}(cached)[/red]" if is_cached else ""
|
796
791
|
if not settings.quiet:
|
792
|
+
chat_doc = (
|
793
|
+
response
|
794
|
+
if isinstance(response, ChatDocument)
|
795
|
+
else ChatDocument.from_LLMResponse(response, displayed=True)
|
796
|
+
)
|
797
797
|
print(cached + "[green]" + escape(str(response)))
|
798
798
|
self.callbacks.show_llm_response(
|
799
799
|
content=str(response),
|
800
|
-
is_tool=self.has_tool_message_attempt(
|
801
|
-
ChatDocument.from_LLMResponse(response, displayed=True),
|
802
|
-
),
|
800
|
+
is_tool=self.has_tool_message_attempt(chat_doc),
|
803
801
|
cached=is_cached,
|
804
802
|
)
|
805
803
|
if isinstance(response, LLMResponse):
|
@@ -70,13 +70,19 @@ class QueryPlanCriticConfig(LanceQueryPlanAgentConfig):
|
|
70
70
|
plan execution FAILED, and your feedback should say INVALID along
|
71
71
|
with the ERROR message, `suggested_fix` that aims to help the assistant
|
72
72
|
fix the problem (or simply equals "address the the error shown in feedback")
|
73
|
+
- Ask yourself, is the ANSWER in the expected form, e.g.
|
74
|
+
if the question is asking for the name of an ENTITY with max SIZE,
|
75
|
+
then the answer should be the ENTITY name, NOT the SIZE!!
|
73
76
|
- If the ANSWER is in the expected form, then the QUERY PLAN is likely VALID,
|
74
77
|
and your feedback should say VALID, with empty `suggested_fix`.
|
78
|
+
===> HOWEVER!!! Watch out for a spurious correct-looking answer, for EXAMPLE:
|
79
|
+
the query was to find the ENTITY with a maximum SIZE,
|
80
|
+
but the dataframe calculation is find the SIZE, NOT the ENTITY!!
|
75
81
|
- If the ANSWER is {NO_ANSWER} or of the wrong form,
|
76
82
|
then try to DIAGNOSE the problem IN THE FOLLOWING ORDER:
|
77
83
|
- DATAFRAME CALCULATION -- is it doing the right thing?
|
78
84
|
Is it finding the Index of a row instead of the value in a column?
|
79
|
-
Or another example:
|
85
|
+
Or another example: maybe it is finding the maximum population
|
80
86
|
rather than the CITY with the maximum population?
|
81
87
|
If you notice a problem with the DATAFRAME CALCULATION, then
|
82
88
|
ONLY SUBMIT FEEDBACK ON THE DATAFRAME CALCULATION, and DO NOT
|
{langroid-0.1.250 → langroid-0.1.251}/langroid/agent/special/lance_rag/query_planner_agent.py
RENAMED
@@ -195,7 +195,7 @@ class LanceQueryPlanAgent(ChatAgent):
|
|
195
195
|
plan=self.curr_query_plan,
|
196
196
|
answer=self.result,
|
197
197
|
)
|
198
|
-
response_tmpl = self.
|
198
|
+
response_tmpl = self.create_agent_response()
|
199
199
|
# ... add the QueryPlanAnswerTool to the response
|
200
200
|
# (Notice how the Agent is directly sending a tool, not the LLM)
|
201
201
|
response_tmpl.tool_messages = [query_plan_answer_tool]
|
@@ -2,13 +2,13 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import copy
|
4
4
|
import logging
|
5
|
-
import
|
6
|
-
from collections import Counter
|
5
|
+
from collections import Counter, deque
|
7
6
|
from types import SimpleNamespace
|
8
7
|
from typing import (
|
9
8
|
Any,
|
10
9
|
Callable,
|
11
10
|
Coroutine,
|
11
|
+
Deque,
|
12
12
|
Dict,
|
13
13
|
List,
|
14
14
|
Optional,
|
@@ -18,6 +18,8 @@ from typing import (
|
|
18
18
|
cast,
|
19
19
|
)
|
20
20
|
|
21
|
+
import numpy as np
|
22
|
+
from pydantic import BaseModel
|
21
23
|
from rich import print
|
22
24
|
from rich.markup import escape
|
23
25
|
|
@@ -30,8 +32,10 @@ from langroid.agent.chat_document import (
|
|
30
32
|
StatusCode,
|
31
33
|
)
|
32
34
|
from langroid.cachedb.redis_cachedb import RedisCache, RedisCacheConfig
|
35
|
+
from langroid.exceptions import InfiniteLoopException
|
33
36
|
from langroid.mytypes import Entity
|
34
37
|
from langroid.parsing.parse_json import extract_top_level_json
|
38
|
+
from langroid.parsing.routing import parse_addressed_message
|
35
39
|
from langroid.utils.configuration import settings
|
36
40
|
from langroid.utils.constants import (
|
37
41
|
DONE,
|
@@ -42,6 +46,7 @@ from langroid.utils.constants import (
|
|
42
46
|
USER_QUIT_STRINGS,
|
43
47
|
)
|
44
48
|
from langroid.utils.logging import RichFileLogger, setup_file_logger
|
49
|
+
from langroid.utils.system import hash
|
45
50
|
|
46
51
|
logger = logging.getLogger(__name__)
|
47
52
|
|
@@ -52,6 +57,18 @@ def noop_fn(*args: List[Any], **kwargs: Dict[str, Any]) -> None:
|
|
52
57
|
pass
|
53
58
|
|
54
59
|
|
60
|
+
class TaskConfig(BaseModel):
|
61
|
+
"""Configuration for a Task. This is a container for any params that
|
62
|
+
we didn't include in the task __init__ method.
|
63
|
+
We may eventually move all the task __init__ params to this class, analogous to how
|
64
|
+
we have config classes for Agent, ChatAgent, LanguageModel, etc."""
|
65
|
+
|
66
|
+
inf_loop_cycle_len: int = 10 # max exact-loop cycle length: 0 => no inf loop test
|
67
|
+
inf_loop_dominance_factor: float = 1.5 # dominance factor for exact-loop detection
|
68
|
+
# wait this * cycle_len msgs before checking for loop
|
69
|
+
inf_loop_wait_factor: float = 5.0
|
70
|
+
|
71
|
+
|
55
72
|
class Task:
|
56
73
|
"""
|
57
74
|
A `Task` wraps an `Agent` object, and sets up the `Agent`'s goals and instructions.
|
@@ -102,6 +119,7 @@ class Task:
|
|
102
119
|
max_stalled_steps: int = 5,
|
103
120
|
done_if_no_response: List[Responder] = [],
|
104
121
|
done_if_response: List[Responder] = [],
|
122
|
+
config: TaskConfig = TaskConfig(),
|
105
123
|
):
|
106
124
|
"""
|
107
125
|
A task to be performed by an agent.
|
@@ -157,6 +175,11 @@ class Task:
|
|
157
175
|
show_subtask_response=noop_fn,
|
158
176
|
set_parent_agent=noop_fn,
|
159
177
|
)
|
178
|
+
self.config = config
|
179
|
+
# counts of distinct pending messages in history,
|
180
|
+
# to help detect (exact) infinite loops
|
181
|
+
self.message_counter: Counter[str] = Counter()
|
182
|
+
self.history_count: Deque[int] = deque(maxlen=self.config.inf_loop_cycle_len)
|
160
183
|
# copy the agent's config, so that we don't modify the original agent's config,
|
161
184
|
# which may be shared by other agents.
|
162
185
|
try:
|
@@ -201,10 +224,11 @@ class Task:
|
|
201
224
|
agent.config.name = name
|
202
225
|
self.name = name or agent.config.name
|
203
226
|
self.value: str = self.name
|
204
|
-
|
227
|
+
|
205
228
|
if default_human_response is not None and default_human_response == "":
|
206
229
|
interactive = False
|
207
230
|
self.interactive = interactive
|
231
|
+
self.agent.interactive = interactive
|
208
232
|
self.message_history_idx = -1
|
209
233
|
if interactive:
|
210
234
|
only_user_quits_root = True
|
@@ -213,6 +237,7 @@ class Task:
|
|
213
237
|
only_user_quits_root = False
|
214
238
|
if default_human_response is not None:
|
215
239
|
self.agent.default_human_response = default_human_response
|
240
|
+
self.default_human_response = default_human_response
|
216
241
|
if self.interactive:
|
217
242
|
self.agent.default_human_response = None
|
218
243
|
self.only_user_quits_root = only_user_quits_root
|
@@ -289,6 +314,7 @@ class Task:
|
|
289
314
|
max_stalled_steps=self.max_stalled_steps,
|
290
315
|
done_if_no_response=[Entity(s) for s in self.done_if_no_response],
|
291
316
|
done_if_response=[Entity(s) for s in self.done_if_response],
|
317
|
+
config=self.config,
|
292
318
|
)
|
293
319
|
|
294
320
|
def __repr__(self) -> str:
|
@@ -448,6 +474,8 @@ class Task:
|
|
448
474
|
self.max_tokens = max_tokens
|
449
475
|
self.session_id = session_id
|
450
476
|
self._set_alive()
|
477
|
+
self.message_counter.clear()
|
478
|
+
self.history_count.clear()
|
451
479
|
|
452
480
|
assert (
|
453
481
|
msg is None or isinstance(msg, str) or isinstance(msg, ChatDocument)
|
@@ -476,9 +504,20 @@ class Task:
|
|
476
504
|
print("[magenta]Bye, hope this was useful!")
|
477
505
|
break
|
478
506
|
i += 1
|
479
|
-
|
507
|
+
max_turns = (
|
508
|
+
min(turns, settings.max_turns)
|
509
|
+
if turns > 0 and settings.max_turns > 0
|
510
|
+
else max(turns, settings.max_turns)
|
511
|
+
)
|
512
|
+
if max_turns > 0 and i >= max_turns:
|
480
513
|
status = StatusCode.MAX_TURNS
|
481
514
|
break
|
515
|
+
if (
|
516
|
+
self.config.inf_loop_cycle_len > 0
|
517
|
+
and i % self.config.inf_loop_cycle_len == 0
|
518
|
+
and self._maybe_infinite_loop()
|
519
|
+
):
|
520
|
+
raise InfiniteLoopException("Possible infinite loop detected!")
|
482
521
|
|
483
522
|
final_result = self.result()
|
484
523
|
if final_result is not None:
|
@@ -528,6 +567,8 @@ class Task:
|
|
528
567
|
self.max_tokens = max_tokens
|
529
568
|
self.session_id = session_id
|
530
569
|
self._set_alive()
|
570
|
+
self.message_counter.clear()
|
571
|
+
self.history_count.clear()
|
531
572
|
|
532
573
|
if (
|
533
574
|
isinstance(msg, ChatDocument)
|
@@ -552,9 +593,20 @@ class Task:
|
|
552
593
|
print("[magenta]Bye, hope this was useful!")
|
553
594
|
break
|
554
595
|
i += 1
|
555
|
-
|
596
|
+
max_turns = (
|
597
|
+
min(turns, settings.max_turns)
|
598
|
+
if turns > 0 and settings.max_turns > 0
|
599
|
+
else max(turns, settings.max_turns)
|
600
|
+
)
|
601
|
+
if max_turns > 0 and i >= max_turns:
|
556
602
|
status = StatusCode.MAX_TURNS
|
557
603
|
break
|
604
|
+
if (
|
605
|
+
self.config.inf_loop_cycle_len > 0
|
606
|
+
and i % self.config.inf_loop_cycle_len == 0
|
607
|
+
and self._maybe_infinite_loop()
|
608
|
+
):
|
609
|
+
raise InfiniteLoopException("Possible infinite loop detected!")
|
558
610
|
|
559
611
|
final_result = self.result()
|
560
612
|
if final_result is not None:
|
@@ -824,6 +876,12 @@ class Task:
|
|
824
876
|
# reset stuck counter since we made progress
|
825
877
|
self.n_stalled_steps = 0
|
826
878
|
|
879
|
+
# update counters for infinite loop detection
|
880
|
+
if self.pending_message is not None:
|
881
|
+
hashed_msg = hash(str(self.pending_message))
|
882
|
+
self.message_counter.update([hashed_msg])
|
883
|
+
self.history_count.append(self.message_counter[hashed_msg])
|
884
|
+
|
827
885
|
def _process_invalid_step_result(self, parent: ChatDocument | None) -> None:
|
828
886
|
"""
|
829
887
|
Since step had no valid result from any responder, decide whether to update the
|
@@ -856,42 +914,6 @@ class Task:
|
|
856
914
|
msg_str = escape(str(self.pending_message))
|
857
915
|
print(f"[grey37][{sender_str}]{msg_str}[/grey37]")
|
858
916
|
|
859
|
-
def _parse_routing(self, msg: ChatDocument | str) -> Tuple[bool | None, str | None]:
|
860
|
-
"""
|
861
|
-
Parse routing instruction if any, of the form:
|
862
|
-
PASS:<recipient> (pass current pending msg to recipient)
|
863
|
-
SEND:<recipient> <content> (send content to recipient)
|
864
|
-
Args:
|
865
|
-
msg (ChatDocument|str|None): message to parse
|
866
|
-
Returns:
|
867
|
-
Tuple[bool,str|None]:
|
868
|
-
bool: true=PASS, false=SEND, or None if neither
|
869
|
-
str: recipient, or None
|
870
|
-
"""
|
871
|
-
# handle routing instruction in result if any,
|
872
|
-
# of the form PASS=<recipient>
|
873
|
-
content = msg.content if isinstance(msg, ChatDocument) else msg
|
874
|
-
content = content.strip()
|
875
|
-
if PASS in content and PASS_TO not in content:
|
876
|
-
return True, None
|
877
|
-
if PASS_TO in content and content.split(":")[1] != "":
|
878
|
-
return True, content.split(":")[1]
|
879
|
-
if SEND_TO in content and (send_parts := re.split(r"[,: ]", content))[1] != "":
|
880
|
-
# assume syntax is SEND_TO:<recipient> <content>
|
881
|
-
# or SEND_TO:<recipient>,<content> or SEND_TO:<recipient>:<content>
|
882
|
-
recipient = send_parts[1].strip()
|
883
|
-
# get content to send, clean out routing instruction, and
|
884
|
-
# start from 1 char after SEND_TO:<recipient>,
|
885
|
-
# because we expect there is either a blank or some other separator
|
886
|
-
# after the recipient
|
887
|
-
content_to_send = content.replace(f"{SEND_TO}{recipient}", "").strip()[1:]
|
888
|
-
# if no content then treat same as PASS_TO
|
889
|
-
if content_to_send == "":
|
890
|
-
return True, recipient
|
891
|
-
else:
|
892
|
-
return False, recipient
|
893
|
-
return None, None
|
894
|
-
|
895
917
|
def response(
|
896
918
|
self,
|
897
919
|
e: Responder,
|
@@ -929,7 +951,8 @@ class Task:
|
|
929
951
|
# process result in case there is a routing instruction
|
930
952
|
if result is None:
|
931
953
|
return None
|
932
|
-
|
954
|
+
# if result content starts with @name, set recipient to name
|
955
|
+
is_pass, recipient, content = parse_routing(result)
|
933
956
|
if is_pass is None: # no routing, i.e. neither PASS nor SEND
|
934
957
|
return result
|
935
958
|
if is_pass:
|
@@ -949,9 +972,7 @@ class Task:
|
|
949
972
|
elif recipient is not None:
|
950
973
|
# we are sending non-empty content to non-null recipient
|
951
974
|
# clean up result.content, set metadata.recipient and return
|
952
|
-
result.content =
|
953
|
-
f"{SEND_TO}:{recipient}", ""
|
954
|
-
).strip()
|
975
|
+
result.content = content or ""
|
955
976
|
result.metadata.recipient = recipient
|
956
977
|
return result
|
957
978
|
else:
|
@@ -1080,38 +1101,76 @@ class Task:
|
|
1080
1101
|
or (not self._is_empty_message(result) and response_says_done)
|
1081
1102
|
)
|
1082
1103
|
|
1083
|
-
def _maybe_infinite_loop(self
|
1104
|
+
def _maybe_infinite_loop(self) -> bool:
|
1084
1105
|
"""
|
1085
|
-
|
1086
|
-
|
1087
|
-
|
1088
|
-
|
1089
|
-
|
1090
|
-
|
1091
|
-
|
1092
|
-
|
1106
|
+
Detect possible infinite loop based on message frequencies.
|
1107
|
+
NOTE: This only (attempts to) detect "exact" loops, i.e. a cycle
|
1108
|
+
of messages that repeats exactly, e.g.
|
1109
|
+
a r b i t r a t e r a t e r a t e r a t e ...
|
1110
|
+
|
1111
|
+
[It does not detect "approximate" loops, where the LLM is generating a
|
1112
|
+
sequence of messages that are similar, but not exactly the same.]
|
1113
|
+
|
1114
|
+
Intuition: when you look at a sufficiently long sequence with an m-message
|
1115
|
+
loop, then the frequencies of these m messages will "dominate" those
|
1116
|
+
of all other messages.
|
1117
|
+
|
1118
|
+
1. First find m "dominant" messages, i.e. when arranged in decreasing
|
1119
|
+
frequency order, find the m such that
|
1120
|
+
freq[m] > F * freq[m+1] and
|
1121
|
+
freq[m] > W + freq[m+1]
|
1122
|
+
where F = config.inf_loop_dominance_factor (default 1.5) and
|
1123
|
+
W = config.inf_loop_wait_factor (default 5).
|
1124
|
+
So if you plot these frequencies in decreasing order,
|
1125
|
+
you will see a big in the plot, from m to m+1.
|
1126
|
+
We call the freqs until m the "dominant" freqs.
|
1127
|
+
2. Say we found m such dominant frequencies.
|
1128
|
+
If these are the same as the freqs of the last m messages,
|
1129
|
+
then we are likely in a loop.
|
1093
1130
|
"""
|
1094
|
-
|
1095
|
-
|
1096
|
-
|
1097
|
-
|
1098
|
-
|
1099
|
-
|
1100
|
-
|
1101
|
-
|
1102
|
-
|
1103
|
-
|
1104
|
-
|
1105
|
-
|
1106
|
-
|
1107
|
-
|
1108
|
-
|
1109
|
-
#
|
1110
|
-
|
1111
|
-
#
|
1112
|
-
|
1113
|
-
|
1114
|
-
|
1131
|
+
max_cycle_len = self.config.inf_loop_cycle_len
|
1132
|
+
if max_cycle_len <= 0:
|
1133
|
+
# no loop detection
|
1134
|
+
return False
|
1135
|
+
wait_factor = self.config.inf_loop_wait_factor
|
1136
|
+
if sum(self.message_counter.values()) < wait_factor * max_cycle_len:
|
1137
|
+
# we haven't seen enough messages to detect a loop
|
1138
|
+
return False
|
1139
|
+
|
1140
|
+
most_common_msg_counts: List[Tuple[str, int]] = (
|
1141
|
+
self.message_counter.most_common(max_cycle_len + 1)
|
1142
|
+
)
|
1143
|
+
# get the most dominant msgs, i.e. these are at least 1.5x more freq
|
1144
|
+
# than the rest
|
1145
|
+
F = self.config.inf_loop_dominance_factor
|
1146
|
+
# counts array in non-increasing order
|
1147
|
+
counts = np.array([c for _, c in most_common_msg_counts])
|
1148
|
+
# find first index where counts[i] > F * counts[i+1]
|
1149
|
+
ratios = counts[:-1] / counts[1:]
|
1150
|
+
diffs = counts[:-1] - counts[1:]
|
1151
|
+
indices = np.where((ratios > F) & (diffs > wait_factor))[0]
|
1152
|
+
m = indices[0] if indices.size > 0 else -1
|
1153
|
+
if m < 0:
|
1154
|
+
# no dominance found, but...
|
1155
|
+
if len(most_common_msg_counts) <= max_cycle_len:
|
1156
|
+
# ...The most-common messages are at most max_cycle_len,
|
1157
|
+
# even though we looked for the most common (max_cycle_len + 1) msgs.
|
1158
|
+
# This means there are only at most max_cycle_len distinct messages,
|
1159
|
+
# which also indicates a possible loop.
|
1160
|
+
m = len(most_common_msg_counts) - 1
|
1161
|
+
else:
|
1162
|
+
# ... we have enough messages, but no dominance found,
|
1163
|
+
# so there COULD be loops longer than max_cycle_len,
|
1164
|
+
# OR there is no loop at all; we can't tell, so we return False.
|
1165
|
+
return False
|
1166
|
+
|
1167
|
+
dominant_msg_counts = most_common_msg_counts[: m + 1]
|
1168
|
+
# if the dominant m message counts are the same as the
|
1169
|
+
# counts of the last m messages, then we are likely in a loop
|
1170
|
+
dominant_counts = sorted([c for _, c in dominant_msg_counts])
|
1171
|
+
recent_counts = sorted(list(self.history_count)[-(m + 1) :])
|
1172
|
+
|
1173
|
+
return dominant_counts == recent_counts
|
1115
1174
|
|
1116
1175
|
def done(
|
1117
1176
|
self, result: ChatDocument | None = None, r: Responder | None = None
|
@@ -1289,7 +1348,9 @@ class Task:
|
|
1289
1348
|
return (
|
1290
1349
|
self.pending_message is not None
|
1291
1350
|
and (recipient := self.pending_message.metadata.recipient) != ""
|
1292
|
-
and recipient
|
1351
|
+
and recipient != e # case insensitive
|
1352
|
+
and recipient != e.name
|
1353
|
+
and recipient != self.name # case sensitive
|
1293
1354
|
)
|
1294
1355
|
|
1295
1356
|
def _can_respond(self, e: Responder) -> bool:
|
@@ -1316,3 +1377,53 @@ class Task:
|
|
1316
1377
|
|
1317
1378
|
"""
|
1318
1379
|
self.color_log = enable
|
1380
|
+
|
1381
|
+
|
1382
|
+
def parse_routing(
|
1383
|
+
msg: ChatDocument | str,
|
1384
|
+
) -> Tuple[bool | None, str | None, str | None]:
|
1385
|
+
"""
|
1386
|
+
Parse routing instruction if any, of the form:
|
1387
|
+
PASS:<recipient> (pass current pending msg to recipient)
|
1388
|
+
SEND:<recipient> <content> (send content to recipient)
|
1389
|
+
@<recipient> <content> (send content to recipient)
|
1390
|
+
Args:
|
1391
|
+
msg (ChatDocument|str|None): message to parse
|
1392
|
+
Returns:
|
1393
|
+
Tuple[bool|None, str|None, str|None]:
|
1394
|
+
bool: true=PASS, false=SEND, or None if neither
|
1395
|
+
str: recipient, or None
|
1396
|
+
str: content to send, or None
|
1397
|
+
"""
|
1398
|
+
# handle routing instruction in result if any,
|
1399
|
+
# of the form PASS=<recipient>
|
1400
|
+
content = msg.content if isinstance(msg, ChatDocument) else msg
|
1401
|
+
content = content.strip()
|
1402
|
+
if PASS in content and PASS_TO not in content:
|
1403
|
+
return True, None, None
|
1404
|
+
if PASS_TO in content and content.split(":")[1] != "":
|
1405
|
+
return True, content.split(":")[1], None
|
1406
|
+
if (
|
1407
|
+
SEND_TO in content
|
1408
|
+
and (addressee_content := parse_addressed_message(content, SEND_TO))[0]
|
1409
|
+
is not None
|
1410
|
+
):
|
1411
|
+
(addressee, content_to_send) = addressee_content
|
1412
|
+
# if no content then treat same as PASS_TO
|
1413
|
+
if content_to_send == "":
|
1414
|
+
return True, addressee, None
|
1415
|
+
else:
|
1416
|
+
return False, addressee, content_to_send
|
1417
|
+
AT = "@"
|
1418
|
+
if (
|
1419
|
+
AT in content
|
1420
|
+
and (addressee_content := parse_addressed_message(content, AT))[0] is not None
|
1421
|
+
):
|
1422
|
+
(addressee, content_to_send) = addressee_content
|
1423
|
+
# if no content then treat same as PASS_TO
|
1424
|
+
if content_to_send == "":
|
1425
|
+
return True, addressee, None
|
1426
|
+
else:
|
1427
|
+
return False, addressee, content_to_send
|
1428
|
+
|
1429
|
+
return None, None, None
|
@@ -52,7 +52,10 @@ class ToolMessage(ABC, BaseModel):
|
|
52
52
|
|
53
53
|
@classmethod
|
54
54
|
def instructions(cls) -> str:
|
55
|
-
return ""
|
55
|
+
return """
|
56
|
+
IMPORTANT: When using this or any other tool/function, you MUST include a
|
57
|
+
`request` field and set it equal to the FUNCTION/TOOL NAME you intend to use.
|
58
|
+
"""
|
56
59
|
|
57
60
|
@classmethod
|
58
61
|
def require_recipient(cls) -> Type["ToolMessage"]:
|