langroid 0.8.0__py3-none-any.whl → 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- langroid/agent/base.py +353 -94
- langroid/agent/chat_agent.py +68 -9
- langroid/agent/chat_document.py +16 -7
- langroid/agent/openai_assistant.py +12 -1
- langroid/agent/special/lance_doc_chat_agent.py +25 -18
- langroid/agent/special/lance_rag/critic_agent.py +37 -5
- langroid/agent/special/lance_rag/query_planner_agent.py +102 -63
- langroid/agent/special/lance_tools.py +10 -2
- langroid/agent/task.py +156 -47
- langroid/agent/tool_message.py +12 -3
- langroid/agent/tools/__init__.py +5 -0
- langroid/agent/tools/orchestration.py +216 -0
- langroid/agent/tools/recipient_tool.py +6 -11
- langroid/agent/typed_task.py +19 -0
- langroid/language_models/base.py +3 -2
- langroid/mytypes.py +0 -1
- langroid/parsing/parse_json.py +19 -2
- langroid/utils/pydantic_utils.py +19 -0
- langroid/vector_store/base.py +3 -1
- langroid/vector_store/lancedb.py +2 -0
- {langroid-0.8.0.dist-info → langroid-0.9.0.dist-info}/METADATA +2 -1
- {langroid-0.8.0.dist-info → langroid-0.9.0.dist-info}/RECORD +25 -28
- pyproject.toml +2 -1
- langroid/agent/special/lance_rag_new/__init__.py +0 -9
- langroid/agent/special/lance_rag_new/critic_agent.py +0 -171
- langroid/agent/special/lance_rag_new/lance_rag_task.py +0 -144
- langroid/agent/special/lance_rag_new/query_planner_agent.py +0 -222
- langroid/agent/team.py +0 -1758
- {langroid-0.8.0.dist-info → langroid-0.9.0.dist-info}/LICENSE +0 -0
- {langroid-0.8.0.dist-info → langroid-0.9.0.dist-info}/WHEEL +0 -0
langroid/agent/chat_agent.py
CHANGED
@@ -47,6 +47,8 @@ class ChatAgentConfig(AgentConfig):
|
|
47
47
|
the OpenAI tool-call API is used, rather than the older/deprecated
|
48
48
|
function-call API. However the tool-call API has some tricky aspects,
|
49
49
|
hence we set this to False by default.
|
50
|
+
enable_orchestration_tool_handling: whether to enable handling of orchestration
|
51
|
+
tools, e.g. ForwardTool, DoneTool, PassTool, etc.
|
50
52
|
"""
|
51
53
|
|
52
54
|
system_message: str = "You are a helpful assistant."
|
@@ -54,6 +56,7 @@ class ChatAgentConfig(AgentConfig):
|
|
54
56
|
use_tools: bool = False
|
55
57
|
use_functions_api: bool = True
|
56
58
|
use_tools_api: bool = False
|
59
|
+
enable_orchestration_tool_handling: bool = True
|
57
60
|
|
58
61
|
def _set_fn_or_tools(self, fn_available: bool) -> None:
|
59
62
|
"""
|
@@ -145,6 +148,30 @@ class ChatAgent(Agent):
|
|
145
148
|
self.llm_functions_usable: Set[str] = set()
|
146
149
|
self.llm_function_force: Optional[Dict[str, str]] = None
|
147
150
|
|
151
|
+
if self.config.enable_orchestration_tool_handling:
|
152
|
+
# Only enable HANDLING by `agent_response`, NOT LLM generation of these.
|
153
|
+
# This is useful where tool-handlers or agent_response generate these
|
154
|
+
# tools, and need to be handled.
|
155
|
+
# We don't want enable orch tool GENERATION by default, since that
|
156
|
+
# might clutter-up the LLM system message unnecessarily.
|
157
|
+
from langroid.agent.tools.orchestration import (
|
158
|
+
AgentDoneTool,
|
159
|
+
AgentSendTool,
|
160
|
+
DonePassTool,
|
161
|
+
DoneTool,
|
162
|
+
ForwardTool,
|
163
|
+
PassTool,
|
164
|
+
SendTool,
|
165
|
+
)
|
166
|
+
|
167
|
+
self.enable_message(ForwardTool, use=False, handle=True)
|
168
|
+
self.enable_message(DoneTool, use=False, handle=True)
|
169
|
+
self.enable_message(AgentDoneTool, use=False, handle=True)
|
170
|
+
self.enable_message(PassTool, use=False, handle=True)
|
171
|
+
self.enable_message(DonePassTool, use=False, handle=True)
|
172
|
+
self.enable_message(SendTool, use=False, handle=True)
|
173
|
+
self.enable_message(AgentSendTool, use=False, handle=True)
|
174
|
+
|
148
175
|
@staticmethod
|
149
176
|
def from_id(id: str) -> "ChatAgent":
|
150
177
|
"""
|
@@ -272,19 +299,25 @@ class ChatAgent(Agent):
|
|
272
299
|
Returns:
|
273
300
|
str: formatting rules
|
274
301
|
"""
|
275
|
-
|
276
|
-
|
302
|
+
# ONLY Usable tools (i.e. LLM-generation allowed),
|
303
|
+
usable_tool_classes: List[Type[ToolMessage]] = [
|
304
|
+
t
|
305
|
+
for t in list(self.llm_tools_map.values())
|
306
|
+
if not t._handle_only
|
307
|
+
and t.default_value("request") in self.llm_tools_usable
|
308
|
+
]
|
309
|
+
|
310
|
+
if len(usable_tool_classes) == 0:
|
277
311
|
return "You can ask questions in natural language."
|
278
312
|
json_instructions = "\n\n".join(
|
279
313
|
[
|
280
314
|
msg_cls.json_instructions(tool=self.config.use_tools)
|
281
|
-
for
|
282
|
-
if msg_cls.default_value("request") in self.llm_tools_usable
|
315
|
+
for msg_cls in usable_tool_classes
|
283
316
|
]
|
284
317
|
)
|
285
318
|
# if any of the enabled classes has json_group_instructions, then use that,
|
286
319
|
# else fall back to ToolMessage.json_group_instructions
|
287
|
-
for msg_cls in
|
320
|
+
for msg_cls in usable_tool_classes:
|
288
321
|
if hasattr(msg_cls, "json_group_instructions") and callable(
|
289
322
|
getattr(msg_cls, "json_group_instructions")
|
290
323
|
):
|
@@ -420,9 +453,16 @@ class ChatAgent(Agent):
|
|
420
453
|
# remove leading and trailing newlines and other whitespace
|
421
454
|
return LLMMessage(role=Role.SYSTEM, content=content.strip())
|
422
455
|
|
456
|
+
def unhanded_tools(self) -> set[str]:
|
457
|
+
"""The set of tools that are known but not handled.
|
458
|
+
Useful in task flow: an agent can refuse to accept an incoming msg
|
459
|
+
when it only has unhandled tools.
|
460
|
+
"""
|
461
|
+
return self.llm_tools_known - self.llm_tools_handled
|
462
|
+
|
423
463
|
def enable_message(
|
424
464
|
self,
|
425
|
-
message_class: Optional[Type[ToolMessage]],
|
465
|
+
message_class: Optional[Type[ToolMessage] | List[Type[ToolMessage]]],
|
426
466
|
use: bool = True,
|
427
467
|
handle: bool = True,
|
428
468
|
force: bool = False,
|
@@ -435,8 +475,10 @@ class ChatAgent(Agent):
|
|
435
475
|
- tool HANDLING (i.e. the agent can handle JSON from this tool),
|
436
476
|
|
437
477
|
Args:
|
438
|
-
message_class: The ToolMessage class to enable,
|
478
|
+
message_class: The ToolMessage class OR List of such classes to enable,
|
439
479
|
for USE, or HANDLING, or both.
|
480
|
+
If this is a list of ToolMessage classes, then the remain args are
|
481
|
+
applied to all classes.
|
440
482
|
Optional; if None, then apply the enabling to all tools in the
|
441
483
|
agent's toolset that have been enabled so far.
|
442
484
|
use: IF True, allow the agent (LLM) to use this tool (or all tools),
|
@@ -448,12 +490,23 @@ class ChatAgent(Agent):
|
|
448
490
|
`force` is ignored if `message_class` is None.
|
449
491
|
require_recipient: whether to require that recipient be specified
|
450
492
|
when using the tool message (only applies if `use` is True).
|
451
|
-
|
493
|
+
include_defaults: whether to include fields that have default values,
|
452
494
|
in the "properties" section of the JSON format instructions.
|
453
495
|
(Normally the OpenAI completion API ignores these fields,
|
454
496
|
but the Assistant fn-calling seems to pay attn to these,
|
455
497
|
and if we don't want this, we should set this to False.)
|
456
498
|
"""
|
499
|
+
if message_class is not None and isinstance(message_class, list):
|
500
|
+
for mc in message_class:
|
501
|
+
self.enable_message(
|
502
|
+
mc,
|
503
|
+
use=use,
|
504
|
+
handle=handle,
|
505
|
+
force=force,
|
506
|
+
require_recipient=require_recipient,
|
507
|
+
include_defaults=include_defaults,
|
508
|
+
)
|
509
|
+
return None
|
457
510
|
if require_recipient and message_class is not None:
|
458
511
|
message_class = message_class.require_recipient()
|
459
512
|
super().enable_message_handling(message_class) # enables handling only
|
@@ -468,6 +521,8 @@ class ChatAgent(Agent):
|
|
468
521
|
self.llm_function_force = None
|
469
522
|
|
470
523
|
for t in tools:
|
524
|
+
self.llm_tools_known.add(t)
|
525
|
+
|
471
526
|
if handle:
|
472
527
|
self.llm_tools_handled.add(t)
|
473
528
|
self.llm_functions_handled.add(t)
|
@@ -662,7 +717,11 @@ class ChatAgent(Agent):
|
|
662
717
|
# either the message is a str, or it is a fresh ChatDocument
|
663
718
|
# different from the last message in the history
|
664
719
|
llm_msgs = ChatDocument.to_LLMMessage(message, self.oai_tool_calls)
|
665
|
-
|
720
|
+
# LLM only responds to the content, so only those msgs with
|
721
|
+
# non-empty content should be kept
|
722
|
+
llm_msgs = [m for m in llm_msgs if m.content != ""]
|
723
|
+
if len(llm_msgs) == 0:
|
724
|
+
return [], 0
|
666
725
|
# process tools if any
|
667
726
|
done_tools = [m.tool_call_id for m in llm_msgs if m.role == Role.TOOL]
|
668
727
|
self.oai_tool_calls = [
|
langroid/agent/chat_document.py
CHANGED
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import copy
|
4
4
|
import json
|
5
|
+
from collections import OrderedDict
|
5
6
|
from enum import Enum
|
6
7
|
from typing import Any, Dict, List, Optional, Union, cast
|
7
8
|
|
@@ -96,14 +97,16 @@ class ChatDocument(Document):
|
|
96
97
|
and so does the Task.run() method.
|
97
98
|
|
98
99
|
Attributes:
|
99
|
-
oai_tool_calls (List[OpenAIToolCall]):
|
100
|
-
|
101
|
-
|
100
|
+
oai_tool_calls (Optional[List[OpenAIToolCall]]):
|
101
|
+
Tool-calls from an OpenAI-compatible API
|
102
|
+
oai_tool_id2results (Optional[OrderedDict[str, str]]):
|
103
|
+
Results of tool-calls from OpenAI (dict is a map of tool_id -> result)
|
102
104
|
oai_tool_choice: ToolChoiceTypes | Dict[str, str]: Param controlling how the
|
103
105
|
LLM should choose tool-use in its response
|
104
106
|
(auto, none, required, or a specific tool)
|
105
|
-
function_call (LLMFunctionCall):
|
106
|
-
|
107
|
+
function_call (Optional[LLMFunctionCall]):
|
108
|
+
Function-call from an OpenAI-compatible API
|
109
|
+
(deprecated by OpenAI, in favor of tool-calls)
|
107
110
|
tool_messages (List[ToolMessage]): Langroid ToolMessages extracted from
|
108
111
|
- `content` field (via JSON parsing),
|
109
112
|
- `oai_tool_calls`, or
|
@@ -113,10 +116,14 @@ class ChatDocument(Document):
|
|
113
116
|
"""
|
114
117
|
|
115
118
|
oai_tool_calls: Optional[List[OpenAIToolCall]] = None
|
116
|
-
oai_tool_id2result: Optional[
|
119
|
+
oai_tool_id2result: Optional[OrderedDict[str, str]] = None
|
117
120
|
oai_tool_choice: ToolChoiceTypes | Dict[str, Dict[str, str] | str] = "auto"
|
118
121
|
function_call: Optional[LLMFunctionCall] = None
|
119
|
-
tool_messages: List[ToolMessage] = []
|
122
|
+
tool_messages: List[ToolMessage] = [] # only handle-able tools
|
123
|
+
# all known tools in the msg that are in an agent's llm_tools_known list,
|
124
|
+
# even if non-used/handled
|
125
|
+
all_tool_messages: List[ToolMessage] = []
|
126
|
+
|
120
127
|
metadata: ChatDocMetaData
|
121
128
|
attachment: None | ChatDocAttachment = None
|
122
129
|
|
@@ -136,6 +143,8 @@ class ChatDocument(Document):
|
|
136
143
|
def deepcopy(doc: ChatDocument) -> ChatDocument:
|
137
144
|
new_doc = copy.deepcopy(doc)
|
138
145
|
new_doc.metadata.id = ObjectRegistry.new_id()
|
146
|
+
new_doc.metadata.child_id = ""
|
147
|
+
new_doc.metadata.parent_id = ""
|
139
148
|
ObjectRegistry.register_object(new_doc)
|
140
149
|
return new_doc
|
141
150
|
|
@@ -160,7 +160,7 @@ class OpenAIAssistant(ChatAgent):
|
|
160
160
|
|
161
161
|
def enable_message(
|
162
162
|
self,
|
163
|
-
message_class: Optional[Type[ToolMessage]],
|
163
|
+
message_class: Optional[Type[ToolMessage] | List[Type[ToolMessage]]],
|
164
164
|
use: bool = True,
|
165
165
|
handle: bool = True,
|
166
166
|
force: bool = False,
|
@@ -173,6 +173,17 @@ class OpenAIAssistant(ChatAgent):
|
|
173
173
|
fn-calling seems to pay attn to these, and if we don't want this,
|
174
174
|
we should set this to False.
|
175
175
|
"""
|
176
|
+
if message_class is not None and isinstance(message_class, list):
|
177
|
+
for msg_class in message_class:
|
178
|
+
self.enable_message(
|
179
|
+
msg_class,
|
180
|
+
use=use,
|
181
|
+
handle=handle,
|
182
|
+
force=force,
|
183
|
+
require_recipient=require_recipient,
|
184
|
+
include_defaults=include_defaults,
|
185
|
+
)
|
186
|
+
return
|
176
187
|
super().enable_message(
|
177
188
|
message_class,
|
178
189
|
use=use,
|
@@ -17,10 +17,11 @@ from typing import Any, Dict, List, Tuple
|
|
17
17
|
import pandas as pd
|
18
18
|
|
19
19
|
from langroid.agent.special.doc_chat_agent import DocChatAgent, DocChatAgentConfig
|
20
|
-
from langroid.agent.special.lance_tools import QueryPlanTool
|
20
|
+
from langroid.agent.special.lance_tools import AnswerTool, QueryPlanTool
|
21
|
+
from langroid.agent.tools.orchestration import AgentDoneTool
|
21
22
|
from langroid.mytypes import DocMetaData, Document
|
22
23
|
from langroid.parsing.table_loader import describe_dataframe
|
23
|
-
from langroid.utils.constants import
|
24
|
+
from langroid.utils.constants import NO_ANSWER
|
24
25
|
from langroid.utils.pydantic_utils import (
|
25
26
|
dataframe_to_documents,
|
26
27
|
)
|
@@ -106,7 +107,7 @@ class LanceDocChatAgent(DocChatAgent):
|
|
106
107
|
"""
|
107
108
|
return schema
|
108
109
|
|
109
|
-
def query_plan(self, msg: QueryPlanTool) -> str:
|
110
|
+
def query_plan(self, msg: QueryPlanTool) -> AgentDoneTool | str:
|
110
111
|
"""
|
111
112
|
Handle the LLM's use of the FilterTool.
|
112
113
|
Temporarily set the config filter and either return the final answer
|
@@ -120,13 +121,15 @@ class LanceDocChatAgent(DocChatAgent):
|
|
120
121
|
except Exception as e:
|
121
122
|
logger.error(f"Error setting up documents: {e}")
|
122
123
|
# say DONE with err msg so it goes back to LanceFilterAgent
|
123
|
-
return
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
124
|
+
return AgentDoneTool(
|
125
|
+
content=f"""
|
126
|
+
Possible Filter Error:\n {e}
|
127
|
+
|
128
|
+
Note that only the following fields are allowed in the filter
|
129
|
+
of a query plan:
|
130
|
+
{", ".join(self.config.filter_fields)}
|
131
|
+
"""
|
132
|
+
)
|
130
133
|
|
131
134
|
# update the filter so it is used in the DocChatAgent
|
132
135
|
self.config.filter = plan.filter or None
|
@@ -139,22 +142,25 @@ class LanceDocChatAgent(DocChatAgent):
|
|
139
142
|
# The calc step can later be done with a separate Agent/Tool.
|
140
143
|
if plan.query is None or plan.query.strip() == "":
|
141
144
|
if plan.filter is None or plan.filter.strip() == "":
|
142
|
-
return
|
143
|
-
|
144
|
-
|
145
|
-
|
145
|
+
return AgentDoneTool(
|
146
|
+
content="""
|
147
|
+
Cannot execute Query Plan since filter as well as
|
148
|
+
rephrased query are empty.
|
149
|
+
"""
|
150
|
+
)
|
146
151
|
else:
|
147
152
|
# no query to match, so just get all docs matching filter
|
148
153
|
docs = self.vecdb.get_all_documents(plan.filter)
|
149
154
|
else:
|
150
155
|
_, docs = self.get_relevant_extracts(plan.query)
|
151
156
|
if len(docs) == 0:
|
152
|
-
return
|
153
|
-
|
154
|
-
return DONE + " " + result
|
157
|
+
return AgentDoneTool(content=NO_ANSWER)
|
158
|
+
answer = self.vecdb.compute_from_docs(docs, plan.dataframe_calc)
|
155
159
|
else:
|
156
160
|
# pass on the query so LLM can handle it
|
157
|
-
|
161
|
+
response = self.llm_response(plan.query)
|
162
|
+
answer = NO_ANSWER if response is None else response.content
|
163
|
+
return AgentDoneTool(tools=[AnswerTool(answer=answer)])
|
158
164
|
|
159
165
|
def ingest_docs(
|
160
166
|
self,
|
@@ -242,6 +248,7 @@ class LanceDocChatAgent(DocChatAgent):
|
|
242
248
|
.replace("NOT", "not")
|
243
249
|
.replace("'", "")
|
244
250
|
.replace('"', "")
|
251
|
+
.replace(":", "--")
|
245
252
|
)
|
246
253
|
|
247
254
|
tbl = self.vecdb.client.open_table(self.vecdb.config.collection_name)
|
@@ -26,8 +26,8 @@ from langroid.agent.special.lance_tools import (
|
|
26
26
|
QueryPlanAnswerTool,
|
27
27
|
QueryPlanFeedbackTool,
|
28
28
|
)
|
29
|
-
from langroid.
|
30
|
-
from langroid.utils.constants import
|
29
|
+
from langroid.agent.tools.orchestration import AgentDoneTool
|
30
|
+
from langroid.utils.constants import NO_ANSWER
|
31
31
|
|
32
32
|
logger = logging.getLogger(__name__)
|
33
33
|
|
@@ -50,6 +50,9 @@ class QueryPlanCriticConfig(LanceQueryPlanAgentConfig):
|
|
50
50
|
to create a QUERY PLAN, to be handled by an ASSISTANT.
|
51
51
|
- PANDAS-LIKE FILTER, WHICH CAN BE EMPTY (and it's fine if results sound reasonable)
|
52
52
|
FILTER SHOULD ONLY BE USED IF EXPLICITLY REQUIRED BY THE QUERY.
|
53
|
+
This filter selects the documents over which the REPHRASED QUERY will be applied,
|
54
|
+
thus naturally, the Re-phrased Query should NOT mention any FILTER fields,
|
55
|
+
since it applies to the documents AFTER FILTERING.
|
53
56
|
- REPHRASED QUERY (CANNOT BE EMPTY) that will be used to match against the
|
54
57
|
CONTENT (not filterable) of the documents.
|
55
58
|
In general the REPHRASED QUERY should be relied upon to match the CONTENT
|
@@ -61,9 +64,31 @@ class QueryPlanCriticConfig(LanceQueryPlanAgentConfig):
|
|
61
64
|
The assistant will answer based on documents whose CONTENTS match the QUERY,
|
62
65
|
possibly REPHRASED.
|
63
66
|
!!!!****THE REPHRASED QUERY SHOULD NEVER BE EMPTY****!!!
|
67
|
+
|
68
|
+
|
64
69
|
- DATAFRAME CALCULATION, which must be a SINGLE LINE calculation (or empty),
|
65
70
|
[NOTE ==> This calculation is applied AFTER the FILTER and REPHRASED QUERY.],
|
66
71
|
- ANSWER received from an assistant that used this QUERY PLAN.
|
72
|
+
IT IS TOTALLY FINE FOR THE ANSWER TO NOT MENTION ANY FILTERING CONDITIONS,
|
73
|
+
or if the ANSWER STATEMENT is MISSING SOME CRITERIA in the ORIGINAL QUERY.
|
74
|
+
|
75
|
+
Here is an example of a VALID Plan + Answer:
|
76
|
+
|
77
|
+
ORIGINAL QUERY: "Which crime novels were written by Russian authors after 1900?"
|
78
|
+
FILTER: "author_nationality == 'Russian' and year_written > 1900"
|
79
|
+
REPHRASED QUERY: "crime novel" [NOTICE NO FILTER FIELDS MENTIONED!!!]
|
80
|
+
DATAFRAME CALC: ""
|
81
|
+
ANSWER: "The Master and Margarita by Mikhail Bulgakov"
|
82
|
+
[NOTICE the answer does NOT need to say "crime novel" or "russian author"]
|
83
|
+
|
84
|
+
|
85
|
+
Other examples of VALID ANSWER for a given ORIGINAL QUERY:
|
86
|
+
|
87
|
+
ORIGINAL QUERY: "Which mountain is taller than 8000 meters?"
|
88
|
+
ANSWER: "Mount Everest" [NOTICE no mention of "taller than 8000 meters"]
|
89
|
+
|
90
|
+
ORIGINAL QUERY: "Which country has hosted the most olympics?"
|
91
|
+
ANSWER: "United States" [NOTICE no mention of "most olympics"]
|
67
92
|
|
68
93
|
In addition to the above SCHEMA fields there is a `content` field which:
|
69
94
|
- CANNOT appear in a FILTER,
|
@@ -141,21 +166,28 @@ class QueryPlanCritic(ChatAgent):
|
|
141
166
|
self.config = cfg
|
142
167
|
self.enable_message(QueryPlanAnswerTool, use=False, handle=True)
|
143
168
|
self.enable_message(QueryPlanFeedbackTool, use=True, handle=True)
|
169
|
+
self.enable_message(AgentDoneTool, use=False, handle=True)
|
170
|
+
|
171
|
+
def init_state(self) -> None:
|
172
|
+
self.expecting_feedback_tool = False
|
144
173
|
|
145
174
|
def query_plan_answer(self, msg: QueryPlanAnswerTool) -> str:
|
146
175
|
"""Present query plan + answer in plain text (not JSON)
|
147
176
|
so LLM can give feedback"""
|
177
|
+
self.expecting_feedback_tool = True
|
148
178
|
return plain_text_query_plan(msg)
|
149
179
|
|
150
|
-
def query_plan_feedback(self, msg: QueryPlanFeedbackTool) ->
|
180
|
+
def query_plan_feedback(self, msg: QueryPlanFeedbackTool) -> AgentDoneTool:
|
151
181
|
"""Format Valid so return to Query Planner"""
|
152
|
-
|
182
|
+
self.expecting_feedback_tool = False
|
183
|
+
# indicate this task is Done, and return the tool as result
|
184
|
+
return AgentDoneTool(tools=[msg])
|
153
185
|
|
154
186
|
def handle_message_fallback(
|
155
187
|
self, msg: str | ChatDocument
|
156
188
|
) -> str | ChatDocument | None:
|
157
189
|
"""Remind the LLM to use QueryPlanFeedbackTool since it forgot"""
|
158
|
-
if
|
190
|
+
if self.expecting_feedback_tool:
|
159
191
|
return """
|
160
192
|
You forgot to use the `query_plan_feedback` tool/function.
|
161
193
|
Re-try your response using the `query_plan_feedback` tool/function,
|
@@ -15,17 +15,19 @@ This agent has access to two tools:
|
|
15
15
|
"""
|
16
16
|
|
17
17
|
import logging
|
18
|
+
from typing import Optional
|
18
19
|
|
19
|
-
import langroid as lr
|
20
20
|
from langroid.agent.chat_agent import ChatAgent, ChatAgentConfig
|
21
21
|
from langroid.agent.chat_document import ChatDocument
|
22
22
|
from langroid.agent.special.lance_tools import (
|
23
|
+
AnswerTool,
|
23
24
|
QueryPlan,
|
24
25
|
QueryPlanAnswerTool,
|
25
26
|
QueryPlanFeedbackTool,
|
26
27
|
QueryPlanTool,
|
27
28
|
)
|
28
|
-
from langroid.
|
29
|
+
from langroid.agent.tools.orchestration import AgentDoneTool, ForwardTool
|
30
|
+
from langroid.utils.constants import NO_ANSWER
|
29
31
|
|
30
32
|
logger = logging.getLogger(__name__)
|
31
33
|
|
@@ -39,14 +41,14 @@ class LanceQueryPlanAgentConfig(ChatAgentConfig):
|
|
39
41
|
max_retries: int = 5 # max number of retries for query plan
|
40
42
|
use_functions_api = True
|
41
43
|
|
42
|
-
system_message =
|
44
|
+
system_message = """
|
43
45
|
You will receive a QUERY, to be answered based on an EXTREMELY LARGE collection
|
44
46
|
of documents you DO NOT have access to, but your ASSISTANT does.
|
45
47
|
You only know that these documents have a special `content` field
|
46
48
|
and additional FILTERABLE fields in the SCHEMA below, along with the
|
47
49
|
SAMPLE VALUES for each field, and the DTYPE in PANDAS TERMINOLOGY.
|
48
50
|
|
49
|
-
{
|
51
|
+
{doc_schema}
|
50
52
|
|
51
53
|
Based on the QUERY and the above SCHEMA, your task is to determine a QUERY PLAN,
|
52
54
|
consisting of:
|
@@ -116,10 +118,7 @@ class LanceQueryPlanAgentConfig(ChatAgentConfig):
|
|
116
118
|
You may receive FEEDBACK on your QUERY PLAN and received ANSWER,
|
117
119
|
from the 'QueryPlanCritic' who may offer suggestions for
|
118
120
|
a better FILTER, REPHRASED QUERY, or DATAFRAME CALCULATION.
|
119
|
-
|
120
|
-
If you keep getting feedback or keep getting a {NO_ANSWER} from the assistant
|
121
|
-
at least 3 times, then simply say '{DONE} {NO_ANSWER}' and nothing else.
|
122
|
-
|
121
|
+
|
123
122
|
At the BEGINNING if there is no query, ASK the user what they want to know.
|
124
123
|
"""
|
125
124
|
|
@@ -133,88 +132,128 @@ class LanceQueryPlanAgent(ChatAgent):
|
|
133
132
|
def __init__(self, config: LanceQueryPlanAgentConfig):
|
134
133
|
super().__init__(config)
|
135
134
|
self.config: LanceQueryPlanAgentConfig = config
|
136
|
-
self.curr_query_plan: QueryPlan | None = None
|
137
|
-
# how many times re-trying query plan in response to feedback:
|
138
|
-
self.n_retries: int = 0
|
139
|
-
self.result: str = "" # answer received from LanceRAG
|
140
135
|
# This agent should generate the QueryPlanTool
|
141
136
|
# as well as handle it for validation
|
142
137
|
self.enable_message(QueryPlanTool, use=True, handle=True)
|
143
138
|
self.enable_message(QueryPlanFeedbackTool, use=False, handle=True)
|
139
|
+
self.enable_message(AnswerTool, use=False, handle=True)
|
140
|
+
# neither use nor handle! Added to "known" tools so that the Planner agent
|
141
|
+
# can avoid processing it
|
142
|
+
self.enable_message(QueryPlanAnswerTool, use=False, handle=False)
|
143
|
+
# LLM will not use this, so set use=False (Agent generates it)
|
144
|
+
self.enable_message(AgentDoneTool, use=False, handle=True)
|
145
|
+
|
146
|
+
def init_state(self) -> None:
|
147
|
+
self.curr_query_plan: QueryPlan | None = None
|
148
|
+
self.expecting_query_plan: bool = False
|
149
|
+
# how many times re-trying query plan in response to feedback:
|
150
|
+
self.n_retries: int = 0
|
151
|
+
self.n_query_plan_reminders: int = 0
|
152
|
+
self.result: str = "" # answer received from LanceRAG
|
153
|
+
|
154
|
+
def llm_response(
|
155
|
+
self, message: Optional[str | ChatDocument] = None
|
156
|
+
) -> Optional[ChatDocument]:
|
157
|
+
self.expecting_query_plan = True
|
158
|
+
return super().llm_response(message)
|
144
159
|
|
145
|
-
def query_plan(self, msg: QueryPlanTool) -> str:
|
146
|
-
"""Valid, forward to RAG Agent
|
160
|
+
def query_plan(self, msg: QueryPlanTool) -> ForwardTool | str:
|
161
|
+
"""Valid, tool msg, forward chat_doc to RAG Agent.
|
162
|
+
Note this chat_doc will already have the
|
163
|
+
QueryPlanTool in its tool_messages list.
|
164
|
+
We just update the recipient to the doc_agent_name.
|
165
|
+
"""
|
147
166
|
# save, to be used to assemble QueryPlanResultTool
|
148
167
|
if len(msg.plan.dataframe_calc.split("\n")) > 1:
|
149
168
|
return "DATAFRAME CALCULATION must be a SINGLE LINE; Retry the `query_plan`"
|
150
169
|
self.curr_query_plan = msg.plan
|
151
|
-
|
170
|
+
self.expecting_query_plan = False
|
171
|
+
|
172
|
+
# To forward the QueryPlanTool to doc_agent, we could either:
|
173
|
+
|
174
|
+
# (a) insert `recipient` in the QueryPlanTool:
|
175
|
+
# QPWithRecipient = QueryPlanTool.require_recipient()
|
176
|
+
# qp = QPWithRecipient(**msg.dict(), recipient=self.config.doc_agent_name)
|
177
|
+
# return qp
|
178
|
+
#
|
179
|
+
# OR
|
180
|
+
#
|
181
|
+
# (b) create an agent response with recipient and tool_messages.
|
182
|
+
# response = self.create_agent_response(
|
183
|
+
# recipient=self.config.doc_agent_name, tool_messages=[msg]
|
184
|
+
# )
|
185
|
+
# return response
|
152
186
|
|
153
|
-
|
187
|
+
# OR
|
188
|
+
# (c) use the ForwardTool:
|
189
|
+
return ForwardTool(agent=self.config.doc_agent_name)
|
190
|
+
|
191
|
+
def query_plan_feedback(self, msg: QueryPlanFeedbackTool) -> str | AgentDoneTool:
|
154
192
|
"""Process Critic feedback on QueryPlan + Answer from RAG Agent"""
|
155
193
|
# We should have saved answer in self.result by this time,
|
156
194
|
# since this Agent seeks feedback only after receiving RAG answer.
|
157
|
-
if
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
195
|
+
if (
|
196
|
+
msg.suggested_fix == ""
|
197
|
+
and NO_ANSWER not in self.result
|
198
|
+
and self.result != ""
|
199
|
+
):
|
200
|
+
# This means the result is good AND Query Plan is fine,
|
201
|
+
# as judged by Critic
|
202
|
+
# (Note sometimes critic may have empty suggested_fix even when
|
203
|
+
# the result is NO_ANSWER)
|
204
|
+
self.n_retries = 0 # good answer, so reset this
|
205
|
+
return AgentDoneTool(content=self.result)
|
167
206
|
self.n_retries += 1
|
168
207
|
if self.n_retries >= self.config.max_retries:
|
169
208
|
# bail out to avoid infinite loop
|
170
209
|
self.n_retries = 0
|
171
|
-
return
|
210
|
+
return AgentDoneTool(content=NO_ANSWER)
|
211
|
+
|
212
|
+
# there is a suggested_fix, OR the result is empty or NO_ANSWER
|
213
|
+
if self.result == "" or NO_ANSWER in self.result:
|
214
|
+
# if result is empty or NO_ANSWER, we should retry the query plan
|
215
|
+
feedback = """
|
216
|
+
There was no answer, which might mean there is a problem in your query.
|
217
|
+
"""
|
218
|
+
suggested = "Retry the `query_plan` to try to get a non-null answer"
|
219
|
+
else:
|
220
|
+
feedback = msg.feedback
|
221
|
+
suggested = msg.suggested_fix
|
222
|
+
|
223
|
+
self.expecting_query_plan = True
|
224
|
+
|
172
225
|
return f"""
|
173
226
|
here is FEEDBACK about your QUERY PLAN, and a SUGGESTED FIX.
|
174
227
|
Modify the QUERY PLAN if needed:
|
175
|
-
|
176
|
-
|
228
|
+
ANSWER: {self.result}
|
229
|
+
FEEDBACK: {feedback}
|
230
|
+
SUGGESTED FIX: {suggested}
|
177
231
|
"""
|
178
232
|
|
233
|
+
def answer_tool(self, msg: AnswerTool) -> QueryPlanAnswerTool:
|
234
|
+
"""Handle AnswerTool received from LanceRagAgent:
|
235
|
+
Construct a QueryPlanAnswerTool with the answer"""
|
236
|
+
self.result = msg.answer # save answer to interpret feedback later
|
237
|
+
assert self.curr_query_plan is not None
|
238
|
+
query_plan_answer_tool = QueryPlanAnswerTool(
|
239
|
+
plan=self.curr_query_plan,
|
240
|
+
answer=msg.answer,
|
241
|
+
)
|
242
|
+
self.curr_query_plan = None # reset
|
243
|
+
return query_plan_answer_tool
|
244
|
+
|
179
245
|
def handle_message_fallback(
|
180
246
|
self, msg: str | ChatDocument
|
181
247
|
) -> str | ChatDocument | None:
|
182
248
|
"""
|
183
|
-
|
184
|
-
Construct a QueryPlanAnswerTool with the answer,
|
185
|
-
and forward to Critic for feedback.
|
249
|
+
Remind to use QueryPlanTool if we are expecting it.
|
186
250
|
"""
|
187
|
-
|
188
|
-
|
189
|
-
# then we know there was no tool, so we run below code
|
190
|
-
if (
|
191
|
-
isinstance(msg, ChatDocument)
|
192
|
-
and self.curr_query_plan is not None
|
193
|
-
and msg.metadata.parent is not None
|
194
|
-
):
|
195
|
-
# save result, to be used in query_plan_feedback()
|
196
|
-
self.result = msg.content
|
197
|
-
# assemble QueryPlanAnswerTool...
|
198
|
-
query_plan_answer_tool = QueryPlanAnswerTool( # type: ignore
|
199
|
-
plan=self.curr_query_plan,
|
200
|
-
answer=self.result,
|
201
|
-
)
|
202
|
-
response_tmpl = self.create_agent_response()
|
203
|
-
# ... add the QueryPlanAnswerTool to the response
|
204
|
-
# (Notice how the Agent is directly sending a tool, not the LLM)
|
205
|
-
response_tmpl.tool_messages = [query_plan_answer_tool]
|
206
|
-
# set the recipient to the Critic so it can give feedback
|
207
|
-
response_tmpl.metadata.recipient = self.config.critic_name
|
208
|
-
self.curr_query_plan = None # reset
|
209
|
-
return response_tmpl
|
210
|
-
if (
|
211
|
-
isinstance(msg, ChatDocument)
|
212
|
-
and not self.has_tool_message_attempt(msg)
|
213
|
-
and msg.metadata.sender == lr.Entity.LLM
|
214
|
-
):
|
215
|
-
# remind LLM to use the QueryPlanFeedbackTool
|
251
|
+
if self.expecting_query_plan and self.n_query_plan_reminders < 5:
|
252
|
+
self.n_query_plan_reminders += 1
|
216
253
|
return """
|
217
|
-
You
|
218
|
-
|
254
|
+
You FORGOT to use the `query_plan` tool/function,
|
255
|
+
OR you had a WRONG JSON SYNTAX when trying to use it.
|
256
|
+
Re-try your response using the `query_plan` tool/function CORRECTLY.
|
219
257
|
"""
|
258
|
+
self.n_query_plan_reminders = 0 # reset
|
220
259
|
return None
|
@@ -34,9 +34,17 @@ class QueryPlanTool(ToolMessage):
|
|
34
34
|
plan: QueryPlan
|
35
35
|
|
36
36
|
|
37
|
+
class AnswerTool(ToolMessage):
|
38
|
+
"""Wrapper for answer from LanceDocChatAgent"""
|
39
|
+
|
40
|
+
purpose: str = "To package the answer from LanceDocChatAgent"
|
41
|
+
request: str = "answer_tool"
|
42
|
+
answer: str
|
43
|
+
|
44
|
+
|
37
45
|
class QueryPlanAnswerTool(ToolMessage):
|
38
|
-
request = "query_plan_answer" # the agent method name that handles this tool
|
39
|
-
purpose = """
|
46
|
+
request: str = "query_plan_answer" # the agent method name that handles this tool
|
47
|
+
purpose: str = """
|
40
48
|
Assemble query <plan> and <answer>
|
41
49
|
"""
|
42
50
|
plan: QueryPlan
|