langroid 0.54.2__py3-none-any.whl → 0.55.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.
@@ -0,0 +1,151 @@
1
+ """Parser for done sequence DSL (Domain Specific Language).
2
+
3
+ Converts string patterns into DoneSequence objects for convenient task completion
4
+ configuration.
5
+
6
+ Examples:
7
+ "T, A" -> Tool followed by Agent response
8
+ "T[calculator], A" -> Specific tool 'calculator' followed by Agent response
9
+ "L, T, A, L" -> LLM, Tool, Agent, LLM sequence
10
+ "C[quit|exit]" -> Content matching regex pattern
11
+ """
12
+
13
+ import re
14
+ from typing import List, Union
15
+
16
+ from langroid.pydantic_v1 import BaseModel
17
+
18
+ from .task import AgentEvent, DoneSequence, EventType
19
+
20
+
21
+ def parse_done_sequence(sequence: Union[str, DoneSequence]) -> DoneSequence:
22
+ """Parse a string pattern or return existing DoneSequence unchanged.
23
+
24
+ Args:
25
+ sequence: Either a DoneSequence object or a string pattern to parse
26
+
27
+ Returns:
28
+ DoneSequence object
29
+
30
+ Raises:
31
+ ValueError: If the string pattern is invalid
32
+ """
33
+ if isinstance(sequence, DoneSequence):
34
+ return sequence
35
+
36
+ if not isinstance(sequence, str):
37
+ raise ValueError(f"Expected string or DoneSequence, got {type(sequence)}")
38
+
39
+ events = _parse_string_pattern(sequence)
40
+ return DoneSequence(events=events)
41
+
42
+
43
+ def _parse_string_pattern(pattern: str) -> List[AgentEvent]:
44
+ """Parse a string pattern into a list of AgentEvent objects.
45
+
46
+ Pattern format:
47
+ - Single letter codes: T, A, L, U, N, C
48
+ - Specific tools: T[tool_name]
49
+ - Content match: C[regex_pattern]
50
+ - Separated by commas, spaces allowed
51
+
52
+ Args:
53
+ pattern: String pattern to parse
54
+
55
+ Returns:
56
+ List of AgentEvent objects
57
+
58
+ Raises:
59
+ ValueError: If pattern is invalid
60
+ """
61
+ events = []
62
+
63
+ # Split by comma and strip whitespace
64
+ parts = [p.strip() for p in pattern.split(",")]
65
+
66
+ for part in parts:
67
+ if not part:
68
+ continue
69
+
70
+ event = _parse_event_token(part)
71
+ events.append(event)
72
+
73
+ if not events:
74
+ raise ValueError(f"No valid events found in pattern: {pattern}")
75
+
76
+ return events
77
+
78
+
79
+ def _parse_event_token(token: str) -> AgentEvent:
80
+ """Parse a single event token into an AgentEvent.
81
+
82
+ Args:
83
+ token: Single event token (e.g., "T", "T[calc]", "C[quit|exit]")
84
+
85
+ Returns:
86
+ AgentEvent object
87
+
88
+ Raises:
89
+ ValueError: If token is invalid
90
+ """
91
+ # Check for bracket notation
92
+ bracket_match = re.match(r"^([A-Z])\[([^\]]+)\]$", token)
93
+
94
+ if bracket_match:
95
+ event_code = bracket_match.group(1)
96
+ param = bracket_match.group(2)
97
+
98
+ if event_code == "T":
99
+ # Specific tool: T[tool_name]
100
+ return AgentEvent(event_type=EventType.SPECIFIC_TOOL, tool_name=param)
101
+ elif event_code == "C":
102
+ # Content match: C[regex_pattern]
103
+ return AgentEvent(event_type=EventType.CONTENT_MATCH, content_pattern=param)
104
+ else:
105
+ raise ValueError(
106
+ f"Invalid event code with brackets: {event_code}. "
107
+ "Only T[tool] and C[pattern] are supported."
108
+ )
109
+
110
+ # Simple single-letter codes
111
+ event_map = {
112
+ "T": EventType.TOOL,
113
+ "A": EventType.AGENT_RESPONSE,
114
+ "L": EventType.LLM_RESPONSE,
115
+ "U": EventType.USER_RESPONSE,
116
+ "N": EventType.NO_RESPONSE,
117
+ "C": EventType.CONTENT_MATCH, # C without brackets matches any content
118
+ }
119
+
120
+ if token in event_map:
121
+ return AgentEvent(event_type=event_map[token])
122
+
123
+ # If not a single letter, could be a full event type name
124
+ token_upper = token.upper()
125
+ if token_upper == "TOOL":
126
+ return AgentEvent(event_type=EventType.TOOL)
127
+ elif token_upper == "AGENT":
128
+ return AgentEvent(event_type=EventType.AGENT_RESPONSE)
129
+ elif token_upper == "LLM":
130
+ return AgentEvent(event_type=EventType.LLM_RESPONSE)
131
+ elif token_upper == "USER":
132
+ return AgentEvent(event_type=EventType.USER_RESPONSE)
133
+ else:
134
+ raise ValueError(
135
+ f"Invalid event token: '{token}'. "
136
+ "Valid tokens are: T, A, L, U, N, C, or T[tool_name], C[pattern]"
137
+ )
138
+
139
+
140
+ def parse_done_sequences(
141
+ sequences: List[Union[str, DoneSequence]]
142
+ ) -> List[DoneSequence]:
143
+ """Parse a list of mixed string patterns and DoneSequence objects.
144
+
145
+ Args:
146
+ sequences: List containing strings and/or DoneSequence objects
147
+
148
+ Returns:
149
+ List of DoneSequence objects
150
+ """
151
+ return [parse_done_sequence(seq) for seq in sequences]
langroid/agent/task.py CHANGED
@@ -6,6 +6,7 @@ import logging
6
6
  import re
