deepanalysts 0.9.3__tar.gz → 0.9.4__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.9.3 → deepanalysts-0.9.4}/PKG-INFO +1 -1
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts/middleware/filesystem.py +43 -15
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts.egg-info/PKG-INFO +1 -1
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts.egg-info/SOURCES.txt +1 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/pyproject.toml +1 -1
- deepanalysts-0.9.4/tests/test_regression_media_tool_results.py +65 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/README.md +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts/__init__.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts/_messages_reducer.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts/backends/__init__.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts/backends/basement.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts/backends/composite.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts/backends/e2b.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts/backends/filesystem.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts/backends/protocol.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts/backends/sandbox.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts/backends/state.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts/backends/store.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts/backends/supabase_storage.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts/backends/utils.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts/clients/__init__.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts/clients/basement.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts/middleware/__init__.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts/middleware/_state.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts/middleware/_utils.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts/middleware/memory.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts/middleware/patch_tool_calls.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts/middleware/skills.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts/middleware/subagents.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts/middleware/summarization.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts/middleware/tool_errors.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts/state.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts/streaming.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts/utils/__init__.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts/utils/retry.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts.egg-info/dependency_links.txt +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts.egg-info/requires.txt +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/deepanalysts.egg-info/top_level.txt +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/setup.cfg +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/tests/test_basement.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/tests/test_composite_backend.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/tests/test_e2b_backend.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/tests/test_e2b_batch_upload.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/tests/test_e2b_integration.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/tests/test_e2b_watch.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/tests/test_e2b_watch_live.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/tests/test_filesystem_middleware.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/tests/test_messages_reducer.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/tests/test_prompt_sections.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/tests/test_protocol_bridge.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/tests/test_regression_files_delta.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/tests/test_regression_fs_backport.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/tests/test_regression_middleware_tools.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/tests/test_regression_new_api.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/tests/test_regression_prompt_contracts.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/tests/test_regression_storage_stale_reads.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/tests/test_regression_subagents.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/tests/test_regression_tier1_backports.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/tests/test_sandbox_backend.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/tests/test_sandbox_hardening.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/tests/test_sandbox_hardening_integration.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/tests/test_sandbox_scan_bounds.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/tests/test_skills_middleware.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/tests/test_store_backend.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/tests/test_streaming_transformer.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/tests/test_summarization_middleware.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/tests/test_supabase_storage_backend.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/tests/test_supabase_storage_integration.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/tests/test_tool_errors.py +0 -0
- {deepanalysts-0.9.3 → deepanalysts-0.9.4}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepanalysts
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.4
|
|
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
|
|
@@ -520,6 +520,13 @@ def _supports_execution(backend: BackendProtocol) -> bool:
|
|
|
520
520
|
return isinstance(backend, SandboxBackendProtocol) or hasattr(backend, "execute")
|
|
521
521
|
|
|
522
522
|
|
|
523
|
+
_MEDIA_BLOCK_TYPES = frozenset({"image", "image_url", "audio", "video", "file", "input_image"})
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _is_media_block(block: object) -> bool:
|
|
527
|
+
return isinstance(block, dict) and block.get("type") in _MEDIA_BLOCK_TYPES
|
|
528
|
+
|
|
529
|
+
|
|
523
530
|
TOO_LARGE_TOOL_MSG = """Tool result too large. The full result of this tool call {tool_call_id} was saved to the filesystem at: {file_path}
|
|
524
531
|
Read it back with `read_file`, one slice at a time, using `offset` and `limit`
|
|
525
532
|
(for example offset=0, limit=100 for the first 100 lines). Do not read the whole
|
|
@@ -1157,18 +1164,33 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
1157
1164
|
return await handler(request)
|
|
1158
1165
|
|
|
1159
1166
|
def _extract_text_content(self, content: object) -> str:
|
|
1160
|
-
"""Extract text from message content, handling multimodal content.
|
|
1167
|
+
"""Extract text from message content, handling multimodal content.
|
|
1168
|
+
|
|
1169
|
+
Media blocks (image/audio/video/file) never count as text: a chart tool
|
|
1170
|
+
returning ``[image, text]`` used to be stringified whole, so its base64
|
|
1171
|
+
crossed the eviction limit and the model never saw the image.
|
|
1172
|
+
"""
|
|
1161
1173
|
if isinstance(content, str):
|
|
1162
1174
|
return content
|
|
1163
|
-
if isinstance(content, list)
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1175
|
+
if isinstance(content, list):
|
|
1176
|
+
parts: list[str] = []
|
|
1177
|
+
for item in content:
|
|
1178
|
+
if isinstance(item, str):
|
|
1179
|
+
parts.append(item)
|
|
1180
|
+
elif isinstance(item, dict) and item.get("type") == "text":
|
|
1181
|
+
parts.append(item.get("text", ""))
|
|
1182
|
+
elif not _is_media_block(item):
|
|
1183
|
+
parts.append(str(item))
|
|
1184
|
+
return "\n".join(parts)
|
|
1169
1185
|
# Fallback: stringify complex content
|
|
1170
1186
|
return str(content)
|
|
1171
1187
|
|
|
1188
|
+
@staticmethod
|
|
1189
|
+
def _evicted_content(message: ToolMessage, notice: str) -> str | list:
|
|
1190
|
+
"""Replacement content: the eviction notice, plus any media blocks kept as-is."""
|
|
1191
|
+
media = [b for b in message.content if _is_media_block(b)] if isinstance(message.content, list) else []
|
|
1192
|
+
return [*media, {"type": "text", "text": notice}] if media else notice
|
|
1193
|
+
|
|
1172
1194
|
def _process_large_message(
|
|
1173
1195
|
self,
|
|
1174
1196
|
message: ToolMessage,
|
|
@@ -1186,10 +1208,13 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
1186
1208
|
# Use smart preview showing head and tail instead of just first lines
|
|
1187
1209
|
content_sample = create_content_preview(content)
|
|
1188
1210
|
processed_message = ToolMessage(
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1211
|
+
self._evicted_content(
|
|
1212
|
+
message,
|
|
1213
|
+
TOO_LARGE_TOOL_MSG.format(
|
|
1214
|
+
tool_call_id=message.tool_call_id,
|
|
1215
|
+
file_path=file_path,
|
|
1216
|
+
content_sample=content_sample,
|
|
1217
|
+
),
|
|
1193
1218
|
),
|
|
1194
1219
|
tool_call_id=message.tool_call_id,
|
|
1195
1220
|
)
|
|
@@ -1273,10 +1298,13 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
1273
1298
|
# Use smart preview showing head and tail instead of just first lines
|
|
1274
1299
|
content_sample = create_content_preview(content)
|
|
1275
1300
|
processed_message = ToolMessage(
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1301
|
+
self._evicted_content(
|
|
1302
|
+
message,
|
|
1303
|
+
TOO_LARGE_TOOL_MSG.format(
|
|
1304
|
+
tool_call_id=message.tool_call_id,
|
|
1305
|
+
file_path=file_path,
|
|
1306
|
+
content_sample=content_sample,
|
|
1307
|
+
),
|
|
1280
1308
|
),
|
|
1281
1309
|
tool_call_id=message.tool_call_id,
|
|
1282
1310
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepanalysts
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.4
|
|
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
|
|
@@ -47,6 +47,7 @@ tests/test_prompt_sections.py
|
|
|
47
47
|
tests/test_protocol_bridge.py
|
|
48
48
|
tests/test_regression_files_delta.py
|
|
49
49
|
tests/test_regression_fs_backport.py
|
|
50
|
+
tests/test_regression_media_tool_results.py
|
|
50
51
|
tests/test_regression_middleware_tools.py
|
|
51
52
|
tests/test_regression_new_api.py
|
|
52
53
|
tests/test_regression_prompt_contracts.py
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Regression: tool results carrying images were evicted as text (2026-10-13).
|
|
2
|
+
|
|
3
|
+
`_extract_text_content` fell back to `str(content)` for any multi-block result,
|
|
4
|
+
so a chart tool returning `[image, text]` counted its base64 PNG (~300K chars)
|
|
5
|
+
as text, crossed the eviction limit, and the model got a file path + a base64
|
|
6
|
+
preview instead of the image — no agent ever saw a chart.
|
|
7
|
+
|
|
8
|
+
Locks:
|
|
9
|
+
1. Only text blocks count toward the eviction size; media blocks never do.
|
|
10
|
+
2. A small text part next to an image is NOT evicted; the message is untouched.
|
|
11
|
+
3. When the text part itself is oversized, only the text is evicted and the
|
|
12
|
+
media blocks are kept on the replacement message.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
from langchain_core.messages import ToolMessage
|
|
17
|
+
|
|
18
|
+
from deepanalysts.backends.state import StateBackend
|
|
19
|
+
from deepanalysts.middleware.filesystem import FilesystemMiddleware
|
|
20
|
+
|
|
21
|
+
pytestmark = pytest.mark.anyio
|
|
22
|
+
|
|
23
|
+
B64 = "iVBORw0KGgo" + "A" * 400_000
|
|
24
|
+
IMAGE = {"type": "image", "base64": B64, "mime_type": "image/png"}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture
|
|
28
|
+
def rt(state_runtime):
|
|
29
|
+
return state_runtime()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _mw():
|
|
33
|
+
return FilesystemMiddleware(backend=lambda r: StateBackend(r), tool_token_limit_before_evict=5000)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _chart(text: str) -> ToolMessage:
|
|
37
|
+
return ToolMessage(content=[IMAGE, {"type": "text", "text": text}], tool_call_id="c1", name="view_chart")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_media_blocks_do_not_count_as_text(rt):
|
|
41
|
+
assert _mw()._extract_text_content(_chart("Chart: BTC/USD 4h").content) == "Chart: BTC/USD 4h"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_small_text_with_image_is_not_evicted(rt):
|
|
45
|
+
msg = _chart("Chart: BTC/USD 4h")
|
|
46
|
+
out = _mw()._intercept_large_tool_result(msg, rt)
|
|
47
|
+
assert out is msg
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_oversized_text_is_evicted_but_image_is_kept(rt):
|
|
51
|
+
msg = _chart("x" * 30_000)
|
|
52
|
+
out = _mw()._intercept_large_tool_result(msg, rt)
|
|
53
|
+
replacement = out.update["messages"][0] if hasattr(out, "update") else out
|
|
54
|
+
blocks = replacement.content
|
|
55
|
+
assert isinstance(blocks, list)
|
|
56
|
+
assert any(isinstance(b, dict) and b.get("type") == "image" for b in blocks)
|
|
57
|
+
text = " ".join(b.get("text", "") for b in blocks if isinstance(b, dict) and b.get("type") == "text")
|
|
58
|
+
assert "/large_tool_results/" in text
|
|
59
|
+
assert B64 not in text
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def test_async_path_keeps_small_chart_results(rt):
|
|
63
|
+
msg = _chart("Chart: BTC/USD 4h")
|
|
64
|
+
out = await _mw()._aintercept_large_tool_result(msg, rt)
|
|
65
|
+
assert out is msg
|
|
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
|
|
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
|