langroid 0.1.249__py3-none-any.whl → 0.1.251__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
@@ -87,6 +87,7 @@ class Agent(ABC):
87
87
  self.llm_tools_map: Dict[str, Type[ToolMessage]] = {}
88
88
  self.llm_tools_handled: Set[str] = set()
89
89
  self.llm_tools_usable: Set[str] = set()
90
+ self.interactive: bool | None = None
90
91
  self.total_llm_token_cost = 0.0
91
92
  self.total_llm_token_usage = 0
92
93
  self.token_stats_str = ""
@@ -223,8 +224,8 @@ class Agent(ABC):
223
224
  ):
224
225
  setattr(self, tool, lambda obj: obj.response(self))
225
226
 
226
- if hasattr(message_class, "handle_message_fallback") and inspect.isfunction(
227
- message_class.handle_message_fallback
227
+ if hasattr(message_class, "handle_message_fallback") and (
228
+ inspect.isfunction(message_class.handle_message_fallback)
228
229
  ):
229
230
  setattr(
230
231
  self,
@@ -279,9 +280,9 @@ class Agent(ABC):
279
280
  ]
280
281
  return "\n\n".join(sample_convo)
281
282
 
282
- def agent_response_template(self) -> ChatDocument:
283
+ def create_agent_response(self, content: str | None = None) -> ChatDocument:
283
284
  """Template for agent_response."""
284
- return self._response_template(Entity.AGENT)
285
+ return self._response_template(Entity.AGENT, content)
285
286
 
286
287
  async def agent_response_async(
287
288
  self,
@@ -342,19 +343,19 @@ class Agent(ABC):
342
343
  ),
343
344
  )
344
345
 
345
- def _response_template(self, e: Entity) -> ChatDocument:
346
+ def _response_template(self, e: Entity, content: str | None = None) -> ChatDocument:
346
347
  """Template for response from entity `e`."""
347
348
  return ChatDocument(
348
- content="",
349
+ content=content or "",
349
350
  tool_messages=[],
350
351
  metadata=ChatDocMetaData(
351
352
  source=e, sender=e, sender_name=self.config.name, tool_ids=[]
352
353
  ),
353
354
  )
354
355
 
355
- def user_response_template(self) -> ChatDocument:
356
+ def create_user_response(self, content: str | None = None) -> ChatDocument:
356
357
  """Template for user_response."""
357
- return self._response_template(Entity.USER)
358
+ return self._response_template(Entity.USER, content)
358
359
 