7
7
  import threading
8
8
  from collections import Counter, OrderedDict, deque
9
+ from enum import Enum
9
10
  from pathlib import Path
10
11
  from types import SimpleNamespace
11
12
  from typing import (
@@ -20,6 +21,7 @@ from typing import (
20
21
  Tuple,
21
22
  Type,
22
23
  TypeVar,
24
+ Union,
23
25
  cast,
24
26
  overload,
25
27
  )
@@ -69,6 +71,38 @@ def noop_fn(*args: List[Any], **kwargs: Dict[str, Any]) -> None:
69
71
  pass
70
72
 
71
73
 
74
+ class EventType(str, Enum):
75
+ """Types of events that can occur in a task"""
76
+
77
+ TOOL = "tool" # Any tool generated
78
+ SPECIFIC_TOOL = "specific_tool" # Specific tool by name
79
+ LLM_RESPONSE = "llm_response" # LLM generates response
80
+ AGENT_RESPONSE = "agent_response" # Agent responds
81
+ USER_RESPONSE = "user_response" # User responds
82
+ CONTENT_MATCH = "content_match" # Response matches pattern
83
+ NO_RESPONSE = "no_response" # No valid response from entity
84
+ CUSTOM = "custom" # Custom condition
85
+
86
+
87
+ class AgentEvent(BaseModel):
88
+ """Single event in a task sequence"""
89
+
90
+ event_type: EventType
91
+ tool_name: Optional[str] = None # For SPECIFIC_TOOL
92
+ content_pattern: Optional[str] = None # For CONTENT_MATCH (regex)
93
+ responder: Optional[str] = None # Specific responder name
94
+ # Optionally match only if the responder was specific entity/task
95
+ sender: Optional[str] = None # Entity name or Task name that sent the message
96
+
97
+
98
+ class DoneSequence(BaseModel):
99
+ """A sequence of events that triggers task completion"""
100
+
101
+ events: List[AgentEvent]
102
+ # Optional name for debugging
103
+ name: Optional[str] = None
104
+
105
+
72
106
  class TaskConfig(BaseModel):
73
107
  """Configuration for a Task. This is a container for any params that
74
108
  we didn't include in the task `__init__` method.
@@ -108,6 +142,9 @@ class TaskConfig(BaseModel):
108
142
  contains a Tool attempt by the LLM
109
143
  (including tools not handled by the agent).
110
144
  Default is False.
145
+ done_sequences (List[DoneSequence]): List of event sequences that trigger task
146
+ completion. Task is done if ANY sequence matches the recent event history.
147
+ Each sequence is checked against the message parent chain.
111
148
 
112
149
  """
113
150
 
@@ -116,10 +153,12 @@ class TaskConfig(BaseModel):
116
153
  inf_loop_wait_factor: int = 5
117
154
  restart_as_subtask: bool = False
118
155
  logs_dir: str = "logs"
156
+ enable_loggers: bool = True
119
157
  addressing_prefix: str = ""
120
158
  allow_subtask_multi_oai_tools: bool = True
121
159
  recognize_string_signals: bool = True
122
160
  done_if_tool: bool = False
161
+ done_sequences: Optional[List[Union[str, DoneSequence]]] = None
123
162
 
124
163
 
125
164
  class Task:
@@ -257,6 +296,14 @@ class Task:
257
296
  set_parent_agent=noop_fn,
258
297
  )
