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 +6 -1
- langroid/agent/base.py +53 -14
- langroid/agent/chat_agent.py +9 -11
- langroid/agent/special/lance_rag/critic_agent.py +7 -1
- langroid/agent/special/lance_rag/query_planner_agent.py +1 -1
- langroid/agent/task.py +204 -76
- langroid/agent/tool_message.py +4 -1
- langroid/agent/tools/recipient_tool.py +17 -9
- langroid/exceptions.py +3 -0
- langroid/mytypes.py +12 -0
- langroid/parsing/routing.py +27 -0
- langroid/utils/configuration.py +1 -0
- langroid/utils/system.py +20 -0
- {langroid-0.1.250.dist-info → langroid-0.1.252.dist-info}/METADATA +1 -1
- {langroid-0.1.250.dist-info → langroid-0.1.252.dist-info}/RECORD +17 -15
- {langroid-0.1.250.dist-info → langroid-0.1.252.dist-info}/LICENSE +0 -0
- {langroid-0.1.250.dist-info → langroid-0.1.252.dist-info}/WHEEL +0 -0
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
|
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
|
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
|
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
|
-
|
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
|
384
|
-
|
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
|
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
|
langroid/agent/chat_agent.py
CHANGED
@@ -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
|
279
|
-
else ("GUIDANCE: " +
|
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:
|
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.
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 =
|
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
|
1121
|
+
def _maybe_infinite_loop(self) -> bool:
|
1084
1122
|
"""
|
1085
|
-
|
1086
|
-
|
1087
|
-
|
1088
|
-
|
1089
|
-
|
1090
|
-
|
1091
|
-
|
1092
|
-
|
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
|
-
|
1095
|
-
|
1096
|
-
|
1097
|
-
|
1098
|
-
|
1099
|
-
|
1100
|
-
|
1101
|
-
|
1102
|
-
|
1103
|
-
|
1104
|
-
|
1105
|
-
|
1106
|
-
|
1107
|
-
|
1108
|
-
|
1109
|
-
#
|
1110
|
-
|
1111
|
-
#
|
1112
|
-
|
1113
|
-
|
1114
|
-
|
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
|
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
|
langroid/agent/tool_message.py
CHANGED
@@ -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,
|
137
|
-
|
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.
|
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
|
202
|
-
|
203
|
-
|
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
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
|
langroid/utils/configuration.py
CHANGED
@@ -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,10 +1,10 @@
|
|
1
|
-
langroid/__init__.py,sha256=
|
1
|
+
langroid/__init__.py,sha256=GXjDRv1rVNCxEB0BkfPIT4xgldKkB20b7kGPuGxNdiI,1803
|
2
2
|
langroid/agent/__init__.py,sha256=_D8dxnfdr92ch1CIrUkKjrB5HVvsQdn62b1Fb2kBxV8,785
|
3
|
-
langroid/agent/base.py,sha256=
|
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=
|
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=
|
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=
|
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=
|
36
|
-
langroid/agent/tool_message.py,sha256=
|
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=
|
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=
|
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=
|
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=
|
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.
|
126
|
-
langroid-0.1.
|
127
|
-
langroid-0.1.
|
128
|
-
langroid-0.1.
|
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,,
|
File without changes
|
File without changes
|