langroid 0.20.0__py3-none-any.whl → 0.20.1__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 CHANGED
@@ -880,7 +880,13 @@ class Agent(ABC):
880
880
  return cdoc
881
881
 
882
882
  def has_tool_message_attempt(self, msg: str | ChatDocument | None) -> bool:
883
- """Check whether msg contains a Tool/fn-call attempt (by the LLM)"""
883
+ """
884
+ Check whether msg contains a Tool/fn-call attempt (by the LLM).
885
+
886
+ CAUTION: This uses self.get_tool_messages(msg) which as a side-effect
887
+ may update msg.tool_messages when msg is a ChatDocument, if there are
888
+ any tools in msg.
889
+ """
884
890
  if msg is None:
885
891
  return False
886
892
  try:
@@ -921,6 +927,9 @@ class Agent(ABC):
921
927
  ) -> List[ToolMessage]:
922
928
  """
923
929
  Get ToolMessages recognized in msg, handle-able by this agent.
930
+ NOTE: as a side-effect, this will update msg.tool_messages
931
+ when msg is a ChatDocument and msg contains tool messages.
932
+
924
933
  If all_tools is True:
925
934
  - return all tools, i.e. any tool in self.llm_tools_known,
926
935
  whether it is handled by this agent or not;
@@ -27,6 +27,7 @@ from langroid.agent.special.arangodb.tools import (
27
27
  aql_retrieval_tool_name,
28
28
  arango_schema_tool_name,
29
29
  )
30
+ from langroid.agent.special.arangodb.utils import count_fields, trim_schema
30
31
  from langroid.agent.tools.orchestration import DoneTool, ForwardTool
31
32
  from langroid.exceptions import LangroidImportError
32
33
  from langroid.mytypes import Entity
@@ -88,11 +89,14 @@ class QueryResult(BaseModel):
88
89
  class ArangoChatAgentConfig(ChatAgentConfig):
89
90
  arango_settings: ArangoSettings = ArangoSettings()
90
91
  system_message: str = DEFAULT_ARANGO_CHAT_SYSTEM_MESSAGE
91
- kg_schema: Optional[Dict[str, List[Dict[str, Any]]]] = None
92
+ kg_schema: str | Dict[str, List[Dict[str, Any]]] | None = None
92
93
  database_created: bool = False
93
- use_schema_tools: bool = True
94
+ prepopulate_schema: bool = True
94
95
  use_functions_api: bool = True
96
+ max_num_results: int = 10 # how many results to return from AQL query
95
97
  max_result_tokens: int = 1000 # truncate long results to this many tokens
98
+ max_schema_fields: int = 500 # max fields to show in schema
99
+ max_tries: int = 10 # how many attempts to answer user question
96
100
  use_tools: bool = False
97
101
  schema_sample_pct: float = 0
98
102
  # whether the agent is used in a continuous chat with user,
@@ -103,16 +107,65 @@ class ArangoChatAgentConfig(ChatAgentConfig):
103
107
 
104
108
  class ArangoChatAgent(ChatAgent):
105
109
  def __init__(self, config: ArangoChatAgentConfig):
110
+ super().__init__(config)
106
111
  self.config: ArangoChatAgentConfig = config
112
+ self.init_state()
107
113
  self._validate_config()
108
114
  self._import_arango()
109
115
  self._initialize_db()
110
116
  self._init_tools_sys_message()
111
- self.init_state()
112
117
 
113
118
  def init_state(self) -> None:
114
119
  super().init_state()
115
120
  self.current_retrieval_aql_query: str = ""
121
+ self.num_tries = 0 # how many attempts to answer user question
122
+
123
+ def user_response(
124
+ self,
125
+ msg: Optional[str | ChatDocument] = None,
126
+ ) -> Optional[ChatDocument]:
127
+ response = super().user_response(msg)
128
+ response_str = response.content if response is not None else ""
129
+ if response_str != "":
130
+ self.num_tries = 0 # reset number of tries if user responds
131
+ return response
132
+
133
+ def llm_response(
134
+ self, message: Optional[str | ChatDocument] = None
135
+ ) -> Optional[ChatDocument]:
136
+ if self.num_tries > self.config.max_tries:
137
+ if self.config.chat_mode:
138
+ return self.create_llm_response(
139
+ content=f"""
140
+ {self.config.addressing_prefix}User
141
+ I give up, since I have exceeded the
142
+ maximum number of tries ({self.config.max_tries}).
143
+ Feel free to give me some hints!
144
+ """
145
+ )
146
+ else:
147
+ return self.create_llm_response(
148
+ tool_messages=[
149
+ DoneTool(
150
+ content=f"""
151
+ Exceeded maximum number of tries ({self.config.max_tries}).
152
+ """
153
+ )
154
+ ]
155
+ )
156
+
157
+ if isinstance(message, ChatDocument) and message.metadata.sender == Entity.USER:
158
+ message.content = (
159
+ message.content
160
+ + "\n"
161
+ + """
162
+ (REMEMBER, Do NOT use more than ONE TOOL/FUNCTION at a time!
163
+ you must WAIT for a helper to send you the RESULT(S) before
164
+ making another TOOL/FUNCTION call)
165
+ """
166
+ )
167
+
168
+ return super().llm_response(message)
116
169
 
117
170
  def _validate_config(self) -> None:
118
171
  assert isinstance(self.config, ArangoChatAgentConfig)
@@ -230,6 +283,7 @@ class ArangoChatAgent(ChatAgent):
230
283
  try:
231
284
  cursor = self.db.aql.execute(query, bind_vars=bind_vars)
232
285
  records = [doc for doc in cursor] # type: ignore
286
+ records = records[: self.config.max_num_results]
233
287
  logger.warning(f"Records retrieved: {records}")
234
288
  return QueryResult(success=True, data=records if records else [])
235
289
  except Exception as e:
@@ -273,6 +327,28 @@ class ArangoChatAgent(ChatAgent):
273
327
  success=False, data=f"Failed after max retries: {str(e)}"
274
328
  )
275
329
 
330
+ def _limit_tokens(self, text: str) -> str:
331
+ result = text
332
+ n_toks = self.num_tokens(result)
333
+ if n_toks > self.config.max_result_tokens:
334
+ logger.warning(
335
+ f"""
336
+ Your query resulted in a large result of
337
+ {n_toks} tokens,
338
+ which will be truncated to {self.config.max_result_tokens} tokens.
339
+ If this does not give satisfactory results,
340
+ please retry with a more focused query.
341
+ """
342
+ )
343
+ if self.parser is not None:
344
+ result = self.parser.truncate_tokens(
345
+ result,
346
+ self.config.max_result_tokens,
347
+ )
348
+ else:
349
+ result = result[: self.config.max_result_tokens * 4] # truncate roughly
350
+ return result
351
+
276
352
  def aql_retrieval_tool(self, msg: AQLRetrievalTool) -> str:
277
353
  """Handle AQL query for data retrieval"""
278
354
  if not self.tried_schema:
@@ -285,6 +361,7 @@ class ArangoChatAgent(ChatAgent):
285
361
  return """