259
298
  self.config = config
299
+ # Store parsed done sequences
300
+ self._parsed_done_sequences: Optional[List[DoneSequence]] = None
301
+ if self.config.done_sequences:
302
+ from .done_sequence_parser import parse_done_sequences
303
+
304
+ self._parsed_done_sequences = parse_done_sequences(
305
+ self.config.done_sequences
306
+ )
260
307
  # how to behave as a sub-task; can be overridden by `add_sub_task()`
261
308
  self.config_sub_task = copy.deepcopy(config)
262
309
  # counts of distinct pending messages in history,
@@ -360,6 +407,8 @@ class Task:
360
407
  self.single_round = single_round
361
408
  self.turns = -1 # no limit
362
409
  self.llm_delegate = llm_delegate
410
+ # Track last responder for done sequence checking
411
+ self._last_responder: Optional[Responder] = None
363
412
  if llm_delegate:
364
413
  if self.single_round:
365
414
  # 0: User instructs (delegating to LLM);
@@ -586,6 +635,9 @@ class Task:
586
635
  """Initialise per-task Rich and TSV loggers."""
587
636
  from langroid.utils.logging import RichFileLogger
588
637
 
638
+ if not self.config.enable_loggers:
639
+ return
640
+
589
641
  if self.caller is not None and self.caller.logger is not None:
590
642
  self.logger = self.caller.logger
591
643
  elif self.logger is None:
@@ -1276,6 +1328,9 @@ class Task:
1276
1328
 
1277
1329
  self._update_no_answer_vars(result)
1278
1330
 
1331
+ # Store the last responder for done sequence checking
1332
+ self._last_responder = r
1333
+
1279
1334
  # pending_sender is of type Responder,
1280
1335
  # i.e. it is either one of the agent's entities
1281
1336
  # OR a sub-task, that has produced a valid response.
@@ -1841,6 +1896,23 @@ class Task:
1841
1896
  ):
1842
1897
  return (True, StatusCode.DONE)
1843
1898
 
1899
+ # Check done sequences
1900
+ if self._parsed_done_sequences and result is not None:
1901
+ # Get the message chain from the current result
1902
+ msg_chain = self._get_message_chain(result)
1903
+
1904
+ # Use last responder if r not provided
1905
+ responder = r if r is not None else self._last_responder
1906
+
1907
+ # Check each sequence
1908
+ for sequence in self._parsed_done_sequences:
1909
+ if self._matches_sequence_with_current(
1910
+ msg_chain, sequence, result, responder
1911
+ ):
1912
+ seq_name = sequence.name or "unnamed"
1913
+ logger.info(f"Task {self.name} done: matched sequence '{seq_name}'")
1914
+ return (True, StatusCode.DONE)
1915
+
1844
1916
  allow_done_string = self.config.recognize_string_signals
1845
1917
  # An entity decided task is done, either via DoneTool,
1846
1918
  # or by explicitly saying DONE
@@ -2127,3 +2199,196 @@ class Task:
2127
2199
  return False, addressee, content_to_send
2128
2200
 
2129
2201
  return None, None, None
