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 +10 -1
- langroid/agent/special/arangodb/arangodb_agent.py +204 -69
- langroid/agent/special/arangodb/system_messages.py +33 -7
- langroid/agent/special/arangodb/tools.py +64 -1
- langroid/agent/special/arangodb/utils.py +36 -0
- langroid/agent/task.py +77 -55
- langroid/agent/tools/orchestration.py +7 -7
- {langroid-0.20.0.dist-info → langroid-0.20.1.dist-info}/METADATA +1 -1
- {langroid-0.20.0.dist-info → langroid-0.20.1.dist-info}/RECORD +12 -11
- pyproject.toml +1 -1
- {langroid-0.20.0.dist-info → langroid-0.20.1.dist-info}/LICENSE +0 -0
- {langroid-0.20.0.dist-info → langroid-0.20.1.dist-info}/WHEEL +0 -0
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
|
-
"""
|
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:
|
92
|
+
kg_schema: str | Dict[str, List[Dict[str, Any]]] | None = None
|
92
93
|
database_created: bool = False
|
93
|
-
|
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
|
-
|
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
|
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
|
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("_"):
|
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:
|
443
|
+
if col_size == 0:
|
359
444
|
continue
|
360
445
|
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
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
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
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.
|
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.
|
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
|
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.
|
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
|
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
|
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 =
|
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
|
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
|
-
|
1945
|
-
|
1946
|
-
|
1947
|
-
|
1948
|
-
|
1949
|
-
|
1950
|
-
|
1951
|
-
|
1952
|
-
|
1953
|
-
|
1954
|
-
|
1955
|
-
|
1956
|
-
|
1957
|
-
|
1958
|
-
|
1959
|
-
|
1960
|
-
|
1961
|
-
|
1962
|
-
|
1963
|
-
|
1964
|
-
|
1965
|
-
|
1966
|
-
|
1967
|
-
|
1968
|
-
|
1969
|
-
|
1970
|
-
|
1971
|
-
|
1972
|
-
|
1973
|
-
|
1974
|
-
|
1975
|
-
|
1976
|
-
|
1977
|
-
|
1978
|
-
|
1979
|
-
|
1980
|
-
|
1981
|
-
|
1982
|
-
|
1983
|
-
|
1984
|
-
|
1985
|
-
|
1986
|
-
|
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
|
-
|
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
|
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
|
langroid/__init__.py,sha256=z_fCOLQJPOw3LLRPBlFB5-2HyCjpPgQa4m4iY5Fvb8Y,1800
|
2
2
|
langroid/agent/__init__.py,sha256=ll0Cubd2DZ-fsCMl7e10hf9ZjFGKzphfBco396IKITY,786
|
3
|
-
langroid/agent/base.py,sha256=
|
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=
|
15
|
-
langroid/agent/special/arangodb/system_messages.py,sha256=
|
16
|
-
langroid/agent/special/arangodb/tools.py,sha256=
|
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=
|
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=
|
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=
|
145
|
-
langroid-0.20.
|
146
|
-
langroid-0.20.
|
147
|
-
langroid-0.20.
|
148
|
-
langroid-0.20.
|
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
File without changes
|
File without changes
|