359
360
  async def user_response_async(
360
361
  self,
@@ -377,11 +378,21 @@ class Agent(ABC):
377
378
  (str) User response, packaged as a ChatDocument
378
379
 
379
380
  """
380
- if self.default_human_response is not None:
381
+
382
+ # When msg explicitly addressed to user, this means an actual human response
383
+ # is being sought.
384
+ need_human_response = (
385
+ isinstance(msg, ChatDocument) and msg.metadata.recipient == Entity.USER
386
+ )
387
+
388
+ interactive = (
389
+ self.interactive if self.interactive is not None else settings.interactive
390
+ )
391
+ if self.default_human_response is not None and not need_human_response:
381
392
  # useful for automated testing
382
393
  user_msg = self.default_human_response
383
- elif not settings.interactive:
384
- user_msg = ""
394
+ elif not interactive and not need_human_response:
395
+ return None
385
396
  else:
386
397
  if self.callbacks.get_user_response is not None:
387
398
  # ask user with empty prompt: no need for prompt
@@ -440,9 +451,9 @@ class Agent(ABC):
440
451
 
441
452
  return True
442
453
 
443
- def llm_response_template(self) -> ChatDocument:
454
+ def create_llm_response(self, content: str | None = None) -> ChatDocument:
444
455
  """Template for llm_response."""
445
- return self._response_template(Entity.LLM)
456
+ return self._response_template(Entity.LLM, content)
446
457
 
447
458
  @no_type_check
448
459
  async def llm_response_async(
@@ -736,6 +747,24 @@ class Agent(ABC):
736
747
 
737
748
  def _get_one_tool_message(self, json_str: str) -> Optional[ToolMessage]:
738
749
  json_data = json.loads(json_str)
750
+ # check if the json_data contains a "properties" field
751
+ # which further contains the actual tool-call
752
+ # (some weak LLMs do this). E.g. gpt-4o sometimes generates this:
753
+ # TOOL: {
754
+ # "type": "object",
755
+ # "properties": {
756
+ # "request": "square",
757
+ # "number": 9
758
+ # },
759
+ # "required": [
760
+ # "number",
761
+ # "request"
762
+ # ]
763
+ # }
764
+
765
+ properties = json_data.get("properties")
766
+ if properties is not None:
767
+ json_data = properties
739
768
  request = json_data.get("request")
740
769
  if (
741
770
  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):
@@ -14,7 +14,6 @@ pip install "langroid[hf-embeddings]"
14
14
  """
15
15
 
16
16
  import logging
17
- import re
18
17
  from functools import cache
19
18
  from typing import Any, Dict, List, Optional, Set, Tuple, no_type_check
20
19
 
@@ -31,6 +30,7 @@ from langroid.agent.special.relevance_extractor_agent import (
31
30
  RelevanceExtractorAgentConfig,
32
31
  )
33
32
  from langroid.agent.task import Task
33
+ from langroid.agent.tools.retrieval_tool import RetrievalTool
34
34
  from langroid.embedding_models.models import OpenAIEmbeddingsConfig
35
35
  from langroid.language_models.base import StreamingIfAllowed
36
36
  from langroid.language_models.openai_gpt import OpenAIChatModel, OpenAIGPTConfig
@@ -82,21 +82,47 @@ except ImportError:
82
82
  pass
83
83
 
84
84
 
85
- def extract_citations(text: str) -> List[int]:
86
- # Find all patterns that match [[<numbers>]]
87
- matches = re.findall(r"\[\[([\d,]+)\]\]", text)
85
+ def extract_markdown_references(md_string: str) -> list[int]:
86
+ """
87
+ Extracts markdown references (e.g., [^1], [^2]) from a string and returns
88
+ them as a sorted list of integers.
89
+
90
+ Args:
91
+ md_string (str): The markdown string containing references.
92
+
93
+ Returns:
94
+ list[int]: A sorted list of unique integers from the markdown references.
95
+ """
96
+ import re
97
+
98
+ # Regex to find all occurrences of [^<number>]
99
+ matches = re.findall(r"\[\^(\d+)\]", md_string)
100
+ # Convert matches to integers, remove duplicates with set, and sort
101
+ return sorted(set(int(match) for match in matches))
88
102
 
89
- # Initialize a set to hold distinct citation numbers
90
- citations: Set[int] = set()
91
103
 
92
- # Process each match
93
- for match in matches:
94
- # Split numbers by comma and convert to integers
95
- numbers = match.split(",")
96
- citations.update(int(number) for number in numbers)
104
+ def format_footnote_text(content: str, width: int = 80) -> str:
105
+ """
106
+ Formats the content part of a footnote (i.e. not the first line that
107
+ appears right after the reference [^4])
108
+ It wraps the text so that no line is longer than the specified width and indents
109
+ lines as necessary for markdown footnotes.
110
+
111
+ Args:
112
+ content (str): The text of the footnote to be formatted.
113
+ width (int): Maximum width of the text lines.
97
114
 
98
- # Return a sorted list of unique citations
99
- return sorted(citations)
115
+ Returns:
116
+ str: Properly formatted markdown footnote text.
117
+ """
118
+ import textwrap
119
+
120
+ # Wrap the text to the specified width
121
+ wrapped_lines = textwrap.wrap(content, width)
122
+ if len(wrapped_lines) == 0:
123
+ return ""
124
+ indent = " " # Indentation for markdown footnotes
125
+ return indent + ("\n" + indent).join(wrapped_lines)
100
126
 
101
127
 
102
128
  class DocChatAgentConfig(ChatAgentConfig):
@@ -438,6 +464,13 @@ class DocChatAgent(ChatAgent):
438
464
  self.setup_documents(docs, filter=self.config.filter)
439
465
  return len(docs)
440
466
 
467
+ def retrieval_tool(self, msg: RetrievalTool) -> str:
468
+ """Handle the RetrievalTool message"""
469
+ self.config.retrieve_only = True
470
+ self.config.parsing.n_similar_docs = msg.num_results
471
+ content_doc = self.answer_from_docs(msg.query)
472
+ return content_doc.content
473
+
441
474
  @staticmethod
442
475
  def document_compatible_dataframe(
443
476
  df: pd.DataFrame,
@@ -808,14 +841,15 @@ class DocChatAgent(ChatAgent):
808
841
  final_answer = answer_doc.content.strip()
809
842
  show_if_debug(final_answer, "SUMMARIZE_RESPONSE= ")
810
843
 
811
- citations = extract_citations(final_answer)
844
+ citations = extract_markdown_references(final_answer)
812
845
 
813
846
  citations_str = ""
814
847
  if len(citations) > 0:
815
848
  # append [i] source, content for each citation
816
849
  citations_str = "\n".join(
817
850
  [
818
- f"[{c}] {passages[c-1].metadata.source}\n{passages[c-1].content}"
851
+ f"[^{c}] {passages[c-1].metadata.source}"
852
+ f"\n{format_footnote_text(passages[c-1].content)}"
819
853
  for c in citations
820
854
  ]
821
855
  )
@@ -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
@@ -2,13 +2,13 @@ from __future__ import annotations
2
2
 
3
3
  import copy
4
4
  import logging
5
- import re
6
- from collections import Counter
5
+ from collections import Counter, deque
7
6
  from types import SimpleNamespace
8
7
  from typing import (
9
8
  Any,
10
9
  Callable,
11
10
  Coroutine,
11
+ Deque,
12
12
  Dict,
13
13
  List,
14
14
  Optional,
@@ -18,6 +18,8 @@ from typing import (
18
18
  cast,
19
19
  )
20
20
 
21
+ import numpy as np
22
+ from pydantic import BaseModel
21
23
  from rich import print
22
24
  from rich.markup import escape
23
25
 
@@ -30,8 +32,10 @@ from langroid.agent.chat_document import (
30
32
  StatusCode,
31
33
  )
32
34
  from langroid.cachedb.redis_cachedb import RedisCache, RedisCacheConfig
35
+ from langroid.exceptions import InfiniteLoopException
33
36
  from langroid.mytypes import Entity
34
37
  from langroid.parsing.parse_json import extract_top_level_json
38
+ from langroid.parsing.routing import parse_addressed_message
35
39
  from langroid.utils.configuration import settings
36
40
  from langroid.utils.constants import (
37
41
  DONE,
@@ -42,6 +46,7 @@ from langroid.utils.constants import (
42
46
  USER_QUIT_STRINGS,
43
47
  )
44
48
  from langroid.utils.logging import RichFileLogger, setup_file_logger
49
+ from langroid.utils.system import hash
45
50
 
46
51
  logger = logging.getLogger(__name__)
47
52
 
@@ -52,6 +57,18 @@ def noop_fn(*args: List[Any], **kwargs: Dict[str, Any]) -> None:
52
57
  pass
53
58
 
54
59
 
60
+ class TaskConfig(BaseModel):
61
+ """Configuration for a Task. This is a container for any params that
62
+ we didn't include in the task __init__ method.
63
+ We may eventually move all the task __init__ params to this class, analogous to how
64
+ we have config classes for Agent, ChatAgent, LanguageModel, etc."""
65
+
66
+ inf_loop_cycle_len: int = 10 # max exact-loop cycle length: 0 => no inf loop test
67
+ inf_loop_dominance_factor: float = 1.5 # dominance factor for exact-loop detection
68
+ # wait this * cycle_len msgs before checking for loop
69
+ inf_loop_wait_factor: float = 5.0
70
+
71
+
55
72
  class Task:
56
73
  """
57
74
  A `Task` wraps an `Agent` object, and sets up the `Agent`'s goals and instructions.
@@ -102,6 +119,7 @@ class Task:
102
119
  max_stalled_steps: int = 5,
103
120
  done_if_no_response: List[Responder] = [],
104
121
  done_if_response: List[Responder] = [],
122
+ config: TaskConfig = TaskConfig(),
105
123
  ):
106
124
  """
107
125
  A task to be performed by an agent.
@@ -157,6 +175,11 @@ class Task:
157
175
  show_subtask_response=noop_fn,
158
176
  set_parent_agent=noop_fn,
159
177
  )
178
+ self.config = config
179
+ # counts of distinct pending messages in history,
180
+ # to help detect (exact) infinite loops
181
+ self.message_counter: Counter[str] = Counter()
182
+ self.history_count: Deque[int] = deque(maxlen=self.config.inf_loop_cycle_len)
160
183
  # copy the agent's config, so that we don't modify the original agent's config,
161
184
  # which may be shared by other agents.
162
185
  try:
@@ -201,10 +224,11 @@ class Task:
201
224
  agent.config.name = name
202
225
  self.name = name or agent.config.name
203
226
  self.value: str = self.name
204
- self.default_human_response = default_human_response
227
+
205
228
  if default_human_response is not None and default_human_response == "":
206
229
  interactive = False
207
230
  self.interactive = interactive
231
+ self.agent.interactive = interactive
208
232
  self.message_history_idx = -1
209
233
  if interactive:
210
234
  only_user_quits_root = True
@@ -213,6 +237,7 @@ class Task:
213
237
  only_user_quits_root = False
214
238
  if default_human_response is not None:
215
239
  self.agent.default_human_response = default_human_response
240
+ self.default_human_response = default_human_response
216
241
  if self.interactive:
217
242
  self.agent.default_human_response = None
218
243
  self.only_user_quits_root = only_user_quits_root
@@ -289,6 +314,7 @@ class Task:
289
314
  max_stalled_steps=self.max_stalled_steps,
290
315
  done_if_no_response=[Entity(s) for s in self.done_if_no_response],
291
316
  done_if_response=[Entity(s) for s in self.done_if_response],
317
+ config=self.config,
292
318
  )
293
319
 
294
320
  def __repr__(self) -> str:
@@ -448,6 +474,8 @@ class Task:
448
474
  self.max_tokens = max_tokens
449
475
  self.session_id = session_id
450
476
  self._set_alive()
477
+ self.message_counter.clear()
478
+ self.history_count.clear()
451
479
 
452
480
  assert (
453
481
  msg is None or isinstance(msg, str) or isinstance(msg, ChatDocument)
@@ -476,9 +504,20 @@ class Task:
476
504
  print("[magenta]Bye, hope this was useful!")
477
505
  break
478
506
  i += 1
479
- if turns > 0 and i >= turns:
507
+ max_turns = (
508
+ min(turns, settings.max_turns)
509
+ if turns > 0 and settings.max_turns > 0
510
+ else max(turns, settings.max_turns)
511
+ )
512
+ if max_turns > 0 and i >= max_turns:
480
513
  status = StatusCode.MAX_TURNS
481
514
  break
515
+ if (
516
+ self.config.inf_loop_cycle_len > 0
517
+ and i % self.config.inf_loop_cycle_len == 0
518
+ and self._maybe_infinite_loop()
519
+ ):
520
+ raise InfiniteLoopException("Possible infinite loop detected!")
482
521
 
483
522
  final_result = self.result()
484
523
  if final_result is not None:
@@ -528,6 +567,8 @@ class Task:
528
567
  self.max_tokens = max_tokens
529
568
  self.session_id = session_id
530
569
  self._set_alive()
570
+ self.message_counter.clear()
571
+ self.history_count.clear()
531
572
 
532
573
  if (
533
574
  isinstance(msg, ChatDocument)
@@ -552,9 +593,20 @@ class Task:
552
593
  print("[magenta]Bye, hope this was useful!")
553
594
  break
554
595
  i += 1
555
- if turns > 0 and i >= turns:
596
+ max_turns = (
597
+ min(turns, settings.max_turns)
598
+ if turns > 0 and settings.max_turns > 0
599
+ else max(turns, settings.max_turns)
600
+ )
601
+ if max_turns > 0 and i >= max_turns:
556
602
  status = StatusCode.MAX_TURNS
557
603
  break
604
+ if (
605
+ self.config.inf_loop_cycle_len > 0
606
+ and i % self.config.inf_loop_cycle_len == 0
607
+ and self._maybe_infinite_loop()
608
+ ):
609
+ raise InfiniteLoopException("Possible infinite loop detected!")
558
610
 
559
611
  final_result = self.result()
560
612
  if final_result is not None:
@@ -824,6 +876,12 @@ class Task:
824
876
  # reset stuck counter since we made progress
825
877
  self.n_stalled_steps = 0
826
878
 
879
+ # update counters for infinite loop detection
880
+ if self.pending_message is not None:
881
+ hashed_msg = hash(str(self.pending_message))
882
+ self.message_counter.update([hashed_msg])
883
+ self.history_count.append(self.message_counter[hashed_msg])
884
+
827
885
  def _process_invalid_step_result(self, parent: ChatDocument | None) -> None:
828
886
  """
829
887
  Since step had no valid result from any responder, decide whether to update the
@@ -856,42 +914,6 @@ class Task:
856
914
  msg_str = escape(str(self.pending_message))
857
915
  print(f"[grey37][{sender_str}]{msg_str}[/grey37]")
858
916
 
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
917
  def response(
896
918
  self,
897
919
  e: Responder,
@@ -929,7 +951,8 @@ class Task:
929
951
  # process result in case there is a routing instruction
930
952
  if result is None:
931
953
  return None
932
- is_pass, recipient = self._parse_routing(result)
954
+ # if result content starts with @name, set recipient to name
955
+ is_pass, recipient, content = parse_routing(result)
933
956
  if is_pass is None: # no routing, i.e. neither PASS nor SEND
934
957
  return result
935
958
  if is_pass:
@@ -949,9 +972,7 @@ class Task:
949
972
  elif recipient is not None:
950
973
  # we are sending non-empty content to non-null recipient
951
974
  # clean up result.content, set metadata.recipient and return
952
- result.content = result.content.replace(
953
- f"{SEND_TO}:{recipient}", ""
954
- ).strip()
975
+ result.content = content or ""
955
976
  result.metadata.recipient = recipient
956
977
  return result
957
978
  else:
@@ -1080,38 +1101,76 @@ class Task:
1080
1101
  or (not self._is_empty_message(result) and response_says_done)
1081
1102
  )
1082
1103
 
1083
- def _maybe_infinite_loop(self, history: int = 10) -> bool:
1104
+ def _maybe_infinite_loop(self) -> bool:
1084
1105
  """
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).
1106
+ Detect possible infinite loop based on message frequencies.
1107
+ NOTE: This only (attempts to) detect "exact" loops, i.e. a cycle
1108
+ of messages that repeats exactly, e.g.
1109
+ a r b i t r a t e r a t e r a t e r a t e ...
1110
+
1111
+ [It does not detect "approximate" loops, where the LLM is generating a
1112
+ sequence of messages that are similar, but not exactly the same.]
1113
+
1114
+ Intuition: when you look at a sufficiently long sequence with an m-message
1115
+ loop, then the frequencies of these m messages will "dominate" those
1116
+ of all other messages.
1117
+
1118
+ 1. First find m "dominant" messages, i.e. when arranged in decreasing
1119
+ frequency order, find the m such that
1120
+ freq[m] > F * freq[m+1] and
1121
+ freq[m] > W + freq[m+1]
1122
+ where F = config.inf_loop_dominance_factor (default 1.5) and
1123
+ W = config.inf_loop_wait_factor (default 5).
1124
+ So if you plot these frequencies in decreasing order,
1125
+ you will see a big in the plot, from m to m+1.
1126
+ We call the freqs until m the "dominant" freqs.
1127
+ 2. Say we found m such dominant frequencies.
1128
+ If these are the same as the freqs of the last m messages,
1129
+ then we are likely in a loop.
1093
1130
  """
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
1131
+ max_cycle_len = self.config.inf_loop_cycle_len
1132
+ if max_cycle_len <= 0:
1133
+ # no loop detection
1134
+ return False
1135
+ wait_factor = self.config.inf_loop_wait_factor
1136
+ if sum(self.message_counter.values()) < wait_factor * max_cycle_len:
1137
+ # we haven't seen enough messages to detect a loop
1138
+ return False
1139
+
1140
+ most_common_msg_counts: List[Tuple[str, int]] = (
1141
+ self.message_counter.most_common(max_cycle_len + 1)
1142
+ )
1143
+ # get the most dominant msgs, i.e. these are at least 1.5x more freq
1144
+ # than the rest
1145
+ F = self.config.inf_loop_dominance_factor
1146
+ # counts array in non-increasing order
1147
+ counts = np.array([c for _, c in most_common_msg_counts])
1148
+ # find first index where counts[i] > F * counts[i+1]
1149
+ ratios = counts[:-1] / counts[1:]
1150
+ diffs = counts[:-1] - counts[1:]
1151
+ indices = np.where((ratios > F) & (diffs > wait_factor))[0]
1152
+ m = indices[0] if indices.size > 0 else -1
1153
+ if m < 0:
1154
+ # no dominance found, but...
1155
+ if len(most_common_msg_counts) <= max_cycle_len:
1156
+ # ...The most-common messages are at most max_cycle_len,
1157
+ # even though we looked for the most common (max_cycle_len + 1) msgs.
1158
+ # This means there are only at most max_cycle_len distinct messages,
1159
+ # which also indicates a possible loop.
1160
+ m = len(most_common_msg_counts) - 1
1161
+ else:
1162
+ # ... we have enough messages, but no dominance found,
1163
+ # so there COULD be loops longer than max_cycle_len,
1164
+ # OR there is no loop at all; we can't tell, so we return False.
1165
+ return False
1166
+
1167
+ dominant_msg_counts = most_common_msg_counts[: m + 1]
1168
+ # if the dominant m message counts are the same as the
1169
+ # counts of the last m messages, then we are likely in a loop
1170
+ dominant_counts = sorted([c for _, c in dominant_msg_counts])
1171
+ recent_counts = sorted(list(self.history_count)[-(m + 1) :])
1172
+
1173
+ return dominant_counts == recent_counts
1115
1174
 
1116
1175
  def done(
1117
1176
  self, result: ChatDocument | None = None, r: Responder | None = None
@@ -1289,7 +1348,9 @@ class Task:
1289
1348
  return (
1290
1349
  self.pending_message is not None
1291
1350
  and (recipient := self.pending_message.metadata.recipient) != ""
1292
- and recipient not in (e.name, self.name)
1351
+ and recipient != e # case insensitive
1352
+ and recipient != e.name
1353
+ and recipient != self.name # case sensitive
1293
1354
  )
1294
1355
 
1295
1356
  def _can_respond(self, e: Responder) -> bool:
@@ -1316,3 +1377,53 @@ class Task:
1316
1377
 
1317
1378
  """
1318
1379
  self.color_log = enable
1380
+
1381
+
1382
+ def parse_routing(
1383
+ msg: ChatDocument | str,
1384
+ ) -> Tuple[bool | None, str | None, str | None]:
1385
+ """
1386
+ Parse routing instruction if any, of the form:
1387
+ PASS:<recipient> (pass current pending msg to recipient)
1388
+ SEND:<recipient> <content> (send content to recipient)
1389
+ @<recipient> <content> (send content to recipient)
1390
+ Args:
1391
+ msg (ChatDocument|str|None): message to parse
1392
+ Returns:
1393
+ Tuple[bool|None, str|None, str|None]:
1394
+ bool: true=PASS, false=SEND, or None if neither
1395
+ str: recipient, or None
1396
+ str: content to send, or None
1397
+ """
1398
+ # handle routing instruction in result if any,
1399
+ # of the form PASS=<recipient>
1400
+ content = msg.content if isinstance(msg, ChatDocument) else msg
1401
+ content = content.strip()
1402
+ if PASS in content and PASS_TO not in content:
1403
+ return True, None, None
1404
+ if PASS_TO in content and content.split(":")[1] != "":
1405
+ return True, content.split(":")[1], None
1406
+ if (
1407
+ SEND_TO in content
1408
+ and (addressee_content := parse_addressed_message(content, SEND_TO))[0]
1409
+ is not None
1410
+ ):
1411
+ (addressee, content_to_send) = addressee_content
1412
+ # if no content then treat same as PASS_TO
1413
+ if content_to_send == "":
1414
+ return True, addressee, None
1415
+ else:
1416
+ return False, addressee, content_to_send
1417
+ AT = "@"
1418
+ if (
1419
+ AT in content
1420
+ and (addressee_content := parse_addressed_message(content, AT))[0] is not None
1421
+ ):
1422
+ (addressee, content_to_send) = addressee_content
1423
+ # if no content then treat same as PASS_TO
1424
+ if content_to_send == "":
1425
+ return True, addressee, None
1426
+ else:
1427
+ return False, addressee, content_to_send
1428
+
1429
+ 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
@@ -0,0 +1,29 @@
1
+ from typing import List
2
+
3
+ from langroid.agent.tool_message import ToolMessage
4
+
5
+
6
+ class RetrievalTool(ToolMessage):
7
+ """Retrieval tool, only to be used by a DocChatAgent."""
8
+
9
+ request: str = "retrieval_tool"
10
+ purpose: str = """
11
+ To retrieve up to <num_results> passages from a document-set, that are
12
+ relevant to a <query>, which could be a question or simply a topic or
13
+ search phrase.
14
+ """
15
+ query: str
16
+ num_results: int
17
+
18
+ @classmethod
19
+ def examples(cls) -> List["ToolMessage"]:
20
+ return [
21
+ cls(
22
+ query="What are the eligibility criteria for the scholarship?",
23
+ num_results=3,
24
+ ),
25
+ cls(
26
+ query="Self-Attention mechanism in RNNs",
27
+ num_results=5,
28
+ ),
29
+ ]
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
@@ -55,13 +55,14 @@ SUMMARY_ANSWER_PROMPT_GPT4 = f"""
55
55
  information in these extracts, even if your answer is factually incorrect,
56
56
  and even if the answer contradicts other parts of the document. The only
57
57
  important thing is that your answer is consistent with and supported by the
58
- extracts. Compose your complete answer, inserting CITATIONS
59
- in the format [[i,j,...]] where i,j,... are the extract NUMBERS you are citing.
58
+ extracts. Compose your complete answer, inserting CITATIONS in MARKDOWN format
59
+ [^i][^j] where i,j,... are the extract NUMBERS you are
60
+ citing.
60
61
  For example your answer might look like this (NOTE HOW multiple citations
61
- are grouped as [[2,5]]):
62
+ are grouped as [^2][^5]):
62
63
 
63
- Beethoven composed the 9th symphony in 1824. [[1]] After that he became deaf
64
- and could not hear his own music. [[2,5]] He was a prolific composer and
64
+ Beethoven composed the 9th symphony in 1824.[^1] After that he became deaf
65
+ and could not hear his own music. [^2][^5]. He was a prolific composer and
65
66
  wrote many famous pieces.
66
67
 
67
68
  NUMBERED EXTRACTS:
@@ -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.249
3
+ Version: 0.1.251
4
4
  Summary: Harness LLMs with Multi-Agent Programming
5
5
  License: MIT
6
6
  Author: Prasad Chalasani
@@ -237,6 +237,8 @@ teacher_task.run()
237
237
  <summary> <b>Click to expand</b></summary>
238
238
 
239
239
  - **May 2024:**
240
+ - [Much-Improved Citation](https://github.com/langroid/langroid/issues/477)
241
+ generation and display when using `DocChatAgent`.
240
242
  - `gpt-4o` is now the default LLM throughout; Update tests and examples to work
241
243
  with this LLM; use tokenizer corresponding to the LLM.
242
244
  - `gemini 1.5 pro` support via `litellm`
@@ -1,21 +1,21 @@
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=WzZ7l74brAF_li2dp6s3743-CGSXOMWhd6kd_fLGU-Q,36838
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
11
11
  langroid/agent/openai_assistant.py,sha256=kIVDI4r-xGvplLU5s0nShPVHs6Jq-wOsfWE0kcMhAdQ,33056
12
12
  langroid/agent/special/__init__.py,sha256=NG0JkB5y4K0bgnd9Q9UIvFExun3uTfVOWEVLVymff1M,1207
13
- langroid/agent/special/doc_chat_agent.py,sha256=SBatLDoa2_Ju_Gk_El8FmlMekgHmgpBU1ihx26yFIvc,54008
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,16 @@ 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=9Arj-ULbaJBCV97Cv5Qe30RLwhRKwIA3gxL5Cog7BL0,58527
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
+ langroid/agent/tools/retrieval_tool.py,sha256=6uvRNg-kG_ItPa3sF9NWkthQ5frHn8bkB1Z3GSd3Oas,836
44
45
  langroid/agent/tools/run_python_code.py,sha256=V3mHdHQYn0M0PAtyoHxjNvk6KvWWcQ4ugo0TOKc8HyI,1752
45
46
  langroid/agent/tools/segment_extract_tool.py,sha256=W39poS7Av2EuJ34tGKhLhzgj3zEyZnBplpSt2goRAp4,1285
46
47
  langroid/agent_config.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -58,6 +59,7 @@ langroid/embedding_models/protoc/embeddings_pb2.py,sha256=4Q57PhOunv-uZNJrxYrWBX
58
59
  langroid/embedding_models/protoc/embeddings_pb2.pyi,sha256=UkNy7BrNsmQm0vLb3NtGXy8jVtz-kPWwwFsX-QbQBhQ,1475
59
60
  langroid/embedding_models/protoc/embeddings_pb2_grpc.py,sha256=9dYQqkW3JPyBpSEjeGXTNpSqAkC-6FPtBHyteVob2Y8,2452
60
61
  langroid/embedding_models/remote_embeds.py,sha256=6_kjXByVbqhY9cGwl9R83ZcYC2km-nGieNNAo1McHaY,5151
62
+ langroid/exceptions.py,sha256=aQwRVRb04flh1GVbLo3PmoIf3sP9RC7Q5gJH9aVSEkI,169
61
63
  langroid/language_models/__init__.py,sha256=5L9ndEEC8iLJHjDJmYFTnv6-2-3xsxWUMHcugR8IeDs,821
62
64
  langroid/language_models/azure_openai.py,sha256=ncRCbKooqLVOY-PWQUIo9C3yTuKEFbAwyngXT_M4P7k,5989
63
65
  langroid/language_models/base.py,sha256=2JhacnbQ-DJzLbOaJqyZPnl867xyiz_W-ODiAlEcp98,21131
@@ -69,7 +71,7 @@ langroid/language_models/prompt_formatter/base.py,sha256=eDS1sgRNZVnoajwV_ZIha6c
69
71
  langroid/language_models/prompt_formatter/hf_formatter.py,sha256=TFL6ppmeQWnzr6CKQzRZFYY810zE1mr8DZnhw6i85ok,5217
70
72
  langroid/language_models/prompt_formatter/llama2_formatter.py,sha256=YdcO88qyBeuMENVIVvVqSYuEpvYSTndUe_jd6hVTko4,2899
71
73
  langroid/language_models/utils.py,sha256=j8xEEm__-2b9eql1oTiWQk5dHW59UwmrRKs5kMHaGGo,4803
72
- langroid/mytypes.py,sha256=opL488mtHKob1uJeK_h1-kNjU5GZwkgCfXhBQCsONWU,2614
74
+ langroid/mytypes.py,sha256=qD3o2v1pccICz-xeei4cwkvJviVC2llJ3eIYgBP9RDE,3045
73
75
  langroid/parsing/__init__.py,sha256=2O5HOW8nDE3v-JInc5z2wIbFGejf4h5ZTdPqxsFtaWE,870
74
76
  langroid/parsing/agent_chats.py,sha256=sbZRV9ujdM5QXvvuHVjIi2ysYSYlap-uqfMMUKulrW0,1068
75
77
  langroid/parsing/code-parsing.md,sha256=--cyyNiSZSDlIwcjAV4-shKrSiRe2ytF3AdSoS_hD2g,3294
@@ -81,6 +83,7 @@ langroid/parsing/para_sentence_split.py,sha256=AJBzZojP3zpB-_IMiiHismhqcvkrVBQ3Z
81
83
  langroid/parsing/parse_json.py,sha256=tgB_oatcrgt6L9ZplC-xBBXjLzL1gjSQf1L2_W5kwFA,4230
82
84
  langroid/parsing/parser.py,sha256=2TT6YMgEe79Kz9bPIqI-1RIEK77V2H2gbpbX5DhNNrY,10743
83
85
  langroid/parsing/repo_loader.py,sha256=My5UIe-h1xr0I-6Icu0ZVwRHmGRRRW8SrJYMc9J1M9Q,29361
86
+ langroid/parsing/routing.py,sha256=_NFPe7wLjd5B6s47w3M8-5vldL8e2Sz51Gb5bwF5ooY,1072
84
87
  langroid/parsing/search.py,sha256=plQtjarB9afGfJLB0CyPXPq3mM4m7kRsfd0_4brziEI,8846
85
88
  langroid/parsing/spider.py,sha256=w_mHR1B4KOmxsBLoVI8kMkMTEbwTzeK3ath9fOMJrTk,3043
86
89
  langroid/parsing/table_loader.py,sha256=qNM4obT_0Y4tjrxNBCNUYjKQ9oETCZ7FbolKBTcz-GM,3410
@@ -93,12 +96,12 @@ langroid/prompts/__init__.py,sha256=B0vpJzIJlMR3mFRtoQwyALsFzBHvLp9f92acD8xJA_0,
93
96
  langroid/prompts/chat-gpt4-system-prompt.md,sha256=Q3uLCJTPQvmUkZN2XDnkBC7M2K3X0F3C3GIQBaFvYvw,5329
94
97
  langroid/prompts/dialog.py,sha256=SpfiSyofSgy2pwD1YboHR_yHO3LEEMbv6j2sm874jKo,331
95
98
  langroid/prompts/prompts_config.py,sha256=XRQHzod7KBnoKn3B_V878jZiqBA7rcn-CtGPkuAe_yM,131
96
- langroid/prompts/templates.py,sha256=NxMyPIhDjmL3pNXBaNLrIsebETPQHr6VG5NWO_93NeA,6303
99
+ langroid/prompts/templates.py,sha256=kz0rPiM6iLGhhpDonF3Y87OznSe9FRI6A0pHU0wgW4Q,6314
97
100
  langroid/prompts/transforms.py,sha256=GsQo1klGxUy0fACh6j0lTblk6XEl2erRnhRWlN2M4-c,2706
98
101
  langroid/utils/__init__.py,sha256=ARx5To4Hsv1K5QAzK4uUqdEoB_iq5HK797vae1AcMBI,300
99
102
  langroid/utils/algorithms/__init__.py,sha256=WylYoZymA0fnzpB4vrsH_0n7WsoLhmuZq8qxsOCjUpM,41
100
103
  langroid/utils/algorithms/graph.py,sha256=JbdpPnUOhw4-D6O7ou101JLA3xPCD0Lr3qaPoFCaRfo,2866
101
- langroid/utils/configuration.py,sha256=TiDZrQVeEthMFA4QY_HTgQaDCJwS4I5S-aR_taOdc00,3201
104
+ langroid/utils/configuration.py,sha256=FvkbWf0A5iNdmtORfjlY6ZAHp4Fov_OTL6A8U4C3y-A,3282
102
105
  langroid/utils/constants.py,sha256=Y_8p7CyLF5b3xsEV5O3wuutLHQCtegsaxWgr8yNTlIE,563
103
106
  langroid/utils/docker.py,sha256=kJQOLTgM0x9j9pgIIqp0dZNZCTvoUDhp6i8tYBq1Jr0,1105
104
107
  langroid/utils/globals.py,sha256=VkTHhlqSz86oOPq65sjul0XU8I52UNaFC5vwybMQ74w,1343
@@ -110,7 +113,7 @@ langroid/utils/output/printing.py,sha256=5EsYB1O4qKhocW19aebOUzK82RD9U5nygbY21yo
110
113
  langroid/utils/output/status.py,sha256=rzbE7mDJcgNNvdtylCseQcPGCGghtJvVq3lB-OPJ49E,1049
111
114
  langroid/utils/pandas_utils.py,sha256=UctS986Jtl_MvU5rA7-GfrjEHXP7MNu8ePhepv0bTn0,755
112
115
  langroid/utils/pydantic_utils.py,sha256=yb-ghaQYL7EIYeiZ0tailvZvAuJZNF7UBXkd3z35OYc,21728
113
- langroid/utils/system.py,sha256=tWoEbzHzJ6ywdsoa9EwsQrZfGk2t7q87_zKNwau2C8s,4546
116
+ langroid/utils/system.py,sha256=RfAcQODu4tjl-pAO8zZ65yKB9-6WsvzSz2dEPkJdSdw,4909
114
117
  langroid/utils/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
115
118
  langroid/utils/web/login.py,sha256=1iz9eUAHa87vpKIkzwkmFa00avwFWivDSAr7QUhK7U0,2528
116
119
  langroid/vector_store/__init__.py,sha256=D82ioqPWxKTTbN0qiPNB-I1GjovhLw1MgDuYhcB3hCs,831
@@ -121,7 +124,7 @@ langroid/vector_store/meilisearch.py,sha256=d2huA9P-NoYRuAQ9ZeXJmMKr7ry8u90RUSR2
121
124
  langroid/vector_store/momento.py,sha256=9cui31TTrILid2KIzUpBkN2Ey3g_CZWOQVdaFsA4Ors,10045
122
125
  langroid/vector_store/qdrant_cloud.py,sha256=3im4Mip0QXLkR6wiqVsjV1QvhSElfxdFSuDKddBDQ-4,188
123
126
  langroid/vector_store/qdrantdb.py,sha256=sk5Qb2ZNbooi0rorsMuqIMokF7WADw6PJ0D6goM2XBw,16802
124
- langroid-0.1.249.dist-info/LICENSE,sha256=EgVbvA6VSYgUlvC3RvPKehSg7MFaxWDsFuzLOsPPfJg,1065
125
- langroid-0.1.249.dist-info/METADATA,sha256=1md_MzQhAHs9J7_OEEOfiL8C4N8GlWj5BBQGK4HrkGw,49426
126
- langroid-0.1.249.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
127
- langroid-0.1.249.dist-info/RECORD,,
127
+ langroid-0.1.251.dist-info/LICENSE,sha256=EgVbvA6VSYgUlvC3RvPKehSg7MFaxWDsFuzLOsPPfJg,1065
128
+ langroid-0.1.251.dist-info/METADATA,sha256=7zas4QChwjyKRKbSblv32RMUquw6ldzrtV57oceZmb4,49559
129
+ langroid-0.1.251.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
130
+ langroid-0.1.251.dist-info/RECORD,,