2202
+
2203
+ def _classify_event(
2204
+ self, msg: ChatDocument | None, responder: Responder | None
2205
+ ) -> Optional[AgentEvent]:
2206
+ """Classify a message into an AgentEvent for sequence matching."""
2207
+ if msg is None:
2208
+ return AgentEvent(event_type=EventType.NO_RESPONSE)
2209
+
2210
+ # Determine the event type based on responder and message content
2211
+ event_type = EventType.NO_RESPONSE
2212
+ tool_name = None
2213
+
2214
+ # Check if there are tool messages
2215
+ tool_messages = self.agent.try_get_tool_messages(msg, all_tools=True)
2216
+ if tool_messages:
2217
+ event_type = EventType.TOOL
2218
+ if len(tool_messages) == 1:
2219
+ tool_name = tool_messages[0].request
2220
+
2221
+ # Check responder type
2222
+ if responder == Entity.LLM and not tool_messages:
2223
+ event_type = EventType.LLM_RESPONSE
2224
+ elif responder == Entity.AGENT:
2225
+ event_type = EventType.AGENT_RESPONSE
2226
+ elif responder == Entity.USER:
2227
+ event_type = EventType.USER_RESPONSE
2228
+ elif isinstance(responder, Task):
2229
+ # For sub-task responses, check the sender in metadata
2230
+ if msg.metadata.sender == Entity.LLM:
2231
+ event_type = EventType.LLM_RESPONSE
2232
+ elif msg.metadata.sender == Entity.AGENT:
2233
+ event_type = EventType.AGENT_RESPONSE
2234
+ else:
2235
+ event_type = EventType.USER_RESPONSE
2236
+
2237
+ # Get sender name
2238
+ sender_name = None
2239
+ if isinstance(responder, Entity):
2240
+ sender_name = responder.value
2241
+ elif isinstance(responder, Task):
2242
+ sender_name = responder.name
2243
+
2244
+ return AgentEvent(
2245
+ event_type=event_type,
2246
+ tool_name=tool_name,
2247
+ sender=sender_name,
2248
+ )
2249
+
2250
+ def _get_message_chain(
2251
+ self, msg: ChatDocument | None, max_depth: Optional[int] = None
2252
+ ) -> List[ChatDocument]:
2253
+ """Get the chain of messages by following parent pointers."""
2254
+ if max_depth is None:
2255
+ # Get max depth needed from all sequences
2256
+ max_depth = 50 # default fallback
2257
+ if self._parsed_done_sequences:
2258
+ max_depth = max(len(seq.events) for seq in self._parsed_done_sequences)
2259
+
2260
+ chain = []
2261
+ current = msg
2262
+ depth = 0
2263
+
2264
+ while current is not None and depth < max_depth:
2265
+ chain.append(current)
2266
+ current = current.parent
2267
+ depth += 1
2268
+
2269
+ # Reverse to get chronological order (oldest first)
2270
+ return list(reversed(chain))
2271
+
2272
+ def _matches_event(self, actual: AgentEvent, expected: AgentEvent) -> bool:
2273
+ """Check if an actual event matches an expected event pattern."""
2274
+ # Check event type
2275
+ if expected.event_type == EventType.SPECIFIC_TOOL:
2276
+ if actual.event_type != EventType.TOOL:
2277
+ return False
2278
+ if expected.tool_name and actual.tool_name != expected.tool_name:
2279
+ return False
2280
+ elif actual.event_type != expected.event_type:
2281
+ return False
2282
+
2283
+ # Check sender if specified
2284
+ if expected.sender and actual.sender != expected.sender:
2285
+ return False
2286
+
2287
+ # TODO: Add content pattern matching for CONTENT_MATCH type
2288
+
2289
+ return True
2290
+
2291
+ def _matches_sequence(
2292
+ self, msg_chain: List[ChatDocument], sequence: DoneSequence
2293
+ ) -> bool:
2294
+ """Check if a message chain matches a done sequence.
2295
+
2296
+ We traverse the message chain and try to match the sequence events.
2297
+ The events don't have to be consecutive in the chain.
2298
+ """
2299
+ if not sequence.events:
2300
+ return False
2301
+
2302
+ # Convert messages to events
2303
+ events = []
2304
+ for i, msg in enumerate(msg_chain):
2305
+ # Determine responder from metadata or by checking previous message
2306
+ responder = None
2307
+ if msg.metadata.sender:
2308
+ responder = msg.metadata.sender
2309
+ elif msg.metadata.sender_name:
2310
+ # Could be a task name - keep as None for now since we can't resolve
2311
+ # the actual Task object from just the name
2312
+ responder = None
2313
+
2314
+ event = self._classify_event(msg, responder)
2315
+ if event:
2316
+ events.append(event)
2317
+
2318
+ # Try to match the sequence
2319
+ seq_idx = 0
2320
+ for event in events:
2321
+ if seq_idx >= len(sequence.events):
2322
+ break
2323
+
2324
+ expected = sequence.events[seq_idx]
2325
+ if self._matches_event(event, expected):
2326
+ seq_idx += 1
2327
+
2328
+ # Check if we matched the entire sequence
2329
+ return seq_idx == len(sequence.events)
2330
+
2331
+ def _matches_sequence_with_current(
2332
+ self,
2333
+ msg_chain: List[ChatDocument],
2334
+ sequence: DoneSequence,
2335
+ current_msg: ChatDocument,
2336
+ current_responder: Optional[Responder],
2337
+ ) -> bool:
2338
+ """Check if the message chain plus current message matches a done sequence.
2339
+
2340
+ Process messages in reverse order (newest first) and match against
2341
+ the sequence events in reverse order.
2342
+ """
2343
+ # Add current message to chain if not already there
2344
+ if not msg_chain or msg_chain[-1].id() != current_msg.id():
2345
+ msg_chain = msg_chain + [current_msg]
2346
+
2347
+ # If we don't have enough messages for the sequence, can't match
2348
+ if len(msg_chain) < len(sequence.events):
2349
+ return False
2350
+
2351
+ # Process in reverse order - start from the end of both lists
2352
+ seq_idx = len(sequence.events) - 1
2353
+ msg_idx = len(msg_chain) - 1
2354
+
2355
+ while seq_idx >= 0 and msg_idx >= 0:
2356
+ msg = msg_chain[msg_idx]
2357
+ expected = sequence.events[seq_idx]
2358
+
2359
+ # Determine responder for this message
2360
+ if msg_idx == len(msg_chain) - 1 and current_responder is not None:
2361
+ # For the last message, use the current responder
2362
+ responder = current_responder
2363
+ else:
2364
+ # For other messages, determine from metadata
2365
+ responder = msg.metadata.sender
2366
+
2367
+ # Classify the event
2368
+ event = self._classify_event(msg, responder)
2369
+ if not event:
2370
+ return False
2371
+
2372
+ # Check if it matches
2373
+ matched = False
2374
+
2375
+ # Special handling for CONTENT_MATCH
2376
+ if (
2377
+ expected.event_type == EventType.CONTENT_MATCH
2378
+ and expected.content_pattern
2379
+ ):
2380
+ if re.search(expected.content_pattern, msg.content, re.IGNORECASE):
2381
+ matched = True
2382
+ elif self._matches_event(event, expected):
2383
+ matched = True
2384
+
2385
+ if not matched:
2386
+ # Strict matching - no skipping allowed
2387
+ return False
2388
+ else:
2389
+ # Matched! Move to next expected event
2390
+ seq_idx -= 1
2391
+ msg_idx -= 1
2392
+
2393
+ # We matched if we've matched all events in the sequence
2394
+ return seq_idx < 0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langroid
3
- Version: 0.54.2
3
+ Version: 0.55.1
4
4
  Summary: Harness LLMs with Multi-Agent Programming
