langroid 0.1.250__py3-none-any.whl → 0.1.252__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/__init__.py CHANGED
@@ -41,7 +41,7 @@ from .agent.chat_agent import (
41
41
  ChatAgentConfig,
42
42
  )
43
43
 
44
- from .agent.task import Task
44
+ from .agent.task import Task, TaskConfig
45
45
 
46
46
  try:
47
47
  from .agent.callbacks.chainlit import (
@@ -64,8 +64,11 @@ from .mytypes import (
64
64
  Entity,
65
65
  )
66
66
 
67
+ from .exceptions import InfiniteLoopException
68
+
67
69
  __all__ = [
68
70
  "mytypes",
71
+ "exceptions",
69
72
  "utils",
70
73
  "parsing",
71
74
  "prompts",
@@ -82,6 +85,7 @@ __all__ = [
82
85
  "ChatDocument",
83
86
  "ChatDocMetaData",
84
87
  "Task",
88
+ "TaskConfig",
85
89
  "DocMetaData",
86
90
  "Document",
87
91
  "Entity",
@@ -89,6 +93,7 @@ __all__ = [
89
93
  "run_batch_tasks",
90
94
  "llm_response_batch",
91
95
  "agent_response_batch",
96
+ "InfiniteLoopException",
92
97
  ]
93
98
  if chainlit_available:
94
99
  __all__.extend(
langroid/agent/base.py CHANGED
@@ -2,6 +2,7 @@ import asyncio
2
2
  import inspect
3
3
  import json
4
4
  import logging
5
+ import re
5
6
  from abc import ABC
6
7
  from contextlib import ExitStack
7
8
  from types import SimpleNamespace
@@ -19,7 +20,7 @@ from typing import (
19
20
  no_type_check,
20
21
  )
21
22
 
22
- from pydantic import BaseSettings, ValidationError
23
+ from pydantic import BaseSettings, ValidationError, validator
23
24
  from rich import print
24
25
  from rich.console import Console
25
26
  from rich.markup import escape
@@ -64,6 +65,15 @@ class AgentConfig(BaseSettings):
64
65
  prompts: Optional[PromptsConfig] = PromptsConfig()
65
66
  show_stats: bool = True # show token usage/cost stats?
66
67
 
68
+ @validator("name")
69
+ def check_name_alphanum(cls, v: str) -> str:
70
+ if not re.match(r"^[a-zA-Z0-9_-]+$", v):
71
+ raise ValueError(
72
+ "The name must only contain alphanumeric characters, "
73
+ "underscores, or hyphens, with no spaces"
74
+ )
75
+ return v
76
+
67
77
 
68
78
  def noop_fn(*args: List[Any], **kwargs: Dict[str, Any]) -> None:
69
79
  pass
@@ -87,6 +97,7 @@ class Agent(ABC):
87
97
  self.llm_tools_map: Dict[str, Type[ToolMessage]] = {}
88
98
  self.llm_tools_handled: Set[str] = set()
89
99
  self.llm_tools_usable: Set[str] = set()
100
+ self.interactive: bool | None = None
90
101
  self.total_llm_token_cost = 0.0
91
102
  self.total_llm_token_usage = 0
92
103
  self.token_stats_str = ""
@@ -223,8 +234,8 @@ class Agent(ABC):
223
234
  ):
224
235
  setattr(self, tool, lambda obj: obj.response(self))
225
236
 
226
- if hasattr(message_class, "handle_message_fallback") and inspect.isfunction(
227
- message_class.handle_message_fallback
237
+ if hasattr(message_class, "handle_message_fallback") and (
238
+ inspect.isfunction(message_class.handle_message_fallback)
228
239
  ):
229
240
  setattr(
230
241
  self,
@@ -279,9 +290,9 @@ class Agent(ABC):
279
290
  ]
280
291
  return "\n\n".join(sample_convo)
281
292
 
282
- def agent_response_template(self) -> ChatDocument:
293
+ def create_agent_response(self, content: str | None = None) -> ChatDocument:
283
294
  """Template for agent_response."""
284
- return self._response_template(Entity.AGENT)
295
+ return self._response_template(Entity.AGENT, content)
285
296
 
286
297
  async def agent_response_async(
287
298
  self,
@@ -342,19 +353,19 @@ class Agent(ABC):
342
353
  ),
343
354
  )
344
355
 
345
- def _response_template(self, e: Entity) -> ChatDocument:
356
+ def _response_template(self, e: Entity, content: str | None = None) -> ChatDocument:
346
357
  """Template for response from entity `e`."""
347
358
  return ChatDocument(
348
- content="",
359
+ content=content or "",
349
360
  tool_messages=[],
350
361
  metadata=ChatDocMetaData(
351
362
  source=e, sender=e, sender_name=self.config.name, tool_ids=[]
352
363
  ),
353
364
  )
354
365
 
355
- def user_response_template(self) -> ChatDocument:
366
+ def create_user_response(self, content: str | None = None) -> ChatDocument:
356
367
  """Template for user_response."""
357
- return self._response_template(Entity.USER)
368
+ return self._response_template(Entity.USER, content)
358
369
 
359
370
  async def user_response_async(
360
371
  self,
@@ -377,11 +388,21 @@ class Agent(ABC):
377
388
  (str) User response, packaged as a ChatDocument
378
389
 
379
390
  """
380
- if self.default_human_response is not None:
391
+
392
+ # When msg explicitly addressed to user, this means an actual human response
393
+ # is being sought.
394
+ need_human_response = (
395
+ isinstance(msg, ChatDocument) and msg.metadata.recipient == Entity.USER
396
+ )
397
+
398
+ interactive = (
399
+ self.interactive if self.interactive is not None else settings.interactive
400
+ )
401
+ if self.default_human_response is not None and not need_human_response:
381
402
  # useful for automated testing
382
403
  user_msg = self.default_human_response
383
- elif not settings.interactive:
384
- user_msg = ""
404
+ elif not interactive and not need_human_response:
405
+ return None
385
406
  else:
386
407
  if self.callbacks.get_user_response is not None:
387
408
  # ask user with empty prompt: no need for prompt
@@ -440,9 +461,9 @@ class Agent(ABC):
440
461
 
441
462
  return True
442
463
 
443
- def llm_response_template(self) -> ChatDocument:
464
+ def create_llm_response(self, content: str | None = None) -> ChatDocument:
444
465
  """Template for llm_response."""
445
- return self._response_template(Entity.LLM)
466
+ return self._response_template(Entity.LLM, content)
446
467
 
447
468
  @no_type_check
448
469
  async def llm_response_async(
@@ -736,6 +757,24 @@ class Agent(ABC):
736
757
 
737
758
  def _get_one_tool_message(self, json_str: str) -> Optional[ToolMessage]:
738
759
  json_data = json.loads(json_str)
760
+ # check if the json_data contains a "properties" field
761
+ # which further contains the actual tool-call
762
+ # (some weak LLMs do this). E.g. gpt-4o sometimes generates this:
763
+ # TOOL: {
764
+ # "type": "object",
765
+ # "properties": {
766
+ # "request": "square",
767
+ # "number": 9
768
+ # },
769
+ # "required": [
770
+ # "number",
771
+ # "request"
772
+ # ]
773
+ # }
774
+
775
+ properties = json_data.get("properties")
776
+ if properties is not None:
777
+ json_data = properties
739
778
  request = json_data.get("request")
740
779
  if (
741
780
  request is None
@@ -273,10 +273,11 @@ class ChatAgent(Agent):
273
273
  example = "" if self.config.use_tools else (msg_cls.usage_example())
274
274
  if example != "":
275
275
  example = "EXAMPLE: " + example
276
+ class_instructions = msg_cls.instructions()
276
277
  guidance = (
277
278
  ""
278
- if msg_cls.instructions() == ""
279
- else ("GUIDANCE: " + msg_cls.instructions())
279
+ if class_instructions == ""
280
+ else ("GUIDANCE: " + class_instructions)
280
281
  )
281
282
  if guidance == "" and example == "":
282
283
  continue
@@ -783,23 +784,20 @@ class ChatAgent(Agent):
783
784
  if self.llm is None:
784
785
  return
785
786
  if not citation_only and (not self.llm.get_stream() or is_cached):
786
- # We expect response to be LLMResponse in this context
787
- if not isinstance(response, LLMResponse):
788
- raise ValueError(
789
- "Expected response to be LLMResponse, but got "
790
- f"{type(response)} instead."
791
- )
792
787
  # We would have already displayed the msg "live" ONLY if
793
788
  # streaming was enabled, AND we did not find a cached response.
794
789
  # If we are here, it means the response has not yet been displayed.
795
790
  cached = f"[red]{self.indent}(cached)[/red]" if is_cached else ""
796
791
  if not settings.quiet:
792
+ chat_doc = (
793
+ response
794
+ if isinstance(response, ChatDocument)
795
+ else ChatDocument.from_LLMResponse(response, displayed=True)
796
+ )
797
797
  print(cached + "[green]" + escape(str(response)))
798
798
  self.callbacks.show_llm_response(
799
799
  content=str(response),
800
- is_tool=self.has_tool_message_attempt(
801
- ChatDocument.from_LLMResponse(response, displayed=True),
802
- ),
800
+ is_tool=self.has_tool_message_attempt(chat_doc),
803
801
  cached=is_cached,
804
802
  )
805
803
  if isinstance(response, LLMResponse):
@@ -70,13 +70,19 @@ class QueryPlanCriticConfig(LanceQueryPlanAgentConfig):
70
70
  plan execution FAILED, and your feedback should say INVALID along
71
71
  with the ERROR message, `suggested_fix` that aims to help the assistant
72
72
  fix the problem (or simply equals "address the the error shown in feedback")
73
+ - Ask yourself, is the ANSWER in the expected form, e.g.
74
+ if the question is asking for the name of an ENTITY with max SIZE,
75
+ then the answer should be the ENTITY name, NOT the SIZE!!
73
76
  - If the ANSWER is in the expected form, then the QUERY PLAN is likely VALID,
74
77
  and your feedback should say VALID, with empty `suggested_fix`.
78
+ ===> HOWEVER!!! Watch out for a spurious correct-looking answer, for EXAMPLE:
79
+ the query was to find the ENTITY with a maximum SIZE,
80
+ but the dataframe calculation is find the SIZE, NOT the ENTITY!!
75
81
  - If the ANSWER is {NO_ANSWER} or of the wrong form,
76
82
  then try to DIAGNOSE the problem IN THE FOLLOWING ORDER:
77
83
  - DATAFRAME CALCULATION -- is it doing the right thing?
78
84
  Is it finding the Index of a row instead of the value in a column?
79
- Or another example: mmaybe it is finding the maximum population
85
+ Or another example: maybe it is finding the maximum population
80
86
  rather than the CITY with the maximum population?
81
87
  If you notice a problem with the DATAFRAME CALCULATION, then
82
88
  ONLY SUBMIT FEEDBACK ON THE DATAFRAME CALCULATION, and DO NOT
@@ -195,7 +195,7 @@ class LanceQueryPlanAgent(ChatAgent):
195
195
  plan=self.curr_query_plan,
196
196
  answer=self.result,
197
197
  )
198
- response_tmpl = self.agent_response_template()
198
+ response_tmpl = self.create_agent_response()
199
199
  # ... add the QueryPlanAnswerTool to the response
200
200
  # (Notice how the Agent is directly sending a tool, not the LLM)
201
201
  response_tmpl.tool_messages = [query_plan_answer_tool]
langroid/agent/task.py CHANGED
@@ -1,14 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import asyncio
3
4
  import copy
4
5
  import logging
5
- import re
6
- from collections import Counter
6
+ from collections import Counter, deque
7
7
  from types import SimpleNamespace
8
8
  from typing import (
9
9
  Any,
10
10
  Callable,
11
11
  Coroutine,
12
+ Deque,
12
13
  Dict,
13
14
  List,
14
15
  Optional,
@@ -18,6 +19,8 @@ from typing import (
18
19
  cast,
19
20
  )
20
21
 
22
+ import numpy as np
23
+ from pydantic import BaseModel
21
24
  from rich import print
22
25
  from rich.markup import escape
23
26
 
@@ -30,8 +33,10 @@ from langroid.agent.chat_document import (
30
33
  StatusCode,
31
34
  )
32
35
  from langroid.cachedb.redis_cachedb import RedisCache, RedisCacheConfig
36
+ from langroid.exceptions import InfiniteLoopException
33
37
  from langroid.mytypes import Entity
34
38
  from langroid.parsing.parse_json import extract_top_level_json
39
+ from langroid.parsing.routing import parse_addressed_message
35
40
  from langroid.utils.configuration import settings
36
41
  from langroid.utils.constants import (
37
42
  DONE,
@@ -42,6 +47,7 @@ from langroid.utils.constants import (
42
47
  USER_QUIT_STRINGS,
43
48
  )
44
49
  from langroid.utils.logging import RichFileLogger, setup_file_logger
50
+ from langroid.utils.system import hash
45
51
 
46
52
  logger = logging.getLogger(__name__)
47
53
 
@@ -52,6 +58,23 @@ def noop_fn(*args: List[Any], **kwargs: Dict[str, Any]) -> None:
52
58
  pass
53
59
 
54
60
 
61
+ class TaskConfig(BaseModel):
62
+ """Configuration for a Task. This is a container for any params that
63
+ we didn't include in the task `__init__` method.
64
+ We may eventually move all the task __init__ params to this class, analogous to how
65
+ we have config classes for `Agent`, `ChatAgent`, `LanguageModel`, etc.
66
+
67
+ Attributes:
68
+ inf_loop_cycle_len: max exact-loop cycle length: 0 => no inf loop test
69
+ inf_loop_dominance_factor: dominance factor for exact-loop detection
70
+ inf_loop_wait_factor: wait this * cycle_len msgs before loop-check
71
+ """
72
+
73
+ inf_loop_cycle_len: int = 10
74
+ inf_loop_dominance_factor: float = 1.5
75
+ inf_loop_wait_factor: float = 5.0
76
+
77
+
55
78
  class Task:
56
79
  """
57
80
  A `Task` wraps an `Agent` object, and sets up the `Agent`'s goals and instructions.
@@ -102,6 +125,7 @@ class Task:
102
125
  max_stalled_steps: int = 5,
103
126
  done_if_no_response: List[Responder] = [],
104
127
  done_if_response: List[Responder] = [],
128
+ config: TaskConfig = TaskConfig(),
105
129
  ):
106
130
  """
107
131
  A task to be performed by an agent.
@@ -157,6 +181,11 @@ class Task:
157
181
  show_subtask_response=noop_fn,
158
182
  set_parent_agent=noop_fn,
159
183
  )
184
+ self.config = config
185
+ # counts of distinct pending messages in history,
186
+ # to help detect (exact) infinite loops
187
+ self.message_counter: Counter[str] = Counter()
188
+ self.history_count: Deque[int] = deque(maxlen=self.config.inf_loop_cycle_len)
160
189
  # copy the agent's config, so that we don't modify the original agent's config,
161
190
  # which may be shared by other agents.
162
191
  try:
@@ -201,10 +230,11 @@ class Task:
201
230
  agent.config.name = name
202
231
  self.name = name or agent.config.name
203
232
  self.value: str = self.name
204
- self.default_human_response = default_human_response
233
+
205
234
  if default_human_response is not None and default_human_response == "":
206
235
  interactive = False
207
236
  self.interactive = interactive
237
+ self.agent.interactive = interactive
208
238
  self.message_history_idx = -1
209
239
  if interactive:
210
240
  only_user_quits_root = True
@@ -213,6 +243,7 @@ class Task:
213
243
  only_user_quits_root = False
214
244
  if default_human_response is not None:
215
245
  self.agent.default_human_response = default_human_response
246
+ self.default_human_response = default_human_response
216
247
  if self.interactive:
217
248
  self.agent.default_human_response = None
218
249
  self.only_user_quits_root = only_user_quits_root
@@ -289,6 +320,7 @@ class Task:
289
320
  max_stalled_steps=self.max_stalled_steps,
290
321
  done_if_no_response=[Entity(s) for s in self.done_if_no_response],
291
322
  done_if_response=[Entity(s) for s in self.done_if_response],
323
+ config=self.config,
292
324
  )
293
325
 
294
326
  def __repr__(self) -> str:
@@ -448,6 +480,8 @@ class Task:
448
480
  self.max_tokens = max_tokens
449
481
  self.session_id = session_id
450
482
  self._set_alive()
483
+ self.message_counter.clear()
484
+ self.history_count.clear()
451
485
 
452
486
  assert (
453
487
  msg is None or isinstance(msg, str) or isinstance(msg, ChatDocument)
@@ -476,9 +510,25 @@ class Task:
476
510
  print("[magenta]Bye, hope this was useful!")
477
511
  break
478
512
  i += 1
479
- if turns > 0 and i >= turns:
513
+ max_turns = (
514
+ min(turns, settings.max_turns)
515
+ if turns > 0 and settings.max_turns > 0
516
+ else max(turns, settings.max_turns)
517
+ )
518
+ if max_turns > 0 and i >= max_turns:
480
519
  status = StatusCode.MAX_TURNS
481
520
  break
521
+ if (
522
+ self.config.inf_loop_cycle_len > 0
523
+ and i % self.config.inf_loop_cycle_len == 0
524
+ and self._maybe_infinite_loop()
525
+ ):
526
+ raise InfiniteLoopException(
527
+ """Possible infinite loop detected!
528
+ You can adjust infinite loop detection by changing the params
529
+ in the TaskConfig passed to the Task constructor:
530
+ e.g. set inf_loop_cycle_len=0 to disable loop detection."""
531
+ )
482
532
 
483
533
  final_result = self.result()
484
534
  if final_result is not None:
@@ -528,6 +578,8 @@ class Task:
528
578
  self.max_tokens = max_tokens
529
579
  self.session_id = session_id
530
580
  self._set_alive()
581
+ self.message_counter.clear()
582
+ self.history_count.clear()
531
583
 
532
584
  if (
533
585
  isinstance(msg, ChatDocument)
@@ -546,15 +598,32 @@ class Task:
546
598
  i = 0
547
599
  while True:
548
600
  await self.step_async()
601
+ await asyncio.sleep(0.01) # temp yield to avoid blocking
549
602
  done, status = self.done()
550
603
  if done:
551
604
  if self._level == 0 and not settings.quiet:
552
605
  print("[magenta]Bye, hope this was useful!")
553
606
  break
554
607
  i += 1
555
- if turns > 0 and i >= turns:
608
+ max_turns = (
609
+ min(turns, settings.max_turns)
610
+ if turns > 0 and settings.max_turns > 0
611
+ else max(turns, settings.max_turns)
612
+ )
613
+ if max_turns > 0 and i >= max_turns:
556
614
  status = StatusCode.MAX_TURNS
557
615
  break
616
+ if (
617
+ self.config.inf_loop_cycle_len > 0
618
+ and i % self.config.inf_loop_cycle_len == 0
619
+ and self._maybe_infinite_loop()
620
+ ):
621
+ raise InfiniteLoopException(
622
+ """Possible infinite loop detected!
623
+ You can adjust infinite loop detection by changing the params
624
+ in the TaskConfig passed to the Task constructor:
625
+ e.g. set inf_loop_cycle_len=0 to disable loop detection."""
626
+ )
558
627
 
559
628
  final_result = self.result()
560
629
  if final_result is not None:
@@ -824,6 +893,12 @@ class Task:
824
893
  # reset stuck counter since we made progress
825
894
  self.n_stalled_steps = 0
826
895
 
896
+ # update counters for infinite loop detection
897
+ if self.pending_message is not None:
898
+ hashed_msg = hash(str(self.pending_message))
899
+ self.message_counter.update([hashed_msg])
900
+ self.history_count.append(self.message_counter[hashed_msg])
901
+
827
902
  def _process_invalid_step_result(self, parent: ChatDocument | None) -> None:
828
903
  """
829
904
  Since step had no valid result from any responder, decide whether to update the
@@ -856,42 +931,6 @@ class Task:
856
931
  msg_str = escape(str(self.pending_message))
857
932
  print(f"[grey37][{sender_str}]{msg_str}[/grey37]")
858
933
 
859
- def _parse_routing(self, msg: ChatDocument | str) -> Tuple[bool | None, str | None]:
860
- """
861
- Parse routing instruction if any, of the form:
862
- PASS:<recipient> (pass current pending msg to recipient)
863
- SEND:<recipient> <content> (send content to recipient)
864
- Args:
865
- msg (ChatDocument|str|None): message to parse
866
- Returns:
867
- Tuple[bool,str|None]:
868
- bool: true=PASS, false=SEND, or None if neither
869
- str: recipient, or None
870
- """
871
- # handle routing instruction in result if any,
872
- # of the form PASS=<recipient>
873
- content = msg.content if isinstance(msg, ChatDocument) else msg
874
- content = content.strip()
875
- if PASS in content and PASS_TO not in content:
876
- return True, None
877
- if PASS_TO in content and content.split(":")[1] != "":
878
- return True, content.split(":")[1]
879
- if SEND_TO in content and (send_parts := re.split(r"[,: ]", content))[1] != "":
880
- # assume syntax is SEND_TO:<recipient> <content>
881
- # or SEND_TO:<recipient>,<content> or SEND_TO:<recipient>:<content>
882
- recipient = send_parts[1].strip()
883
- # get content to send, clean out routing instruction, and
884
- # start from 1 char after SEND_TO:<recipient>,
885
- # because we expect there is either a blank or some other separator
886
- # after the recipient
887
- content_to_send = content.replace(f"{SEND_TO}{recipient}", "").strip()[1:]
888
- # if no content then treat same as PASS_TO
889
- if content_to_send == "":
890
- return True, recipient
891
- else:
892
- return False, recipient
893
- return None, None
894
-
895
934
  def response(
896
935
  self,
897
936
  e: Responder,
@@ -929,7 +968,8 @@ class Task:
929
968
  # process result in case there is a routing instruction
930
969
  if result is None:
931
970
  return None
932
- is_pass, recipient = self._parse_routing(result)
971
+ # if result content starts with @name, set recipient to name
972
+ is_pass, recipient, content = parse_routing(result)
933
973
  if is_pass is None: # no routing, i.e. neither PASS nor SEND
934
974
  return result
935
975
  if is_pass:
@@ -949,9 +989,7 @@ class Task:
949
989
  elif recipient is not None:
950
990
  # we are sending non-empty content to non-null recipient
951
991
  # clean up result.content, set metadata.recipient and return
952
- result.content = result.content.replace(
953
- f"{SEND_TO}:{recipient}", ""
954
- ).strip()
992
+ result.content = content or ""
955
993
  result.metadata.recipient = recipient
956
994
  return result
957
995
  else:
@@ -1080,38 +1118,76 @@ class Task:
1080
1118
  or (not self._is_empty_message(result) and response_says_done)
1081
1119
  )
1082
1120
 
1083
- def _maybe_infinite_loop(self, history: int = 10) -> bool:
1121
+ def _maybe_infinite_loop(self) -> bool:
1084
1122
  """
1085
- TODO Not currently used, until we figure out best way.
1086
- Check if {NO_ANSWER}, empty answer, or a specific non-LLM msg occurs too
1087
- often in history of pending messages -- this can be an indicator of a possible
1088
- multi-step infinite loop that we should exit.
1089
- (A single-step infinite loop is where individual steps don't show progress
1090
- and are easy to detect via n_stalled_steps, but a multi-step infinite loop
1091
- could show "progress" at each step, but can still be an infinite loop, e.g.
1092
- if the steps are just alternating between two messages).
1123
+ Detect possible infinite loop based on message frequencies.
1124
+ NOTE: This only (attempts to) detect "exact" loops, i.e. a cycle
1125
+ of messages that repeats exactly, e.g.
1126
+ a r b i t r a t e r a t e r a t e r a t e ...
1127
+
1128
+ [It does not detect "approximate" loops, where the LLM is generating a
1129
+ sequence of messages that are similar, but not exactly the same.]
1130
+
1131
+ Intuition: when you look at a sufficiently long sequence with an m-message
1132
+ loop, then the frequencies of these m messages will "dominate" those
1133
+ of all other messages.
1134
+
1135
+ 1. First find m "dominant" messages, i.e. when arranged in decreasing
1136
+ frequency order, find the m such that
1137
+ freq[m] > F * freq[m+1] and
1138
+ freq[m] > W + freq[m+1]
1139
+ where F = config.inf_loop_dominance_factor (default 1.5) and
1140
+ W = config.inf_loop_wait_factor (default 5).
1141
+ So if you plot these frequencies in decreasing order,
1142
+ you will see a big drop in the plot, from m to m+1.
1143
+ We call the freqs until m the "dominant" freqs.
1144
+ 2. Say we found m such dominant frequencies.
1145
+ If these are the same as the freqs of the last m messages,
1146
+ then we are likely in a loop.
1093
1147
  """
1094
- p = self.pending_message
1095
- n_no_answers = 0
1096
- n_empty_answers = 0
1097
- counter: Counter[str] = Counter()
1098
- # count number of NO_ANSWER and empty answers in last up to 10 messages
1099
- # in ancestors of self.pending_message
1100
- for _ in range(history):
1101
- if p is None:
1102
- break
1103
- n_no_answers += p.content.strip() == NO_ANSWER
1104
- n_empty_answers += p.content.strip() == "" and p.function_call is None
1105
- if p.metadata.sender != Entity.LLM and PASS not in p.content:
1106
- counter.update([p.metadata.sender + ":" + p.content])
1107
- p = p.metadata.parent
1108
-
1109
- # freq of most common message in history
1110
- high_freq = (counter.most_common(1) or [("", 0)])[0][1]
1111
- # We deem this a potential infinite loop if:
1112
- # - a specific non-LLM msg occurs too often, or
1113
- # - a NO_ANSWER or empty answer occurs too often
1114
- return max(high_freq, n_no_answers) > self.max_stalled_steps
1148
+ max_cycle_len = self.config.inf_loop_cycle_len
1149
+ if max_cycle_len <= 0:
1150
+ # no loop detection
1151
+ return False
1152
+ wait_factor = self.config.inf_loop_wait_factor
1153
+ if sum(self.message_counter.values()) < wait_factor * max_cycle_len:
1154
+ # we haven't seen enough messages to detect a loop
1155
+ return False
1156
+
1157
+ most_common_msg_counts: List[Tuple[str, int]] = (
1158
+ self.message_counter.most_common(max_cycle_len + 1)
1159
+ )
1160
+ # get the most dominant msgs, i.e. these are at least 1.5x more freq
1161
+ # than the rest
1162
+ F = self.config.inf_loop_dominance_factor
1163
+ # counts array in non-increasing order
1164
+ counts = np.array([c for _, c in most_common_msg_counts])
1165
+ # find first index where counts[i] > F * counts[i+1]
1166
+ ratios = counts[:-1] / counts[1:]
1167
+ diffs = counts[:-1] - counts[1:]
1168
+ indices = np.where((ratios > F) & (diffs > wait_factor))[0]
1169
+ m = indices[0] if indices.size > 0 else -1
1170
+ if m < 0:
1171
+ # no dominance found, but...
1172
+ if len(most_common_msg_counts) <= max_cycle_len:
1173
+ # ...The most-common messages are at most max_cycle_len,
1174
+ # even though we looked for the most common (max_cycle_len + 1) msgs.
1175
+ # This means there are only at most max_cycle_len distinct messages,
1176
+ # which also indicates a possible loop.
1177
+ m = len(most_common_msg_counts) - 1
1178
+ else:
1179
+ # ... we have enough messages, but no dominance found,
1180
+ # so there COULD be loops longer than max_cycle_len,
1181
+ # OR there is no loop at all; we can't tell, so we return False.
1182
+ return False
1183
+
1184
+ dominant_msg_counts = most_common_msg_counts[: m + 1]
1185
+ # if the dominant m message counts are the same as the
1186
+ # counts of the last m messages, then we are likely in a loop
1187
+ dominant_counts = sorted([c for _, c in dominant_msg_counts])
1188
+ recent_counts = sorted(list(self.history_count)[-(m + 1) :])
1189
+
1190
+ return dominant_counts == recent_counts
1115
1191
 
1116
1192
  def done(
1117
1193
  self, result: ChatDocument | None = None, r: Responder | None = None
@@ -1289,7 +1365,9 @@ class Task:
1289
1365
  return (
1290
1366
  self.pending_message is not None
1291
1367
  and (recipient := self.pending_message.metadata.recipient) != ""
1292
- and recipient not in (e.name, self.name)
1368
+ and recipient != e # case insensitive
1369
+ and recipient != e.name
1370
+ and recipient != self.name # case sensitive
1293
1371
  )
1294
1372
 
1295
1373
  def _can_respond(self, e: Responder) -> bool:
@@ -1316,3 +1394,53 @@ class Task:
1316
1394
 
1317
1395
  """
1318
1396
  self.color_log = enable
1397
+
1398
+
1399
+ def parse_routing(
1400
+ msg: ChatDocument | str,
1401
+ ) -> Tuple[bool | None, str | None, str | None]:
1402
+ """
1403
+ Parse routing instruction if any, of the form:
1404
+ PASS:<recipient> (pass current pending msg to recipient)
1405
+ SEND:<recipient> <content> (send content to recipient)
1406
+ @<recipient> <content> (send content to recipient)
1407
+ Args:
1408
+ msg (ChatDocument|str|None): message to parse
1409
+ Returns:
1410
+ Tuple[bool|None, str|None, str|None]:
1411
+ bool: true=PASS, false=SEND, or None if neither
1412
+ str: recipient, or None
1413
+ str: content to send, or None
1414
+ """
1415
+ # handle routing instruction in result if any,
1416
+ # of the form PASS=<recipient>
1417
+ content = msg.content if isinstance(msg, ChatDocument) else msg
1418
+ content = content.strip()
1419
+ if PASS in content and PASS_TO not in content:
1420
+ return True, None, None
1421
+ if PASS_TO in content and content.split(":")[1] != "":
1422
+ return True, content.split(":")[1], None
1423
+ if (
1424
+ SEND_TO in content
1425
+ and (addressee_content := parse_addressed_message(content, SEND_TO))[0]
1426
+ is not None
1427
+ ):
1428
+ (addressee, content_to_send) = addressee_content
1429
+ # if no content then treat same as PASS_TO
1430
+ if content_to_send == "":
1431
+ return True, addressee, None
1432
+ else:
1433
+ return False, addressee, content_to_send
1434
+ AT = "@"
1435
+ if (
1436
+ AT in content
1437
+ and (addressee_content := parse_addressed_message(content, AT))[0] is not None
1438
+ ):
1439
+ (addressee, content_to_send) = addressee_content
1440
+ # if no content then treat same as PASS_TO
1441
+ if content_to_send == "":
1442
+ return True, addressee, None
1443
+ else:
1444
+ return False, addressee, content_to_send
1445
+
1446
+ return None, None, None
@@ -52,7 +52,10 @@ class ToolMessage(ABC, BaseModel):
52
52
 
53
53
  @classmethod
54
54
  def instructions(cls) -> str:
55
- return ""
55
+ return """
56
+ IMPORTANT: When using this or any other tool/function, you MUST include a
57
+ `request` field and set it equal to the FUNCTION/TOOL NAME you intend to use.
58
+ """
56
59
 
57
60
  @classmethod
58
61
  def require_recipient(cls) -> Type["ToolMessage"]:
@@ -97,9 +97,13 @@ class RecipientTool(ToolMessage):
97
97
  content: str
98
98
 
99
99
  @classmethod
100
- def create(cls, recipients: List[str]) -> Type["RecipientTool"]:
100
+ def create(cls, recipients: List[str], default: str = "") -> Type["RecipientTool"]:
101
+ """Create a restricted version of RecipientTool that
102
+ only allows certain recipients, and possibly sets a default recipient."""
103
+
101
104
  class RecipientToolRestricted(cls): # type: ignore
102
105
  allowed_recipients = recipients
106
+ default_recipient = default
103
107
 
104
108
  return RecipientToolRestricted
105
109
 
@@ -133,16 +137,18 @@ class RecipientTool(ToolMessage):
133
137
 
134
138
  def response(self, agent: ChatAgent) -> str | ChatDocument:
135
139
  """
136
- When LLM has correctly used this tool, set the agent's `recipient_tool_used`
137
- field to True, and construct a ChatDocument with an explicit recipient,
140
+ When LLM has correctly used this tool,
141
+ construct a ChatDocument with an explicit recipient,
138
142
  and make it look like it is from the LLM.
139
143
 
140
144
  Returns:
141
145
  (ChatDocument): with content set to self.content and
142
- metadata.recipient set to self.recipient.
146
+ metadata.recipient set to self.intended_recipient.
143
147
  """
144
-
145
- if self.intended_recipient == "":
148
+ default_recipient = self.__class__.default_value("default_recipient")
149
+ if self.intended_recipient == "" and default_recipient not in ["", None]:
150
+ self.intended_recipient = default_recipient
151
+ elif self.intended_recipient == "":
146
152
  # save the content as a class-variable, so that
147
153
  # we can construct the ChatDocument once the LLM specifies a recipient.
148
154
  # This avoids having to re-generate the entire message, saving time + cost.
@@ -198,9 +204,11 @@ class RecipientTool(ToolMessage):
198
204
  # since the recipient will differ from the task name.
199
205
  # So if this method is called, we can be sure that the recipient has not
200
206
  # been specified.
201
- if isinstance(msg, str):
202
- return None
203
- if msg.metadata.sender != Entity.LLM:
207
+ if (
208
+ isinstance(msg, str)
209
+ or msg.metadata.sender != Entity.LLM
210
+ or msg.metadata.recipient != "" # there IS an explicit recipient
211
+ ):
204
212
  return None
205
213
  content = msg if isinstance(msg, str) else msg.content
206
214
  # save the content as a class-variable, so that
langroid/exceptions.py ADDED
@@ -0,0 +1,3 @@
1
+ class InfiniteLoopException(Exception):
2
+ def __init__(self, message: str = "Infinite loop detected", *args: object) -> None:
3
+ super().__init__(message, *args)
langroid/mytypes.py CHANGED
@@ -22,6 +22,18 @@ class Entity(str, Enum):
22
22
  USER = "User"
23
23
  SYSTEM = "System"
24
24
 
25
+ def __eq__(self, other: object) -> bool:
26
+ """Allow case-insensitive comparison with strings."""
27
+ if isinstance(other, str):
28
+ return self.value.lower() == other.lower()
29
+ return super().__eq__(other)
30
+
31
+ def __hash__(self) -> int:
32
+ """Override this to ensure hashability of the enum,
33
+ so it can be used sets and dictionary keys.
34
+ """
35
+ return hash(self.value.lower())
36
+
25
37
 
26
38
  class DocMetaData(BaseModel):
27
39
  """Metadata for a document."""
@@ -0,0 +1,27 @@
1
+ import re
2
+ from typing import Optional, Tuple
3
+
4
+
5
+ def parse_addressed_message(
6
+ content: str, addressing: str = "@"
7
+ ) -> Tuple[Optional[str], str]:
8
+ # escape special characters in addressing prefix for regex use
9
+ addressing_escaped = re.escape(addressing)
10
+ pattern = rf"{addressing_escaped}(\w+)[,:\s]?"
11
+ # Regular expression to find a username prefixed by addressing character or string
12
+ match = re.findall(pattern, content)
13
+
14
+ addressee = None
15
+ if match:
16
+ # select the last match as the addressee
17
+ addressee = match[-1]
18
+
19
+ # Remove the last occurrence of the addressing prefix followed by the
20
+ # username and optional punctuation or whitespace
21
+ # To remove only the last occurrence, we'll construct a new pattern that
22
+ # specifically matches the last addressee
23
+ last_occurrence_pattern = rf"{addressing_escaped}{addressee}[,:\\s]?"
24
+ # Replace the last occurrence found in the content
25
+ content = re.sub(last_occurrence_pattern, "", content, count=1).strip()
26
+
27
+ return addressee, content
@@ -11,6 +11,7 @@ class Settings(BaseSettings):
11
11
  # NOTE all of these can be overridden in your .env file with upper-case names,
12
12
  # for example CACHE_TYPE=momento
13
13
  debug: bool = False # show debug messages?
14
+ max_turns: int = -1 # maximum number of turns in a task (to avoid inf loop)
14
15
  progress: bool = False # show progress spinners/bars?
15
16
  stream: bool = True # stream output?
16
17
  cache: bool = True # use cache?
langroid/utils/system.py CHANGED
@@ -6,6 +6,7 @@ import logging
6
6
  import shutil
7
7
  import socket
8
8
  import traceback
9
+ import uuid
9
10
  from typing import Any
10
11
 
11
12
  logger = logging.getLogger(__name__)
@@ -153,3 +154,22 @@ def update_hash(hash: str | None = None, s: str = "") -> str:
153
154
 
154
155
  # Return the updated hash in hexadecimal format and the original string
155
156
  return hash_obj.hexdigest()
157
+
158
+
159
+ def hash(s: str) -> str:
160
+ """
161
+ Generate a SHA256 hash of a string.
162
+
163
+ Args:
164
+ s (str): The string to hash.
165
+
166
+ Returns:
167
+ str: The SHA256 hash of the string.
168
+ """
169
+ return update_hash(s=s)
170
+
171
+
172
+ def generate_unique_id() -> str:
173
+ """Generate a unique ID using UUID4."""
174
+ unique_id = str(uuid.uuid4())
175
+ return unique_id
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: langroid
3
- Version: 0.1.250
3
+ Version: 0.1.252
4
4
  Summary: Harness LLMs with Multi-Agent Programming
5
5
  License: MIT
6
6
  Author: Prasad Chalasani
@@ -1,10 +1,10 @@
1
- langroid/__init__.py,sha256=zsYpGiAUsvyzZzjm964NUamsJImrXSJPVGz9a2jE_uY,1679
1
+ langroid/__init__.py,sha256=GXjDRv1rVNCxEB0BkfPIT4xgldKkB20b7kGPuGxNdiI,1803
2
2
  langroid/agent/__init__.py,sha256=_D8dxnfdr92ch1CIrUkKjrB5HVvsQdn62b1Fb2kBxV8,785
3
- langroid/agent/base.py,sha256=5HQ9fAFTQL771rhx7UkVkL90bjxSVA1DjtvxV8_1RJA,35652
3
+ langroid/agent/base.py,sha256=6EznAMa4zeRdouo3U3_UzOI_cblvdpNH4v5CAM-fgbA,37171
4
4
  langroid/agent/batch.py,sha256=feRA_yRG768ElOQjrKEefcRv6Aefd_yY7qktuYUQDwc,10040
5
5
  langroid/agent/callbacks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  langroid/agent/callbacks/chainlit.py,sha256=LboE3zlLLzClKxpBkzHX4XU6fW4lNZW97zwwN97uuaU,21067
7
- langroid/agent/chat_agent.py,sha256=YwlRMWoK_7vEl9pxgMeUjQAgsukrnZjDGJR0WVomSuQ,39592
7
+ langroid/agent/chat_agent.py,sha256=_xsBfGBkwcHd8IRErsW7tKNz7qn0h2oKSg_BFleOPCs,39475
8
8
  langroid/agent/chat_document.py,sha256=uwCq53SHRyxQw6qyhjzPYuJG48VHBgOf2122Ew3fk6c,9316
9
9
  langroid/agent/helpers.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  langroid/agent/junk,sha256=LxfuuW7Cijsg0szAzT81OjWWv1PMNI-6w_-DspVIO2s,339
@@ -13,9 +13,9 @@ langroid/agent/special/__init__.py,sha256=NG0JkB5y4K0bgnd9Q9UIvFExun3uTfVOWEVLVy
13
13
  langroid/agent/special/doc_chat_agent.py,sha256=MTUrUyCZ7_wksTo11AwSTHMOKZf1WX-cAJowi_sfT2o,55320
14
14
  langroid/agent/special/lance_doc_chat_agent.py,sha256=USp0U3eTaJzwF_3bdqE7CedSLbaqAi2tm-VzygcyLaA,10175
15
15
  langroid/agent/special/lance_rag/__init__.py,sha256=QTbs0IVE2ZgDg8JJy1zN97rUUg4uEPH7SLGctFNumk4,174
16
- langroid/agent/special/lance_rag/critic_agent.py,sha256=OsOcpcU_AmU2MagpZ5X5yxFeXyteKN9QJMzJGqIITig,6871
16
+ langroid/agent/special/lance_rag/critic_agent.py,sha256=ufTdpHSeHgCzN85Q0sfWOrpBpsCjGVZdAg5yOH1ogU8,7296
17
17
  langroid/agent/special/lance_rag/lance_rag_task.py,sha256=l_HQgrYY-CX2FwIsS961aEF3bYog3GDYo98fj0C0mSk,2889
18
- langroid/agent/special/lance_rag/query_planner_agent.py,sha256=U_2V8l3M44R3mX-El3wG1k-u2lTToU-HGfKvRkEWoEA,9816
18
+ langroid/agent/special/lance_rag/query_planner_agent.py,sha256=wSkrtY3Qz98KAqpVf0xMf4LRgKbHLASWVNUrbqwUAB0,9814
19
19
  langroid/agent/special/lance_tools.py,sha256=btMwKdcT8RdwAjmzbtN1xxm3s1H7ipO9GSpUamryYx8,1456
20
20
  langroid/agent/special/neo4j/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  langroid/agent/special/neo4j/csv_kg_chat.py,sha256=koL3sKtHm3aRkLTiARs54ngrcU3lOR1WaLLc_i8rWOU,6374
@@ -32,15 +32,15 @@ langroid/agent/special/sql/utils/populate_metadata.py,sha256=x2OMKfmIBnJESBG3qKt
32
32
  langroid/agent/special/sql/utils/system_message.py,sha256=qKLHkvQWRQodTtPLPxr1GSLUYUFASZU8x-ybV67cB68,1885
33
33
  langroid/agent/special/sql/utils/tools.py,sha256=6uB2424SLtmapui9ggcEr0ZTiB6_dL1-JRGgN8RK9Js,1332
34
34
  langroid/agent/special/table_chat_agent.py,sha256=xz4nWndTYTykET_oaveHcZUQ8IEpmA5yB8QGTXfOifw,9624
35
- langroid/agent/task.py,sha256=b_d46txohISETxXJoWpmIX0hinvt1wjHbK08LZRBEz8,54020
36
- langroid/agent/tool_message.py,sha256=2kPsQUwi3ZzINTUNj10huKnZLjLp5SXmefacTHx8QDc,8304
35
+ langroid/agent/task.py,sha256=J4GIvbPMlL9uXKr7J-bSPHGvFK5tI2R0INgbnbFRmqk,59265
36
+ langroid/agent/tool_message.py,sha256=aNSYLiRCszPZXJpKt7bGXWjBRzJVJIZoTjQXy7RHMQs,8486
37
37
  langroid/agent/tools/__init__.py,sha256=q-maq3k2BXhPAU99G0H6-j_ozoRvx15I1RFpPVicQIU,304
38
38
  langroid/agent/tools/duckduckgo_search_tool.py,sha256=mLGhlgs6pwbYZIwrOs9shfh1dMBVT4DtkR29pYL3cCQ,1900
39
39
  langroid/agent/tools/extract_tool.py,sha256=u5lL9rKBzaLBOrRyLnTAZ97pQ1uxyLP39XsWMnpaZpw,3789
40
40
  langroid/agent/tools/generator_tool.py,sha256=y0fB0ZObjA0b3L0uSTtrqRCKHDUR95arBftqiUeKD2o,663
41
41
  langroid/agent/tools/google_search_tool.py,sha256=cQxcNtb8XCNpOo_yCeYRwG_y-OATjPgkbr01kea9qWE,1421
42
42
  langroid/agent/tools/metaphor_search_tool.py,sha256=NKHss-AkI942_XhfMgUctAwHjIHpqp5NfYIebKV4UcE,2454
43
- langroid/agent/tools/recipient_tool.py,sha256=61vdKv06qgVdtnE3gxjzV8RvUEy8JhbC9eWa0J0BPdw,9171
43
+ langroid/agent/tools/recipient_tool.py,sha256=NrLxIeQT-kbMv7AeYX0uqvGeMK4Q3fIDvG15OVzlgk8,9624
44
44
  langroid/agent/tools/retrieval_tool.py,sha256=6uvRNg-kG_ItPa3sF9NWkthQ5frHn8bkB1Z3GSd3Oas,836
45
45
  langroid/agent/tools/run_python_code.py,sha256=V3mHdHQYn0M0PAtyoHxjNvk6KvWWcQ4ugo0TOKc8HyI,1752
46
46
  langroid/agent/tools/segment_extract_tool.py,sha256=W39poS7Av2EuJ34tGKhLhzgj3zEyZnBplpSt2goRAp4,1285
@@ -59,6 +59,7 @@ langroid/embedding_models/protoc/embeddings_pb2.py,sha256=4Q57PhOunv-uZNJrxYrWBX
59
59
  langroid/embedding_models/protoc/embeddings_pb2.pyi,sha256=UkNy7BrNsmQm0vLb3NtGXy8jVtz-kPWwwFsX-QbQBhQ,1475
60
60
  langroid/embedding_models/protoc/embeddings_pb2_grpc.py,sha256=9dYQqkW3JPyBpSEjeGXTNpSqAkC-6FPtBHyteVob2Y8,2452
61
61
  langroid/embedding_models/remote_embeds.py,sha256=6_kjXByVbqhY9cGwl9R83ZcYC2km-nGieNNAo1McHaY,5151
62
+ langroid/exceptions.py,sha256=aQwRVRb04flh1GVbLo3PmoIf3sP9RC7Q5gJH9aVSEkI,169
62
63
  langroid/language_models/__init__.py,sha256=5L9ndEEC8iLJHjDJmYFTnv6-2-3xsxWUMHcugR8IeDs,821
63
64
  langroid/language_models/azure_openai.py,sha256=ncRCbKooqLVOY-PWQUIo9C3yTuKEFbAwyngXT_M4P7k,5989
64
65
  langroid/language_models/base.py,sha256=2JhacnbQ-DJzLbOaJqyZPnl867xyiz_W-ODiAlEcp98,21131
@@ -70,7 +71,7 @@ langroid/language_models/prompt_formatter/base.py,sha256=eDS1sgRNZVnoajwV_ZIha6c
70
71
  langroid/language_models/prompt_formatter/hf_formatter.py,sha256=TFL6ppmeQWnzr6CKQzRZFYY810zE1mr8DZnhw6i85ok,5217
71
72
  langroid/language_models/prompt_formatter/llama2_formatter.py,sha256=YdcO88qyBeuMENVIVvVqSYuEpvYSTndUe_jd6hVTko4,2899
72
73
  langroid/language_models/utils.py,sha256=j8xEEm__-2b9eql1oTiWQk5dHW59UwmrRKs5kMHaGGo,4803
73
- langroid/mytypes.py,sha256=opL488mtHKob1uJeK_h1-kNjU5GZwkgCfXhBQCsONWU,2614
74
+ langroid/mytypes.py,sha256=qD3o2v1pccICz-xeei4cwkvJviVC2llJ3eIYgBP9RDE,3045
74
75
  langroid/parsing/__init__.py,sha256=2O5HOW8nDE3v-JInc5z2wIbFGejf4h5ZTdPqxsFtaWE,870
75
76
  langroid/parsing/agent_chats.py,sha256=sbZRV9ujdM5QXvvuHVjIi2ysYSYlap-uqfMMUKulrW0,1068
76
77
  langroid/parsing/code-parsing.md,sha256=--cyyNiSZSDlIwcjAV4-shKrSiRe2ytF3AdSoS_hD2g,3294
@@ -82,6 +83,7 @@ langroid/parsing/para_sentence_split.py,sha256=AJBzZojP3zpB-_IMiiHismhqcvkrVBQ3Z
82
83
  langroid/parsing/parse_json.py,sha256=tgB_oatcrgt6L9ZplC-xBBXjLzL1gjSQf1L2_W5kwFA,4230
83
84
  langroid/parsing/parser.py,sha256=2TT6YMgEe79Kz9bPIqI-1RIEK77V2H2gbpbX5DhNNrY,10743
84
85
  langroid/parsing/repo_loader.py,sha256=My5UIe-h1xr0I-6Icu0ZVwRHmGRRRW8SrJYMc9J1M9Q,29361
86
+ langroid/parsing/routing.py,sha256=_NFPe7wLjd5B6s47w3M8-5vldL8e2Sz51Gb5bwF5ooY,1072
85
87
  langroid/parsing/search.py,sha256=plQtjarB9afGfJLB0CyPXPq3mM4m7kRsfd0_4brziEI,8846
86
88
  langroid/parsing/spider.py,sha256=w_mHR1B4KOmxsBLoVI8kMkMTEbwTzeK3ath9fOMJrTk,3043
87
89
  langroid/parsing/table_loader.py,sha256=qNM4obT_0Y4tjrxNBCNUYjKQ9oETCZ7FbolKBTcz-GM,3410
@@ -99,7 +101,7 @@ langroid/prompts/transforms.py,sha256=GsQo1klGxUy0fACh6j0lTblk6XEl2erRnhRWlN2M4-
99
101
  langroid/utils/__init__.py,sha256=ARx5To4Hsv1K5QAzK4uUqdEoB_iq5HK797vae1AcMBI,300
100
102
  langroid/utils/algorithms/__init__.py,sha256=WylYoZymA0fnzpB4vrsH_0n7WsoLhmuZq8qxsOCjUpM,41
101
103
  langroid/utils/algorithms/graph.py,sha256=JbdpPnUOhw4-D6O7ou101JLA3xPCD0Lr3qaPoFCaRfo,2866
102
- langroid/utils/configuration.py,sha256=TiDZrQVeEthMFA4QY_HTgQaDCJwS4I5S-aR_taOdc00,3201
104
+ langroid/utils/configuration.py,sha256=FvkbWf0A5iNdmtORfjlY6ZAHp4Fov_OTL6A8U4C3y-A,3282
103
105
  langroid/utils/constants.py,sha256=Y_8p7CyLF5b3xsEV5O3wuutLHQCtegsaxWgr8yNTlIE,563
104
106
  langroid/utils/docker.py,sha256=kJQOLTgM0x9j9pgIIqp0dZNZCTvoUDhp6i8tYBq1Jr0,1105
105
107
  langroid/utils/globals.py,sha256=VkTHhlqSz86oOPq65sjul0XU8I52UNaFC5vwybMQ74w,1343
@@ -111,7 +113,7 @@ langroid/utils/output/printing.py,sha256=5EsYB1O4qKhocW19aebOUzK82RD9U5nygbY21yo
111
113
  langroid/utils/output/status.py,sha256=rzbE7mDJcgNNvdtylCseQcPGCGghtJvVq3lB-OPJ49E,1049
112
114
  langroid/utils/pandas_utils.py,sha256=UctS986Jtl_MvU5rA7-GfrjEHXP7MNu8ePhepv0bTn0,755
113
115
  langroid/utils/pydantic_utils.py,sha256=yb-ghaQYL7EIYeiZ0tailvZvAuJZNF7UBXkd3z35OYc,21728
114
- langroid/utils/system.py,sha256=tWoEbzHzJ6ywdsoa9EwsQrZfGk2t7q87_zKNwau2C8s,4546
116
+ langroid/utils/system.py,sha256=RfAcQODu4tjl-pAO8zZ65yKB9-6WsvzSz2dEPkJdSdw,4909
115
117
  langroid/utils/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
116
118
  langroid/utils/web/login.py,sha256=1iz9eUAHa87vpKIkzwkmFa00avwFWivDSAr7QUhK7U0,2528
117
119
  langroid/vector_store/__init__.py,sha256=D82ioqPWxKTTbN0qiPNB-I1GjovhLw1MgDuYhcB3hCs,831
@@ -122,7 +124,7 @@ langroid/vector_store/meilisearch.py,sha256=d2huA9P-NoYRuAQ9ZeXJmMKr7ry8u90RUSR2
122
124
  langroid/vector_store/momento.py,sha256=9cui31TTrILid2KIzUpBkN2Ey3g_CZWOQVdaFsA4Ors,10045
123
125
  langroid/vector_store/qdrant_cloud.py,sha256=3im4Mip0QXLkR6wiqVsjV1QvhSElfxdFSuDKddBDQ-4,188
124
126
  langroid/vector_store/qdrantdb.py,sha256=sk5Qb2ZNbooi0rorsMuqIMokF7WADw6PJ0D6goM2XBw,16802
125
- langroid-0.1.250.dist-info/LICENSE,sha256=EgVbvA6VSYgUlvC3RvPKehSg7MFaxWDsFuzLOsPPfJg,1065
126
- langroid-0.1.250.dist-info/METADATA,sha256=FOv0qbbaZxR3TJWncDj2NvvslRp7RXOI0Xahb4yBT3I,49559
127
- langroid-0.1.250.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
128
- langroid-0.1.250.dist-info/RECORD,,
127
+ langroid-0.1.252.dist-info/LICENSE,sha256=EgVbvA6VSYgUlvC3RvPKehSg7MFaxWDsFuzLOsPPfJg,1065
128
+ langroid-0.1.252.dist-info/METADATA,sha256=zRgxQH1C04RM1tofjy6tNtYSE1YalSYHvqVz7oVjYU8,49559
129
+ langroid-0.1.252.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
130
+ langroid-0.1.252.dist-info/RECORD,,