deepagents 0.2.1rc1__tar.gz → 0.2.1rc2__tar.gz
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.
- {deepagents-0.2.1rc1/src/deepagents.egg-info → deepagents-0.2.1rc2}/PKG-INFO +2 -2
- {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/pyproject.toml +2 -2
- {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents/backends/utils.py +9 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents/middleware/filesystem.py +84 -46
- {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents/middleware/patch_tool_calls.py +3 -3
- {deepagents-0.2.1rc1 → deepagents-0.2.1rc2/src/deepagents.egg-info}/PKG-INFO +2 -2
- {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents.egg-info/requires.txt +1 -1
- {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/tests/test_middleware.py +176 -60
- {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/LICENSE +0 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/README.md +0 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/setup.cfg +0 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents/__init__.py +0 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents/backends/__init__.py +0 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents/backends/composite.py +0 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents/backends/filesystem.py +0 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents/backends/protocol.py +0 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents/backends/state.py +0 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents/backends/store.py +0 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents/graph.py +0 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents/middleware/__init__.py +0 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents/middleware/subagents.py +0 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents.egg-info/SOURCES.txt +0 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents.egg-info/dependency_links.txt +0 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents.egg-info/top_level.txt +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepagents
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1rc2
|
|
4
4
|
Summary: General purpose 'deep agent' with sub-agent spawning, todo list capabilities, and mock file system. Built on LangGraph.
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: <4.0,>=3.11
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
License-File: LICENSE
|
|
9
9
|
Requires-Dist: langchain-anthropic<2.0.0,>=1.0.0
|
|
10
|
-
Requires-Dist: langchain<2.0.0,>=1.0.
|
|
10
|
+
Requires-Dist: langchain<2.0.0,>=1.0.2
|
|
11
11
|
Requires-Dist: langchain-core<2.0.0,>=1.0.0
|
|
12
12
|
Requires-Dist: wcmatch
|
|
13
13
|
Provides-Extra: dev
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "deepagents"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.1rc2"
|
|
4
4
|
description = "General purpose 'deep agent' with sub-agent spawning, todo list capabilities, and mock file system. Built on LangGraph."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
7
7
|
requires-python = ">=3.11,<4.0"
|
|
8
8
|
dependencies = [
|
|
9
9
|
"langchain-anthropic>=1.0.0,<2.0.0",
|
|
10
|
-
"langchain>=1.0.
|
|
10
|
+
"langchain>=1.0.2,<2.0.0",
|
|
11
11
|
"langchain-core>=1.0.0,<2.0.0",
|
|
12
12
|
"wcmatch"
|
|
13
13
|
]
|
|
@@ -37,6 +37,15 @@ class GrepMatch(TypedDict):
|
|
|
37
37
|
text: str
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
def sanitize_tool_call_id(tool_call_id: str) -> str:
|
|
41
|
+
"""Sanitize tool_call_id to prevent path traversal and separator issues.
|
|
42
|
+
|
|
43
|
+
Replaces dangerous characters (., /, \) with underscores.
|
|
44
|
+
"""
|
|
45
|
+
sanitized = tool_call_id.replace(".", "_").replace("/", "_").replace("\\", "_")
|
|
46
|
+
return sanitized
|
|
47
|
+
|
|
48
|
+
|
|
40
49
|
def format_content_with_line_numbers(
|
|
41
50
|
content: str | list[str],
|
|
42
51
|
start_line: int = 1,
|
|
@@ -24,11 +24,11 @@ from typing_extensions import TypedDict
|
|
|
24
24
|
from deepagents.backends.protocol import BackendProtocol, BackendFactory, WriteResult, EditResult
|
|
25
25
|
from deepagents.backends import StateBackend
|
|
26
26
|
from deepagents.backends.utils import (
|
|
27
|
-
create_file_data,
|
|
28
27
|
update_file_data,
|
|
29
28
|
format_content_with_line_numbers,
|
|
30
29
|
format_grep_matches,
|
|
31
30
|
truncate_if_too_long,
|
|
31
|
+
sanitize_tool_call_id,
|
|
32
32
|
)
|
|
33
33
|
|
|
34
34
|
EMPTY_CONTENT_WARNING = "System reminder: File exists but has empty contents"
|
|
@@ -227,6 +227,15 @@ All file paths must start with a /.
|
|
|
227
227
|
|
|
228
228
|
|
|
229
229
|
def _get_backend(backend: BACKEND_TYPES, runtime: ToolRuntime) -> BackendProtocol:
|
|
230
|
+
"""Get the resolved backend instance from backend or factory.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
backend: Backend instance or factory function.
|
|
234
|
+
runtime: The tool runtime context.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Resolved backend instance.
|
|
238
|
+
"""
|
|
230
239
|
if callable(backend):
|
|
231
240
|
return backend(runtime)
|
|
232
241
|
return backend
|
|
@@ -532,6 +541,19 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
532
541
|
|
|
533
542
|
self.tools = _get_filesystem_tools(self.backend, custom_tool_descriptions)
|
|
534
543
|
|
|
544
|
+
def _get_backend(self, runtime: ToolRuntime) -> BackendProtocol:
|
|
545
|
+
"""Get the resolved backend instance from backend or factory.
|
|
546
|
+
|
|
547
|
+
Args:
|
|
548
|
+
runtime: The tool runtime context.
|
|
549
|
+
|
|
550
|
+
Returns:
|
|
551
|
+
Resolved backend instance.
|
|
552
|
+
"""
|
|
553
|
+
if callable(self.backend):
|
|
554
|
+
return self.backend(runtime)
|
|
555
|
+
return self.backend
|
|
556
|
+
|
|
535
557
|
def wrap_model_call(
|
|
536
558
|
self,
|
|
537
559
|
request: ModelRequest,
|
|
@@ -568,54 +590,70 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
568
590
|
request.system_prompt = request.system_prompt + "\n\n" + self.system_prompt if request.system_prompt else self.system_prompt
|
|
569
591
|
return await handler(request)
|
|
570
592
|
|
|
571
|
-
def
|
|
593
|
+
def _process_large_message(
|
|
594
|
+
self,
|
|
595
|
+
message: ToolMessage,
|
|
596
|
+
resolved_backend: BackendProtocol,
|
|
597
|
+
) -> tuple[ToolMessage, dict[str, FileData] | None]:
|
|
598
|
+
content = message.content
|
|
599
|
+
if not isinstance(content, str) or len(content) <= 4 * self.tool_token_limit_before_evict:
|
|
600
|
+
return message, None
|
|
601
|
+
|
|
602
|
+
sanitized_id = sanitize_tool_call_id(message.tool_call_id)
|
|
603
|
+
file_path = f"/large_tool_results/{sanitized_id}"
|
|
604
|
+
result = resolved_backend.write(file_path, content)
|
|
605
|
+
if result.error:
|
|
606
|
+
return message, None
|
|
607
|
+
content_sample = format_content_with_line_numbers(content.splitlines()[:10], start_line=1)
|
|
608
|
+
processed_message = ToolMessage(
|
|
609
|
+
TOO_LARGE_TOOL_MSG.format(
|
|
610
|
+
tool_call_id=message.tool_call_id,
|
|
611
|
+
file_path=file_path,
|
|
612
|
+
content_sample=content_sample,
|
|
613
|
+
),
|
|
614
|
+
tool_call_id=message.tool_call_id,
|
|
615
|
+
)
|
|
616
|
+
return processed_message, result.files_update
|
|
617
|
+
|
|
618
|
+
def _intercept_large_tool_result(self, tool_result: ToolMessage | Command, runtime: ToolRuntime) -> ToolMessage | Command:
|
|
572
619
|
if isinstance(tool_result, ToolMessage) and isinstance(tool_result.content, str):
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
)
|
|
587
|
-
],
|
|
588
|
-
"files": {file_path: file_data},
|
|
589
|
-
}
|
|
590
|
-
return Command(update=state_update)
|
|
620
|
+
if not (self.tool_token_limit_before_evict and
|
|
621
|
+
len(tool_result.content) > 4 * self.tool_token_limit_before_evict):
|
|
622
|
+
return tool_result
|
|
623
|
+
resolved_backend = self._get_backend(runtime)
|
|
624
|
+
processed_message, files_update = self._process_large_message(
|
|
625
|
+
tool_result,
|
|
626
|
+
resolved_backend,
|
|
627
|
+
)
|
|
628
|
+
return (Command(update={
|
|
629
|
+
"files": files_update,
|
|
630
|
+
"messages": [processed_message],
|
|
631
|
+
}) if files_update is not None else processed_message)
|
|
632
|
+
|
|
591
633
|
elif isinstance(tool_result, Command):
|
|
592
634
|
update = tool_result.update
|
|
593
635
|
if update is None:
|
|
594
636
|
return tool_result
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
for message in
|
|
600
|
-
if self.tool_token_limit_before_evict and
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
file_updates[file_path] = file_data
|
|
616
|
-
continue
|
|
617
|
-
edited_message_updates.append(message)
|
|
618
|
-
return Command(update={**update, "messages": edited_message_updates, "files": file_updates})
|
|
637
|
+
command_messages = update.get("messages", [])
|
|
638
|
+
accumulated_file_updates = dict(update.get("files", {}))
|
|
639
|
+
resolved_backend = self._get_backend(runtime)
|
|
640
|
+
processed_messages = []
|
|
641
|
+
for message in command_messages:
|
|
642
|
+
if not (self.tool_token_limit_before_evict and
|
|
643
|
+
isinstance(message, ToolMessage) and
|
|
644
|
+
isinstance(message.content, str) and
|
|
645
|
+
len(message.content) > 4 * self.tool_token_limit_before_evict):
|
|
646
|
+
processed_messages.append(message)
|
|
647
|
+
continue
|
|
648
|
+
processed_message, files_update = self._process_large_message(
|
|
649
|
+
message,
|
|
650
|
+
resolved_backend,
|
|
651
|
+
)
|
|
652
|
+
processed_messages.append(processed_message)
|
|
653
|
+
if files_update is not None:
|
|
654
|
+
accumulated_file_updates.update(files_update)
|
|
655
|
+
return Command(update={**update, "messages": processed_messages, "files": accumulated_file_updates})
|
|
656
|
+
|
|
619
657
|
return tool_result
|
|
620
658
|
|
|
621
659
|
def wrap_tool_call(
|
|
@@ -636,7 +674,7 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
636
674
|
return handler(request)
|
|
637
675
|
|
|
638
676
|
tool_result = handler(request)
|
|
639
|
-
return self._intercept_large_tool_result(tool_result)
|
|
677
|
+
return self._intercept_large_tool_result(tool_result, request.runtime)
|
|
640
678
|
|
|
641
679
|
async def awrap_tool_call(
|
|
642
680
|
self,
|
|
@@ -656,4 +694,4 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
656
694
|
return await handler(request)
|
|
657
695
|
|
|
658
696
|
tool_result = await handler(request)
|
|
659
|
-
return self._intercept_large_tool_result(tool_result)
|
|
697
|
+
return self._intercept_large_tool_result(tool_result, request.runtime)
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
5
|
from langchain.agents.middleware import AgentMiddleware, AgentState
|
|
6
|
-
from langchain_core.messages import
|
|
7
|
-
from langgraph.graph.message import REMOVE_ALL_MESSAGES
|
|
6
|
+
from langchain_core.messages import ToolMessage
|
|
8
7
|
from langgraph.runtime import Runtime
|
|
8
|
+
from langgraph.types import Overwrite
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class PatchToolCallsMiddleware(AgentMiddleware):
|
|
@@ -41,4 +41,4 @@ class PatchToolCallsMiddleware(AgentMiddleware):
|
|
|
41
41
|
)
|
|
42
42
|
)
|
|
43
43
|
|
|
44
|
-
return {"messages":
|
|
44
|
+
return {"messages": Overwrite(patched_messages)}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepagents
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1rc2
|
|
4
4
|
Summary: General purpose 'deep agent' with sub-agent spawning, todo list capabilities, and mock file system. Built on LangGraph.
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: <4.0,>=3.11
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
License-File: LICENSE
|
|
9
9
|
Requires-Dist: langchain-anthropic<2.0.0,>=1.0.0
|
|
10
|
-
Requires-Dist: langchain<2.0.0,>=1.0.
|
|
10
|
+
Requires-Dist: langchain<2.0.0,>=1.0.2
|
|
11
11
|
Requires-Dist: langchain-core<2.0.0,>=1.0.0
|
|
12
12
|
Requires-Dist: wcmatch
|
|
13
13
|
Provides-Extra: dev
|
|
@@ -8,7 +8,7 @@ from langchain_core.messages import (
|
|
|
8
8
|
ToolCall,
|
|
9
9
|
ToolMessage,
|
|
10
10
|
)
|
|
11
|
-
from langgraph.
|
|
11
|
+
from langgraph.types import Overwrite
|
|
12
12
|
from langgraph.store.memory import InMemoryStore
|
|
13
13
|
|
|
14
14
|
from deepagents.middleware.filesystem import (
|
|
@@ -824,6 +824,113 @@ class TestFilesystemMiddleware:
|
|
|
824
824
|
assert lines[1].count("m") == 2000
|
|
825
825
|
assert " 4\tline4" in lines[2]
|
|
826
826
|
|
|
827
|
+
def test_intercept_short_toolmessage(self):
|
|
828
|
+
"""Test that small ToolMessages pass through unchanged."""
|
|
829
|
+
middleware = FilesystemMiddleware(tool_token_limit_before_evict=1000)
|
|
830
|
+
state = FilesystemState(messages=[], files={})
|
|
831
|
+
runtime = ToolRuntime(state=state, context=None, tool_call_id="test_123", store=None, stream_writer=lambda _: None, config={})
|
|
832
|
+
|
|
833
|
+
small_content = "x" * 1000
|
|
834
|
+
tool_message = ToolMessage(content=small_content, tool_call_id="test_123")
|
|
835
|
+
result = middleware._intercept_large_tool_result(tool_message, runtime)
|
|
836
|
+
|
|
837
|
+
assert result == tool_message
|
|
838
|
+
|
|
839
|
+
def test_intercept_long_toolmessage(self):
|
|
840
|
+
"""Test that large ToolMessages are intercepted and saved to filesystem."""
|
|
841
|
+
from langgraph.types import Command
|
|
842
|
+
|
|
843
|
+
middleware = FilesystemMiddleware(tool_token_limit_before_evict=1000)
|
|
844
|
+
state = FilesystemState(messages=[], files={})
|
|
845
|
+
runtime = ToolRuntime(state=state, context=None, tool_call_id="test_123", store=None, stream_writer=lambda _: None, config={})
|
|
846
|
+
|
|
847
|
+
large_content = "x" * 5000
|
|
848
|
+
tool_message = ToolMessage(content=large_content, tool_call_id="test_123")
|
|
849
|
+
result = middleware._intercept_large_tool_result(tool_message, runtime)
|
|
850
|
+
|
|
851
|
+
assert isinstance(result, Command)
|
|
852
|
+
assert "/large_tool_results/test_123" in result.update["files"]
|
|
853
|
+
assert "Tool result too large" in result.update["messages"][0].content
|
|
854
|
+
|
|
855
|
+
def test_intercept_command_with_short_toolmessage(self):
|
|
856
|
+
"""Test that Commands with small messages pass through unchanged."""
|
|
857
|
+
from langgraph.types import Command
|
|
858
|
+
|
|
859
|
+
middleware = FilesystemMiddleware(tool_token_limit_before_evict=1000)
|
|
860
|
+
state = FilesystemState(messages=[], files={})
|
|
861
|
+
runtime = ToolRuntime(state=state, context=None, tool_call_id="test_123", store=None, stream_writer=lambda _: None, config={})
|
|
862
|
+
|
|
863
|
+
small_content = "x" * 1000
|
|
864
|
+
tool_message = ToolMessage(content=small_content, tool_call_id="test_123")
|
|
865
|
+
command = Command(update={"messages": [tool_message], "files": {}})
|
|
866
|
+
result = middleware._intercept_large_tool_result(command, runtime)
|
|
867
|
+
|
|
868
|
+
assert isinstance(result, Command)
|
|
869
|
+
assert result.update["messages"][0].content == small_content
|
|
870
|
+
|
|
871
|
+
def test_intercept_command_with_long_toolmessage(self):
|
|
872
|
+
"""Test that Commands with large messages are intercepted."""
|
|
873
|
+
from langgraph.types import Command
|
|
874
|
+
|
|
875
|
+
middleware = FilesystemMiddleware(tool_token_limit_before_evict=1000)
|
|
876
|
+
state = FilesystemState(messages=[], files={})
|
|
877
|
+
runtime = ToolRuntime(state=state, context=None, tool_call_id="test_123", store=None, stream_writer=lambda _: None, config={})
|
|
878
|
+
|
|
879
|
+
large_content = "y" * 5000
|
|
880
|
+
tool_message = ToolMessage(content=large_content, tool_call_id="test_123")
|
|
881
|
+
command = Command(update={"messages": [tool_message], "files": {}})
|
|
882
|
+
result = middleware._intercept_large_tool_result(command, runtime)
|
|
883
|
+
|
|
884
|
+
assert isinstance(result, Command)
|
|
885
|
+
assert "/large_tool_results/test_123" in result.update["files"]
|
|
886
|
+
assert "Tool result too large" in result.update["messages"][0].content
|
|
887
|
+
|
|
888
|
+
def test_intercept_command_with_files_and_long_toolmessage(self):
|
|
889
|
+
"""Test that file updates are properly merged with existing files and other keys preserved."""
|
|
890
|
+
from langgraph.types import Command
|
|
891
|
+
|
|
892
|
+
middleware = FilesystemMiddleware(tool_token_limit_before_evict=1000)
|
|
893
|
+
state = FilesystemState(messages=[], files={})
|
|
894
|
+
runtime = ToolRuntime(state=state, context=None, tool_call_id="test_123", store=None, stream_writer=lambda _: None, config={})
|
|
895
|
+
|
|
896
|
+
large_content = "z" * 5000
|
|
897
|
+
tool_message = ToolMessage(content=large_content, tool_call_id="test_123")
|
|
898
|
+
existing_file = FileData(content=["existing"], created_at="2021-01-01", modified_at="2021-01-01")
|
|
899
|
+
command = Command(update={
|
|
900
|
+
"messages": [tool_message],
|
|
901
|
+
"files": {"/existing.txt": existing_file},
|
|
902
|
+
"custom_key": "custom_value"
|
|
903
|
+
})
|
|
904
|
+
result = middleware._intercept_large_tool_result(command, runtime)
|
|
905
|
+
|
|
906
|
+
assert isinstance(result, Command)
|
|
907
|
+
assert "/existing.txt" in result.update["files"]
|
|
908
|
+
assert "/large_tool_results/test_123" in result.update["files"]
|
|
909
|
+
assert result.update["custom_key"] == "custom_value"
|
|
910
|
+
|
|
911
|
+
def test_sanitize_tool_call_id(self):
|
|
912
|
+
"""Test that tool_call_id is sanitized to prevent path traversal."""
|
|
913
|
+
from deepagents.backends.utils import sanitize_tool_call_id
|
|
914
|
+
|
|
915
|
+
assert sanitize_tool_call_id("call_123") == "call_123"
|
|
916
|
+
assert sanitize_tool_call_id("call/123") == "call_123"
|
|
917
|
+
assert sanitize_tool_call_id("test.id") == "test_id"
|
|
918
|
+
|
|
919
|
+
def test_intercept_sanitizes_tool_call_id(self):
|
|
920
|
+
"""Test that tool_call_id with dangerous characters is sanitized in file path."""
|
|
921
|
+
from langgraph.types import Command
|
|
922
|
+
|
|
923
|
+
middleware = FilesystemMiddleware(tool_token_limit_before_evict=1000)
|
|
924
|
+
state = FilesystemState(messages=[], files={})
|
|
925
|
+
runtime = ToolRuntime(state=state, context=None, tool_call_id="test_123", store=None, stream_writer=lambda _: None, config={})
|
|
926
|
+
|
|
927
|
+
large_content = "x" * 5000
|
|
928
|
+
tool_message = ToolMessage(content=large_content, tool_call_id="test/call.id")
|
|
929
|
+
result = middleware._intercept_large_tool_result(tool_message, runtime)
|
|
930
|
+
|
|
931
|
+
assert isinstance(result, Command)
|
|
932
|
+
assert "/large_tool_results/test_call_id" in result.update["files"]
|
|
933
|
+
|
|
827
934
|
|
|
828
935
|
@pytest.mark.requires("langchain_openai")
|
|
829
936
|
class TestSubagentMiddleware:
|
|
@@ -867,13 +974,14 @@ class TestPatchToolCallsMiddleware:
|
|
|
867
974
|
middleware = PatchToolCallsMiddleware()
|
|
868
975
|
state_update = middleware.before_agent({"messages": input_messages}, None)
|
|
869
976
|
assert state_update is not None
|
|
870
|
-
assert
|
|
871
|
-
|
|
872
|
-
assert
|
|
873
|
-
assert
|
|
874
|
-
assert
|
|
875
|
-
assert
|
|
876
|
-
assert
|
|
977
|
+
assert isinstance(state_update["messages"], Overwrite)
|
|
978
|
+
patched_messages = state_update["messages"].value
|
|
979
|
+
assert len(patched_messages) == 2
|
|
980
|
+
assert patched_messages[0].type == "system"
|
|
981
|
+
assert patched_messages[0].content == "You are a helpful assistant."
|
|
982
|
+
assert patched_messages[1].type == "human"
|
|
983
|
+
assert patched_messages[1].content == "Hello, how are you?"
|
|
984
|
+
assert patched_messages[1].id == "2"
|
|
877
985
|
|
|
878
986
|
def test_missing_tool_call(self) -> None:
|
|
879
987
|
input_messages = [
|
|
@@ -889,24 +997,23 @@ class TestPatchToolCallsMiddleware:
|
|
|
889
997
|
middleware = PatchToolCallsMiddleware()
|
|
890
998
|
state_update = middleware.before_agent({"messages": input_messages}, None)
|
|
891
999
|
assert state_update is not None
|
|
892
|
-
assert
|
|
893
|
-
|
|
894
|
-
assert
|
|
895
|
-
assert
|
|
896
|
-
assert
|
|
897
|
-
assert
|
|
898
|
-
assert
|
|
899
|
-
assert
|
|
900
|
-
assert
|
|
901
|
-
|
|
902
|
-
assert
|
|
903
|
-
assert
|
|
904
|
-
assert
|
|
905
|
-
assert
|
|
906
|
-
assert
|
|
907
|
-
assert
|
|
908
|
-
assert
|
|
909
|
-
assert updated_messages[4] == input_messages[3]
|
|
1000
|
+
assert isinstance(state_update["messages"], Overwrite)
|
|
1001
|
+
patched_messages = state_update["messages"].value
|
|
1002
|
+
assert len(patched_messages) == 5
|
|
1003
|
+
assert patched_messages[0].type == "system"
|
|
1004
|
+
assert patched_messages[0].content == "You are a helpful assistant."
|
|
1005
|
+
assert patched_messages[1].type == "human"
|
|
1006
|
+
assert patched_messages[1].content == "Hello, how are you?"
|
|
1007
|
+
assert patched_messages[2].type == "ai"
|
|
1008
|
+
assert len(patched_messages[2].tool_calls) == 1
|
|
1009
|
+
assert patched_messages[2].tool_calls[0]["id"] == "123"
|
|
1010
|
+
assert patched_messages[2].tool_calls[0]["name"] == "get_events_for_days"
|
|
1011
|
+
assert patched_messages[2].tool_calls[0]["args"] == {"date_str": "2025-01-01"}
|
|
1012
|
+
assert patched_messages[3].type == "tool"
|
|
1013
|
+
assert patched_messages[3].name == "get_events_for_days"
|
|
1014
|
+
assert patched_messages[3].tool_call_id == "123"
|
|
1015
|
+
assert patched_messages[4].type == "human"
|
|
1016
|
+
assert patched_messages[4].content == "What is the weather in Tokyo?"
|
|
910
1017
|
|
|
911
1018
|
def test_no_missing_tool_calls(self) -> None:
|
|
912
1019
|
input_messages = [
|
|
@@ -923,12 +1030,22 @@ class TestPatchToolCallsMiddleware:
|
|
|
923
1030
|
middleware = PatchToolCallsMiddleware()
|
|
924
1031
|
state_update = middleware.before_agent({"messages": input_messages}, None)
|
|
925
1032
|
assert state_update is not None
|
|
926
|
-
assert
|
|
927
|
-
|
|
928
|
-
assert
|
|
929
|
-
|
|
930
|
-
assert
|
|
931
|
-
assert
|
|
1033
|
+
assert isinstance(state_update["messages"], Overwrite)
|
|
1034
|
+
patched_messages = state_update["messages"].value
|
|
1035
|
+
assert len(patched_messages) == 5
|
|
1036
|
+
assert patched_messages[0].type == "system"
|
|
1037
|
+
assert patched_messages[0].content == "You are a helpful assistant."
|
|
1038
|
+
assert patched_messages[1].type == "human"
|
|
1039
|
+
assert patched_messages[1].content == "Hello, how are you?"
|
|
1040
|
+
assert patched_messages[2].type == "ai"
|
|
1041
|
+
assert len(patched_messages[2].tool_calls) == 1
|
|
1042
|
+
assert patched_messages[2].tool_calls[0]["id"] == "123"
|
|
1043
|
+
assert patched_messages[2].tool_calls[0]["name"] == "get_events_for_days"
|
|
1044
|
+
assert patched_messages[2].tool_calls[0]["args"] == {"date_str": "2025-01-01"}
|
|
1045
|
+
assert patched_messages[3].type == "tool"
|
|
1046
|
+
assert patched_messages[3].tool_call_id == "123"
|
|
1047
|
+
assert patched_messages[4].type == "human"
|
|
1048
|
+
assert patched_messages[4].content == "What is the weather in Tokyo?"
|
|
932
1049
|
|
|
933
1050
|
def test_two_missing_tool_calls(self) -> None:
|
|
934
1051
|
input_messages = [
|
|
@@ -950,34 +1067,33 @@ class TestPatchToolCallsMiddleware:
|
|
|
950
1067
|
middleware = PatchToolCallsMiddleware()
|
|
951
1068
|
state_update = middleware.before_agent({"messages": input_messages}, None)
|
|
952
1069
|
assert state_update is not None
|
|
953
|
-
assert
|
|
954
|
-
|
|
955
|
-
assert
|
|
956
|
-
assert
|
|
957
|
-
assert
|
|
958
|
-
assert
|
|
959
|
-
assert
|
|
960
|
-
assert
|
|
961
|
-
assert
|
|
962
|
-
assert
|
|
963
|
-
assert
|
|
964
|
-
assert
|
|
965
|
-
assert
|
|
966
|
-
assert
|
|
967
|
-
|
|
968
|
-
assert
|
|
969
|
-
assert
|
|
970
|
-
assert
|
|
971
|
-
assert
|
|
972
|
-
assert
|
|
973
|
-
assert
|
|
974
|
-
assert
|
|
975
|
-
assert
|
|
976
|
-
assert
|
|
977
|
-
assert
|
|
978
|
-
assert
|
|
979
|
-
assert
|
|
980
|
-
assert updated_messages[7] == input_messages[5]
|
|
1070
|
+
assert isinstance(state_update["messages"], Overwrite)
|
|
1071
|
+
patched_messages = state_update["messages"].value
|
|
1072
|
+
assert len(patched_messages) == 8
|
|
1073
|
+
assert patched_messages[0].type == "system"
|
|
1074
|
+
assert patched_messages[0].content == "You are a helpful assistant."
|
|
1075
|
+
assert patched_messages[1].type == "human"
|
|
1076
|
+
assert patched_messages[1].content == "Hello, how are you?"
|
|
1077
|
+
assert patched_messages[2].type == "ai"
|
|
1078
|
+
assert len(patched_messages[2].tool_calls) == 1
|
|
1079
|
+
assert patched_messages[2].tool_calls[0]["id"] == "123"
|
|
1080
|
+
assert patched_messages[2].tool_calls[0]["name"] == "get_events_for_days"
|
|
1081
|
+
assert patched_messages[2].tool_calls[0]["args"] == {"date_str": "2025-01-01"}
|
|
1082
|
+
assert patched_messages[3].type == "tool"
|
|
1083
|
+
assert patched_messages[3].name == "get_events_for_days"
|
|
1084
|
+
assert patched_messages[3].tool_call_id == "123"
|
|
1085
|
+
assert patched_messages[4].type == "human"
|
|
1086
|
+
assert patched_messages[4].content == "What is the weather in Tokyo?"
|
|
1087
|
+
assert patched_messages[5].type == "ai"
|
|
1088
|
+
assert len(patched_messages[5].tool_calls) == 1
|
|
1089
|
+
assert patched_messages[5].tool_calls[0]["id"] == "456"
|
|
1090
|
+
assert patched_messages[5].tool_calls[0]["name"] == "get_events_for_days"
|
|
1091
|
+
assert patched_messages[5].tool_calls[0]["args"] == {"date_str": "2025-01-01"}
|
|
1092
|
+
assert patched_messages[6].type == "tool"
|
|
1093
|
+
assert patched_messages[6].name == "get_events_for_days"
|
|
1094
|
+
assert patched_messages[6].tool_call_id == "456"
|
|
1095
|
+
assert patched_messages[7].type == "human"
|
|
1096
|
+
assert patched_messages[7].content == "What is the weather in Tokyo?"
|
|
981
1097
|
|
|
982
1098
|
|
|
983
1099
|
class TestTruncation:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|