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.
Files changed (44) hide show
  1. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/PKG-INFO +1 -1
  2. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/middleware/tool_errors.py +15 -2
  3. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts.egg-info/PKG-INFO +1 -1
  4. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts.egg-info/SOURCES.txt +1 -0
  5. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/pyproject.toml +8 -1
  6. deepanalysts-0.2.5/tests/test_tool_errors.py +106 -0
  7. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/README.md +0 -0
  8. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/__init__.py +0 -0
  9. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/backends/__init__.py +0 -0
  10. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/backends/basement.py +0 -0
  11. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/backends/composite.py +0 -0
  12. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/backends/filesystem.py +0 -0
  13. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/backends/protocol.py +0 -0
  14. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/backends/sandbox.py +0 -0
  15. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/backends/state.py +0 -0
  16. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/backends/store.py +0 -0
  17. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/backends/supabase_storage.py +0 -0
  18. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/backends/utils.py +0 -0
  19. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/clients/__init__.py +0 -0
  20. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/clients/basement.py +0 -0
  21. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/middleware/__init__.py +0 -0
  22. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/middleware/_utils.py +0 -0
  23. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/middleware/filesystem.py +0 -0
  24. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/middleware/memory.py +0 -0
  25. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/middleware/patch_tool_calls.py +0 -0
  26. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/middleware/skills.py +0 -0
  27. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/middleware/subagents.py +0 -0
  28. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/middleware/summarization.py +0 -0
  29. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/utils/__init__.py +0 -0
  30. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts/utils/retry.py +0 -0
  31. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts.egg-info/dependency_links.txt +0 -0
  32. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts.egg-info/requires.txt +0 -0
  33. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/deepanalysts.egg-info/top_level.txt +0 -0
  34. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/setup.cfg +0 -0
  35. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/tests/test_basement.py +0 -0
  36. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/tests/test_composite_backend.py +0 -0
  37. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/tests/test_filesystem_middleware.py +0 -0
  38. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/tests/test_prompt_sections.py +0 -0
  39. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/tests/test_sandbox_backend.py +0 -0
  40. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/tests/test_skills_middleware.py +0 -0
  41. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/tests/test_store_backend.py +0 -0
  42. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/tests/test_summarization_middleware.py +0 -0
  43. {deepanalysts-0.2.4 → deepanalysts-0.2.5}/tests/test_supabase_storage_backend.py +0 -0
  44. {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.4
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(content=content, tool_call_id=tool_call_id)
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.4
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
@@ -38,4 +38,5 @@ tests/test_skills_middleware.py
38
38
  tests/test_store_backend.py
39
39
  tests/test_summarization_middleware.py
40
40
  tests/test_supabase_storage_backend.py
41
+ tests/test_tool_errors.py
41
42
  tests/test_utils.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "deepanalysts"
3
- version = "0.2.4"
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