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.
Files changed (37) hide show
  1. langroid/agent/base.py +499 -55
  2. langroid/agent/callbacks/chainlit.py +1 -1
  3. langroid/agent/chat_agent.py +191 -37
  4. langroid/agent/chat_document.py +142 -29
  5. langroid/agent/openai_assistant.py +20 -4
  6. langroid/agent/special/lance_doc_chat_agent.py +25 -18
  7. langroid/agent/special/lance_rag/critic_agent.py +37 -5
  8. langroid/agent/special/lance_rag/query_planner_agent.py +102 -63
  9. langroid/agent/special/lance_tools.py +10 -2
  10. langroid/agent/special/sql/sql_chat_agent.py +69 -13
  11. langroid/agent/task.py +179 -43
  12. langroid/agent/tool_message.py +19 -7
  13. langroid/agent/tools/__init__.py +5 -0
  14. langroid/agent/tools/orchestration.py +216 -0
  15. langroid/agent/tools/recipient_tool.py +6 -11
  16. langroid/agent/tools/rewind_tool.py +1 -1
  17. langroid/agent/typed_task.py +19 -0
  18. langroid/language_models/.chainlit/config.toml +121 -0
  19. langroid/language_models/.chainlit/translations/en-US.json +231 -0
  20. langroid/language_models/base.py +114 -12
  21. langroid/language_models/mock_lm.py +10 -1
  22. langroid/language_models/openai_gpt.py +260 -36
  23. langroid/mytypes.py +0 -1
  24. langroid/parsing/parse_json.py +19 -2
  25. langroid/utils/pydantic_utils.py +19 -0
  26. langroid/vector_store/base.py +3 -1
  27. langroid/vector_store/lancedb.py +2 -0
  28. {langroid-0.6.7.dist-info → langroid-0.9.0.dist-info}/METADATA +4 -1
  29. {langroid-0.6.7.dist-info → langroid-0.9.0.dist-info}/RECORD +32 -33
  30. pyproject.toml +2 -1
  31. langroid/agent/special/lance_rag_new/__init__.py +0 -9
  32. langroid/agent/special/lance_rag_new/critic_agent.py +0 -171
  33. langroid/agent/special/lance_rag_new/lance_rag_task.py +0 -144
  34. langroid/agent/special/lance_rag_new/query_planner_agent.py +0 -222
  35. langroid/agent/team.py +0 -1758
  36. {langroid-0.6.7.dist-info → langroid-0.9.0.dist-info}/LICENSE +0 -0
  37. {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.utils.constants import DONE, NO_ANSWER, PASS_TO
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 = f"""
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
- {{doc_schema}}
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
- return PASS_TO + self.config.doc_agent_name
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
- def query_plan_feedback(self, msg: QueryPlanFeedbackTool) -> str:
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 msg.suggested_fix == "":
158
- self.n_retries = 0
159
- # This means the Query Plan or Result is good, as judged by Critic
160
- if self.result == "":
161
- # This was feedback for query with no result
162
- return "QUERY PLAN LOOKS GOOD!"
163
- elif self.result == NO_ANSWER:
164
- return NO_ANSWER
165
- else: # non-empty and non-null answer
166
- return DONE + " " + self.result
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 DONE + " " + NO_ANSWER
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
- FEEDBACK: {msg.feedback}
176
- SUGGESTED FIX: {msg.suggested_fix}
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
- Process answer received from RAG Agent:
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
- # TODO we don't need to use this fallback method. instead we can
188
- # first call result = super().agent_response(), and if result is None,
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 forgot to use the `query_plan` tool/function.
218
- Re-try your response using the `query_plan` tool/function.
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 agent_response(
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
- response_message = ""
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
- rows = query_result.fetchall()
338
- response_message = self._format_rows(rows)
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()