286
362
  You need to create the database first using `{aql_creation_tool_name}`.
287
363
  """
364
+ self.num_tries += 1
288
365
  query = msg.aql_query
289
366
  self.current_retrieval_aql_query = query
290
367
  logger.info(f"Executing AQL query: {query}")
@@ -299,28 +376,11 @@ class ArangoChatAgent(ChatAgent):
299
376
  """
300
377
  # truncate long results
301
378
  result = str(response.data)
302
- n_toks = self.num_tokens(result)
303
- if n_toks > self.config.max_result_tokens:
304
- logger.warning(
305
- f"""
306
- Your query resulted in a large result of
307
- {n_toks} tokens,
308
- which will be truncated to {self.config.max_result_tokens} tokens.
309
- If this does not give satisfactory results,
310
- please retry with a more focused query.
311
- """
312
- )
313
- if self.parser is not None:
314
- result = self.parser.truncate_tokens(
315
- result,
316
- self.config.max_result_tokens,
317
- )
318
- else:
319
- result = result[: self.config.max_result_tokens * 4] # truncate roughly
320
- return result
379
+ return self._limit_tokens(result)
321
380
 
322
381
  def aql_creation_tool(self, msg: AQLCreationTool) -> str:
323
382
  """Handle AQL query for creating data"""
383
+ self.num_tries += 1
324
384
  query = msg.aql_query
325
385
  logger.info(f"Executing AQL query: {query}")
326
386
  response = self.write_query(query)
@@ -334,12 +394,34 @@ class ArangoChatAgent(ChatAgent):
334
394
  self,
335
395
  msg: ArangoSchemaTool | None,
336
396
  ) -> Dict[str, List[Dict[str, Any]]] | str:
337
- """Get database schema including collections, properties, and relationships"""
397
+ """Get database schema. If collections=None, include all collections.
398
+ If properties=False, show only connection info,
399
+ else show all properties and example-docs.
400
+ """
401
+
402
+ if msg is not None:
403
+ collections = msg.collections
404
+ properties = msg.properties
405
+ else:
406
+ collections = None
407
+ properties = True
338
408
  self.tried_schema = True
339
- if self.config.kg_schema is not None and len(self.config.kg_schema) > 0:
409
+ if (
410
+ self.config.kg_schema is not None
411
+ and len(self.config.kg_schema) > 0
412
+ and msg is None
413
+ ):
414
+ # we are trying to pre-populate full schema before the agent runs,
415
+ # so get it if it's already available
416
+ # (Note of course that this "full schema" may actually be incomplete)
340
417
  return self.config.kg_schema
418
+
419
+ # increment tries only if the LLM is asking for the schema,
420
+ # in which case msg will not be None
421
+ self.num_tries += msg is not None
422
+
341
423
  try:
