deepanalysts 0.2.2__tar.gz → 0.2.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.2.2 → deepanalysts-0.2.4}/PKG-INFO +7 -7
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/README.md +2 -2
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/backends/sandbox.py +4 -4
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/clients/basement.py +3 -3
- deepanalysts-0.2.4/deepanalysts/middleware/_utils.py +82 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/middleware/memory.py +20 -6
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/middleware/skills.py +15 -2
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts.egg-info/PKG-INFO +7 -7
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts.egg-info/SOURCES.txt +1 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/pyproject.toml +5 -5
- deepanalysts-0.2.4/tests/test_prompt_sections.py +177 -0
- deepanalysts-0.2.2/deepanalysts/middleware/_utils.py +0 -26
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/__init__.py +0 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/backends/__init__.py +0 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/backends/basement.py +0 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/backends/composite.py +0 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/backends/filesystem.py +0 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/backends/protocol.py +0 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/backends/state.py +0 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/backends/store.py +0 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/backends/supabase_storage.py +0 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/backends/utils.py +0 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/clients/__init__.py +0 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/middleware/__init__.py +0 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/middleware/filesystem.py +0 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/middleware/patch_tool_calls.py +0 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/middleware/subagents.py +0 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/middleware/summarization.py +0 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/middleware/tool_errors.py +0 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/utils/__init__.py +0 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/utils/retry.py +0 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts.egg-info/dependency_links.txt +0 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts.egg-info/requires.txt +0 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts.egg-info/top_level.txt +0 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/setup.cfg +0 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/tests/test_basement.py +0 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/tests/test_composite_backend.py +0 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/tests/test_filesystem_middleware.py +0 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/tests/test_sandbox_backend.py +0 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/tests/test_skills_middleware.py +0 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/tests/test_store_backend.py +0 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/tests/test_summarization_middleware.py +0 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/tests/test_supabase_storage_backend.py +0 -0
- {deepanalysts-0.2.2 → deepanalysts-0.2.4}/tests/test_utils.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepanalysts
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.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
|
|
7
|
-
Project-URL: Homepage, https://github.com/SKE-Labs/
|
|
8
|
-
Project-URL: Documentation, https://github.com/SKE-Labs/
|
|
9
|
-
Project-URL: Repository, https://github.com/SKE-Labs/
|
|
10
|
-
Project-URL: Issues, https://github.com/SKE-Labs/
|
|
7
|
+
Project-URL: Homepage, https://github.com/SKE-Labs/deepalpha-cli
|
|
8
|
+
Project-URL: Documentation, https://github.com/SKE-Labs/deepalpha-cli/tree/main/libs/deepanalysts
|
|
9
|
+
Project-URL: Repository, https://github.com/SKE-Labs/deepalpha-cli.git
|
|
10
|
+
Project-URL: Issues, https://github.com/SKE-Labs/deepalpha-cli/issues
|
|
11
11
|
Keywords: langchain,langgraph,agents,middleware,ai,trading
|
|
12
12
|
Classifier: Development Status :: 3 - Alpha
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
@@ -57,7 +57,7 @@ Deep Analysts provides a complete middleware stack for LangChain agents:
|
|
|
57
57
|
- **Backends**: Store (LangGraph BaseStore), Sandbox (subprocess execution), Composite (path-based routing)
|
|
58
58
|
- **API Integration**: Basement client for syncing skills/memories to cloud
|
|
59
59
|
|
|
60
|
-
Uses only langchain, langgraph, and standard libraries (no external
|
|
60
|
+
Uses only langchain, langgraph, and standard libraries (no external deepalpha dependencies).
|
|
61
61
|
|
|
62
62
|
## Usage
|
|
63
63
|
|
|
@@ -196,7 +196,7 @@ middleware_stack = [
|
|
|
196
196
|
from deepanalysts.clients import BasementClient
|
|
197
197
|
|
|
198
198
|
client = BasementClient(
|
|
199
|
-
base_url="https://basement.
|
|
199
|
+
base_url="https://basement.deepalpha.mn",
|
|
200
200
|
token="jwt-token",
|
|
201
201
|
)
|
|
202
202
|
|
|
@@ -22,7 +22,7 @@ Deep Analysts provides a complete middleware stack for LangChain agents:
|
|
|
22
22
|
- **Backends**: Store (LangGraph BaseStore), Sandbox (subprocess execution), Composite (path-based routing)
|
|
23
23
|
- **API Integration**: Basement client for syncing skills/memories to cloud
|
|
24
24
|
|
|
25
|
-
Uses only langchain, langgraph, and standard libraries (no external
|
|
25
|
+
Uses only langchain, langgraph, and standard libraries (no external deepalpha dependencies).
|
|
26
26
|
|
|
27
27
|
## Usage
|
|
28
28
|
|
|
@@ -161,7 +161,7 @@ middleware_stack = [
|
|
|
161
161
|
from deepanalysts.clients import BasementClient
|
|
162
162
|
|
|
163
163
|
client = BasementClient(
|
|
164
|
-
base_url="https://basement.
|
|
164
|
+
base_url="https://basement.deepalpha.mn",
|
|
165
165
|
token="jwt-token",
|
|
166
166
|
)
|
|
167
167
|
|
|
@@ -88,9 +88,9 @@ os.makedirs(parent_dir, exist_ok=True)
|
|
|
88
88
|
|
|
89
89
|
with open(file_path, 'w') as f:
|
|
90
90
|
f.write(content)
|
|
91
|
-
" <<'
|
|
91
|
+
" <<'__DEEPALPHA_EOF__'
|
|
92
92
|
{payload_b64}
|
|
93
|
-
|
|
93
|
+
__DEEPALPHA_EOF__"""
|
|
94
94
|
|
|
95
95
|
# Use heredoc to pass edit parameters via stdin to avoid ARG_MAX limits.
|
|
96
96
|
# Stdin format: base64-encoded JSON with {{"path": str, "old": str, "new": str}}.
|
|
@@ -146,9 +146,9 @@ with open(file_path, 'w') as f:
|
|
|
146
146
|
f.write(result)
|
|
147
147
|
|
|
148
148
|
print(count)
|
|
149
|
-
" <<'
|
|
149
|
+
" <<'__DEEPALPHA_EOF__'
|
|
150
150
|
{payload_b64}
|
|
151
|
-
|
|
151
|
+
__DEEPALPHA_EOF__"""
|
|
152
152
|
|
|
153
153
|
_READ_COMMAND_TEMPLATE = """python3 -c "
|
|
154
154
|
import os
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Basement API client for skills and memories.
|
|
2
2
|
|
|
3
3
|
This provides a generic client interface that can be configured for either
|
|
4
|
-
cloud (park) or local (
|
|
4
|
+
cloud (park) or local (deepalpha-cli) usage.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
@@ -15,7 +15,7 @@ import httpx
|
|
|
15
15
|
logger = logging.getLogger(__name__)
|
|
16
16
|
|
|
17
17
|
# Default API endpoint (can be overridden via BASEMENT_API env var)
|
|
18
|
-
DEFAULT_BASEMENT_API = os.environ.get("BASEMENT_API", "https://basement.
|
|
18
|
+
DEFAULT_BASEMENT_API = os.environ.get("BASEMENT_API", "https://basement.deepalpha.mn")
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
@runtime_checkable
|
|
@@ -52,7 +52,7 @@ class BasementClient:
|
|
|
52
52
|
"""Initialize the client.
|
|
53
53
|
|
|
54
54
|
Args:
|
|
55
|
-
base_url: API base URL (defaults to basement.
|
|
55
|
+
base_url: API base URL (defaults to basement.deepalpha.mn)
|
|
56
56
|
token: Static JWT token to use
|
|
57
57
|
token_provider: Callable that returns JWT token dynamically
|
|
58
58
|
timeout: Request timeout in seconds
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Utility functions for middleware."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from langchain_core.messages import SystemMessage
|
|
6
|
+
|
|
7
|
+
# Key used in SystemMessage.additional_kwargs to store section names.
|
|
8
|
+
# Parallel to content_blocks: sections[i] names blocks[i].
|
|
9
|
+
SECTIONS_KEY = "_prompt_sections"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _preserve_kwargs(system_message: SystemMessage | None) -> dict:
|
|
13
|
+
"""Return a copy of additional_kwargs from a system message, or empty dict."""
|
|
14
|
+
if system_message is None:
|
|
15
|
+
return {}
|
|
16
|
+
return dict(system_message.additional_kwargs)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def append_to_system_message(
|
|
20
|
+
system_message: SystemMessage | None,
|
|
21
|
+
text: str,
|
|
22
|
+
) -> SystemMessage:
|
|
23
|
+
"""Append text to a system message.
|
|
24
|
+
|
|
25
|
+
Handles both string content and content blocks properly by using
|
|
26
|
+
content_blocks API which always returns a list. Preserves
|
|
27
|
+
``additional_kwargs`` (including section metadata) from the original
|
|
28
|
+
message.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
system_message: Existing system message or None.
|
|
32
|
+
text: Text to add to the system message.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
New SystemMessage with the text appended.
|
|
36
|
+
"""
|
|
37
|
+
new_content: list[str | dict[str, str]] = list(system_message.content_blocks) if system_message else []
|
|
38
|
+
if new_content:
|
|
39
|
+
text = f"\n\n{text}"
|
|
40
|
+
new_content.append({"type": "text", "text": text})
|
|
41
|
+
return SystemMessage(content=new_content, additional_kwargs=_preserve_kwargs(system_message))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def insert_after_section(
|
|
45
|
+
system_message: SystemMessage | None,
|
|
46
|
+
text: str,
|
|
47
|
+
section_name: str,
|
|
48
|
+
) -> SystemMessage:
|
|
49
|
+
"""Insert a content block after a named section.
|
|
50
|
+
|
|
51
|
+
Section names are stored in ``additional_kwargs[SECTIONS_KEY]`` as a list
|
|
52
|
+
parallel to the content blocks. If *section_name* is found, the new text
|
|
53
|
+
is inserted immediately after that block; otherwise the call falls back to
|
|
54
|
+
:func:`append_to_system_message`.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
system_message: Existing system message (may carry section metadata).
|
|
58
|
+
text: Text to insert.
|
|
59
|
+
section_name: Name of the section to insert after.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
New SystemMessage with the text inserted at the correct position.
|
|
63
|
+
"""
|
|
64
|
+
if system_message is None:
|
|
65
|
+
return append_to_system_message(None, text)
|
|
66
|
+
|
|
67
|
+
sections: list[str] = system_message.additional_kwargs.get(SECTIONS_KEY, [])
|
|
68
|
+
if section_name not in sections:
|
|
69
|
+
return append_to_system_message(system_message, text)
|
|
70
|
+
|
|
71
|
+
blocks: list[str | dict[str, str]] = list(system_message.content_blocks)
|
|
72
|
+
idx = sections.index(section_name)
|
|
73
|
+
insert_at = idx + 1
|
|
74
|
+
|
|
75
|
+
blocks.insert(insert_at, {"type": "text", "text": f"\n\n{text}"})
|
|
76
|
+
|
|
77
|
+
new_sections = list(sections)
|
|
78
|
+
new_sections.insert(insert_at, f"_after_{section_name}")
|
|
79
|
+
|
|
80
|
+
kwargs = _preserve_kwargs(system_message)
|
|
81
|
+
kwargs[SECTIONS_KEY] = new_sections
|
|
82
|
+
return SystemMessage(content=blocks, additional_kwargs=kwargs)
|
|
@@ -18,8 +18,8 @@ from deepanalysts.backends import CompositeBackend
|
|
|
18
18
|
middleware = MemoryMiddleware(
|
|
19
19
|
backend=backend,
|
|
20
20
|
sources=[
|
|
21
|
-
"~/.
|
|
22
|
-
"./.
|
|
21
|
+
"~/.deepalpha/AGENTS.md",
|
|
22
|
+
"./.deepalpha/AGENTS.md",
|
|
23
23
|
],
|
|
24
24
|
)
|
|
25
25
|
|
|
@@ -50,7 +50,7 @@ from typing import TYPE_CHECKING, Annotated, NotRequired, Protocol, TypedDict
|
|
|
50
50
|
|
|
51
51
|
from langchain_core.runnables import RunnableConfig
|
|
52
52
|
|
|
53
|
-
from deepanalysts.middleware._utils import append_to_system_message
|
|
53
|
+
from deepanalysts.middleware._utils import append_to_system_message, insert_after_section
|
|
54
54
|
|
|
55
55
|
if TYPE_CHECKING:
|
|
56
56
|
from deepanalysts.backends.protocol import (
|
|
@@ -134,6 +134,7 @@ class MemoryMiddleware(AgentMiddleware):
|
|
|
134
134
|
backend: BACKEND_TYPES | None = None,
|
|
135
135
|
sources: list[str] | None = None,
|
|
136
136
|
loader: MemoryLoaderProtocol | None = None,
|
|
137
|
+
insert_after: str | None = None,
|
|
137
138
|
) -> None:
|
|
138
139
|
"""Initialize the memory middleware.
|
|
139
140
|
|
|
@@ -141,15 +142,20 @@ class MemoryMiddleware(AgentMiddleware):
|
|
|
141
142
|
backend: Backend instance or factory function that takes runtime
|
|
142
143
|
and returns a backend. Use a factory for StateBackend.
|
|
143
144
|
Optional if using loader mode.
|
|
144
|
-
sources: List of memory file paths to load (e.g., ["~/.
|
|
145
|
-
"./.
|
|
145
|
+
sources: List of memory file paths to load (e.g., ["~/.deepalpha/AGENTS.md",
|
|
146
|
+
"./.deepalpha/AGENTS.md"]). Display names are automatically derived
|
|
146
147
|
from the paths. Sources are loaded in order. Optional if using loader.
|
|
147
148
|
loader: Optional loader for API-based memory loading.
|
|
148
149
|
When provided, takes precedence over backend/sources.
|
|
150
|
+
insert_after: If set, insert the memory section after this named prompt
|
|
151
|
+
section instead of appending at the end. Section names are
|
|
152
|
+
tracked via ``additional_kwargs["_prompt_sections"]`` on the
|
|
153
|
+
``SystemMessage``. Falls back to append if not found.
|
|
149
154
|
"""
|
|
150
155
|
self._backend = backend
|
|
151
156
|
self.sources = sources or []
|
|
152
157
|
self.loader = loader
|
|
158
|
+
self.insert_after = insert_after
|
|
153
159
|
self.system_prompt_template = MEMORY_SYSTEM_PROMPT
|
|
154
160
|
|
|
155
161
|
def _get_backend(self, state: MemoryState, runtime: Runtime, config: RunnableConfig) -> BackendProtocol:
|
|
@@ -358,6 +364,9 @@ class MemoryMiddleware(AgentMiddleware):
|
|
|
358
364
|
instead of just string concatenation. Skips injection entirely
|
|
359
365
|
when no memory content is loaded.
|
|
360
366
|
|
|
367
|
+
When ``insert_after`` is set, the memory section is inserted after
|
|
368
|
+
the named prompt section rather than appended at the end.
|
|
369
|
+
|
|
361
370
|
Args:
|
|
362
371
|
request: Model request to modify.
|
|
363
372
|
|
|
@@ -376,7 +385,12 @@ class MemoryMiddleware(AgentMiddleware):
|
|
|
376
385
|
memory_contents=memory_contents,
|
|
377
386
|
)
|
|
378
387
|
|
|
379
|
-
|
|
388
|
+
if self.insert_after:
|
|
389
|
+
system_message = insert_after_section(
|
|
390
|
+
request.system_message, memory_section, self.insert_after
|
|
391
|
+
)
|
|
392
|
+
else:
|
|
393
|
+
system_message = append_to_system_message(request.system_message, memory_section)
|
|
380
394
|
return request.override(system_message=system_message)
|
|
381
395
|
|
|
382
396
|
def wrap_model_call(
|
|
@@ -71,7 +71,7 @@ from langchain_core.runnables import RunnableConfig
|
|
|
71
71
|
from langgraph.prebuilt import ToolRuntime
|
|
72
72
|
from langgraph.runtime import Runtime
|
|
73
73
|
|
|
74
|
-
from deepanalysts.middleware._utils import append_to_system_message
|
|
74
|
+
from deepanalysts.middleware._utils import append_to_system_message, insert_after_section
|
|
75
75
|
|
|
76
76
|
logger = logging.getLogger(__name__)
|
|
77
77
|
|
|
@@ -450,6 +450,7 @@ class SkillsMiddleware(AgentMiddleware):
|
|
|
450
450
|
sources: list[str] | None = None,
|
|
451
451
|
loader: SkillsLoaderProtocol | None = None,
|
|
452
452
|
agent_name: str = "orchestrator",
|
|
453
|
+
insert_after: str | None = None,
|
|
453
454
|
) -> None:
|
|
454
455
|
"""Initialize the skills middleware.
|
|
455
456
|
|
|
@@ -462,11 +463,15 @@ class SkillsMiddleware(AgentMiddleware):
|
|
|
462
463
|
loader: Optional loader for API-based skill loading.
|
|
463
464
|
When provided, takes precedence over backend/sources.
|
|
464
465
|
agent_name: Agent name for filtering skills by target_agents (default: "orchestrator").
|
|
466
|
+
insert_after: If set, insert the skills section after this named prompt
|
|
467
|
+
section instead of appending at the end. Falls back to
|
|
468
|
+
append if the section is not found.
|
|
465
469
|
"""
|
|
466
470
|
self._backend = backend
|
|
467
471
|
self.sources = sources or []
|
|
468
472
|
self.loader = loader
|
|
469
473
|
self.agent_name = agent_name
|
|
474
|
+
self.insert_after = insert_after
|
|
470
475
|
self.system_prompt_template = SKILLS_SYSTEM_PROMPT
|
|
471
476
|
|
|
472
477
|
def _get_backend(self, state: SkillsState, runtime: Runtime, config: RunnableConfig) -> BackendProtocol:
|
|
@@ -528,6 +533,9 @@ class SkillsMiddleware(AgentMiddleware):
|
|
|
528
533
|
instead of just string concatenation. Skips injection entirely
|
|
529
534
|
when no skills are available.
|
|
530
535
|
|
|
536
|
+
When ``insert_after`` is set, the skills section is inserted after
|
|
537
|
+
the named prompt section rather than appended at the end.
|
|
538
|
+
|
|
531
539
|
Args:
|
|
532
540
|
request: Model request to modify
|
|
533
541
|
|
|
@@ -546,7 +554,12 @@ class SkillsMiddleware(AgentMiddleware):
|
|
|
546
554
|
skills_list=skills_list,
|
|
547
555
|
)
|
|
548
556
|
|
|
549
|
-
|
|
557
|
+
if self.insert_after:
|
|
558
|
+
system_message = insert_after_section(
|
|
559
|
+
request.system_message, skills_section, self.insert_after
|
|
560
|
+
)
|
|
561
|
+
else:
|
|
562
|
+
system_message = append_to_system_message(request.system_message, skills_section)
|
|
550
563
|
return request.override(system_message=system_message)
|
|
551
564
|
|
|
552
565
|
def before_agent(self, state: SkillsState, runtime: Runtime, config: RunnableConfig) -> SkillsStateUpdate | None:
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepanalysts
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.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
|
|
7
|
-
Project-URL: Homepage, https://github.com/SKE-Labs/
|
|
8
|
-
Project-URL: Documentation, https://github.com/SKE-Labs/
|
|
9
|
-
Project-URL: Repository, https://github.com/SKE-Labs/
|
|
10
|
-
Project-URL: Issues, https://github.com/SKE-Labs/
|
|
7
|
+
Project-URL: Homepage, https://github.com/SKE-Labs/deepalpha-cli
|
|
8
|
+
Project-URL: Documentation, https://github.com/SKE-Labs/deepalpha-cli/tree/main/libs/deepanalysts
|
|
9
|
+
Project-URL: Repository, https://github.com/SKE-Labs/deepalpha-cli.git
|
|
10
|
+
Project-URL: Issues, https://github.com/SKE-Labs/deepalpha-cli/issues
|
|
11
11
|
Keywords: langchain,langgraph,agents,middleware,ai,trading
|
|
12
12
|
Classifier: Development Status :: 3 - Alpha
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
@@ -57,7 +57,7 @@ Deep Analysts provides a complete middleware stack for LangChain agents:
|
|
|
57
57
|
- **Backends**: Store (LangGraph BaseStore), Sandbox (subprocess execution), Composite (path-based routing)
|
|
58
58
|
- **API Integration**: Basement client for syncing skills/memories to cloud
|
|
59
59
|
|
|
60
|
-
Uses only langchain, langgraph, and standard libraries (no external
|
|
60
|
+
Uses only langchain, langgraph, and standard libraries (no external deepalpha dependencies).
|
|
61
61
|
|
|
62
62
|
## Usage
|
|
63
63
|
|
|
@@ -196,7 +196,7 @@ middleware_stack = [
|
|
|
196
196
|
from deepanalysts.clients import BasementClient
|
|
197
197
|
|
|
198
198
|
client = BasementClient(
|
|
199
|
-
base_url="https://basement.
|
|
199
|
+
base_url="https://basement.deepalpha.mn",
|
|
200
200
|
token="jwt-token",
|
|
201
201
|
)
|
|
202
202
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "deepanalysts"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.4"
|
|
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" }
|
|
@@ -26,10 +26,10 @@ dependencies = [
|
|
|
26
26
|
]
|
|
27
27
|
|
|
28
28
|
[project.urls]
|
|
29
|
-
Homepage = "https://github.com/SKE-Labs/
|
|
30
|
-
Documentation = "https://github.com/SKE-Labs/
|
|
31
|
-
Repository = "https://github.com/SKE-Labs/
|
|
32
|
-
Issues = "https://github.com/SKE-Labs/
|
|
29
|
+
Homepage = "https://github.com/SKE-Labs/deepalpha-cli"
|
|
30
|
+
Documentation = "https://github.com/SKE-Labs/deepalpha-cli/tree/main/libs/deepanalysts"
|
|
31
|
+
Repository = "https://github.com/SKE-Labs/deepalpha-cli.git"
|
|
32
|
+
Issues = "https://github.com/SKE-Labs/deepalpha-cli/issues"
|
|
33
33
|
|
|
34
34
|
[project.optional-dependencies]
|
|
35
35
|
postgres = [
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Tests for section-aware system message utilities.
|
|
2
|
+
|
|
3
|
+
Tests append_to_system_message (preserves additional_kwargs), insert_after_section,
|
|
4
|
+
and SECTIONS_KEY constant from deepanalysts.middleware._utils.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from langchain_core.messages import SystemMessage
|
|
9
|
+
|
|
10
|
+
from deepanalysts.middleware._utils import (
|
|
11
|
+
SECTIONS_KEY,
|
|
12
|
+
append_to_system_message,
|
|
13
|
+
insert_after_section,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# Fixtures
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def empty_msg() -> SystemMessage:
|
|
24
|
+
"""SystemMessage with no section metadata."""
|
|
25
|
+
return SystemMessage(content="plain text")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.fixture
|
|
29
|
+
def sectioned_msg() -> SystemMessage:
|
|
30
|
+
"""SystemMessage with three named sections as content blocks."""
|
|
31
|
+
return SystemMessage(
|
|
32
|
+
content=[
|
|
33
|
+
{"type": "text", "text": "Base prompt"},
|
|
34
|
+
{"type": "text", "text": "Session context"},
|
|
35
|
+
{"type": "text", "text": "User account"},
|
|
36
|
+
],
|
|
37
|
+
additional_kwargs={
|
|
38
|
+
SECTIONS_KEY: ["base", "session", "user_account"],
|
|
39
|
+
},
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# append_to_system_message
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TestAppendToSystemMessage:
|
|
49
|
+
def test_append_to_none_creates_message(self):
|
|
50
|
+
msg = append_to_system_message(None, "hello")
|
|
51
|
+
assert len(msg.content_blocks) == 1
|
|
52
|
+
assert msg.content_blocks[0]["text"] == "hello"
|
|
53
|
+
|
|
54
|
+
def test_append_adds_block(self, empty_msg: SystemMessage):
|
|
55
|
+
msg = append_to_system_message(empty_msg, "extra")
|
|
56
|
+
assert len(msg.content_blocks) == 2
|
|
57
|
+
assert msg.content_blocks[1]["text"] == "\n\nextra"
|
|
58
|
+
|
|
59
|
+
def test_preserves_additional_kwargs(self, sectioned_msg: SystemMessage):
|
|
60
|
+
msg = append_to_system_message(sectioned_msg, "appended")
|
|
61
|
+
assert SECTIONS_KEY in msg.additional_kwargs
|
|
62
|
+
assert msg.additional_kwargs[SECTIONS_KEY] == ["base", "session", "user_account"]
|
|
63
|
+
|
|
64
|
+
def test_preserves_other_kwargs(self):
|
|
65
|
+
original = SystemMessage(
|
|
66
|
+
content="text",
|
|
67
|
+
additional_kwargs={"custom_key": "custom_value", SECTIONS_KEY: ["a"]},
|
|
68
|
+
)
|
|
69
|
+
msg = append_to_system_message(original, "more")
|
|
70
|
+
assert msg.additional_kwargs["custom_key"] == "custom_value"
|
|
71
|
+
assert msg.additional_kwargs[SECTIONS_KEY] == ["a"]
|
|
72
|
+
|
|
73
|
+
def test_does_not_mutate_original(self, sectioned_msg: SystemMessage):
|
|
74
|
+
original_blocks = len(sectioned_msg.content_blocks)
|
|
75
|
+
append_to_system_message(sectioned_msg, "new stuff")
|
|
76
|
+
assert len(sectioned_msg.content_blocks) == original_blocks
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# insert_after_section
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class TestInsertAfterSection:
|
|
85
|
+
def test_inserts_after_named_section(self, sectioned_msg: SystemMessage):
|
|
86
|
+
msg = insert_after_section(sectioned_msg, "Preferences", "user_account")
|
|
87
|
+
blocks = msg.content_blocks
|
|
88
|
+
assert len(blocks) == 4
|
|
89
|
+
# New block should be at index 3 (after user_account at index 2)
|
|
90
|
+
assert "Preferences" in blocks[3]["text"]
|
|
91
|
+
|
|
92
|
+
def test_inserts_after_first_section(self, sectioned_msg: SystemMessage):
|
|
93
|
+
msg = insert_after_section(sectioned_msg, "After base", "base")
|
|
94
|
+
blocks = msg.content_blocks
|
|
95
|
+
assert len(blocks) == 4
|
|
96
|
+
assert "After base" in blocks[1]["text"]
|
|
97
|
+
# Original session should shift to index 2
|
|
98
|
+
assert blocks[2]["text"] == "Session context"
|
|
99
|
+
|
|
100
|
+
def test_inserts_after_middle_section(self, sectioned_msg: SystemMessage):
|
|
101
|
+
msg = insert_after_section(sectioned_msg, "After session", "session")
|
|
102
|
+
blocks = msg.content_blocks
|
|
103
|
+
assert len(blocks) == 4
|
|
104
|
+
assert "After session" in blocks[2]["text"]
|
|
105
|
+
assert blocks[3]["text"] == "User account"
|
|
106
|
+
|
|
107
|
+
def test_updates_section_names(self, sectioned_msg: SystemMessage):
|
|
108
|
+
msg = insert_after_section(sectioned_msg, "Prefs", "user_account")
|
|
109
|
+
sections = msg.additional_kwargs[SECTIONS_KEY]
|
|
110
|
+
assert sections == ["base", "session", "user_account", "_after_user_account"]
|
|
111
|
+
|
|
112
|
+
def test_falls_back_to_append_when_section_not_found(self, sectioned_msg: SystemMessage):
|
|
113
|
+
msg = insert_after_section(sectioned_msg, "Fallback", "nonexistent")
|
|
114
|
+
blocks = msg.content_blocks
|
|
115
|
+
assert len(blocks) == 4
|
|
116
|
+
assert "Fallback" in blocks[-1]["text"]
|
|
117
|
+
|
|
118
|
+
def test_falls_back_to_append_on_none_message(self):
|
|
119
|
+
msg = insert_after_section(None, "New content", "any_section")
|
|
120
|
+
assert len(msg.content_blocks) == 1
|
|
121
|
+
assert "New content" in msg.content_blocks[0]["text"]
|
|
122
|
+
|
|
123
|
+
def test_falls_back_when_no_sections_metadata(self, empty_msg: SystemMessage):
|
|
124
|
+
msg = insert_after_section(empty_msg, "Content", "base")
|
|
125
|
+
# Should append (2 blocks: original + appended)
|
|
126
|
+
assert len(msg.content_blocks) == 2
|
|
127
|
+
|
|
128
|
+
def test_preserves_other_kwargs(self):
|
|
129
|
+
original = SystemMessage(
|
|
130
|
+
content=[{"type": "text", "text": "A"}],
|
|
131
|
+
additional_kwargs={
|
|
132
|
+
SECTIONS_KEY: ["a"],
|
|
133
|
+
"custom": "value",
|
|
134
|
+
},
|
|
135
|
+
)
|
|
136
|
+
msg = insert_after_section(original, "B", "a")
|
|
137
|
+
assert msg.additional_kwargs["custom"] == "value"
|
|
138
|
+
|
|
139
|
+
def test_does_not_mutate_original(self, sectioned_msg: SystemMessage):
|
|
140
|
+
original_blocks = len(sectioned_msg.content_blocks)
|
|
141
|
+
original_sections = list(sectioned_msg.additional_kwargs[SECTIONS_KEY])
|
|
142
|
+
insert_after_section(sectioned_msg, "New", "base")
|
|
143
|
+
assert len(sectioned_msg.content_blocks) == original_blocks
|
|
144
|
+
assert sectioned_msg.additional_kwargs[SECTIONS_KEY] == original_sections
|
|
145
|
+
|
|
146
|
+
def test_multiple_inserts_chain_correctly(self, sectioned_msg: SystemMessage):
|
|
147
|
+
"""Two sequential inserts should both land in the right place."""
|
|
148
|
+
msg = insert_after_section(sectioned_msg, "After account", "user_account")
|
|
149
|
+
msg = insert_after_section(msg, "After session", "session")
|
|
150
|
+
sections = msg.additional_kwargs[SECTIONS_KEY]
|
|
151
|
+
assert sections == [
|
|
152
|
+
"base",
|
|
153
|
+
"session",
|
|
154
|
+
"_after_session",
|
|
155
|
+
"user_account",
|
|
156
|
+
"_after_user_account",
|
|
157
|
+
]
|
|
158
|
+
assert len(msg.content_blocks) == 5
|
|
159
|
+
|
|
160
|
+
def test_prepends_newlines_to_inserted_text(self, sectioned_msg: SystemMessage):
|
|
161
|
+
msg = insert_after_section(sectioned_msg, "Content", "base")
|
|
162
|
+
inserted = msg.content_blocks[1]["text"]
|
|
163
|
+
assert inserted.startswith("\n\n")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# ---------------------------------------------------------------------------
|
|
167
|
+
# SECTIONS_KEY constant
|
|
168
|
+
# ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class TestSectionsKey:
|
|
172
|
+
def test_key_is_string(self):
|
|
173
|
+
assert isinstance(SECTIONS_KEY, str)
|
|
174
|
+
|
|
175
|
+
def test_key_starts_with_underscore(self):
|
|
176
|
+
"""Convention: private metadata keys start with underscore."""
|
|
177
|
+
assert SECTIONS_KEY.startswith("_")
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
"""Utility functions for middleware."""
|
|
2
|
-
|
|
3
|
-
from langchain_core.messages import SystemMessage
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def append_to_system_message(
|
|
7
|
-
system_message: SystemMessage | None,
|
|
8
|
-
text: str,
|
|
9
|
-
) -> SystemMessage:
|
|
10
|
-
"""Append text to a system message.
|
|
11
|
-
|
|
12
|
-
Handles both string content and content blocks properly by using
|
|
13
|
-
content_blocks API which always returns a list.
|
|
14
|
-
|
|
15
|
-
Args:
|
|
16
|
-
system_message: Existing system message or None.
|
|
17
|
-
text: Text to add to the system message.
|
|
18
|
-
|
|
19
|
-
Returns:
|
|
20
|
-
New SystemMessage with the text appended.
|
|
21
|
-
"""
|
|
22
|
-
new_content: list[str | dict[str, str]] = list(system_message.content_blocks) if system_message else []
|
|
23
|
-
if new_content:
|
|
24
|
-
text = f"\n\n{text}"
|
|
25
|
-
new_content.append({"type": "text", "text": text})
|
|
26
|
-
return SystemMessage(content=new_content)
|
|
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
|