5
5
  Author-email: Prasad Chalasani <pchalasani@gmail.com>
6
6
  License: MIT
@@ -209,7 +209,7 @@ Description-Content-Type: text/markdown
209
209
  [![PyPI - Version](https://img.shields.io/pypi/v/langroid)](https://pypi.org/project/langroid/)
210
210
  [![Downloads](https://img.shields.io/pypi/dm/langroid)](https://pypi.org/project/langroid/)
211
211
  [![Pytest](https://github.com/langroid/langroid/actions/workflows/pytest.yml/badge.svg)](https://github.com/langroid/langroid/actions/workflows/pytest.yml)
212
- [![codecov](https://codecov.io/gh/langroid/langroid/branch/main/graph/badge.svg?token=H94BX5F0TE)](https://codecov.io/gh/langroid/langroid)
212
+ [![codecov](https://codecov.io/gh/langroid/langroid/graph/badge.svg)](https://codecov.io/gh/langroid/langroid)
213
213
  [![Multi-Architecture DockerHub](https://github.com/langroid/langroid/actions/workflows/docker-publish.yml/badge.svg)](https://github.com/langroid/langroid/actions/workflows/docker-publish.yml)
214
214
 
215
215
  [![Static Badge](https://img.shields.io/badge/Documentation-blue?link=https%3A%2F%2Flangroid.github.io%2Flangroid%2F&link=https%3A%2F%2Flangroid.github.io%2Flangroid%2F)](https://langroid.github.io/langroid)
@@ -7,8 +7,9 @@ langroid/agent/base.py,sha256=a45iqoWet2W60h4wUIOyHDYlotgS0asqIjfbONT4fZQ,85706
7
7
  langroid/agent/batch.py,sha256=wpE9RqCNDVDhAXkCB7wEqfCIEAi6qKcrhaZ-Zr9T4C0,21375
8
8
  langroid/agent/chat_agent.py,sha256=2HIYzYxkrGkRIS97ioKfIqjaW3RbX89M39LjzBobBEY,88381
9
9
  langroid/agent/chat_document.py,sha256=6O20Fp4QrquykaF2jFtwNHkvcoDte1LLwVZNk9mVH9c,18057
10
+ langroid/agent/done_sequence_parser.py,sha256=GiLTVtzzKb_YQcPpwqOajKu97RSUzZC6ae_ZagDmt_A,4425
10
11
  langroid/agent/openai_assistant.py,sha256=JkAcs02bIrgPNVvUWVR06VCthc5-ulla2QMBzux_q6o,34340
11
- langroid/agent/task.py,sha256=Ns9SoeMoAHDW7OEcPHJ22cgQKsHzWSt9UAWi9BFl4NM,91366
12
+ langroid/agent/task.py,sha256=PbHVcyFgyxiMZXHOB5xr5t8qaeACYfrjNF_lZQc8d8Y,101308
12
13
  langroid/agent/tool_message.py,sha256=BhjP-_TfQ2tgxuY4Yo_JHLOwwt0mJ4BwjPnREvEY4vk,14744
13
14
  langroid/agent/xml_tool_message.py,sha256=oeBKnJNoGaKdtz39XoWGMTNlVyXew2MWH5lgtYeh8wQ,15496
14
15
  langroid/agent/callbacks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -133,7 +134,7 @@ langroid/vector_store/pineconedb.py,sha256=otxXZNaBKb9f_H75HTaU3lMHiaR2NUp5MqwLZ
133
134
  langroid/vector_store/postgres.py,sha256=wHPtIi2qM4fhO4pMQr95pz1ZCe7dTb2hxl4VYspGZoA,16104
134
135
  langroid/vector_store/qdrantdb.py,sha256=O6dSBoDZ0jzfeVBd7LLvsXu083xs2fxXtPa9gGX3JX4,18443
135
136
  langroid/vector_store/weaviatedb.py,sha256=Yn8pg139gOy3zkaPfoTbMXEEBCiLiYa1MU5d_3UA1K4,11847
136
- langroid-0.54.2.dist-info/METADATA,sha256=MyoCOXBAWkcArSbJp4sYz9ORHKlYwpcyo2zOAUxe5TQ,65395
137
- langroid-0.54.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
138
- langroid-0.54.2.dist-info/licenses/LICENSE,sha256=EgVbvA6VSYgUlvC3RvPKehSg7MFaxWDsFuzLOsPPfJg,1065
139
- langroid-0.54.2.dist-info/RECORD,,
137
+ langroid-0.55.1.dist-info/METADATA,sha256=iBKanly7KGZOhgDMib-Ll2ZqvLqNlpR2g2M2_KLBQRk,65366
138
+ langroid-0.55.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
139
+ langroid-0.55.1.dist-info/licenses/LICENSE,sha256=EgVbvA6VSYgUlvC3RvPKehSg7MFaxWDsFuzLOsPPfJg,1065
140
+ langroid-0.55.1.dist-info/RECORD,,