342
- # Get graph schemas
424
+ # Get graph schemas (keeping full graph info)
343
425
  graph_schema = [
344
426
  {"graph_name": g["name"], "edge_definitions": g["edge_definitions"]}
345
427
  for g in self.db.graphs() # type: ignore
@@ -348,57 +430,78 @@ class ArangoChatAgent(ChatAgent):
348
430
  # Get collection schemas
349
431
  collection_schema = []
350
432
  for collection in self.db.collections(): # type: ignore
351
- if collection["name"].startswith("_"): # Skip system collections
433
+ if collection["name"].startswith("_"):
352
434
  continue
353
435
 
354
436
  col_name = collection["name"]
437
+ if collections and col_name not in collections:
438
+ continue
439
+
355
440
  col_type = collection["type"]
356
441
  col_size = self.db.collection(col_name).count()
357
442
 
358
- if col_size == 0: # Skip empty collections
443
+ if col_size == 0:
359
444
  continue
360
445
 
361
- # Calculate sample size
362
- limit_amount = (
363
- ceil(
364
- self.config.schema_sample_pct * col_size / 100.0 # type: ignore
365
- )
366
- or 1
367
- )
368
-
369
- # Query to get sample documents and their properties
370
- sample_query = f"""
371
- FOR doc in {col_name}
372
- LIMIT {limit_amount}
373
- RETURN doc
374
- """
446
+ if properties:
447
+ # Full property collection with sampling
448
+ lim = self.config.schema_sample_pct * col_size # type: ignore
449
+ limit_amount = ceil(lim / 100.0) or 1
450
+ sample_query = f"""
451
+ FOR doc in {col_name}
452
+ LIMIT {limit_amount}
453
+ RETURN doc
454
+ """
375
455
 
376
- properties = []
377
- example_doc = None
378
-
379
- def simplify_doc(doc: Any) -> Any:
380
- if isinstance(doc, list) and len(doc) > 0:
381
- return [simplify_doc(doc[0])]
382
- if isinstance(doc, dict):
383
- return {k: simplify_doc(v) for k, v in doc.items()}
384
- return doc
385
-
386
- for doc in self.db.aql.execute(sample_query): # type: ignore
387
- if example_doc is None:
388
- example_doc = simplify_doc(doc)
389
- for key, value in doc.items():
390
- prop = {"name": key, "type": type(value).__name__}
391
- if prop not in properties:
392
- properties.append(prop)
393
-
394
- collection_schema.append(
395
- {
456
+ properties_list = []
457
+ example_doc = None
458
+
459
+ def simplify_doc(doc: Any) -> Any:
460
+ if isinstance(doc, list) and len(doc) > 0:
461
+ return [simplify_doc(doc[0])]
462
+ if isinstance(doc, dict):
463
+ return {k: simplify_doc(v) for k, v in doc.items()}
464
+ return doc
465
+
466
+ for doc in self.db.aql.execute(sample_query): # type: ignore
467
+ if example_doc is None:
468
+ example_doc = simplify_doc(doc)
469
+ for key, value in doc.items():
470
+ prop = {"name": key, "type": type(value).__name__}
471
+ if prop not in properties_list:
472
+ properties_list.append(prop)
473
+
474
+ collection_schema.append(
475
+ {
476
+ "collection_name": col_name,
477
+ "collection_type": col_type,
478
+ f"{col_type}_properties": properties_list,
479
+ f"example_{col_type}": example_doc,
480
+ }
481
+ )
482
+ else:
483
+ # Basic info + from/to for edges only
484
+ collection_info = {
396
485
  "collection_name": col_name,
397
486
  "collection_type": col_type,
398
- f"{col_type}_properties": properties,
399
- f"example_{col_type}": example_doc,
400
487
  }
401
- )
488
+ if col_type == "edge":
489
+ # Get a sample edge to extract from/to fields
490
+ sample_edge = next(
491
+ self.db.aql.execute( # type: ignore
492
+ f"FOR e IN {col_name} LIMIT 1 RETURN e"
493
+ ),
494
+ None,
495
+ )
496
+ if sample_edge:
497
+ collection_info["from_collection"] = sample_edge[
498
+ "_from"
499
+ ].split("/")[0]
500
+ collection_info["to_collection"] = sample_edge["_to"].split(
501
+ "/"
502
+ )[0]
503
+
504
+ collection_schema.append(collection_info)
402
505
 
403
506
  schema = {
404
507
  "Graph Schema": graph_schema,
@@ -406,10 +509,41 @@ class ArangoChatAgent(ChatAgent):
406
509
  }
407
510
  schema_str = json.dumps(schema, indent=2)
408
511
  logger.warning(f"Schema retrieved:\n{schema_str}")
409
- # save schema to file "logs/arangoo-schema.json"
410
512
  with open("logs/arango-schema.json", "w") as f:
411
513
  f.write(schema_str)
412
- self.config.kg_schema = schema # type: ignore
514
+ if (n_fields := count_fields(schema)) > self.config.max_schema_fields:
515
+ logger.warning(
516
+ f"""
517
+ Schema has {n_fields} fields, which exceeds the maximum of
518
+ {self.config.max_schema_fields}. Showing a trimmed version
519
+ that only includes edge info and no other properties.
520
+ """
521
+ )
522
+ schema = trim_schema(schema)
523
+ n_fields = count_fields(schema)
524
+ logger.warning(f"Schema trimmed down to {n_fields} fields.")
525
+ schema_str = (
526
+ json.dumps(schema)
527
+ + "\n"
528
+ + f"""
529
+
530
+ CAUTION: The requested schema was too large, so
531
+ the schema has been trimmed down to show only all collection names,
532
+ their types,
533
+ and edge relationships (from/to collections) without any properties.
534
+ To find out more about the schema, you can EITHER:
535
+ - Use the `{arango_schema_tool_name}` tool again with the
536
+ `properties` arg set to True, and `collections` arg set to
537
+ specific collections you want to know more about, OR
538
+ - Use the `{aql_retrieval_tool_name}` tool to learn more about
539
+ the schema by querying the database.
540
+
541
+ """
542
+ )
543
+ if msg is None:
544
+ self.config.kg_schema = schema_str
545
+ return schema_str
546
+ self.config.kg_schema = schema
413
547
  return schema
414
548
 
415
549
  except Exception as e:
@@ -432,9 +566,10 @@ class ArangoChatAgent(ChatAgent):
432
566
 
433
567
  super().__init__(self.config)
434
568
  # Note we are enabling GraphSchemaTool regardless of whether
435
- # self.config.use_schema_tools is True or False, because
569
+ # self.config.prepopulate_schema is True or False, because
436
570
  # even when schema provided, the agent may later want to get the schema,
437
- # e.g. if the db evolves, or if it needs to bring in the schema
571
+ # e.g. if the db evolves, or schema was trimmed due to size, or
572
+ # if it needs to bring in the schema into recent context.
438
573
 
439
574
  self.enable_message(
440
575
  [
@@ -454,7 +589,7 @@ class ArangoChatAgent(ChatAgent):
454
589
  assert isinstance(self.config, ArangoChatAgentConfig)
455
590
  return (
456
591
  SCHEMA_TOOLS_SYS_MSG
457
- if self.config.use_schema_tools
592
+ if not self.config.prepopulate_schema
458
593
  else SCHEMA_PROVIDED_SYS_MSG.format(schema=self.arango_schema_tool(None))
459
594
  )
460
595
 
@@ -9,7 +9,7 @@ done_tool_name = DoneTool.default_value("request")
9
9
 
10
10
  arango_schema_tool_description = f"""
11
11
  `{arango_schema_tool_name}` tool/function-call to find the schema
12
- of the graph database, i.e. get all the collections
12
+ of the graph database, or for some SPECIFIC collections, i.e. get information on
13
13
  (document and edge), their attributes, and graph definitions available in your
14
14
  ArangoDB database. You MUST use this tool BEFORE attempting to use the
15
15
  `{aql_retrieval_tool_name}` tool/function-call, to ensure that you are using the
@@ -18,7 +18,8 @@ correct collection names and attributes in your `{aql_retrieval_tool_name}` tool
18
18
 
19
19
  aql_retrieval_tool_description = f"""
20
20
  `{aql_retrieval_tool_name}` tool/function-call to retrieve information from
21
- the database using AQL (ArangoDB Query Language) queries.
21
+ the database using AQL (ArangoDB Query Language) queries, to answer
22
+ the user's questions, OR for you to learn more about the SCHEMA of the database.
22
23
  """
23
24
 
24
25
  aql_creation_tool_description = f"""
@@ -26,6 +27,29 @@ aql_creation_tool_description = f"""
26
27
  documents/edges in the database.
27
28
  """
28
29
 
30
+ aql_retrieval_query_example = """
31
+ EXAMPLE:
32
+ Suppose you are asked this question "Does Bob have a father?".
33
+ Then you will go through the following steps, where YOU indicates
34
+ the message YOU will be sending, and RESULTS indicates the RESULTS
35
+ you will receive from the helper executing the query:
36
+
37
+ 1. YOU:
38
+ {{ "request": "aql_retrieval_tool",
39
+ "aql_query": "FOR v, e, p in ... [query truncated for brevity]..."}}
40
+
41
+ 2. RESULTS:
42
+ [.. results from the query...]
43
+ 3. YOU: [ since results were not satisfactory, you try ANOTHER query]
44
+ {{ "request": "aql_retrieval_tool",
45
+ "aql_query": "blah blah ... [query truncated for brevity]..."}}
46
+ }}
47
+ 4. RESULTS:
48
+ [.. results from the query...]
49
+ 5. YOU: [ now you have the answer, you can generate your response ]
50
+ The answer is YES, Bob has a father, and his name is John.
51
+ """
52
+
29
53
  aql_query_instructions = """
30
54
  When writing AQL queries:
31
55
  1. Use the exact property names shown in the schema
@@ -63,6 +87,7 @@ REMEMBER:
63
87
  with your response. DO NOT MAKE UP RESULTS FROM A TOOL!
64
88
  [3] YOU MUST NOT ANSWER queries from your OWN KNOWLEDGE; ALWAYS RELY ON
65
89
  the result of a TOOL/FUNCTION to compose your response.
90
+ [4] Use ONLY ONE TOOL/FUNCTION at a TIME!
66
91
  """
67
92
  # sys msg to use when schema already provided initially,
68
93
  # so agent should not use schema tool
@@ -77,6 +102,7 @@ and their attribute keys available in your ArangoDB database.
77
102
  {{schema}}
78
103
  === END SCHEMA ===
79
104
 
105
+
80
106
  To help with the user's question or database update/creation request,
81
107
  you have access to these tools:
82
108
 
@@ -84,10 +110,6 @@ you have access to these tools:
84
110
 
85
111
  - {aql_creation_tool_description}
86
112
 
87
- Since the schema has been provided, you may not need to use the tool below,
88
- but you may use it if you need to remind yourself about the schema:
89
-
90
- - {arango_schema_tool_description}
91
113
 
92
114
  {tool_result_instruction}
93
115
  """
@@ -113,7 +135,9 @@ DEFAULT_ARANGO_CHAT_SYSTEM_MESSAGE = f"""
113
135
  {{mode}}
114
136
 
115
137
  You do not need to be able to answer a question with just one query.
116
- You could make a sequence of AQL queries to find the answer to the question.
138
+ You can make a query, WAIT for the result,
139
+ THEN make ANOTHER query, WAIT for result,
140
+ THEN make ANOTHER query, and so on, until you have the answer.
117
141
 
118
142
  {aql_query_instructions}
119
143
 
@@ -134,6 +158,8 @@ If you receive a null or other unexpected result,
134
158
  Start by asking what the user needs help with.
135
159
 
136
160
  {tool_result_instruction}
161
+
162
+ {aql_retrieval_query_example}
137
163
  """
138
164
 
139
165
  ADDRESSING_INSTRUCTION = """
@@ -1,3 +1,5 @@
1
+ from typing import List, Tuple
2
+
1
3
  from langroid.agent.tool_message import ToolMessage
2
4
 
3
5
 
@@ -5,11 +7,46 @@ class AQLRetrievalTool(ToolMessage):
5
7
  request: str = "aql_retrieval_tool"
6
8
  purpose: str = """
7
9
  To send an <aql_query> in response to a user's request/question,
10
+ OR to find SCHEMA information,
8
11
  and WAIT for results of the <aql_query> BEFORE continuing with response.
9
12
  You will receive RESULTS from this tool, and ONLY THEN you can continue.
10
13
  """
11
14
  aql_query: str
12
15
 
16
+ @classmethod
17
+ def examples(cls) -> List[ToolMessage | Tuple[str, ToolMessage]]:
18
+ """Few-shot examples to include in tool instructions."""
19
+ return [
20
+ (
21
+ "I want to see who Bob's Father is",
22
+ cls(
23
+ aql_query="""
24
+ FOR v, e, p IN 1..1 OUTBOUND 'users/Bob' GRAPH 'family_tree'
25
+ FILTER p.edges[0].type == 'father'
26
+ RETURN v
27
+ """
28
+ ),
29
+ ),
30
+ (
31
+ "I want to know the properties of the Actor node",
32
+ cls(
33
+ aql_query="""
34
+ FOR doc IN Actor
35
+ LIMIT 1
36
+ RETURN ATTRIBUTES(doc)
37
+ """
38
+ ),
39
+ ),
40
+ ]
41
+
42
+ @classmethod
43
+ def instructions(cls) -> str:
44
+ return """
45
+ When using this TOOL/Function-call, you must WAIT to receive the RESULTS
46
+ of the AQL query, before continuing your response!
47
+ DO NOT ASSUME YOU KNOW THE RESULTs BEFORE RECEIVING THEM.
48
+ """
49
+
13
50
 
14
51
  aql_retrieval_tool_name = AQLRetrievalTool.default_value("request")
15
52
 
@@ -23,6 +60,23 @@ class AQLCreationTool(ToolMessage):
23
60
  """
24
61
  aql_query: str
25
62
 
63
+ @classmethod
64
+ def examples(cls) -> List[ToolMessage | Tuple[str, ToolMessage]]:
65
+ """Few-shot examples to include in tool instructions."""
66
+ return [
67
+ (
68
+ "Create a new document in the collection 'users'",
69
+ cls(
70
+ aql_query="""
71
+ INSERT {
72
+ "name": "Alice",
73
+ "age": 30
74
+ } INTO users
75
+ """
76
+ ),
77
+ ),
78
+ ]
79
+
26
80
 
27
81
  aql_creation_tool_name = AQLCreationTool.default_value("request")
28
82
 
@@ -30,10 +84,19 @@ aql_creation_tool_name = AQLCreationTool.default_value("request")
30
84
  class ArangoSchemaTool(ToolMessage):
31
85
  request: str = "arango_schema_tool"
32
86
  purpose: str = """
33
- To get the schema of the Arango graph database.
87
+ To get the schema of the Arango graph database,
88
+ or some part of it. Follow these instructions:
89
+ 1. Set <properties> to True to get the properties of the collections,
90
+ and False if you only want to see the graph structure and get only the
91
+ from/to relations of the edges.
92
+ 2. Set <collections> to a list of collection names if you want to see,
93
+ or leave it as None to see all ALL collections.
34
94
  IMPORTANT: YOU MUST WAIT FOR THE RESULT OF THE TOOL BEFORE CONTINUING.
35
95
  You will receive RESULTS from this tool, and ONLY THEN you can continue.
36
96
  """
37
97
 
98
+ properties: bool = True
99
+ collections: List[str] | None = None
100
+
38
101
 
39
102
  arango_schema_tool_name = ArangoSchemaTool.default_value("request")
@@ -0,0 +1,36 @@
1
+ from typing import Any, Dict, List
2
+
3
+
4
+ def count_fields(schema: Dict[str, List[Dict[str, Any]]]) -> int:
5
+ total = 0
6
+ for coll in schema["Collection Schema"]:
7
+ # Count all keys in each collection's dict
8
+ total += len(coll)
9
+ # Also count properties if they exist
10
+ props = coll.get(f"{coll['collection_type']}_properties", [])
11
+ total += len(props)
12
+ return total
13
+
14
+
15
+ def trim_schema(
16
+ schema: Dict[str, List[Dict[str, Any]]]
17
+ ) -> Dict[str, List[Dict[str, Any]]]:
18
+ """Keep only edge connection info, remove properties and examples"""
19
+ trimmed: Dict[str, List[Dict[str, Any]]] = {
20
+ "Graph Schema": schema["Graph Schema"],
21
+ "Collection Schema": [],
22
+ }
23
+ for coll in schema["Collection Schema"]:
24
+ col_info: Dict[str, Any] = {
25
+ "collection_name": coll["collection_name"],
26
+ "collection_type": coll["collection_type"],
27
+ }
28
+ if coll["collection_type"] == "edge":
29
+ # preserve from/to info if present
30
+ if f"example_{coll['collection_type']}" in coll:
31
+ example = coll[f"example_{coll['collection_type']}"]
32
+ if example and "_from" in example:
33
+ col_info["from_collection"] = example["_from"].split("/")[0]
34
+ col_info["to_collection"] = example["_to"].split("/")[0]
35
+ trimmed["Collection Schema"].append(col_info)
36
+ return trimmed
langroid/agent/task.py CHANGED
@@ -1330,6 +1330,9 @@ class Task:
1330
1330
  max_cost=self.max_cost,
1331
1331
  max_tokens=self.max_tokens,
1332
1332
  )
1333
+ # update result.tool_messages if any
1334
+ if isinstance(result, ChatDocument):
1335
+ self.agent.get_tool_messages(result)
1333
1336
  if result is not None:
1334
1337
  content, id2result, oai_tool_id = self.agent.process_tool_results(
1335
1338
  result.content,
@@ -1358,6 +1361,9 @@ class Task:
1358
1361
  else:
1359
1362
  response_fn = self._entity_responder_map[cast(Entity, e)]
1360
1363
  result = response_fn(self.pending_message)
1364
+ # update result.tool_messages if any
1365
+ if isinstance(result, ChatDocument):
1366
+ self.agent.get_tool_messages(result)
1361
1367
 
1362
1368
  result_chat_doc = self.agent.to_ChatDocument(
1363
1369
  result,
@@ -1388,7 +1394,7 @@ class Task:
1388
1394
  # ignore all string-based signaling/routing
1389
1395
  return result
1390
1396
  # parse various routing/addressing strings in result
1391
- is_pass, recipient, content = parse_routing(
1397
+ is_pass, recipient, content = self._parse_routing(
1392
1398
  result,
1393
1399
  addressing_prefix=self.config.addressing_prefix,
1394
1400
  )
@@ -1521,7 +1527,7 @@ class Task:
1521
1527
  oai_tool_id2result = result_msg.oai_tool_id2result if result_msg else None
1522
1528
  fun_call = result_msg.function_call if result_msg else None
1523
1529
  tool_messages = result_msg.tool_messages if result_msg else []
1524
- # if there is an LLMDoneTool or AgentDoneTool among these,
1530
+ # if there is an DoneTool or AgentDoneTool among these,
1525
1531
  # we extract content and tools from here, and ignore all others
1526
1532
  for t in tool_messages:
1527
1533
  if isinstance(t, FinalResultTool):
@@ -1533,6 +1539,8 @@ class Task:
1533
1539
  # there shouldn't be multiple tools like this; just take the first
1534
1540
  content = to_string(t.content)
1535
1541
  content_any = t.content
1542
+ fun_call = None
1543
+ oai_tool_calls = None
1536
1544
  if isinstance(t, AgentDoneTool):
1537
1545
  # AgentDoneTool may have tools, unlike DoneTool
1538
1546
  tool_messages = t.tools
@@ -1940,58 +1948,72 @@ class Task:
1940
1948
  """
1941
1949
  self.color_log = enable
1942
1950
 
1951
+ def _parse_routing(
1952
+ self,
1953
+ msg: ChatDocument | str,
1954
+ addressing_prefix: str = "",
1955
+ ) -> Tuple[bool | None, str | None, str | None]:
1956
+ """
1957
+ Parse routing instruction if any, of the form:
1958
+ PASS:<recipient> (pass current pending msg to recipient)
1959
+ SEND:<recipient> <content> (send content to recipient)
1960
+ @<recipient> <content> (send content to recipient)
1961
+ Args:
1962
+ msg (ChatDocument|str|None): message to parse
1963
+ addressing_prefix (str): prefix to address other agents or entities,
1964
+ (e.g. "@". See documentation of `TaskConfig` for details).
1965
+ Returns:
1966
+ Tuple[bool|None, str|None, str|None]:
1967
+ bool: true=PASS, false=SEND, or None if neither
1968
+ str: recipient, or None
1969
+ str: content to send, or None
1970
+ """
1971
+ # handle routing instruction-strings in result if any,
1972
+ # such as PASS, PASS_TO, or SEND
1943
1973
 
1944
- def parse_routing(
1945
- msg: ChatDocument | str,
1946
- addressing_prefix: str = "",
1947
- ) -> Tuple[bool | None, str | None, str | None]:
1948
- """
1949
- Parse routing instruction if any, of the form:
1950
- PASS:<recipient> (pass current pending msg to recipient)
1951
- SEND:<recipient> <content> (send content to recipient)
1952
- @<recipient> <content> (send content to recipient)
1953
- Args:
1954
- msg (ChatDocument|str|None): message to parse
1955
- addressing_prefix (str): prefix to address other agents or entities,
1956
- (e.g. "@". See documentation of `TaskConfig` for details).
1957
- Returns:
1958
- Tuple[bool|None, str|None, str|None]:
1959
- bool: true=PASS, false=SEND, or None if neither
1960
- str: recipient, or None
1961
- str: content to send, or None
1962
- """
1963
- # handle routing instruction in result if any,
1964
- # of the form PASS=<recipient>
1965
- content = msg.content if isinstance(msg, ChatDocument) else msg
1966
- content = content.strip()
1967
- if PASS in content and PASS_TO not in content:
1968
- return True, None, None
1969
- if PASS_TO in content and content.split(":")[1] != "":
1970
- return True, content.split(":")[1], None
1971
- if (
1972
- SEND_TO in content
1973
- and (addressee_content := parse_addressed_message(content, SEND_TO))[0]
1974
- is not None
1975
- ):
1976
- (addressee, content_to_send) = addressee_content
1977
- # if no content then treat same as PASS_TO
1978
- if content_to_send == "":
1979
- return True, addressee, None
1980
- else:
1981
- return False, addressee, content_to_send
1982
- if (
1983
- addressing_prefix != ""
1984
- and addressing_prefix in content
1985
- and (addressee_content := parse_addressed_message(content, addressing_prefix))[
1986
- 0
1987
- ]
1988
- is not None
1989
- ):
1990
- (addressee, content_to_send) = addressee_content
1991
- # if no content then treat same as PASS_TO
1992
- if content_to_send == "":
1993
- return True, addressee, None
1994
- else:
1995
- return False, addressee, content_to_send
1974
+ msg_str = msg.content if isinstance(msg, ChatDocument) else msg
1975
+ if (
1976
+ self.agent.has_tool_message_attempt(msg)
1977
+ and not msg_str.startswith(PASS)
1978
+ and not msg_str.startswith(PASS_TO)
1979
+ and not msg_str.startswith(SEND_TO)
1980
+ ):
1981
+ # if there's an attempted tool-call, we ignore any routing strings,
1982
+ # unless they are at the start of the msg
1983
+ return None, None, None
1984
+
1985
+ content = msg.content if isinstance(msg, ChatDocument) else msg
1986
+ content = content.strip()
1987
+ if PASS in content and PASS_TO not in content:
1988
+ return True, None, None
1989
+ if PASS_TO in content and content.split(":")[1] != "":
1990
+ return True, content.split(":")[1], None
1991
+ if (
1992
+ SEND_TO in content
1993
+ and (addressee_content := parse_addressed_message(content, SEND_TO))[0]
1994
+ is not None
1995
+ ):
1996
+ # Note this will discard any portion of content BEFORE SEND_TO.
1997
+ # TODO maybe make this configurable.
1998
+ (addressee, content_to_send) = addressee_content
1999
+ # if no content then treat same as PASS_TO
2000
+ if content_to_send == "":
2001
+ return True, addressee, None
2002
+ else:
2003
+ return False, addressee, content_to_send
2004
+ if (
2005
+ addressing_prefix != ""
2006
+ and addressing_prefix in content
2007
+ and (
2008
+ addressee_content := parse_addressed_message(content, addressing_prefix)
2009
+ )[0]
2010
+ is not None
2011
+ ):
2012
+ (addressee, content_to_send) = addressee_content
2013
+ # if no content then treat same as PASS_TO
2014
+ if content_to_send == "":
2015
+ return True, addressee, None
2016
+ else:
2017
+ return False, addressee, content_to_send
1996
2018
 
1997
- return None, None, None
2019
+ return None, None, None
@@ -41,11 +41,11 @@ class DoneTool(ToolMessage):
41
41
  """Tool for Agent Entity (i.e. agent_response) or LLM entity (i.e. llm_response) to
42
42
  signal the current task is done, with some content as the result."""
43
43
 
44
- purpose = """
44
+ purpose: str = """
45
45
  To signal the current task is done, along with an optional message <content>
46
46
  of arbitrary type (default None).
47
47
  """
48
- request = "done_tool"
48
+ request: str = "done_tool"
49
49
  content: str = ""
50
50
 
51
51
  def response(self, agent: ChatAgent) -> ChatDocument:
@@ -77,7 +77,7 @@ class ResultTool(ToolMessage):
77
77
  Note:
78
78
  - when defining a tool handler or agent_response, you can directly return
79
79
  ResultTool(field1 = val1, ...),
80
- where the values can be aribitrary data structures, including nested
80
+ where the values can be arbitrary data structures, including nested
81
81
  Pydantic objs, or you can define a subclass of ResultTool with the
82
82
  fields you want to return.
83
83
  - This is a special ToolMessage that is NOT meant to be used or handled
@@ -143,10 +143,10 @@ class PassTool(ToolMessage):
143
143
  Similar to ForwardTool, but without specifying the recipient agent.
144
144
  """
145
145
 
146
- purpose = """
146
+ purpose: str = """
147
147
  To pass the current message so that other agents can handle it.
148
148
  """
149
- request = "pass_tool"
149
+ request: str = "pass_tool"
150
150
 
151
151
  def response(self, agent: ChatAgent, chat_doc: ChatDocument) -> ChatDocument:
152
152
  """When this tool is enabled for an Agent, this will result in a method
@@ -178,10 +178,10 @@ class DonePassTool(PassTool):
178
178
  Similar to PassTool, except we append a DoneTool to the result tool_messages.
179
179
  """
180
180
 
181
- purpose = """
181
+ purpose: str = """
182
182
  To signal the current task is done, with results set to the current/incoming msg.
183
183
  """
184
- request = "done_pass_tool"
184
+ request: str = "done_pass_tool"
185
185
 
186
186
  def response(self, agent: ChatAgent, chat_doc: ChatDocument) -> ChatDocument:
187
187
  # use PassTool to get the right ChatDocument to pass...
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: langroid
3
- Version: 0.20.0
3
+ Version: 0.20.1
4
4
  Summary: Harness LLMs with Multi-Agent Programming
5
5
  License: MIT
6
6
  Author: Prasad Chalasani
@@ -1,6 +1,6 @@
1
1
  langroid/__init__.py,sha256=z_fCOLQJPOw3LLRPBlFB5-2HyCjpPgQa4m4iY5Fvb8Y,1800
2
2
  langroid/agent/__init__.py,sha256=ll0Cubd2DZ-fsCMl7e10hf9ZjFGKzphfBco396IKITY,786
3
- langroid/agent/base.py,sha256=uCjFJ0Mjm6hu4MSCsz7vzGwzpbkrxddDIduhxS24jps,65034
3
+ langroid/agent/base.py,sha256=sOZapdzHaB4kbCLu8vI_zZx78jIhv9fmWn0EWV4yTAE,65371
4
4
  langroid/agent/batch.py,sha256=QZdlt1563hx4l3AXrCaGovE-PNG93M3DsvQAbDzdiS8,13705
5
5
  langroid/agent/callbacks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  langroid/agent/callbacks/chainlit.py,sha256=JJXI3UGTyTDg2FFath4rqY1GyUo_0pbVBt8CZpvdtn4,23289
@@ -11,9 +11,10 @@ langroid/agent/junk,sha256=LxfuuW7Cijsg0szAzT81OjWWv1PMNI-6w_-DspVIO2s,339
11
11
  langroid/agent/openai_assistant.py,sha256=2rjCZw45ysNBEGNzQM4uf0bTC4KkatGYAWcVcW4xcek,34337
12
12
  langroid/agent/special/__init__.py,sha256=gik_Xtm_zV7U9s30Mn8UX3Gyuy4jTjQe9zjiE3HWmEo,1273
13
13
  langroid/agent/special/arangodb/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- langroid/agent/special/arangodb/arangodb_agent.py,sha256=5X8yQr1MdTLchNK6L7g3gArFYVEiql4g35YPMw_4TIU,19579
15
- langroid/agent/special/arangodb/system_messages.py,sha256=j-vNz4SLKzE6-1a31ZFw7NHMD_uBepS8vxJ_bsjgMC4,5801
16
- langroid/agent/special/arangodb/tools.py,sha256=3CW2HsLqcM0JGemHsmihm8F5zZwwbFvAwyHzzaJ2S90,1319
14
+ langroid/agent/special/arangodb/arangodb_agent.py,sha256=y9zp2ZQcKkNsUegvDZryn79exth3T-hsmkR-z4gSU-w,25522
15
+ langroid/agent/special/arangodb/system_messages.py,sha256=Uni6uDOSZpD9miCNzx7CEQjnyxDF8Egbe-hFvkraSt4,6739
16
+ langroid/agent/special/arangodb/tools.py,sha256=WasFERC1cToLOWi1cWqUs-TujU0A68gZWbhbP128obo,3499
17
+ langroid/agent/special/arangodb/utils.py,sha256=LIevtkayIdVVXyj3jlbKH2WgdZTtH5-JLgbXOHC7uxs,1420
17
18
  langroid/agent/special/doc_chat_agent.py,sha256=xIqBOyLax_jMU0UevxqXf_aQUrRkW6MQUKpKnKvaqkQ,59281
18
19
  langroid/agent/special/lance_doc_chat_agent.py,sha256=s8xoRs0gGaFtDYFUSIRchsgDVbS5Q3C2b2mr3V1Fd-Q,10419
19
20
  langroid/agent/special/lance_rag/__init__.py,sha256=QTbs0IVE2ZgDg8JJy1zN97rUUg4uEPH7SLGctFNumk4,174
@@ -37,14 +38,14 @@ langroid/agent/special/sql/utils/system_message.py,sha256=qKLHkvQWRQodTtPLPxr1GS
37
38
  langroid/agent/special/sql/utils/tools.py,sha256=vFYysk6Vi7HJjII8B4RitA3pt_z3gkSglDNdhNVMiFc,1332
38
39
  langroid/agent/special/table_chat_agent.py,sha256=d9v2wsblaRx7oMnKhLV7uO_ujvk9gh59pSGvBXyeyNc,9659
39
40
  langroid/agent/structured_message.py,sha256=y7pud1EgRNeTFZlJmBkLmwME3yQJ_IYik-Xds9kdZbY,282
40
- langroid/agent/task.py,sha256=p7YsccpRIZcX0dFo5ll9EAaetWUcxZnCZal0i7Xsl1Y,85433
41
+ langroid/agent/task.py,sha256=f7clh6p6Md0G4YGHqbFeeT88U4XoP0i3eatekV21hHE,86643
41
42
  langroid/agent/tool_message.py,sha256=jkN7uq7YwUC_wBcSCNUYjrB_His2YCfQay_lqIa4Tww,10498
42
43
  langroid/agent/tools/__init__.py,sha256=IMgCte-_ZIvCkozGQmvMqxIw7_nKLKzD78ccJL1bnQU,804
43
44
  langroid/agent/tools/duckduckgo_search_tool.py,sha256=NhsCaGZkdv28nja7yveAhSK_w6l_Ftym8agbrdzqgfo,1935
44
45
  langroid/agent/tools/file_tools.py,sha256=GjPB5YDILucYapElnvvoYpGJuZQ25ecLs2REv7edPEo,7292
45
46
  langroid/agent/tools/google_search_tool.py,sha256=y7b-3FtgXf0lfF4AYxrZ3K5pH2dhidvibUOAGBE--WI,1456
46
47
  langroid/agent/tools/metaphor_search_tool.py,sha256=qj4gt453cLEX3EGW7nVzVu6X7LCdrwjSlcNY0qJW104,2489
47
- langroid/agent/tools/orchestration.py,sha256=vp2Qx-DYPtDnACosxKqwHGy6DeD1QnEllWz0Ht81Cyc,10880
48
+ langroid/agent/tools/orchestration.py,sha256=EDv1EMVGYqX82x3bCRbTn9gFNs66oosiUM8WTSZkUJg,10909
48
49
  langroid/agent/tools/recipient_tool.py,sha256=dr0yTxgNEIoxUYxH6TtaExC4G_8WdJ0xGohIa4dFLhY,9808
49
50
  langroid/agent/tools/retrieval_tool.py,sha256=2q2pfoYbZNfbWQ0McxrtmfF0ekGglIgRl-6uF26pa-E,871
50
51
  langroid/agent/tools/rewind_tool.py,sha256=XAXL3BpNhCmBGYq_qi_sZfHJuIw7NY2jp4wnojJ7WRs,5606
@@ -141,8 +142,8 @@ langroid/vector_store/meilisearch.py,sha256=6frB7GFWeWmeKzRfLZIvzRjllniZ1cYj3Hmh
141
142
  langroid/vector_store/momento.py,sha256=qR-zBF1RKVHQZPZQYW_7g-XpTwr46p8HJuYPCkfJbM4,10534
142
143
  langroid/vector_store/qdrant_cloud.py,sha256=3im4Mip0QXLkR6wiqVsjV1QvhSElfxdFSuDKddBDQ-4,188
143
144
  langroid/vector_store/qdrantdb.py,sha256=v88lqFkepADvlN6lByUj9I4NEKa9X9lWH16uTPPbYrE,17457
144
- pyproject.toml,sha256=DQbq0Xo4Bc3sJYF2-1qA9qnnKT0X-x5BzC99OrxcTNg,7488
145
- langroid-0.20.0.dist-info/LICENSE,sha256=EgVbvA6VSYgUlvC3RvPKehSg7MFaxWDsFuzLOsPPfJg,1065
146
- langroid-0.20.0.dist-info/METADATA,sha256=5uni8xDGH809QGehHz0YfMguGFtvKDSJ9A7CfbduDnE,56758
147
- langroid-0.20.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
148
- langroid-0.20.0.dist-info/RECORD,,
145
+ pyproject.toml,sha256=m6gT-kRBlRmBQ50VGLcLDXOmo3MmaYPAHxbySfpGd1E,7488
146
+ langroid-0.20.1.dist-info/LICENSE,sha256=EgVbvA6VSYgUlvC3RvPKehSg7MFaxWDsFuzLOsPPfJg,1065
147
+ langroid-0.20.1.dist-info/METADATA,sha256=8qg7AF3qK12XRvtEqpR2P8eyidON7E4womWSiY3QW_E,56758
148
+ langroid-0.20.1.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
149
+ langroid-0.20.1.dist-info/RECORD,,
pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "langroid"
3
- version = "0.20.0"
3
+ version = "0.20.1"
4
4
  description = "Harness LLMs with Multi-Agent Programming"
5
5
  authors = ["Prasad Chalasani <pchalasani@gmail.com>"]
6
6
  readme = "README.md"