deepanalysts 0.2.4__tar.gz → 0.2.5__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.
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/PKG-INFO +1 -1
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/middleware/tool_errors.py +15 -2
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts.egg-info/PKG-INFO +1 -1
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts.egg-info/SOURCES.txt +1 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/pyproject.toml +8 -1
- deepanalysts-0.2.5/tests/test_tool_errors.py +106 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/README.md +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/__init__.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/backends/__init__.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/backends/basement.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/backends/composite.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/backends/filesystem.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/backends/protocol.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/backends/sandbox.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/backends/state.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/backends/store.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/backends/supabase_storage.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/backends/utils.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/clients/__init__.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/clients/basement.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/middleware/__init__.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/middleware/_utils.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/middleware/filesystem.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/middleware/memory.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/middleware/patch_tool_calls.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/middleware/skills.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/middleware/subagents.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/middleware/summarization.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/utils/__init__.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/utils/retry.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts.egg-info/dependency_links.txt +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts.egg-info/requires.txt +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts.egg-info/top_level.txt +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/setup.cfg +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/tests/test_basement.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/tests/test_composite_backend.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/tests/test_filesystem_middleware.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/tests/test_prompt_sections.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/tests/test_sandbox_backend.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/tests/test_skills_middleware.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/tests/test_store_backend.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/tests/test_summarization_middleware.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/tests/test_supabase_storage_backend.py +0 -0
- {deepanalysts-0.2.4 → deepanalysts-0.2.5}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepanalysts
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: LangChain/LangGraph middleware for building AI agents with memory, skills, and filesystem support
|
|
5
5
|
Author-email: Ganchuluun Narantsatsralt <tsatsralt@swifttech.cloud>
|
|
6
6
|
License: MIT
|
|
@@ -57,7 +57,13 @@ class ToolErrorHandlingMiddleware(AgentMiddleware):
|
|
|
57
57
|
self._failure_counts.clear()
|
|
58
58
|
|
|
59
59
|
def _on_failure(self, tool_name: str, error: str, tool_call_id: str) -> ToolMessage:
|
|
60
|
-
"""Track failure and return an appropriate (possibly escalated) error message.
|
|
60
|
+
"""Track failure and return an appropriate (possibly escalated) error message.
|
|
61
|
+
|
|
62
|
+
The returned ToolMessage carries `status="error"` and `name=tool_name`
|
|
63
|
+
so downstream middleware (and any caller scanning history) can
|
|
64
|
+
distinguish failed invocations from successful ones without parsing
|
|
65
|
+
content prefixes.
|
|
66
|
+
"""
|
|
61
67
|
count = self._failure_counts.get(tool_name, 0) + 1
|
|
62
68
|
self._failure_counts[tool_name] = count
|
|
63
69
|
|
|
@@ -70,7 +76,12 @@ class ToolErrorHandlingMiddleware(AgentMiddleware):
|
|
|
70
76
|
else:
|
|
71
77
|
content = f"Tool error: {error}"
|
|
72
78
|
|
|
73
|
-
return ToolMessage(
|
|
79
|
+
return ToolMessage(
|
|
80
|
+
content=content,
|
|
81
|
+
tool_call_id=tool_call_id,
|
|
82
|
+
name=tool_name,
|
|
83
|
+
status="error",
|
|
84
|
+
)
|
|
74
85
|
|
|
75
86
|
def _blocked_message(self, tool_name: str, tool_call_id: str) -> ToolMessage:
|
|
76
87
|
"""Return a message for tools that have already been blocked."""
|
|
@@ -83,6 +94,8 @@ class ToolErrorHandlingMiddleware(AgentMiddleware):
|
|
|
83
94
|
f"again. Inform the user and proceed with available information."
|
|
84
95
|
),
|
|
85
96
|
tool_call_id=tool_call_id,
|
|
97
|
+
name=tool_name,
|
|
98
|
+
status="error",
|
|
86
99
|
)
|
|
87
100
|
|
|
88
101
|
def wrap_tool_call(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepanalysts
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: LangChain/LangGraph middleware for building AI agents with memory, skills, and filesystem support
|
|
5
5
|
Author-email: Ganchuluun Narantsatsralt <tsatsralt@swifttech.cloud>
|
|
6
6
|
License: MIT
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "deepanalysts"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.5"
|
|
4
4
|
description = "LangChain/LangGraph middleware for building AI agents with memory, skills, and filesystem support"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -52,3 +52,10 @@ include = ["deepanalysts*"]
|
|
|
52
52
|
|
|
53
53
|
[tool.setuptools.package-data]
|
|
54
54
|
deepanalysts = ["py.typed"]
|
|
55
|
+
|
|
56
|
+
[dependency-groups]
|
|
57
|
+
dev = [
|
|
58
|
+
"anyio>=4.12.1",
|
|
59
|
+
"pytest>=9.0.2",
|
|
60
|
+
"pytest-anyio>=0.0.0",
|
|
61
|
+
]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Tests for ToolErrorHandlingMiddleware error-message contract.
|
|
2
|
+
|
|
3
|
+
The middleware must mark failure ToolMessages with `status="error"` and
|
|
4
|
+
`name=tool_name` so downstream middleware can distinguish failed tool
|
|
5
|
+
invocations from successful ones without parsing content prefixes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
from langchain.agents.middleware.types import ToolCallRequest
|
|
12
|
+
from langchain_core.messages import ToolMessage
|
|
13
|
+
from langchain_core.tools import ToolException
|
|
14
|
+
|
|
15
|
+
from deepanalysts.middleware import ToolErrorHandlingMiddleware
|
|
16
|
+
|
|
17
|
+
pytest_plugins = ("anyio",)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _request(tool_name: str, call_id: str = "c1") -> ToolCallRequest:
|
|
21
|
+
return ToolCallRequest(
|
|
22
|
+
tool_call={"name": tool_name, "args": {}, "id": call_id},
|
|
23
|
+
tool=None,
|
|
24
|
+
state={"messages": []},
|
|
25
|
+
runtime=None,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def _raise_tool_exc(req: ToolCallRequest) -> ToolMessage:
|
|
30
|
+
raise ToolException("boom")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def _raise_unexpected(req: ToolCallRequest) -> ToolMessage:
|
|
34
|
+
raise RuntimeError("unexpected")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def _success(req: ToolCallRequest) -> ToolMessage:
|
|
38
|
+
return ToolMessage(
|
|
39
|
+
content="ok",
|
|
40
|
+
name=req.tool_call["name"],
|
|
41
|
+
tool_call_id=req.tool_call["id"],
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TestErrorMessageStatus:
|
|
46
|
+
@pytest.mark.anyio
|
|
47
|
+
async def test_tool_exception_marks_status_error(self):
|
|
48
|
+
mw = ToolErrorHandlingMiddleware()
|
|
49
|
+
result = await mw.awrap_tool_call(_request("my_tool"), _raise_tool_exc)
|
|
50
|
+
assert isinstance(result, ToolMessage)
|
|
51
|
+
assert result.status == "error"
|
|
52
|
+
assert result.name == "my_tool"
|
|
53
|
+
assert result.content.startswith("Tool error:")
|
|
54
|
+
|
|
55
|
+
@pytest.mark.anyio
|
|
56
|
+
async def test_unexpected_exception_marks_status_error(self):
|
|
57
|
+
mw = ToolErrorHandlingMiddleware()
|
|
58
|
+
result = await mw.awrap_tool_call(_request("my_tool"), _raise_unexpected)
|
|
59
|
+
assert result.status == "error"
|
|
60
|
+
assert result.name == "my_tool"
|
|
61
|
+
|
|
62
|
+
@pytest.mark.anyio
|
|
63
|
+
async def test_circuit_breaker_stop_marks_status_error(self):
|
|
64
|
+
"""After max_retries failures, the next failure escalates to STOP:
|
|
65
|
+
— that message must also be tagged status=error."""
|
|
66
|
+
mw = ToolErrorHandlingMiddleware(max_retries=2)
|
|
67
|
+
# Two failures to trip the breaker on the next failure-handling path.
|
|
68
|
+
await mw.awrap_tool_call(_request("my_tool", "c1"), _raise_tool_exc)
|
|
69
|
+
result = await mw.awrap_tool_call(_request("my_tool", "c2"), _raise_tool_exc)
|
|
70
|
+
assert result.content.startswith("STOP:")
|
|
71
|
+
assert result.status == "error"
|
|
72
|
+
assert result.name == "my_tool"
|
|
73
|
+
|
|
74
|
+
@pytest.mark.anyio
|
|
75
|
+
async def test_blocked_message_marks_status_error(self):
|
|
76
|
+
"""Once the circuit is open, subsequent calls short-circuit to a
|
|
77
|
+
BLOCKED message — that message must also be status=error."""
|
|
78
|
+
mw = ToolErrorHandlingMiddleware(max_retries=1)
|
|
79
|
+
# Trip the breaker.
|
|
80
|
+
await mw.awrap_tool_call(_request("my_tool", "c1"), _raise_tool_exc)
|
|
81
|
+
# This call is short-circuited via _blocked_message.
|
|
82
|
+
result = await mw.awrap_tool_call(_request("my_tool", "c2"), _success)
|
|
83
|
+
assert result.content.startswith("BLOCKED:")
|
|
84
|
+
assert result.status == "error"
|
|
85
|
+
assert result.name == "my_tool"
|
|
86
|
+
|
|
87
|
+
@pytest.mark.anyio
|
|
88
|
+
async def test_success_passes_through_unchanged(self):
|
|
89
|
+
mw = ToolErrorHandlingMiddleware()
|
|
90
|
+
result = await mw.awrap_tool_call(_request("my_tool"), _success)
|
|
91
|
+
# Successful results are returned as-is — status defaults to success.
|
|
92
|
+
assert result.status == "success"
|
|
93
|
+
assert result.content == "ok"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class TestSyncPath:
|
|
97
|
+
def test_sync_tool_exception_marks_status_error(self):
|
|
98
|
+
mw = ToolErrorHandlingMiddleware()
|
|
99
|
+
|
|
100
|
+
def raise_exc(req: ToolCallRequest) -> ToolMessage:
|
|
101
|
+
raise ToolException("boom")
|
|
102
|
+
|
|
103
|
+
result = mw.wrap_tool_call(_request("my_tool"), raise_exc)
|
|
104
|
+
assert result.status == "error"
|
|
105
|
+
assert result.name == "my_tool"
|
|
106
|
+
assert result.content.startswith("Tool error:")
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|