langroid 0.54.1__py3-none-any.whl → 0.55.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- langroid/agent/base.py +190 -27
- langroid/agent/done_sequence_parser.py +151 -0
- langroid/agent/task.py +261 -0
- langroid/agent/tools/mcp/fastmcp_client.py +143 -33
- {langroid-0.54.1.dist-info → langroid-0.55.0.dist-info}/METADATA +2 -2
- {langroid-0.54.1.dist-info → langroid-0.55.0.dist-info}/RECORD +8 -7
- {langroid-0.54.1.dist-info → langroid-0.55.0.dist-info}/WHEEL +0 -0
- {langroid-0.54.1.dist-info → langroid-0.55.0.dist-info}/licenses/LICENSE +0 -0
langroid/agent/base.py
CHANGED
@@ -251,6 +251,172 @@ class Agent(ABC):
|
|
251
251
|
def clear_dialog(self) -> None:
|
252
252
|
self.dialog = []
|
253
253
|
|
254
|
+
def _analyze_handler_params(
|
255
|
+
self, handler_method: Any
|
256
|
+
) -> Tuple[bool, Optional[str], Optional[str]]:
|
257
|
+
"""
|
258
|
+
Analyze parameters of a handler method to determine their types.
|
259
|
+
|
260
|
+
Returns:
|
261
|
+
Tuple of (has_annotations, agent_param_name, chat_doc_param_name)
|
262
|
+
- has_annotations: True if useful type annotations were found
|
263
|
+
- agent_param_name: Name of the agent parameter if found
|
264
|
+
- chat_doc_param_name: Name of the chat_doc parameter if found
|
265
|
+
"""
|
266
|
+
sig = inspect.signature(handler_method)
|
267
|
+
params = list(sig.parameters.values())
|
268
|
+
# Remove 'self' parameter
|
269
|
+
params = [p for p in params if p.name != "self"]
|
270
|
+
|
271
|
+
agent_param = None
|
272
|
+
chat_doc_param = None
|
273
|
+
has_annotations = False
|
274
|
+
|
275
|
+
for param in params:
|
276
|
+
# First try type annotations
|
277
|
+
if param.annotation != inspect.Parameter.empty:
|
278
|
+
ann_str = str(param.annotation)
|
279
|
+
# Check for Agent-like types
|
280
|
+
if (
|
281
|
+
param.annotation == self.__class__
|
282
|
+
or "Agent" in ann_str
|
283
|
+
or (
|
284
|
+
hasattr(param.annotation, "__name__")
|
285
|
+
and "Agent" in param.annotation.__name__
|
286
|
+
)
|
287
|
+
):
|
288
|
+
agent_param = param.name
|
289
|
+
has_annotations = True
|
290
|
+
# Check for ChatDocument-like types
|
291
|
+
elif "ChatDocument" in ann_str or "ChatDoc" in ann_str:
|
292
|
+
chat_doc_param = param.name
|
293
|
+
has_annotations = True
|
294
|
+
|
295
|
+
# Fallback to parameter names
|
296
|
+
elif param.name == "agent":
|
297
|
+
agent_param = param.name
|
298
|
+
elif param.name == "chat_doc":
|
299
|
+
chat_doc_param = param.name
|
300
|
+
|
301
|
+
return has_annotations, agent_param, chat_doc_param
|
302
|
+
|
303
|
+
@no_type_check
|
304
|
+
def _create_handler_wrapper(
|
305
|
+
self,
|
306
|
+
message_class: Type[ToolMessage],
|
307
|
+
handler_method: Any,
|
308
|
+
is_async: bool = False,
|
309
|
+
) -> Any:
|
310
|
+
"""
|
311
|
+
Create a wrapper function for a handler method based on its signature.
|
312
|
+
|
313
|
+
Args:
|
314
|
+
message_class: The ToolMessage class
|
315
|
+
handler_method: The handle/handle_async method
|
316
|
+
is_async: Whether this is for an async handler
|
317
|
+
|
318
|
+
Returns:
|
319
|
+
Appropriate wrapper function
|
320
|
+
"""
|
321
|
+
sig = inspect.signature(handler_method)
|
322
|
+
params = list(sig.parameters.values())
|
323
|
+
params = [p for p in params if p.name != "self"]
|
324
|
+
|
325
|
+
has_annotations, agent_param, chat_doc_param = self._analyze_handler_params(
|
326
|
+
handler_method,
|
327
|
+
)
|
328
|
+
|
329
|
+
# Build wrapper based on found parameters
|
330
|
+
if len(params) == 0:
|
331
|
+
if is_async:
|
332
|
+
|
333
|
+
async def wrapper(obj: Any) -> Any:
|
334
|
+
return await obj.handle_async()
|
335
|
+
|
336
|
+
else:
|
337
|
+
|
338
|
+
def wrapper(obj: Any) -> Any:
|
339
|
+
return obj.handle()
|
340
|
+
|
341
|
+
elif agent_param and chat_doc_param:
|
342
|
+
# Both parameters present - build wrapper respecting their order
|
343
|
+
param_names = [p.name for p in params]
|
344
|
+
if param_names.index(agent_param) < param_names.index(chat_doc_param):
|
345
|
+
# agent is first parameter
|
346
|
+
if is_async:
|
347
|
+
|
348
|
+
async def wrapper(obj: Any, chat_doc: Any) -> Any:
|
349
|
+
return await obj.handle_async(self, chat_doc)
|
350
|
+
|
351
|
+
else:
|
352
|
+
|
353
|
+
def wrapper(obj: Any, chat_doc: Any) -> Any:
|
354
|
+
return obj.handle(self, chat_doc)
|
355
|
+
|
356
|
+
else:
|
357
|
+
# chat_doc is first parameter
|
358
|
+
if is_async:
|
359
|
+
|
360
|
+
async def wrapper(obj: Any, chat_doc: Any) -> Any:
|
361
|
+
return await obj.handle_async(chat_doc, self)
|
362
|
+
|
363
|
+
else:
|
364
|
+
|
365
|
+
def wrapper(obj: Any, chat_doc: Any) -> Any:
|
366
|
+
return obj.handle(chat_doc, self)
|
367
|
+
|
368
|
+
elif agent_param and not chat_doc_param:
|
369
|
+
# Only agent parameter
|
370
|
+
if is_async:
|
371
|
+
|
372
|
+
async def wrapper(obj: Any) -> Any:
|
373
|
+
return await obj.handle_async(self)
|
374
|
+
|
375
|
+
else:
|
376
|
+
|
377
|
+
def wrapper(obj: Any) -> Any:
|
378
|
+
return obj.handle(self)
|
379
|
+
|
380
|
+
elif chat_doc_param and not agent_param:
|
381
|
+
# Only chat_doc parameter
|
382
|
+
if is_async:
|
383
|
+
|
384
|
+
async def wrapper(obj: Any, chat_doc: Any) -> Any:
|
385
|
+
return await obj.handle_async(chat_doc)
|
386
|
+
|
387
|
+
else:
|
388
|
+
|
389
|
+
def wrapper(obj: Any, chat_doc: Any) -> Any:
|
390
|
+
return obj.handle(chat_doc)
|
391
|
+
|
392
|
+
else:
|
393
|
+
# No recognized parameters - backward compatibility
|
394
|
+
# Assume single parameter is chat_doc (legacy behavior)
|
395
|
+
if len(params) == 1:
|
396
|
+
if is_async:
|
397
|
+
|
398
|
+
async def wrapper(obj: Any, chat_doc: Any) -> Any:
|
399
|
+
return await obj.handle_async(chat_doc)
|
400
|
+
|
401
|
+
else:
|
402
|
+
|
403
|
+
def wrapper(obj: Any, chat_doc: Any) -> Any:
|
404
|
+
return obj.handle(chat_doc)
|
405
|
+
|
406
|
+
else:
|
407
|
+
# Multiple unrecognized parameters - best guess
|
408
|
+
if is_async:
|
409
|
+
|
410
|
+
async def wrapper(obj: Any, chat_doc: Any) -> Any:
|
411
|
+
return await obj.handle_async(chat_doc)
|
412
|
+
|
413
|
+
else:
|
414
|
+
|
415
|
+
def wrapper(obj: Any, chat_doc: Any) -> Any:
|
416
|
+
return obj.handle(chat_doc)
|
417
|
+
|
418
|
+
return wrapper
|
419
|
+
|
254
420
|
def _get_tool_list(
|
255
421
|
self, message_class: Optional[Type[ToolMessage]] = None
|
256
422
|
) -> List[str]:
|
@@ -304,13 +470,12 @@ class Agent(ABC):
|
|
304
470
|
in one place, i.e. in the message class.
|
305
471
|
See `tests/main/test_stateless_tool_messages.py` for an example.
|
306
472
|
"""
|
307
|
-
|
308
|
-
|
473
|
+
wrapper = self._create_handler_wrapper(
|
474
|
+
message_class,
|
475
|
+
message_class.handle,
|
476
|
+
is_async=False,
|
309
477
|
)
|
310
|
-
|
311
|
-
setattr(self, handler, lambda obj, chat_doc: obj.handle(chat_doc))
|
312
|
-
else:
|
313
|
-
setattr(self, handler, lambda obj: obj.handle())
|
478
|
+
setattr(self, handler, wrapper)
|
314
479
|
elif (
|
315
480
|
hasattr(message_class, "response")
|
316
481
|
and inspect.isfunction(message_class.response)
|
@@ -320,11 +485,17 @@ class Agent(ABC):
|
|
320
485
|
len(inspect.signature(message_class.response).parameters) > 2
|
321
486
|
)
|
322
487
|
if has_chat_doc_arg:
|
323
|
-
|
324
|
-
|
325
|
-
|
488
|
+
|
489
|
+
def response_wrapper_with_chat_doc(obj: Any, chat_doc: Any) -> Any:
|
490
|
+
return obj.response(self, chat_doc)
|
491
|
+
|
492
|
+
setattr(self, handler, response_wrapper_with_chat_doc)
|
326
493
|
else:
|
327
|
-
|
494
|
+
|
495
|
+
def response_wrapper_no_chat_doc(obj: Any) -> Any:
|
496
|
+
return obj.response(self)
|
497
|
+
|
498
|
+
setattr(self, handler, response_wrapper_no_chat_doc)
|
328
499
|
|
329
500
|
if hasattr(message_class, "handle_message_fallback") and (
|
330
501
|
inspect.isfunction(message_class.handle_message_fallback)
|
@@ -334,10 +505,13 @@ class Agent(ABC):
|
|
334
505
|
# `handle_message_fallback` method (which does nothing).
|
335
506
|
# It's possible multiple tool messages have a `handle_message_fallback`,
|
336
507
|
# in which case, the last one inserted will be used.
|
508
|
+
def fallback_wrapper(msg: Any) -> Any:
|
509
|
+
return message_class.handle_message_fallback(self, msg)
|
510
|
+
|
337
511
|
setattr(
|
338
512
|
self,
|
339
513
|
"handle_message_fallback",
|
340
|
-
|
514
|
+
fallback_wrapper,
|
341
515
|
)
|
342
516
|
|
343
517
|
async_handler_name = f"{handler}_async"
|
@@ -346,23 +520,12 @@ class Agent(ABC):
|
|
346
520
|
and inspect.isfunction(message_class.handle_async)
|
347
521
|
and not hasattr(self, async_handler_name)
|
348
522
|
):
|
349
|
-
|
350
|
-
|
523
|
+
wrapper = self._create_handler_wrapper(
|
524
|
+
message_class,
|
525
|
+
message_class.handle_async,
|
526
|
+
is_async=True,
|
351
527
|
)
|
352
|
-
|
353
|
-
if has_chat_doc_arg:
|
354
|
-
|
355
|
-
@no_type_check
|
356
|
-
async def handler(obj, chat_doc):
|
357
|
-
return await obj.handle_async(chat_doc)
|
358
|
-
|
359
|
-
else:
|
360
|
-
|
361
|
-
@no_type_check
|
362
|
-
async def handler(obj):
|
363
|
-
return await obj.handle_async()
|
364
|
-
|
365
|
-
setattr(self, async_handler_name, handler)
|
528
|
+
setattr(self, async_handler_name, wrapper)
|
366
529
|
elif (
|
367
530
|
hasattr(message_class, "response_async")
|
368
531
|
and inspect.isfunction(message_class.response_async)
|
@@ -0,0 +1,151 @@
|
|
1
|
+
"""Parser for done sequence DSL (Domain Specific Language).
|
2
|
+
|
3
|
+
Converts string patterns into DoneSequence objects for convenient task completion
|
4
|
+
configuration.
|
5
|
+
|
6
|
+
Examples:
|
7
|
+
"T, A" -> Tool followed by Agent response
|
8
|
+
"T[calculator], A" -> Specific tool 'calculator' followed by Agent response
|
9
|
+
"L, T, A, L" -> LLM, Tool, Agent, LLM sequence
|
10
|
+
"C[quit|exit]" -> Content matching regex pattern
|
11
|
+
"""
|
12
|
+
|
13
|
+
import re
|
14
|
+
from typing import List, Union
|
15
|
+
|
16
|
+
from langroid.pydantic_v1 import BaseModel
|
17
|
+
|
18
|
+
from .task import AgentEvent, DoneSequence, EventType
|
19
|
+
|
20
|
+
|
21
|
+
def parse_done_sequence(sequence: Union[str, DoneSequence]) -> DoneSequence:
|
22
|
+
"""Parse a string pattern or return existing DoneSequence unchanged.
|
23
|
+
|
24
|
+
Args:
|
25
|
+
sequence: Either a DoneSequence object or a string pattern to parse
|
26
|
+
|
27
|
+
Returns:
|
28
|
+
DoneSequence object
|
29
|
+
|
30
|
+
Raises:
|
31
|
+
ValueError: If the string pattern is invalid
|
32
|
+
"""
|
33
|
+
if isinstance(sequence, DoneSequence):
|
34
|
+
return sequence
|
35
|
+
|
36
|
+
if not isinstance(sequence, str):
|
37
|
+
raise ValueError(f"Expected string or DoneSequence, got {type(sequence)}")
|
38
|
+
|
39
|
+
events = _parse_string_pattern(sequence)
|
40
|
+
return DoneSequence(events=events)
|
41
|
+
|
42
|
+
|
43
|
+
def _parse_string_pattern(pattern: str) -> List[AgentEvent]:
|
44
|
+
"""Parse a string pattern into a list of AgentEvent objects.
|
45
|
+
|
46
|
+
Pattern format:
|
47
|
+
- Single letter codes: T, A, L, U, N, C
|
48
|
+
- Specific tools: T[tool_name]
|
49
|
+
- Content match: C[regex_pattern]
|
50
|
+
- Separated by commas, spaces allowed
|
51
|
+
|
52
|
+
Args:
|
53
|
+
pattern: String pattern to parse
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
List of AgentEvent objects
|
57
|
+
|
58
|
+
Raises:
|
59
|
+
ValueError: If pattern is invalid
|
60
|
+
"""
|
61
|
+
events = []
|
62
|
+
|
63
|
+
# Split by comma and strip whitespace
|
64
|
+
parts = [p.strip() for p in pattern.split(",")]
|
65
|
+
|
66
|
+
for part in parts:
|
67
|
+
if not part:
|
68
|
+
continue
|
69
|
+
|
70
|
+
event = _parse_event_token(part)
|
71
|
+
events.append(event)
|
72
|
+
|
73
|
+
if not events:
|
74
|
+
raise ValueError(f"No valid events found in pattern: {pattern}")
|
75
|
+
|
76
|
+
return events
|
77
|
+
|
78
|
+
|
79
|
+
def _parse_event_token(token: str) -> AgentEvent:
|
80
|
+
"""Parse a single event token into an AgentEvent.
|
81
|
+
|
82
|
+
Args:
|
83
|
+
token: Single event token (e.g., "T", "T[calc]", "C[quit|exit]")
|
84
|
+
|
85
|
+
Returns:
|
86
|
+
AgentEvent object
|
87
|
+
|
88
|
+
Raises:
|
89
|
+
ValueError: If token is invalid
|
90
|
+
"""
|
91
|
+
# Check for bracket notation
|
92
|
+
bracket_match = re.match(r"^([A-Z])\[([^\]]+)\]$", token)
|
93
|
+
|
94
|
+
if bracket_match:
|
95
|
+
event_code = bracket_match.group(1)
|
96
|
+
param = bracket_match.group(2)
|
97
|
+
|
98
|
+
if event_code == "T":
|
99
|
+
# Specific tool: T[tool_name]
|
100
|
+
return AgentEvent(event_type=EventType.SPECIFIC_TOOL, tool_name=param)
|
101
|
+
elif event_code == "C":
|
102
|
+
# Content match: C[regex_pattern]
|
103
|
+
return AgentEvent(event_type=EventType.CONTENT_MATCH, content_pattern=param)
|
104
|
+
else:
|
105
|
+
raise ValueError(
|
106
|
+
f"Invalid event code with brackets: {event_code}. "
|
107
|
+
"Only T[tool] and C[pattern] are supported."
|
108
|
+
)
|
109
|
+
|
110
|
+
# Simple single-letter codes
|
111
|
+
event_map = {
|
112
|
+
"T": EventType.TOOL,
|
113
|
+
"A": EventType.AGENT_RESPONSE,
|
114
|
+
"L": EventType.LLM_RESPONSE,
|
115
|
+
"U": EventType.USER_RESPONSE,
|
116
|
+
"N": EventType.NO_RESPONSE,
|
117
|
+
"C": EventType.CONTENT_MATCH, # C without brackets matches any content
|
118
|
+
}
|
119
|
+
|
120
|
+
if token in event_map:
|
121
|
+
return AgentEvent(event_type=event_map[token])
|
122
|
+
|
123
|
+
# If not a single letter, could be a full event type name
|
124
|
+
token_upper = token.upper()
|
125
|
+
if token_upper == "TOOL":
|
126
|
+
return AgentEvent(event_type=EventType.TOOL)
|
127
|
+
elif token_upper == "AGENT":
|
128
|
+
return AgentEvent(event_type=EventType.AGENT_RESPONSE)
|
129
|
+
elif token_upper == "LLM":
|
130
|
+
return AgentEvent(event_type=EventType.LLM_RESPONSE)
|
131
|
+
elif token_upper == "USER":
|
132
|
+
return AgentEvent(event_type=EventType.USER_RESPONSE)
|
133
|
+
else:
|
134
|
+
raise ValueError(
|
135
|
+
f"Invalid event token: '{token}'. "
|
136
|
+
"Valid tokens are: T, A, L, U, N, C, or T[tool_name], C[pattern]"
|
137
|
+
)
|
138
|
+
|
139
|
+
|
140
|
+
def parse_done_sequences(
|
141
|
+
sequences: List[Union[str, DoneSequence]]
|
142
|
+
) -> List[DoneSequence]:
|
143
|
+
"""Parse a list of mixed string patterns and DoneSequence objects.
|
144
|
+
|
145
|
+
Args:
|
146
|
+
sequences: List containing strings and/or DoneSequence objects
|
147
|
+
|
148
|
+
Returns:
|
149
|
+
List of DoneSequence objects
|
150
|
+
"""
|
151
|
+
return [parse_done_sequence(seq) for seq in sequences]
|
langroid/agent/task.py
CHANGED
@@ -6,6 +6,7 @@ import logging
|
|
6
6
|
import re
|
7
7
|
import threading
|
8
8
|
from collections import Counter, OrderedDict, deque
|
9
|
+
from enum import Enum
|
9
10
|
from pathlib import Path
|
10
11
|
from types import SimpleNamespace
|
11
12
|
from typing import (
|
@@ -20,6 +21,7 @@ from typing import (
|
|
20
21
|
Tuple,
|
21
22
|
Type,
|
22
23
|
TypeVar,
|
24
|
+
Union,
|
23
25
|
cast,
|
24
26
|
overload,
|
25
27
|
)
|
@@ -69,6 +71,38 @@ def noop_fn(*args: List[Any], **kwargs: Dict[str, Any]) -> None:
|
|
69
71
|
pass
|
70
72
|
|
71
73
|
|
74
|
+
class EventType(str, Enum):
|
75
|
+
"""Types of events that can occur in a task"""
|
76
|
+
|
77
|
+
TOOL = "tool" # Any tool generated
|
78
|
+
SPECIFIC_TOOL = "specific_tool" # Specific tool by name
|
79
|
+
LLM_RESPONSE = "llm_response" # LLM generates response
|
80
|
+
AGENT_RESPONSE = "agent_response" # Agent responds
|
81
|
+
USER_RESPONSE = "user_response" # User responds
|
82
|
+
CONTENT_MATCH = "content_match" # Response matches pattern
|
83
|
+
NO_RESPONSE = "no_response" # No valid response from entity
|
84
|
+
CUSTOM = "custom" # Custom condition
|
85
|
+
|
86
|
+
|
87
|
+
class AgentEvent(BaseModel):
|
88
|
+
"""Single event in a task sequence"""
|
89
|
+
|
90
|
+
event_type: EventType
|
91
|
+
tool_name: Optional[str] = None # For SPECIFIC_TOOL
|
92
|
+
content_pattern: Optional[str] = None # For CONTENT_MATCH (regex)
|
93
|
+
responder: Optional[str] = None # Specific responder name
|
94
|
+
# Optionally match only if the responder was specific entity/task
|
95
|
+
sender: Optional[str] = None # Entity name or Task name that sent the message
|
96
|
+
|
97
|
+
|
98
|
+
class DoneSequence(BaseModel):
|
99
|
+
"""A sequence of events that triggers task completion"""
|
100
|
+
|
101
|
+
events: List[AgentEvent]
|
102
|
+
# Optional name for debugging
|
103
|
+
name: Optional[str] = None
|
104
|
+
|
105
|
+
|
72
106
|
class TaskConfig(BaseModel):
|
73
107
|
"""Configuration for a Task. This is a container for any params that
|
74
108
|
we didn't include in the task `__init__` method.
|
@@ -108,6 +142,9 @@ class TaskConfig(BaseModel):
|
|
108
142
|
contains a Tool attempt by the LLM
|
109
143
|
(including tools not handled by the agent).
|
110
144
|
Default is False.
|
145
|
+
done_sequences (List[DoneSequence]): List of event sequences that trigger task
|
146
|
+
completion. Task is done if ANY sequence matches the recent event history.
|
147
|
+
Each sequence is checked against the message parent chain.
|
111
148
|
|
112
149
|
"""
|
113
150
|
|
@@ -120,6 +157,7 @@ class TaskConfig(BaseModel):
|
|
120
157
|
allow_subtask_multi_oai_tools: bool = True
|
121
158
|
recognize_string_signals: bool = True
|
122
159
|
done_if_tool: bool = False
|
160
|
+
done_sequences: Optional[List[Union[str, DoneSequence]]] = None
|
123
161
|
|
124
162
|
|
125
163
|
class Task:
|
@@ -257,6 +295,14 @@ class Task:
|
|
257
295
|
set_parent_agent=noop_fn,
|
258
296
|
)
|
259
297
|
self.config = config
|
298
|
+
# Store parsed done sequences
|
299
|
+
self._parsed_done_sequences: Optional[List[DoneSequence]] = None
|
300
|
+
if self.config.done_sequences:
|
301
|
+
from .done_sequence_parser import parse_done_sequences
|
302
|
+
|
303
|
+
self._parsed_done_sequences = parse_done_sequences(
|
304
|
+
self.config.done_sequences
|
305
|
+
)
|
260
306
|
# how to behave as a sub-task; can be overridden by `add_sub_task()`
|
261
307
|
self.config_sub_task = copy.deepcopy(config)
|
262
308
|
# counts of distinct pending messages in history,
|
@@ -360,6 +406,8 @@ class Task:
|
|
360
406
|
self.single_round = single_round
|
361
407
|
self.turns = -1 # no limit
|
362
408
|
self.llm_delegate = llm_delegate
|
409
|
+
# Track last responder for done sequence checking
|
410
|
+
self._last_responder: Optional[Responder] = None
|
363
411
|
if llm_delegate:
|
364
412
|
if self.single_round:
|
365
413
|
# 0: User instructs (delegating to LLM);
|
@@ -1276,6 +1324,9 @@ class Task:
|
|
1276
1324
|
|
1277
1325
|
self._update_no_answer_vars(result)
|
1278
1326
|
|
1327
|
+
# Store the last responder for done sequence checking
|
1328
|
+
self._last_responder = r
|
1329
|
+
|
1279
1330
|
# pending_sender is of type Responder,
|
1280
1331
|
# i.e. it is either one of the agent's entities
|
1281
1332
|
# OR a sub-task, that has produced a valid response.
|
@@ -1841,6 +1892,23 @@ class Task:
|
|
1841
1892
|
):
|
1842
1893
|
return (True, StatusCode.DONE)
|
1843
1894
|
|
1895
|
+
# Check done sequences
|
1896
|
+
if self._parsed_done_sequences and result is not None:
|
1897
|
+
# Get the message chain from the current result
|
1898
|
+
msg_chain = self._get_message_chain(result)
|
1899
|
+
|
1900
|
+
# Use last responder if r not provided
|
1901
|
+
responder = r if r is not None else self._last_responder
|
1902
|
+
|
1903
|
+
# Check each sequence
|
1904
|
+
for sequence in self._parsed_done_sequences:
|
1905
|
+
if self._matches_sequence_with_current(
|
1906
|
+
msg_chain, sequence, result, responder
|
1907
|
+
):
|
1908
|
+
seq_name = sequence.name or "unnamed"
|
1909
|
+
logger.info(f"Task {self.name} done: matched sequence '{seq_name}'")
|
1910
|
+
return (True, StatusCode.DONE)
|
1911
|
+
|
1844
1912
|
allow_done_string = self.config.recognize_string_signals
|
1845
1913
|
# An entity decided task is done, either via DoneTool,
|
1846
1914
|
# or by explicitly saying DONE
|
@@ -2127,3 +2195,196 @@ class Task:
|
|
2127
2195
|
return False, addressee, content_to_send
|
2128
2196
|
|
2129
2197
|
return None, None, None
|
2198
|
+
|
2199
|
+
def _classify_event(
|
2200
|
+
self, msg: ChatDocument | None, responder: Responder | None
|
2201
|
+
) -> Optional[AgentEvent]:
|
2202
|
+
"""Classify a message into an AgentEvent for sequence matching."""
|
2203
|
+
if msg is None:
|
2204
|
+
return AgentEvent(event_type=EventType.NO_RESPONSE)
|
2205
|
+
|
2206
|
+
# Determine the event type based on responder and message content
|
2207
|
+
event_type = EventType.NO_RESPONSE
|
2208
|
+
tool_name = None
|
2209
|
+
|
2210
|
+
# Check if there are tool messages
|
2211
|
+
tool_messages = self.agent.try_get_tool_messages(msg, all_tools=True)
|
2212
|
+
if tool_messages:
|
2213
|
+
event_type = EventType.TOOL
|
2214
|
+
if len(tool_messages) == 1:
|
2215
|
+
tool_name = tool_messages[0].request
|
2216
|
+
|
2217
|
+
# Check responder type
|
2218
|
+
if responder == Entity.LLM and not tool_messages:
|
2219
|
+
event_type = EventType.LLM_RESPONSE
|
2220
|
+
elif responder == Entity.AGENT:
|
2221
|
+
event_type = EventType.AGENT_RESPONSE
|
2222
|
+
elif responder == Entity.USER:
|
2223
|
+
event_type = EventType.USER_RESPONSE
|
2224
|
+
elif isinstance(responder, Task):
|
2225
|
+
# For sub-task responses, check the sender in metadata
|
2226
|
+
if msg.metadata.sender == Entity.LLM:
|
2227
|
+
event_type = EventType.LLM_RESPONSE
|
2228
|
+
elif msg.metadata.sender == Entity.AGENT:
|
2229
|
+
event_type = EventType.AGENT_RESPONSE
|
2230
|
+
else:
|
2231
|
+
event_type = EventType.USER_RESPONSE
|
2232
|
+
|
2233
|
+
# Get sender name
|
2234
|
+
sender_name = None
|
2235
|
+
if isinstance(responder, Entity):
|
2236
|
+
sender_name = responder.value
|
2237
|
+
elif isinstance(responder, Task):
|
2238
|
+
sender_name = responder.name
|
2239
|
+
|
2240
|
+
return AgentEvent(
|
2241
|
+
event_type=event_type,
|
2242
|
+
tool_name=tool_name,
|
2243
|
+
sender=sender_name,
|
2244
|
+
)
|
2245
|
+
|
2246
|
+
def _get_message_chain(
|
2247
|
+
self, msg: ChatDocument | None, max_depth: Optional[int] = None
|
2248
|
+
) -> List[ChatDocument]:
|
2249
|
+
"""Get the chain of messages by following parent pointers."""
|
2250
|
+
if max_depth is None:
|
2251
|
+
# Get max depth needed from all sequences
|
2252
|
+
max_depth = 50 # default fallback
|
2253
|
+
if self._parsed_done_sequences:
|
2254
|
+
max_depth = max(len(seq.events) for seq in self._parsed_done_sequences)
|
2255
|
+
|
2256
|
+
chain = []
|
2257
|
+
current = msg
|
2258
|
+
depth = 0
|
2259
|
+
|
2260
|
+
while current is not None and depth < max_depth:
|
2261
|
+
chain.append(current)
|
2262
|
+
current = current.parent
|
2263
|
+
depth += 1
|
2264
|
+
|
2265
|
+
# Reverse to get chronological order (oldest first)
|
2266
|
+
return list(reversed(chain))
|
2267
|
+
|
2268
|
+
def _matches_event(self, actual: AgentEvent, expected: AgentEvent) -> bool:
|
2269
|
+
"""Check if an actual event matches an expected event pattern."""
|
2270
|
+
# Check event type
|
2271
|
+
if expected.event_type == EventType.SPECIFIC_TOOL:
|
2272
|
+
if actual.event_type != EventType.TOOL:
|
2273
|
+
return False
|
2274
|
+
if expected.tool_name and actual.tool_name != expected.tool_name:
|
2275
|
+
return False
|
2276
|
+
elif actual.event_type != expected.event_type:
|
2277
|
+
return False
|
2278
|
+
|
2279
|
+
# Check sender if specified
|
2280
|
+
if expected.sender and actual.sender != expected.sender:
|
2281
|
+
return False
|
2282
|
+
|
2283
|
+
# TODO: Add content pattern matching for CONTENT_MATCH type
|
2284
|
+
|
2285
|
+
return True
|
2286
|
+
|
2287
|
+
def _matches_sequence(
|
2288
|
+
self, msg_chain: List[ChatDocument], sequence: DoneSequence
|
2289
|
+
) -> bool:
|
2290
|
+
"""Check if a message chain matches a done sequence.
|
2291
|
+
|
2292
|
+
We traverse the message chain and try to match the sequence events.
|
2293
|
+
The events don't have to be consecutive in the chain.
|
2294
|
+
"""
|
2295
|
+
if not sequence.events:
|
2296
|
+
return False
|
2297
|
+
|
2298
|
+
# Convert messages to events
|
2299
|
+
events = []
|
2300
|
+
for i, msg in enumerate(msg_chain):
|
2301
|
+
# Determine responder from metadata or by checking previous message
|
2302
|
+
responder = None
|
2303
|
+
if msg.metadata.sender:
|
2304
|
+
responder = msg.metadata.sender
|
2305
|
+
elif msg.metadata.sender_name:
|
2306
|
+
# Could be a task name - keep as None for now since we can't resolve
|
2307
|
+
# the actual Task object from just the name
|
2308
|
+
responder = None
|
2309
|
+
|
2310
|
+
event = self._classify_event(msg, responder)
|
2311
|
+
if event:
|
2312
|
+
events.append(event)
|
2313
|
+
|
2314
|
+
# Try to match the sequence
|
2315
|
+
seq_idx = 0
|
2316
|
+
for event in events:
|
2317
|
+
if seq_idx >= len(sequence.events):
|
2318
|
+
break
|
2319
|
+
|
2320
|
+
expected = sequence.events[seq_idx]
|
2321
|
+
if self._matches_event(event, expected):
|
2322
|
+
seq_idx += 1
|
2323
|
+
|
2324
|
+
# Check if we matched the entire sequence
|
2325
|
+
return seq_idx == len(sequence.events)
|
2326
|
+
|
2327
|
+
def _matches_sequence_with_current(
|
2328
|
+
self,
|
2329
|
+
msg_chain: List[ChatDocument],
|
2330
|
+
sequence: DoneSequence,
|
2331
|
+
current_msg: ChatDocument,
|
2332
|
+
current_responder: Optional[Responder],
|
2333
|
+
) -> bool:
|
2334
|
+
"""Check if the message chain plus current message matches a done sequence.
|
2335
|
+
|
2336
|
+
Process messages in reverse order (newest first) and match against
|
2337
|
+
the sequence events in reverse order.
|
2338
|
+
"""
|
2339
|
+
# Add current message to chain if not already there
|
2340
|
+
if not msg_chain or msg_chain[-1].id() != current_msg.id():
|
2341
|
+
msg_chain = msg_chain + [current_msg]
|
2342
|
+
|
2343
|
+
# If we don't have enough messages for the sequence, can't match
|
2344
|
+
if len(msg_chain) < len(sequence.events):
|
2345
|
+
return False
|
2346
|
+
|
2347
|
+
# Process in reverse order - start from the end of both lists
|
2348
|
+
seq_idx = len(sequence.events) - 1
|
2349
|
+
msg_idx = len(msg_chain) - 1
|
2350
|
+
|
2351
|
+
while seq_idx >= 0 and msg_idx >= 0:
|
2352
|
+
msg = msg_chain[msg_idx]
|
2353
|
+
expected = sequence.events[seq_idx]
|
2354
|
+
|
2355
|
+
# Determine responder for this message
|
2356
|
+
if msg_idx == len(msg_chain) - 1 and current_responder is not None:
|
2357
|
+
# For the last message, use the current responder
|
2358
|
+
responder = current_responder
|
2359
|
+
else:
|
2360
|
+
# For other messages, determine from metadata
|
2361
|
+
responder = msg.metadata.sender
|
2362
|
+
|
2363
|
+
# Classify the event
|
2364
|
+
event = self._classify_event(msg, responder)
|
2365
|
+
if not event:
|
2366
|
+
return False
|
2367
|
+
|
2368
|
+
# Check if it matches
|
2369
|
+
matched = False
|
2370
|
+
|
2371
|
+
# Special handling for CONTENT_MATCH
|
2372
|
+
if (
|
2373
|
+
expected.event_type == EventType.CONTENT_MATCH
|
2374
|
+
and expected.content_pattern
|
2375
|
+
):
|
2376
|
+
if re.search(expected.content_pattern, msg.content, re.IGNORECASE):
|
2377
|
+
matched = True
|
2378
|
+
elif self._matches_event(event, expected):
|
2379
|
+
matched = True
|
2380
|
+
|
2381
|
+
if not matched:
|
2382
|
+
# Strict matching - no skipping allowed
|
2383
|
+
return False
|
2384
|
+
else:
|
2385
|
+
# Matched! Move to next expected event
|
2386
|
+
seq_idx -= 1
|
2387
|
+
msg_idx -= 1
|
2388
|
+
|
2389
|
+
# We matched if we've matched all events in the sequence
|
2390
|
+
return seq_idx < 0
|
@@ -1,6 +1,8 @@
|
|
1
1
|
import asyncio
|
2
2
|
import datetime
|
3
3
|
import logging
|
4
|
+
from base64 import b64decode
|
5
|
+
from io import BytesIO
|
4
6
|
from typing import Any, Dict, List, Optional, Tuple, Type, TypeAlias, cast
|
5
7
|
|
6
8
|
from dotenv import load_dotenv
|
@@ -16,9 +18,20 @@ from mcp.client.session import (
|
|
16
18
|
LoggingFnT,
|
17
19
|
MessageHandlerFnT,
|
18
20
|
)
|
19
|
-
from mcp.types import
|
21
|
+
from mcp.types import (
|
22
|
+
BlobResourceContents,
|
23
|
+
CallToolResult,
|
24
|
+
EmbeddedResource,
|
25
|
+
ImageContent,
|
26
|
+
TextContent,
|
27
|
+
TextResourceContents,
|
28
|
+
Tool,
|
29
|
+
)
|
20
30
|
|
31
|
+
from langroid.agent.base import Agent
|
32
|
+
from langroid.agent.chat_document import ChatDocument
|
21
33
|
from langroid.agent.tool_message import ToolMessage
|
34
|
+
from langroid.parsing.file_attachment import FileAttachment
|
22
35
|
from langroid.pydantic_v1 import AnyUrl, BaseModel, Field, create_model
|
23
36
|
|
24
37
|
load_dotenv() # load environment variables from .env
|
@@ -39,6 +52,10 @@ class FastMCPClient:
|
|
39
52
|
def __init__(
|
40
53
|
self,
|
41
54
|
server: FastMCPServerSpec,
|
55
|
+
persist_connection: bool = False,
|
56
|
+
forward_images: bool = True,
|
57
|
+
forward_text_resources: bool = False,
|
58
|
+
forward_blob_resources: bool = False,
|
42
59
|
sampling_handler: SamplingHandler | None = None, # type: ignore
|
43
60
|
roots: RootsList | RootsHandler | None = None, # type: ignore
|
44
61
|
log_handler: LoggingFnT | None = None,
|
@@ -58,6 +75,10 @@ class FastMCPClient:
|
|
58
75
|
self.log_handler = log_handler
|
59
76
|
self.message_handler = message_handler
|
60
77
|
self.read_timeout_seconds = read_timeout_seconds
|
78
|
+
self.persist_connection = persist_connection
|
79
|
+
self.forward_text_resources = forward_text_resources
|
80
|
+
self.forward_blob_resources = forward_blob_resources
|
81
|
+
self.forward_images = forward_images
|
61
82
|
|
62
83
|
async def __aenter__(self) -> "FastMCPClient":
|
63
84
|
"""Enter the async context manager and connect inner client."""
|
@@ -96,6 +117,19 @@ class FastMCPClient:
|
|
96
117
|
self.client = None
|
97
118
|
self._cm = None
|
98
119
|
|
120
|
+
def __del__(self) -> None:
|
121
|
+
"""Warn about unclosed persistent connections."""
|
122
|
+
if self.client is not None and self.persist_connection:
|
123
|
+
import warnings
|
124
|
+
|
125
|
+
warnings.warn(
|
126
|
+
f"FastMCPClient with persist_connection=True was not properly closed. "
|
127
|
+
f"Connection to {self.server} may leak resources. "
|
128
|
+
f"Use 'async with' or call await client.close()",
|
129
|
+
ResourceWarning,
|
130
|
+
stacklevel=2,
|
131
|
+
)
|
132
|
+
|
99
133
|
def _schema_to_field(
|
100
134
|
self, name: str, schema: Dict[str, Any], prefix: str
|
101
135
|
) -> Tuple[Any, Any]:
|
@@ -151,7 +185,13 @@ class FastMCPClient:
|
|
151
185
|
with the given `tool_name`.
|
152
186
|
"""
|
153
187
|
if not self.client:
|
154
|
-
|
188
|
+
if self.persist_connection:
|
189
|
+
await self.connect()
|
190
|
+
assert self.client
|
191
|
+
else:
|
192
|
+
raise RuntimeError(
|
193
|
+
"Client not initialized. Use async with FastMCPClient."
|
194
|
+
)
|
155
195
|
target = await self.get_mcp_tool_async(tool_name)
|
156
196
|
if target is None:
|
157
197
|
raise ValueError(f"No tool named {tool_name}")
|
@@ -209,36 +249,68 @@ class FastMCPClient:
|
|
209
249
|
tool_model._renamed_fields = renamed # type: ignore[attr-defined]
|
210
250
|
|
211
251
|
# 2) define an arg-free call_tool_async()
|
212
|
-
async def call_tool_async(
|
252
|
+
async def call_tool_async(itself: ToolMessage) -> Any:
|
213
253
|
from langroid.agent.tools.mcp.fastmcp_client import FastMCPClient
|
214
254
|
|
215
255
|
# pack up the payload
|
216
|
-
payload =
|
217
|
-
exclude=
|
256
|
+
payload = itself.dict(
|
257
|
+
exclude=itself.Config.schema_extra["exclude"].union(
|
218
258
|
["request", "purpose"]
|
219
259
|
),
|
220
260
|
)
|
221
261
|
|
222
262
|
# restore any renamed fields
|
223
|
-
for orig, new in
|
263
|
+
for orig, new in itself.__class__._renamed_fields.items(): # type: ignore
|
224
264
|
if new in payload:
|
225
265
|
payload[orig] = payload.pop(new)
|
226
266
|
|
227
|
-
client_cfg = getattr(
|
267
|
+
client_cfg = getattr(itself.__class__, "_client_config", None) # type: ignore
|
228
268
|
if not client_cfg:
|
229
269
|
# Fallback or error - ideally _client_config should always exist
|
230
|
-
raise RuntimeError(f"Client config missing on {
|
270
|
+
raise RuntimeError(f"Client config missing on {itself.__class__}")
|
271
|
+
|
272
|
+
# Connect the client if not yet connected and keep the connection open
|
273
|
+
if self.persist_connection:
|
274
|
+
if not self.client:
|
275
|
+
await self.connect()
|
276
|
+
|
277
|
+
return await self.call_mcp_tool(itself.request, payload)
|
278
|
+
|
231
279
|
# open a fresh client, call the tool, then close
|
232
280
|
async with FastMCPClient(**client_cfg) as client: # type: ignore
|
233
|
-
return await client.call_mcp_tool(
|
281
|
+
return await client.call_mcp_tool(itself.request, payload)
|
234
282
|
|
235
283
|
tool_model.call_tool_async = call_tool_async # type: ignore
|
236
284
|
|
237
285
|
if not hasattr(tool_model, "handle_async"):
|
238
|
-
# 3) define
|
239
|
-
|
240
|
-
|
241
|
-
|
286
|
+
# 3) define handle_async() method with optional agent parameter
|
287
|
+
from typing import Union
|
288
|
+
|
289
|
+
async def handle_async(
|
290
|
+
self: ToolMessage, agent: Optional[Agent] = None
|
291
|
+
) -> Union[str, Optional[ChatDocument]]:
|
292
|
+
"""
|
293
|
+
Auto-generated handler for MCP tool. Returns ChatDocument with files
|
294
|
+
if files are present and agent is provided, otherwise returns text.
|
295
|
+
|
296
|
+
To override: define your own handle_async method with matching signature
|
297
|
+
if you need file handling, or simpler signature if you only need text.
|
298
|
+
"""
|
299
|
+
response = await self.call_tool_async() # type: ignore[attr-defined]
|
300
|
+
if response is None:
|
301
|
+
return None
|
302
|
+
|
303
|
+
content, files = response
|
304
|
+
|
305
|
+
# If we have files and an agent is provided, return a ChatDocument
|
306
|
+
if files and agent is not None:
|
307
|
+
return agent.create_agent_response(
|
308
|
+
content=content,
|
309
|
+
files=files,
|
310
|
+
)
|
311
|
+
else:
|
312
|
+
# Otherwise, just return the text content
|
313
|
+
return str(content) if content is not None else None
|
242
314
|
|
243
315
|
# add the handle_async() method to the tool model
|
244
316
|
tool_model.handle_async = handle_async # type: ignore
|
@@ -251,7 +323,13 @@ class FastMCPClient:
|
|
251
323
|
handling nested schemas, with `handle_async` methods
|
252
324
|
"""
|
253
325
|
if not self.client:
|
254
|
-
|
326
|
+
if self.persist_connection:
|
327
|
+
await self.connect()
|
328
|
+
assert self.client
|
329
|
+
else:
|
330
|
+
raise RuntimeError(
|
331
|
+
"Client not initialized. Use async with FastMCPClient."
|
332
|
+
)
|
255
333
|
resp = await self.client.list_tools()
|
256
334
|
return [await self.get_tool_async(t.name) for t in resp]
|
257
335
|
|
@@ -267,7 +345,13 @@ class FastMCPClient:
|
|
267
345
|
The raw Tool object from the server, or None.
|
268
346
|
"""
|
269
347
|
if not self.client:
|
270
|
-
|
348
|
+
if self.persist_connection:
|
349
|
+
await self.connect()
|
350
|
+
assert self.client
|
351
|
+
else:
|
352
|
+
raise RuntimeError(
|
353
|
+
"Client not initialized. Use async with FastMCPClient."
|
354
|
+
)
|
271
355
|
resp: List[Tool] = await self.client.list_tools()
|
272
356
|
return next((t for t in resp if t.name == name), None)
|
273
357
|
|
@@ -275,7 +359,7 @@ class FastMCPClient:
|
|
275
359
|
self,
|
276
360
|
tool_name: str,
|
277
361
|
result: CallToolResult,
|
278
|
-
) ->
|
362
|
+
) -> Optional[str | tuple[str, list[FileAttachment]]]:
|
279
363
|
if result.isError:
|
280
364
|
# Log more detailed error information
|
281
365
|
error_content = None
|
@@ -293,26 +377,41 @@ class FastMCPClient:
|
|
293
377
|
)
|
294
378
|
return f"ERROR: Tool call failed - {error_content}"
|
295
379
|
|
296
|
-
|
297
|
-
not isinstance(item, TextContent) for item in result.content
|
298
|
-
)
|
299
|
-
if has_nontext_results:
|
300
|
-
self.logger.warning(
|
301
|
-
f"""
|
302
|
-
MCP Tool {tool_name} returned non-text results,
|
303
|
-
which will be skipped.
|
304
|
-
""",
|
305
|
-
)
|
306
|
-
results = [
|
380
|
+
results_text = [
|
307
381
|
item.text for item in result.content if isinstance(item, TextContent)
|
308
382
|
]
|
309
|
-
|
310
|
-
|
311
|
-
|
383
|
+
results_file = []
|
384
|
+
|
385
|
+
for item in result.content:
|
386
|
+
if isinstance(item, ImageContent) and self.forward_images:
|
387
|
+
results_file.append(
|
388
|
+
FileAttachment.from_bytes(
|
389
|
+
b64decode(item.data),
|
390
|
+
mime_type=item.mimeType,
|
391
|
+
)
|
392
|
+
)
|
393
|
+
elif isinstance(item, EmbeddedResource):
|
394
|
+
if (
|
395
|
+
isinstance(item.resource, TextResourceContents)
|
396
|
+
and self.forward_text_resources
|
397
|
+
):
|
398
|
+
results_text.append(item.resource.text)
|
399
|
+
elif (
|
400
|
+
isinstance(item.resource, BlobResourceContents)
|
401
|
+
and self.forward_blob_resources
|
402
|
+
):
|
403
|
+
results_file.append(
|
404
|
+
FileAttachment.from_io(
|
405
|
+
BytesIO(b64decode(item.resource.blob)),
|
406
|
+
mime_type=item.resource.mimeType,
|
407
|
+
)
|
408
|
+
)
|
409
|
+
|
410
|
+
return "\n".join(results_text), results_file
|
312
411
|
|
313
412
|
async def call_mcp_tool(
|
314
413
|
self, tool_name: str, arguments: Dict[str, Any]
|
315
|
-
) -> str
|
414
|
+
) -> Optional[tuple[str, list[FileAttachment]]]:
|
316
415
|
"""Call an MCP tool with the given arguments.
|
317
416
|
|
318
417
|
Args:
|
@@ -323,12 +422,23 @@ class FastMCPClient:
|
|
323
422
|
The result of the tool call.
|
324
423
|
"""
|
325
424
|
if not self.client:
|
326
|
-
|
425
|
+
if self.persist_connection:
|
426
|
+
await self.connect()
|
427
|
+
assert self.client
|
428
|
+
else:
|
429
|
+
raise RuntimeError(
|
430
|
+
"Client not initialized. Use async with FastMCPClient."
|
431
|
+
)
|
327
432
|
result: CallToolResult = await self.client.session.call_tool(
|
328
433
|
tool_name,
|
329
434
|
arguments,
|
330
435
|
)
|
331
|
-
|
436
|
+
results = self._convert_tool_result(tool_name, result)
|
437
|
+
|
438
|
+
if isinstance(results, str):
|
439
|
+
return results, []
|
440
|
+
|
441
|
+
return results
|
332
442
|
|
333
443
|
|
334
444
|
# ==============================================================================
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: langroid
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.55.0
|
4
4
|
Summary: Harness LLMs with Multi-Agent Programming
|
5
5
|
Author-email: Prasad Chalasani <pchalasani@gmail.com>
|
6
6
|
License: MIT
|
@@ -209,7 +209,7 @@ Description-Content-Type: text/markdown
|
|
209
209
|
[](https://pypi.org/project/langroid/)
|
210
210
|
[](https://pypi.org/project/langroid/)
|
211
211
|
[](https://github.com/langroid/langroid/actions/workflows/pytest.yml)
|
212
|
-
[](https://codecov.io/gh/langroid/langroid)
|
213
213
|
[](https://github.com/langroid/langroid/actions/workflows/docker-publish.yml)
|
214
214
|
|
215
215
|
[](https://langroid.github.io/langroid)
|
@@ -3,12 +3,13 @@ langroid/exceptions.py,sha256=OPjece_8cwg94DLPcOGA1ddzy5bGh65pxzcHMnssTz8,2995
|
|
3
3
|
langroid/mytypes.py,sha256=HIcYAqGeA9OK0Hlscym2FI5Oax9QFljDZoVgRlomhRk,4014
|
4
4
|
langroid/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
5
|
langroid/agent/__init__.py,sha256=ll0Cubd2DZ-fsCMl7e10hf9ZjFGKzphfBco396IKITY,786
|
6
|
-
langroid/agent/base.py,sha256=
|
6
|
+
langroid/agent/base.py,sha256=a45iqoWet2W60h4wUIOyHDYlotgS0asqIjfbONT4fZQ,85706
|
7
7
|
langroid/agent/batch.py,sha256=wpE9RqCNDVDhAXkCB7wEqfCIEAi6qKcrhaZ-Zr9T4C0,21375
|
8
8
|
langroid/agent/chat_agent.py,sha256=2HIYzYxkrGkRIS97ioKfIqjaW3RbX89M39LjzBobBEY,88381
|
9
9
|
langroid/agent/chat_document.py,sha256=6O20Fp4QrquykaF2jFtwNHkvcoDte1LLwVZNk9mVH9c,18057
|
10
|
+
langroid/agent/done_sequence_parser.py,sha256=GiLTVtzzKb_YQcPpwqOajKu97RSUzZC6ae_ZagDmt_A,4425
|
10
11
|
langroid/agent/openai_assistant.py,sha256=JkAcs02bIrgPNVvUWVR06VCthc5-ulla2QMBzux_q6o,34340
|
11
|
-
langroid/agent/task.py,sha256=
|
12
|
+
langroid/agent/task.py,sha256=1xjey94vQRDDmXnhYCVu5D6Nw-3dVUaf_bUmXzRKxIk,101213
|
12
13
|
langroid/agent/tool_message.py,sha256=BhjP-_TfQ2tgxuY4Yo_JHLOwwt0mJ4BwjPnREvEY4vk,14744
|
13
14
|
langroid/agent/xml_tool_message.py,sha256=oeBKnJNoGaKdtz39XoWGMTNlVyXew2MWH5lgtYeh8wQ,15496
|
14
15
|
langroid/agent/callbacks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -56,7 +57,7 @@ langroid/agent/tools/segment_extract_tool.py,sha256=__srZ_VGYLVOdPrITUM8S0HpmX4q
|
|
56
57
|
langroid/agent/tools/tavily_search_tool.py,sha256=soI-j0HdgVQLf09wRQScaEK4b5RpAX9C4cwOivRFWWI,1903
|
57
58
|
langroid/agent/tools/mcp/__init__.py,sha256=DJNM0VeFnFS3pJKCyFGggT8JVjVu0rBzrGzasT1HaSM,387
|
58
59
|
langroid/agent/tools/mcp/decorators.py,sha256=h7dterhsmvWJ8q4mp_OopmuG2DF71ty8cZwOyzdDZuk,1127
|
59
|
-
langroid/agent/tools/mcp/fastmcp_client.py,sha256=
|
60
|
+
langroid/agent/tools/mcp/fastmcp_client.py,sha256=rxdNRinJoxFLbuTEAy7gVocC0jFRwcwDcoz9KAJN7sg,22068
|
60
61
|
langroid/cachedb/__init__.py,sha256=G2KyNnk3Qkhv7OKyxTOnpsxfDycx3NY0O_wXkJlalNY,96
|
61
62
|
langroid/cachedb/base.py,sha256=ztVjB1DtN6pLCujCWnR6xruHxwVj3XkYniRTYAKKqk0,1354
|
62
63
|
langroid/cachedb/redis_cachedb.py,sha256=7kgnbf4b5CKsCrlL97mHWKvdvlLt8zgn7lc528jEpiE,5141
|
@@ -133,7 +134,7 @@ langroid/vector_store/pineconedb.py,sha256=otxXZNaBKb9f_H75HTaU3lMHiaR2NUp5MqwLZ
|
|
133
134
|
langroid/vector_store/postgres.py,sha256=wHPtIi2qM4fhO4pMQr95pz1ZCe7dTb2hxl4VYspGZoA,16104
|
134
135
|
langroid/vector_store/qdrantdb.py,sha256=O6dSBoDZ0jzfeVBd7LLvsXu083xs2fxXtPa9gGX3JX4,18443
|
135
136
|
langroid/vector_store/weaviatedb.py,sha256=Yn8pg139gOy3zkaPfoTbMXEEBCiLiYa1MU5d_3UA1K4,11847
|
136
|
-
langroid-0.
|
137
|
-
langroid-0.
|
138
|
-
langroid-0.
|
139
|
-
langroid-0.
|
137
|
+
langroid-0.55.0.dist-info/METADATA,sha256=5RQdMjfiolUraCednhoHvcQVOaSHcOqxHj5e-JS3M6Q,65366
|
138
|
+
langroid-0.55.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
139
|
+
langroid-0.55.0.dist-info/licenses/LICENSE,sha256=EgVbvA6VSYgUlvC3RvPKehSg7MFaxWDsFuzLOsPPfJg,1065
|
140
|
+
langroid-0.55.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|