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.
Files changed (44) hide show
  1. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/PKG-INFO +7 -7
  2. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/README.md +2 -2
  3. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/backends/sandbox.py +4 -4
  4. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/clients/basement.py +3 -3
  5. deepanalysts-0.2.4/deepanalysts/middleware/_utils.py +82 -0
  6. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/middleware/memory.py +20 -6
  7. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/middleware/skills.py +15 -2
  8. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts.egg-info/PKG-INFO +7 -7
  9. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts.egg-info/SOURCES.txt +1 -0
  10. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/pyproject.toml +5 -5
  11. deepanalysts-0.2.4/tests/test_prompt_sections.py +177 -0
  12. deepanalysts-0.2.2/deepanalysts/middleware/_utils.py +0 -26
  13. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/__init__.py +0 -0
  14. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/backends/__init__.py +0 -0
  15. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/backends/basement.py +0 -0
  16. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/backends/composite.py +0 -0
  17. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/backends/filesystem.py +0 -0
  18. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/backends/protocol.py +0 -0
  19. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/backends/state.py +0 -0
  20. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/backends/store.py +0 -0
  21. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/backends/supabase_storage.py +0 -0
  22. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/backends/utils.py +0 -0
  23. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/clients/__init__.py +0 -0
  24. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/middleware/__init__.py +0 -0
  25. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/middleware/filesystem.py +0 -0
  26. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/middleware/patch_tool_calls.py +0 -0
  27. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/middleware/subagents.py +0 -0
  28. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/middleware/summarization.py +0 -0
  29. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/middleware/tool_errors.py +0 -0
  30. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/utils/__init__.py +0 -0
  31. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts/utils/retry.py +0 -0
  32. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts.egg-info/dependency_links.txt +0 -0
  33. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts.egg-info/requires.txt +0 -0
  34. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/deepanalysts.egg-info/top_level.txt +0 -0
  35. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/setup.cfg +0 -0
  36. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/tests/test_basement.py +0 -0
  37. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/tests/test_composite_backend.py +0 -0
  38. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/tests/test_filesystem_middleware.py +0 -0
  39. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/tests/test_sandbox_backend.py +0 -0
  40. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/tests/test_skills_middleware.py +0 -0
  41. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/tests/test_store_backend.py +0 -0
  42. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/tests/test_summarization_middleware.py +0 -0
  43. {deepanalysts-0.2.2 → deepanalysts-0.2.4}/tests/test_supabase_storage_backend.py +0 -0
  44. {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.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/embient-cli
8
- Project-URL: Documentation, https://github.com/SKE-Labs/embient-cli/tree/main/libs/deepanalysts
9
- Project-URL: Repository, https://github.com/SKE-Labs/embient-cli.git
10
- Project-URL: Issues, https://github.com/SKE-Labs/embient-cli/issues
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 embient dependencies).
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.embient.ai",
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 embient dependencies).
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.embient.ai",
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
- " <<'__EMBIENT_EOF__'
91
+ " <<'__DEEPALPHA_EOF__'
92
92
  {payload_b64}
93
- __EMBIENT_EOF__"""
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
- " <<'__EMBIENT_EOF__'
149
+ " <<'__DEEPALPHA_EOF__'
150
150
  {payload_b64}
151
- __EMBIENT_EOF__"""
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 (embient-cli) usage.
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.embient.ai")
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.embient.ai)
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
- "~/.embient/AGENTS.md",
22
- "./.embient/AGENTS.md",
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., ["~/.embient/AGENTS.md",
145
- "./.embient/AGENTS.md"]). Display names are automatically derived
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
- system_message = append_to_system_message(request.system_message, memory_section)
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
- system_message = append_to_system_message(request.system_message, skills_section)
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.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/embient-cli
8
- Project-URL: Documentation, https://github.com/SKE-Labs/embient-cli/tree/main/libs/deepanalysts
9
- Project-URL: Repository, https://github.com/SKE-Labs/embient-cli.git
10
- Project-URL: Issues, https://github.com/SKE-Labs/embient-cli/issues
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 embient dependencies).
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.embient.ai",
199
+ base_url="https://basement.deepalpha.mn",
200
200
  token="jwt-token",
201
201
  )
202
202
 
@@ -32,6 +32,7 @@ deepanalysts/utils/retry.py
32
32
  tests/test_basement.py
33
33
  tests/test_composite_backend.py
34
34
  tests/test_filesystem_middleware.py
35
+ tests/test_prompt_sections.py
35
36
  tests/test_sandbox_backend.py
36
37
  tests/test_skills_middleware.py
37
38
  tests/test_store_backend.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "deepanalysts"
3
- version = "0.2.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/embient-cli"
30
- Documentation = "https://github.com/SKE-Labs/embient-cli/tree/main/libs/deepanalysts"
31
- Repository = "https://github.com/SKE-Labs/embient-cli.git"
32
- Issues = "https://github.com/SKE-Labs/embient-cli/issues"
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