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
@@ -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
|
@@ -14,11 +14,12 @@ from rich import print
|
|
14
14
|
from rich.console import Console
|
15
15
|
|
16
16
|
from langroid.exceptions import LangroidImportError
|
17
|
+
from langroid.utils.constants import DONE
|
17
18
|
|
18
19
|
try:
|
19
20
|
from sqlalchemy import MetaData, Row, create_engine, inspect, text
|
20
21
|
from sqlalchemy.engine import Engine
|
21
|
-
from sqlalchemy.exc import SQLAlchemyError
|
22
|
+
from sqlalchemy.exc import ResourceClosedError, SQLAlchemyError
|
22
23
|
from sqlalchemy.orm import Session, sessionmaker
|
23
24
|
except ImportError as e:
|
24
25
|
raise LangroidImportError(extra="sql", error=str(e))
|
@@ -49,8 +50,8 @@ logger = logging.getLogger(__name__)
|
|
49
50
|
|
50
51
|
console = Console()
|
51
52
|
|
52
|
-
DEFAULT_SQL_CHAT_SYSTEM_MESSAGE = """
|
53
|
-
{mode}
|
53
|
+
DEFAULT_SQL_CHAT_SYSTEM_MESSAGE = f"""
|
54
|
+
{{mode}}
|
54
55
|
|
55
56
|
You do not need to attempt answering a question with just one query.
|
56
57
|
You could make a sequence of SQL queries to help you write the final query.
|
@@ -64,16 +65,19 @@ are "Male" and "Female".
|
|
64
65
|
|
65
66
|
Start by asking what I would like to know about the data.
|
66
67
|
|
68
|
+
When you have FINISHED the given query or database update task,
|
69
|
+
say {DONE} and show your answer.
|
70
|
+
|
67
71
|
"""
|
68
72
|
|
69
|
-
ADDRESSING_INSTRUCTION = """
|
73
|
+
ADDRESSING_INSTRUCTION = f"""
|
70
74
|
IMPORTANT - Whenever you are NOT writing a SQL query, make sure you address the user
|
71
|
-
using {prefix}User. You MUST use the EXACT syntax {prefix} !!!
|
75
|
+
using {{prefix}}User. You MUST use the EXACT syntax {{prefix}} !!!
|
72
76
|
|
73
77
|
In other words, you ALWAYS write EITHER:
|
74
78
|
- a SQL query using the `run_query` tool,
|
75
|
-
- OR address the user using {prefix}User
|
76
|
-
|
79
|
+
- OR address the user using {{prefix}}User, and include {DONE} to indicate your
|
80
|
+
task is FINISHED.
|
77
81
|
"""
|
78
82
|
|
79
83
|
|
@@ -135,6 +139,9 @@ class SQLChatAgent(ChatAgent):
|
|
135
139
|
Agent for chatting with a SQL database
|
136
140
|
"""
|
137
141
|
|
142
|
+
used_run_query: bool = False
|
143
|
+
llm_responded: bool = False
|
144
|
+
|
138
145
|
def __init__(self, config: "SQLChatAgentConfig") -> None:
|
139
146
|
"""Initialize the SQLChatAgent.
|
140
147
|
|
@@ -246,7 +253,49 @@ class SQLChatAgent(ChatAgent):
|
|
246
253
|
self.enable_message(GetTableSchemaTool)
|
247
254
|
self.enable_message(GetColumnDescriptionsTool)
|
248
255
|
|
249
|
-
def
|
256
|
+
def llm_response(
|
257
|
+
self, message: Optional[str | ChatDocument] = None
|
258
|
+
) -> Optional[ChatDocument]:
|
259
|
+
self.llm_responded = True
|
260
|
+
return super().llm_response(message)
|
261
|
+
|
262
|
+
def user_response(
|
263
|
+
self,
|
264
|
+
msg: Optional[str | ChatDocument] = None,
|
265
|
+
) -> Optional[ChatDocument]:
|
266
|
+
self.llm_responded = False
|
267
|
+
self.used_run_query = False
|
268
|
+
return super().user_response(msg)
|
269
|
+
|
270
|
+
def handle_message_fallback(
|
271
|
+
self, msg: str | ChatDocument
|
272
|
+
) -> str | ChatDocument | None:
|
273
|
+
|
274
|
+
if not self.llm_responded:
|
275
|
+
return None
|
276
|
+
if self.used_run_query:
|
277
|
+
prefix = (
|
278
|
+
self.config.addressing_prefix + "User"
|
279
|
+
if self.config.addressing_prefix
|
280
|
+
else ""
|
281
|
+
)
|
282
|
+
return (
|
283
|
+
DONE + prefix + (msg.content if isinstance(msg, ChatDocument) else msg)
|
284
|
+
)
|
285
|
+
|
286
|
+
else:
|
287
|
+
reminder = """
|
288
|
+
You may have forgotten to use the `run_query` tool to execute an SQL query
|
289
|
+
for the user's question/request
|
290
|
+
"""
|
291
|
+
if self.config.addressing_prefix != "":
|
292
|
+
reminder += f"""
|
293
|
+
OR you may have forgotten to address the user using the prefix
|
294
|
+
{self.config.addressing_prefix}
|
295
|
+
"""
|
296
|
+
return reminder
|
297
|
+
|
298
|
+
def _agent_response(
|
250
299
|
self,
|
251
300
|
msg: Optional[str | ChatDocument] = None,
|
252
301
|
) -> Optional[ChatDocument]:
|
@@ -326,16 +375,23 @@ class SQLChatAgent(ChatAgent):
|
|
326
375
|
"""
|
327
376
|
query = msg.query
|
328
377
|
session = self.Session
|
329
|
-
|
330
|
-
|
378
|
+
self.used_run_query = True
|
331
379
|
try:
|
332
380
|
logger.info(f"Executing SQL query: {query}")
|
333
381
|
|
334
382
|
query_result = session.execute(text(query))
|
335
383
|
session.commit()
|
336
|
-
|
337
|
-
|
338
|
-
|
384
|
+
try:
|
385
|
+
# attempt to fetch results: should work for normal SELECT queries
|
386
|
+
rows = query_result.fetchall()
|
387
|
+
response_message = self._format_rows(rows)
|
388
|
+
except ResourceClosedError:
|
389
|
+
# If we get here, it's a non-SELECT query (UPDATE, INSERT, DELETE)
|
390
|
+
affected_rows = query_result.rowcount # type: ignore
|
391
|
+
response_message = f"""
|
392
|
+
Non-SELECT query executed successfully.
|
393
|
+
Rows affected: {affected_rows}
|
394
|
+
"""
|
339
395
|
|
340
396
|
except SQLAlchemyError as e:
|
341
397
|
session.rollback()
|