langroid 0.6.7__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 +499 -55
- langroid/agent/callbacks/chainlit.py +1 -1
- langroid/agent/chat_agent.py +191 -37
- langroid/agent/chat_document.py +142 -29
- langroid/agent/openai_assistant.py +20 -4
- 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/special/sql/sql_chat_agent.py +69 -13
- langroid/agent/task.py +179 -43
- langroid/agent/tool_message.py +19 -7
- langroid/agent/tools/__init__.py +5 -0
- langroid/agent/tools/orchestration.py +216 -0
- langroid/agent/tools/recipient_tool.py +6 -11
- langroid/agent/tools/rewind_tool.py +1 -1
- langroid/agent/typed_task.py +19 -0
- langroid/language_models/.chainlit/config.toml +121 -0
- langroid/language_models/.chainlit/translations/en-US.json +231 -0
- langroid/language_models/base.py +114 -12
- langroid/language_models/mock_lm.py +10 -1
- langroid/language_models/openai_gpt.py +260 -36
- 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.6.7.dist-info → langroid-0.9.0.dist-info}/METADATA +4 -1
- {langroid-0.6.7.dist-info → langroid-0.9.0.dist-info}/RECORD +32 -33
- 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.6.7.dist-info → langroid-0.9.0.dist-info}/LICENSE +0 -0
- {langroid-0.6.7.dist-info → langroid-0.9.0.dist-info}/WHEEL +0 -0
langroid/agent/chat_document.py
CHANGED
@@ -2,8 +2,9 @@ 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
|
-
from typing import Any, List, Optional, Union, cast
|
7
|
+
from typing import Any, Dict, List, Optional, Union, cast
|
7
8
|
|
8
9
|
from langroid.agent.tool_message import ToolMessage
|
9
10
|
from langroid.language_models.base import (
|
@@ -11,7 +12,9 @@ from langroid.language_models.base import (
|
|
11
12
|
LLMMessage,
|
12
13
|
LLMResponse,
|
13
14
|
LLMTokenUsage,
|
15
|
+
OpenAIToolCall,
|
14
16
|
Role,
|
17
|
+
ToolChoiceTypes,
|
15
18
|
)
|
16
19
|
from langroid.mytypes import DocMetaData, Document, Entity
|
17
20
|
from langroid.parsing.agent_chats import parse_message
|
@@ -51,6 +54,8 @@ class ChatDocMetaData(DocMetaData):
|
|
51
54
|
agent_id: str = "" # ChatAgent that generated this message
|
52
55
|
msg_idx: int = -1 # index of this message in the agent `message_history`
|
53
56
|
sender: Entity # sender of the message
|
57
|
+
# tool_id corresponding to single tool result in ChatDocument.content
|
58
|
+
oai_tool_id: str | None = None
|
54
59
|
tool_ids: List[str] = [] # stack of tool_ids; used by OpenAIAssistant
|
55
60
|
block: None | Entity = None
|
56
61
|
sender_name: str = ""
|
@@ -86,8 +91,39 @@ class ChatDocLoggerFields(BaseModel):
|
|
86
91
|
|
87
92
|
|
88
93
|
class ChatDocument(Document):
|
94
|
+
"""
|
95
|
+
Represents a message in a conversation among agents. All responders of an agent
|
96
|
+
have signature ChatDocument -> ChatDocument (modulo None, str, etc),
|
97
|
+
and so does the Task.run() method.
|
98
|
+
|
99
|
+
Attributes:
|
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)
|
104
|
+
oai_tool_choice: ToolChoiceTypes | Dict[str, str]: Param controlling how the
|
105
|
+
LLM should choose tool-use in its response
|
106
|
+
(auto, none, required, or a specific tool)
|
107
|
+
function_call (Optional[LLMFunctionCall]):
|
108
|
+
Function-call from an OpenAI-compatible API
|
109
|
+
(deprecated by OpenAI, in favor of tool-calls)
|
110
|
+
tool_messages (List[ToolMessage]): Langroid ToolMessages extracted from
|
111
|
+
- `content` field (via JSON parsing),
|
112
|
+
- `oai_tool_calls`, or
|
113
|
+
- `function_call`
|
114
|
+
metadata (ChatDocMetaData): Metadata for the message, e.g. sender, recipient.
|
115
|
+
attachment (None | ChatDocAttachment): Any additional data attached.
|
116
|
+
"""
|
117
|
+
|
118
|
+
oai_tool_calls: Optional[List[OpenAIToolCall]] = None
|
119
|
+
oai_tool_id2result: Optional[OrderedDict[str, str]] = None
|
120
|
+
oai_tool_choice: ToolChoiceTypes | Dict[str, Dict[str, str] | str] = "auto"
|
89
121
|
function_call: Optional[LLMFunctionCall] = None
|
90
|
-
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
|
+
|
91
127
|
metadata: ChatDocMetaData
|
92
128
|
attachment: None | ChatDocAttachment = None
|
93
129
|
|
@@ -107,6 +143,8 @@ class ChatDocument(Document):
|
|
107
143
|
def deepcopy(doc: ChatDocument) -> ChatDocument:
|
108
144
|
new_doc = copy.deepcopy(doc)
|
109
145
|
new_doc.metadata.id = ObjectRegistry.new_id()
|
146
|
+
new_doc.metadata.child_id = ""
|
147
|
+
new_doc.metadata.parent_id = ""
|
110
148
|
ObjectRegistry.register_object(new_doc)
|
111
149
|
return new_doc
|
112
150
|
|
@@ -198,6 +236,24 @@ class ChatDocument(Document):
|
|
198
236
|
if len(self.metadata.tool_ids) > 0:
|
199
237
|
self.metadata.tool_ids.pop()
|
200
238
|
|
239
|
+
@staticmethod
|
240
|
+
def _clean_fn_call(fc: LLMFunctionCall | None) -> None:
|
241
|
+
# Sometimes an OpenAI LLM (esp gpt-4o) may generate a function-call
|
242
|
+
# with odditities:
|
243
|
+
# (a) the `name` is set, as well as `arguments.request` is set,
|
244
|
+
# and in langroid we use the `request` value as the `name`.
|
245
|
+
# In this case we override the `name` with the `request` value.
|
246
|
+
# (b) the `name` looks like "functions blah" or just "functions"
|
247
|
+
# In this case we strip the "functions" part.
|
248
|
+
if fc is None:
|
249
|
+
return
|
250
|
+
fc.name = fc.name.replace("functions", "").strip()
|
251
|
+
if fc.arguments is not None:
|
252
|
+
request = fc.arguments.get("request")
|
253
|
+
if request is not None and request != "":
|
254
|
+
fc.name = request
|
255
|
+
fc.arguments.pop("request")
|
256
|
+
|
201
257
|
@staticmethod
|
202
258
|
def from_LLMResponse(
|
203
259
|
response: LLMResponse,
|
@@ -216,22 +272,14 @@ class ChatDocument(Document):
|
|
216
272
|
if message in ["''", '""']:
|
217
273
|
message = ""
|
218
274
|
if response.function_call is not None:
|
219
|
-
|
220
|
-
|
221
|
-
#
|
222
|
-
|
223
|
-
|
224
|
-
# (b) the `name` looks like "functions blah" or just "functions"
|
225
|
-
# In this case we strip the "functions" part.
|
226
|
-
fc = response.function_call
|
227
|
-
fc.name = fc.name.replace("functions", "").strip()
|
228
|
-
if fc.arguments is not None:
|
229
|
-
request = fc.arguments.get("request")
|
230
|
-
if request is not None and request != "":
|
231
|
-
fc.name = request
|
232
|
-
fc.arguments.pop("request")
|
275
|
+
ChatDocument._clean_fn_call(response.function_call)
|
276
|
+
if response.oai_tool_calls is not None:
|
277
|
+
# there must be at least one if it's not None
|
278
|
+
for oai_tc in response.oai_tool_calls:
|
279
|
+
ChatDocument._clean_fn_call(oai_tc.function)
|
233
280
|
return ChatDocument(
|
234
281
|
content=message,
|
282
|
+
oai_tool_calls=response.oai_tool_calls,
|
235
283
|
function_call=response.function_call,
|
236
284
|
metadata=ChatDocMetaData(
|
237
285
|
source=Entity.LLM,
|
@@ -261,24 +309,33 @@ class ChatDocument(Document):
|
|
261
309
|
)
|
262
310
|
|
263
311
|
@staticmethod
|
264
|
-
def to_LLMMessage(
|
312
|
+
def to_LLMMessage(
|
313
|
+
message: Union[str, "ChatDocument"],
|
314
|
+
oai_tools: Optional[List[OpenAIToolCall]] = None,
|
315
|
+
) -> List[LLMMessage]:
|
265
316
|
"""
|
266
|
-
Convert to LLMMessage
|
317
|
+
Convert to list of LLMMessage, to incorporate into msg-history sent to LLM API.
|
318
|
+
Usually there will be just a single LLMMessage, but when the ChatDocument
|
319
|
+
contains results from multiple OpenAI tool-calls, we would have a sequence
|
320
|
+
LLMMessages, one per tool-call result.
|
267
321
|
|
268
322
|
Args:
|
269
323
|
message (str|ChatDocument): Message to convert.
|
324
|
+
oai_tools (Optional[List[OpenAIToolCall]]): Tool-calls currently awaiting
|
325
|
+
response, from the ChatAgent's latest message.
|
270
326
|
Returns:
|
271
|
-
LLMMessage:
|
272
|
-
|
327
|
+
List[LLMMessage]: list of LLMMessages corresponding to this ChatDocument.
|
273
328
|
"""
|
274
329
|
sender_name = None
|
275
330
|
sender_role = Role.USER
|
276
331
|
fun_call = None
|
277
|
-
|
332
|
+
oai_tool_calls = None
|
333
|
+
tool_id = "" # for OpenAI Assistant
|
278
334
|
chat_document_id: str = ""
|
279
335
|
if isinstance(message, ChatDocument):
|
280
336
|
content = message.content
|
281
337
|
fun_call = message.function_call
|
338
|
+
oai_tool_calls = message.oai_tool_calls
|
282
339
|
if message.metadata.sender == Entity.USER and fun_call is not None:
|
283
340
|
# This may happen when a (parent agent's) LLM generates a
|
284
341
|
# a Function-call, and it ends up being sent to the current task's
|
@@ -289,6 +346,10 @@ class ChatDocument(Document):
|
|
289
346
|
# in the content of the message.
|
290
347
|
content += " " + str(fun_call)
|
291
348
|
fun_call = None
|
349
|
+
if message.metadata.sender == Entity.USER and oai_tool_calls is not None:
|
350
|
+
# same reasoning as for function-call above
|
351
|
+
content += " " + "\n\n".join(str(tc) for tc in oai_tool_calls)
|
352
|
+
oai_tool_calls = None
|
292
353
|
sender_name = message.metadata.sender_name
|
293
354
|
tool_ids = message.metadata.tool_ids
|
294
355
|
tool_id = tool_ids[-1] if len(tool_ids) > 0 else ""
|
@@ -299,22 +360,74 @@ class ChatDocument(Document):
|
|
299
360
|
message.metadata.parent is not None
|
300
361
|
and message.metadata.parent.function_call is not None
|
301
362
|
):
|
363
|
+
# This is a response to a function call, so set the role to FUNCTION.
|
302
364
|
sender_role = Role.FUNCTION
|
303
365
|
sender_name = message.metadata.parent.function_call.name
|
366
|
+
elif oai_tools is not None and len(oai_tools) > 0:
|
367
|
+
pending_tool_ids = [tc.id for tc in oai_tools]
|
368
|
+
# The ChatAgent has pending OpenAI tool-call(s),
|
369
|
+
# so the current ChatDocument contains
|
370
|
+
# results for some/all/none of them.
|
371
|
+
|
372
|
+
if len(oai_tools) == 1:
|
373
|
+
# Case 1:
|
374
|
+
# There was exactly 1 pending tool-call, and in this case
|
375
|
+
# the result would be a plain string in `content`
|
376
|
+
return [
|
377
|
+
LLMMessage(
|
378
|
+
role=Role.TOOL,
|
379
|
+
tool_call_id=oai_tools[0].id,
|
380
|
+
content=content,
|
381
|
+
chat_document_id=chat_document_id,
|
382
|
+
)
|
383
|
+
]
|
384
|
+
|
385
|
+
elif (
|
386
|
+
message.metadata.oai_tool_id is not None
|
387
|
+
and message.metadata.oai_tool_id in pending_tool_ids
|
388
|
+
):
|
389
|
+
# Case 2:
|
390
|
+
# ChatDocument.content has result of a single tool-call
|
391
|
+
return [
|
392
|
+
LLMMessage(
|
393
|
+
role=Role.TOOL,
|
394
|
+
tool_call_id=message.metadata.oai_tool_id,
|
395
|
+
content=content,
|
396
|
+
chat_document_id=chat_document_id,
|
397
|
+
)
|
398
|
+
]
|
399
|
+
elif message.oai_tool_id2result is not None:
|
400
|
+
# Case 2:
|
401
|
+
# There were > 1 tool-calls awaiting response,
|
402
|
+
assert (
|
403
|
+
len(message.oai_tool_id2result) > 1
|
404
|
+
), "oai_tool_id2result must have more than 1 item."
|
405
|
+
return [
|
406
|
+
LLMMessage(
|
407
|
+
role=Role.TOOL,
|
408
|
+
tool_call_id=tool_id,
|
409
|
+
content=result,
|
410
|
+
chat_document_id=chat_document_id,
|
411
|
+
)
|
412
|
+
for tool_id, result in message.oai_tool_id2result.items()
|
413
|
+
]
|
304
414
|
elif message.metadata.sender == Entity.LLM:
|
305
415
|
sender_role = Role.ASSISTANT
|
306
416
|
else:
|
307
417
|
# LLM can only respond to text content, so extract it
|
308
418
|
content = message
|
309
419
|
|
310
|
-
return
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
420
|
+
return [
|
421
|
+
LLMMessage(
|
422
|
+
role=sender_role,
|
423
|
+
tool_id=tool_id, # for OpenAI Assistant
|
424
|
+
content=content,
|
425
|
+
function_call=fun_call,
|
426
|
+
tool_calls=oai_tool_calls,
|
427
|
+
name=sender_name,
|
428
|
+
chat_document_id=chat_document_id,
|
429
|
+
)
|
430
|
+
]
|
318
431
|
|
319
432
|
|
320
433
|
LLMMessage.update_forward_refs()
|
@@ -79,7 +79,7 @@ class OpenAIAssistantConfig(ChatAgentConfig):
|
|
79
79
|
# set to True once we can add Assistant msgs in threads
|
80
80
|
cache_responses: bool = True
|
81
81
|
timeout: int = 30 # can be different from llm.timeout
|
82
|
-
llm = OpenAIGPTConfig(chat_model=OpenAIChatModel.
|
82
|
+
llm = OpenAIGPTConfig(chat_model=OpenAIChatModel.GPT4o)
|
83
83
|
tools: List[AssistantTool] = []
|
84
84
|
files: List[str] = []
|
85
85
|
|
@@ -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,
|
@@ -192,7 +203,7 @@ class OpenAIAssistant(ChatAgent):
|
|
192
203
|
self.set_system_message(sys_msg.content)
|
193
204
|
if not self.config.use_functions_api:
|
194
205
|
return
|
195
|
-
functions, _ = self._function_args()
|
206
|
+
functions, _, _, _ = self._function_args()
|
196
207
|
if functions is None:
|
197
208
|
return
|
198
209
|
# add the functions to the assistant:
|
@@ -720,7 +731,12 @@ class OpenAIAssistant(ChatAgent):
|
|
720
731
|
"""
|
721
732
|
is_tool_output = False
|
722
733
|
if message is not None:
|
723
|
-
|
734
|
+
# note: to_LLMMessage returns a list of LLMMessage,
|
735
|
+
# which is allowed to have len > 1, in case the msg
|
736
|
+
# represents results of multiple (non-assistant) tool-calls.
|
737
|
+
# But for OAI Assistant, we only assume exactly one tool-call at a time.
|
738
|
+
# TODO look into multi-tools
|
739
|
+
llm_msg = ChatDocument.to_LLMMessage(message)[0]
|
724
740
|
tool_id = llm_msg.tool_id
|
725
741
|
if tool_id in self.pending_tool_ids:
|
726
742
|
if isinstance(message, ChatDocument):
|
@@ -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,
|