hexdag 0.5.0.dev1__py3-none-any.whl

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 (261) hide show
  1. hexdag/__init__.py +116 -0
  2. hexdag/__main__.py +30 -0
  3. hexdag/adapters/executors/__init__.py +5 -0
  4. hexdag/adapters/executors/local_executor.py +316 -0
  5. hexdag/builtin/__init__.py +6 -0
  6. hexdag/builtin/adapters/__init__.py +51 -0
  7. hexdag/builtin/adapters/anthropic/__init__.py +5 -0
  8. hexdag/builtin/adapters/anthropic/anthropic_adapter.py +151 -0
  9. hexdag/builtin/adapters/database/__init__.py +6 -0
  10. hexdag/builtin/adapters/database/csv/csv_adapter.py +249 -0
  11. hexdag/builtin/adapters/database/pgvector/__init__.py +5 -0
  12. hexdag/builtin/adapters/database/pgvector/pgvector_adapter.py +478 -0
  13. hexdag/builtin/adapters/database/sqlalchemy/sqlalchemy_adapter.py +252 -0
  14. hexdag/builtin/adapters/database/sqlite/__init__.py +5 -0
  15. hexdag/builtin/adapters/database/sqlite/sqlite_adapter.py +410 -0
  16. hexdag/builtin/adapters/local/README.md +59 -0
  17. hexdag/builtin/adapters/local/__init__.py +7 -0
  18. hexdag/builtin/adapters/local/local_observer_manager.py +696 -0
  19. hexdag/builtin/adapters/memory/__init__.py +47 -0
  20. hexdag/builtin/adapters/memory/file_memory_adapter.py +297 -0
  21. hexdag/builtin/adapters/memory/in_memory_memory.py +216 -0
  22. hexdag/builtin/adapters/memory/schemas.py +57 -0
  23. hexdag/builtin/adapters/memory/session_memory.py +178 -0
  24. hexdag/builtin/adapters/memory/sqlite_memory_adapter.py +215 -0
  25. hexdag/builtin/adapters/memory/state_memory.py +280 -0
  26. hexdag/builtin/adapters/mock/README.md +89 -0
  27. hexdag/builtin/adapters/mock/__init__.py +15 -0
  28. hexdag/builtin/adapters/mock/hexdag.toml +50 -0
  29. hexdag/builtin/adapters/mock/mock_database.py +225 -0
  30. hexdag/builtin/adapters/mock/mock_embedding.py +223 -0
  31. hexdag/builtin/adapters/mock/mock_llm.py +177 -0
  32. hexdag/builtin/adapters/mock/mock_tool_adapter.py +192 -0
  33. hexdag/builtin/adapters/mock/mock_tool_router.py +232 -0
  34. hexdag/builtin/adapters/openai/__init__.py +5 -0
  35. hexdag/builtin/adapters/openai/openai_adapter.py +634 -0
  36. hexdag/builtin/adapters/secret/__init__.py +7 -0
  37. hexdag/builtin/adapters/secret/local_secret_adapter.py +248 -0
  38. hexdag/builtin/adapters/unified_tool_router.py +280 -0
  39. hexdag/builtin/macros/__init__.py +17 -0
  40. hexdag/builtin/macros/conversation_agent.py +390 -0
  41. hexdag/builtin/macros/llm_macro.py +151 -0
  42. hexdag/builtin/macros/reasoning_agent.py +423 -0
  43. hexdag/builtin/macros/tool_macro.py +380 -0
  44. hexdag/builtin/nodes/__init__.py +38 -0
  45. hexdag/builtin/nodes/_discovery.py +123 -0
  46. hexdag/builtin/nodes/agent_node.py +696 -0
  47. hexdag/builtin/nodes/base_node_factory.py +242 -0
  48. hexdag/builtin/nodes/composite_node.py +926 -0
  49. hexdag/builtin/nodes/data_node.py +201 -0
  50. hexdag/builtin/nodes/expression_node.py +487 -0
  51. hexdag/builtin/nodes/function_node.py +454 -0
  52. hexdag/builtin/nodes/llm_node.py +491 -0
  53. hexdag/builtin/nodes/loop_node.py +920 -0
  54. hexdag/builtin/nodes/mapped_input.py +518 -0
  55. hexdag/builtin/nodes/port_call_node.py +269 -0
  56. hexdag/builtin/nodes/tool_call_node.py +195 -0
  57. hexdag/builtin/nodes/tool_utils.py +390 -0
  58. hexdag/builtin/prompts/__init__.py +68 -0
  59. hexdag/builtin/prompts/base.py +422 -0
  60. hexdag/builtin/prompts/chat_prompts.py +303 -0
  61. hexdag/builtin/prompts/error_correction_prompts.py +320 -0
  62. hexdag/builtin/prompts/tool_prompts.py +160 -0
  63. hexdag/builtin/tools/builtin_tools.py +84 -0
  64. hexdag/builtin/tools/database_tools.py +164 -0
  65. hexdag/cli/__init__.py +17 -0
  66. hexdag/cli/__main__.py +7 -0
  67. hexdag/cli/commands/__init__.py +27 -0
  68. hexdag/cli/commands/build_cmd.py +812 -0
  69. hexdag/cli/commands/create_cmd.py +208 -0
  70. hexdag/cli/commands/docs_cmd.py +293 -0
  71. hexdag/cli/commands/generate_types_cmd.py +252 -0
  72. hexdag/cli/commands/init_cmd.py +188 -0
  73. hexdag/cli/commands/pipeline_cmd.py +494 -0
  74. hexdag/cli/commands/plugin_dev_cmd.py +529 -0
  75. hexdag/cli/commands/plugins_cmd.py +441 -0
  76. hexdag/cli/commands/studio_cmd.py +101 -0
  77. hexdag/cli/commands/validate_cmd.py +221 -0
  78. hexdag/cli/main.py +84 -0
  79. hexdag/core/__init__.py +83 -0
  80. hexdag/core/config/__init__.py +20 -0
  81. hexdag/core/config/loader.py +479 -0
  82. hexdag/core/config/models.py +150 -0
  83. hexdag/core/configurable.py +294 -0
  84. hexdag/core/context/__init__.py +37 -0
  85. hexdag/core/context/execution_context.py +378 -0
  86. hexdag/core/docs/__init__.py +26 -0
  87. hexdag/core/docs/extractors.py +678 -0
  88. hexdag/core/docs/generators.py +890 -0
  89. hexdag/core/docs/models.py +120 -0
  90. hexdag/core/domain/__init__.py +10 -0
  91. hexdag/core/domain/dag.py +1225 -0
  92. hexdag/core/exceptions.py +234 -0
  93. hexdag/core/expression_parser.py +569 -0
  94. hexdag/core/logging.py +449 -0
  95. hexdag/core/models/__init__.py +17 -0
  96. hexdag/core/models/base.py +138 -0
  97. hexdag/core/orchestration/__init__.py +46 -0
  98. hexdag/core/orchestration/body_executor.py +481 -0
  99. hexdag/core/orchestration/components/__init__.py +97 -0
  100. hexdag/core/orchestration/components/adapter_lifecycle_manager.py +113 -0
  101. hexdag/core/orchestration/components/checkpoint_manager.py +134 -0
  102. hexdag/core/orchestration/components/execution_coordinator.py +360 -0
  103. hexdag/core/orchestration/components/health_check_manager.py +176 -0
  104. hexdag/core/orchestration/components/input_mapper.py +143 -0
  105. hexdag/core/orchestration/components/lifecycle_manager.py +583 -0
  106. hexdag/core/orchestration/components/node_executor.py +377 -0
  107. hexdag/core/orchestration/components/secret_manager.py +202 -0
  108. hexdag/core/orchestration/components/wave_executor.py +158 -0
  109. hexdag/core/orchestration/constants.py +17 -0
  110. hexdag/core/orchestration/events/README.md +312 -0
  111. hexdag/core/orchestration/events/__init__.py +104 -0
  112. hexdag/core/orchestration/events/batching.py +330 -0
  113. hexdag/core/orchestration/events/decorators.py +139 -0
  114. hexdag/core/orchestration/events/events.py +573 -0
  115. hexdag/core/orchestration/events/observers/__init__.py +30 -0
  116. hexdag/core/orchestration/events/observers/core_observers.py +690 -0
  117. hexdag/core/orchestration/events/observers/models.py +111 -0
  118. hexdag/core/orchestration/events/taxonomy.py +269 -0
  119. hexdag/core/orchestration/hook_context.py +237 -0
  120. hexdag/core/orchestration/hooks.py +437 -0
  121. hexdag/core/orchestration/models.py +418 -0
  122. hexdag/core/orchestration/orchestrator.py +910 -0
  123. hexdag/core/orchestration/orchestrator_factory.py +275 -0
  124. hexdag/core/orchestration/port_wrappers.py +327 -0
  125. hexdag/core/orchestration/prompt/__init__.py +32 -0
  126. hexdag/core/orchestration/prompt/template.py +332 -0
  127. hexdag/core/pipeline_builder/__init__.py +21 -0
  128. hexdag/core/pipeline_builder/component_instantiator.py +386 -0
  129. hexdag/core/pipeline_builder/include_tag.py +265 -0
  130. hexdag/core/pipeline_builder/pipeline_config.py +133 -0
  131. hexdag/core/pipeline_builder/py_tag.py +223 -0
  132. hexdag/core/pipeline_builder/tag_discovery.py +268 -0
  133. hexdag/core/pipeline_builder/yaml_builder.py +1196 -0
  134. hexdag/core/pipeline_builder/yaml_validator.py +569 -0
  135. hexdag/core/ports/__init__.py +65 -0
  136. hexdag/core/ports/api_call.py +133 -0
  137. hexdag/core/ports/database.py +489 -0
  138. hexdag/core/ports/embedding.py +215 -0
  139. hexdag/core/ports/executor.py +237 -0
  140. hexdag/core/ports/file_storage.py +117 -0
  141. hexdag/core/ports/healthcheck.py +87 -0
  142. hexdag/core/ports/llm.py +551 -0
  143. hexdag/core/ports/memory.py +70 -0
  144. hexdag/core/ports/observer_manager.py +130 -0
  145. hexdag/core/ports/secret.py +145 -0
  146. hexdag/core/ports/tool_router.py +94 -0
  147. hexdag/core/ports_builder.py +623 -0
  148. hexdag/core/protocols.py +273 -0
  149. hexdag/core/resolver.py +304 -0
  150. hexdag/core/schema/__init__.py +9 -0
  151. hexdag/core/schema/generator.py +742 -0
  152. hexdag/core/secrets.py +242 -0
  153. hexdag/core/types.py +413 -0
  154. hexdag/core/utils/async_warnings.py +206 -0
  155. hexdag/core/utils/schema_conversion.py +78 -0
  156. hexdag/core/utils/sql_validation.py +86 -0
  157. hexdag/core/validation/secure_json.py +148 -0
  158. hexdag/core/yaml_macro.py +517 -0
  159. hexdag/mcp_server.py +3120 -0
  160. hexdag/studio/__init__.py +10 -0
  161. hexdag/studio/build_ui.py +92 -0
  162. hexdag/studio/server/__init__.py +1 -0
  163. hexdag/studio/server/main.py +100 -0
  164. hexdag/studio/server/routes/__init__.py +9 -0
  165. hexdag/studio/server/routes/execute.py +208 -0
  166. hexdag/studio/server/routes/export.py +558 -0
  167. hexdag/studio/server/routes/files.py +207 -0
  168. hexdag/studio/server/routes/plugins.py +419 -0
  169. hexdag/studio/server/routes/validate.py +220 -0
  170. hexdag/studio/ui/index.html +13 -0
  171. hexdag/studio/ui/package-lock.json +2992 -0
  172. hexdag/studio/ui/package.json +31 -0
  173. hexdag/studio/ui/postcss.config.js +6 -0
  174. hexdag/studio/ui/public/hexdag.svg +5 -0
  175. hexdag/studio/ui/src/App.tsx +251 -0
  176. hexdag/studio/ui/src/components/Canvas.tsx +408 -0
  177. hexdag/studio/ui/src/components/ContextMenu.tsx +187 -0
  178. hexdag/studio/ui/src/components/FileBrowser.tsx +123 -0
  179. hexdag/studio/ui/src/components/Header.tsx +181 -0
  180. hexdag/studio/ui/src/components/HexdagNode.tsx +193 -0
  181. hexdag/studio/ui/src/components/NodeInspector.tsx +512 -0
  182. hexdag/studio/ui/src/components/NodePalette.tsx +262 -0
  183. hexdag/studio/ui/src/components/NodePortsSection.tsx +403 -0
  184. hexdag/studio/ui/src/components/PluginManager.tsx +347 -0
  185. hexdag/studio/ui/src/components/PortsEditor.tsx +481 -0
  186. hexdag/studio/ui/src/components/PythonEditor.tsx +195 -0
  187. hexdag/studio/ui/src/components/ValidationPanel.tsx +105 -0
  188. hexdag/studio/ui/src/components/YamlEditor.tsx +196 -0
  189. hexdag/studio/ui/src/components/index.ts +8 -0
  190. hexdag/studio/ui/src/index.css +92 -0
  191. hexdag/studio/ui/src/main.tsx +10 -0
  192. hexdag/studio/ui/src/types/index.ts +123 -0
  193. hexdag/studio/ui/src/vite-env.d.ts +1 -0
  194. hexdag/studio/ui/tailwind.config.js +29 -0
  195. hexdag/studio/ui/tsconfig.json +37 -0
  196. hexdag/studio/ui/tsconfig.node.json +13 -0
  197. hexdag/studio/ui/vite.config.ts +35 -0
  198. hexdag/visualization/__init__.py +69 -0
  199. hexdag/visualization/dag_visualizer.py +1020 -0
  200. hexdag-0.5.0.dev1.dist-info/METADATA +369 -0
  201. hexdag-0.5.0.dev1.dist-info/RECORD +261 -0
  202. hexdag-0.5.0.dev1.dist-info/WHEEL +4 -0
  203. hexdag-0.5.0.dev1.dist-info/entry_points.txt +4 -0
  204. hexdag-0.5.0.dev1.dist-info/licenses/LICENSE +190 -0
  205. hexdag_plugins/.gitignore +43 -0
  206. hexdag_plugins/README.md +73 -0
  207. hexdag_plugins/__init__.py +1 -0
  208. hexdag_plugins/azure/LICENSE +21 -0
  209. hexdag_plugins/azure/README.md +414 -0
  210. hexdag_plugins/azure/__init__.py +21 -0
  211. hexdag_plugins/azure/azure_blob_adapter.py +450 -0
  212. hexdag_plugins/azure/azure_cosmos_adapter.py +383 -0
  213. hexdag_plugins/azure/azure_keyvault_adapter.py +314 -0
  214. hexdag_plugins/azure/azure_openai_adapter.py +415 -0
  215. hexdag_plugins/azure/pyproject.toml +107 -0
  216. hexdag_plugins/azure/tests/__init__.py +1 -0
  217. hexdag_plugins/azure/tests/test_azure_blob_adapter.py +350 -0
  218. hexdag_plugins/azure/tests/test_azure_cosmos_adapter.py +323 -0
  219. hexdag_plugins/azure/tests/test_azure_keyvault_adapter.py +330 -0
  220. hexdag_plugins/azure/tests/test_azure_openai_adapter.py +329 -0
  221. hexdag_plugins/hexdag_etl/README.md +168 -0
  222. hexdag_plugins/hexdag_etl/__init__.py +53 -0
  223. hexdag_plugins/hexdag_etl/examples/01_simple_pandas_transform.py +270 -0
  224. hexdag_plugins/hexdag_etl/examples/02_simple_pandas_only.py +149 -0
  225. hexdag_plugins/hexdag_etl/examples/03_file_io_pipeline.py +109 -0
  226. hexdag_plugins/hexdag_etl/examples/test_pandas_transform.py +84 -0
  227. hexdag_plugins/hexdag_etl/hexdag.toml +25 -0
  228. hexdag_plugins/hexdag_etl/hexdag_etl/__init__.py +48 -0
  229. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/__init__.py +13 -0
  230. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/api_extract.py +230 -0
  231. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/base_node_factory.py +181 -0
  232. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/file_io.py +415 -0
  233. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/outlook.py +492 -0
  234. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/pandas_transform.py +563 -0
  235. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/sql_extract_load.py +112 -0
  236. hexdag_plugins/hexdag_etl/pyproject.toml +82 -0
  237. hexdag_plugins/hexdag_etl/test_transform.py +54 -0
  238. hexdag_plugins/hexdag_etl/tests/test_plugin_integration.py +62 -0
  239. hexdag_plugins/mysql_adapter/LICENSE +21 -0
  240. hexdag_plugins/mysql_adapter/README.md +224 -0
  241. hexdag_plugins/mysql_adapter/__init__.py +6 -0
  242. hexdag_plugins/mysql_adapter/mysql_adapter.py +408 -0
  243. hexdag_plugins/mysql_adapter/pyproject.toml +93 -0
  244. hexdag_plugins/mysql_adapter/tests/test_mysql_adapter.py +259 -0
  245. hexdag_plugins/storage/README.md +184 -0
  246. hexdag_plugins/storage/__init__.py +19 -0
  247. hexdag_plugins/storage/file/__init__.py +5 -0
  248. hexdag_plugins/storage/file/local.py +325 -0
  249. hexdag_plugins/storage/ports/__init__.py +5 -0
  250. hexdag_plugins/storage/ports/vector_store.py +236 -0
  251. hexdag_plugins/storage/sql/__init__.py +7 -0
  252. hexdag_plugins/storage/sql/base.py +187 -0
  253. hexdag_plugins/storage/sql/mysql.py +27 -0
  254. hexdag_plugins/storage/sql/postgresql.py +27 -0
  255. hexdag_plugins/storage/tests/__init__.py +1 -0
  256. hexdag_plugins/storage/tests/test_local_file_storage.py +161 -0
  257. hexdag_plugins/storage/tests/test_sql_adapters.py +212 -0
  258. hexdag_plugins/storage/vector/__init__.py +7 -0
  259. hexdag_plugins/storage/vector/chromadb.py +223 -0
  260. hexdag_plugins/storage/vector/in_memory.py +285 -0
  261. hexdag_plugins/storage/vector/pgvector.py +502 -0
@@ -0,0 +1,47 @@
1
+ """Memory adapter implementations for HexDAG.
2
+
3
+ This module provides various Memory port implementations:
4
+ - InMemoryMemory: Fast in-memory dictionary storage
5
+ - FileMemoryAdapter: File-based persistent storage
6
+ - SQLiteMemoryAdapter: SQLite-backed persistent storage
7
+
8
+ And memory plugins with structure-specific operations:
9
+ - SessionMemoryPlugin: Conversation history and context
10
+ - StateMemoryPlugin: Structured entities, relationships, and beliefs
11
+
12
+ Schemas for memory plugins:
13
+ - ConversationHistory: Session conversation schema
14
+ - EntityState: Entity and relationship schema
15
+ - BeliefState: Hinton-style belief probability schema
16
+ - EventLog: Reasoning trace schema
17
+ - ReasoningStep: Individual reasoning step schema
18
+ """
19
+
20
+ from .file_memory_adapter import FileMemoryAdapter
21
+ from .in_memory_memory import InMemoryMemory
22
+ from .schemas import (
23
+ BeliefState,
24
+ ConversationHistory,
25
+ EntityState,
26
+ EventLog,
27
+ ReasoningStep,
28
+ )
29
+ from .session_memory import SessionMemoryPlugin
30
+ from .sqlite_memory_adapter import SQLiteMemoryAdapter
31
+ from .state_memory import StateMemoryPlugin
32
+
33
+ __all__ = [
34
+ # Base memory adapters
35
+ "InMemoryMemory",
36
+ "FileMemoryAdapter",
37
+ "SQLiteMemoryAdapter",
38
+ # Memory plugins
39
+ "SessionMemoryPlugin",
40
+ "StateMemoryPlugin",
41
+ # Schemas
42
+ "BeliefState",
43
+ "ConversationHistory",
44
+ "EntityState",
45
+ "EventLog",
46
+ "ReasoningStep",
47
+ ]
@@ -0,0 +1,297 @@
1
+ """File-based Memory adapter for JSON/YAML/pickle storage.
2
+
3
+ This adapter provides file-based key-value storage, allowing Memory Port
4
+ to work with various file formats.
5
+
6
+ SECURITY WARNING: The pickle format can execute arbitrary code during
7
+ deserialization. Only use pickle with trusted data sources. For untrusted
8
+ data, use JSON or TEXT formats instead.
9
+ """
10
+
11
+ import json
12
+ import pickle # nosec B403 - Pickle usage documented, user must choose format
13
+ from enum import StrEnum
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ from hexdag.core.logging import get_logger
18
+
19
+ logger = get_logger(__name__)
20
+
21
+
22
+ class FileFormat(StrEnum):
23
+ """Supported file formats for storage.
24
+
25
+ Security Notes
26
+ --------------
27
+ - JSON: Safe for untrusted data, human-readable
28
+ - TEXT: Safe for untrusted data, stores as plain text
29
+ - PICKLE: **UNSAFE for untrusted data** - can execute arbitrary code
30
+ Only use pickle with data you control or trust completely.
31
+ """
32
+
33
+ JSON = "json"
34
+ PICKLE = "pickle"
35
+ TEXT = "text"
36
+
37
+
38
+ class FileMemoryAdapter:
39
+ """Memory adapter backed by file system.
40
+
41
+ Provides persistent key-value storage using files, with support for
42
+ multiple formats (JSON, pickle, text). Each key is stored as a separate
43
+ file in the specified directory.
44
+
45
+ This adapter is ideal for:
46
+ - Configuration files
47
+ - Pipeline definitions
48
+ - Human-readable checkpoints
49
+ - Data serialization
50
+
51
+ Parameters
52
+ ----------
53
+ base_path : str | Path
54
+ Base directory for file storage
55
+ format : FileFormat, default=FileFormat.JSON
56
+ File format to use (json, pickle, text)
57
+ create_dirs : bool, default=True
58
+ Automatically create directory structure
59
+ extension : str | None
60
+ Custom file extension (defaults to format)
61
+
62
+ Examples
63
+ --------
64
+ Example usage::
65
+
66
+ memory = FileMemoryAdapter(base_path="./data", format="json")
67
+ memory = FileMemoryAdapter(base_path="./cache", format="pickle")
68
+ """
69
+
70
+ def __init__(
71
+ self,
72
+ base_path: str | Path = "./memory_store",
73
+ format: FileFormat | str = FileFormat.JSON,
74
+ create_dirs: bool = True,
75
+ extension: str | None = None,
76
+ ) -> None:
77
+ """Initialize file memory adapter.
78
+
79
+ Parameters
80
+ ----------
81
+ base_path : str | Path
82
+ Base directory for file storage
83
+ format : FileFormat | str
84
+ File format (json, pickle, text)
85
+ create_dirs : bool
86
+ Automatically create directory if it doesn't exist
87
+ extension : str | None
88
+ Custom file extension (defaults to format name)
89
+ """
90
+ self.base_path = Path(base_path)
91
+ self.format = FileFormat(format) if isinstance(format, str) else format
92
+ self.create_dirs = create_dirs
93
+ self.extension = extension or self.format.value
94
+
95
+ if self.create_dirs:
96
+ self.base_path.mkdir(parents=True, exist_ok=True)
97
+ logger.debug(f"Initialized file storage at '{self.base_path}'")
98
+
99
+ def _get_file_path(self, key: str) -> Path:
100
+ """Get file path for a key.
101
+
102
+ Parameters
103
+ ----------
104
+ key : str
105
+ Storage key
106
+
107
+ Returns
108
+ -------
109
+ Path
110
+ Full file path for the key
111
+ """
112
+ # Sanitize key to be filesystem-safe
113
+ safe_key = key.replace("/", "_").replace(":", "_")
114
+ return self.base_path / f"{safe_key}.{self.extension}"
115
+
116
+ def _serialize(self, value: Any) -> bytes | str:
117
+ """Serialize value based on format.
118
+
119
+ Parameters
120
+ ----------
121
+ value : Any
122
+ Value to serialize
123
+
124
+ Returns
125
+ -------
126
+ bytes | str
127
+ Serialized value
128
+ """
129
+ if self.format == FileFormat.JSON:
130
+ return json.dumps(value, indent=2)
131
+ if self.format == FileFormat.PICKLE:
132
+ return pickle.dumps(value)
133
+ # TEXT
134
+ return str(value)
135
+
136
+ def _deserialize(self, data: bytes | str) -> Any:
137
+ """Deserialize value based on format.
138
+
139
+ Parameters
140
+ ----------
141
+ data : bytes | str
142
+ Serialized data
143
+
144
+ Returns
145
+ -------
146
+ Any
147
+ Deserialized value
148
+ """
149
+ if self.format == FileFormat.JSON:
150
+ # JSON only accepts str, not bytes
151
+ if isinstance(data, bytes):
152
+ data = data.decode("utf-8")
153
+ return json.loads(data)
154
+ if self.format == FileFormat.PICKLE:
155
+ # Pickle only accepts bytes
156
+ if isinstance(data, str):
157
+ data = data.encode("utf-8")
158
+ # WARNING: Pickle can execute arbitrary code - only use with trusted data
159
+ return pickle.loads(data) # nosec B301 - User explicitly chose pickle format
160
+ # TEXT
161
+ return data
162
+
163
+ async def aget(self, key: str) -> Any:
164
+ """Retrieve a value from file storage.
165
+
166
+ Parameters
167
+ ----------
168
+ key : str
169
+ The key to retrieve
170
+
171
+ Returns
172
+ -------
173
+ Any
174
+ The stored value, or None if key doesn't exist
175
+ """
176
+ file_path = self._get_file_path(key)
177
+
178
+ if not file_path.exists():
179
+ return None
180
+
181
+ try:
182
+ data: bytes | str
183
+ if self.format == FileFormat.PICKLE:
184
+ data = file_path.read_bytes()
185
+ else:
186
+ data = file_path.read_text(encoding="utf-8")
187
+
188
+ return self._deserialize(data)
189
+ except Exception as e:
190
+ logger.error(f"Failed to read key '{key}' from {file_path}: {e}")
191
+ return None
192
+
193
+ async def aset(self, key: str, value: Any) -> None:
194
+ """Store a value in file storage.
195
+
196
+ Parameters
197
+ ----------
198
+ key : str
199
+ The key to store under
200
+ value : Any
201
+ The value to store
202
+ """
203
+ file_path = self._get_file_path(key)
204
+
205
+ # Ensure parent directory exists
206
+ if self.create_dirs:
207
+ file_path.parent.mkdir(parents=True, exist_ok=True)
208
+
209
+ try:
210
+ serialized = self._serialize(value)
211
+
212
+ if self.format == FileFormat.PICKLE:
213
+ # Pickle always returns bytes
214
+ if not isinstance(serialized, bytes):
215
+ raise TypeError(f"Expected bytes for pickle format, got {type(serialized)}")
216
+ file_path.write_bytes(serialized)
217
+ else:
218
+ # JSON and TEXT always return str
219
+ if not isinstance(serialized, str):
220
+ raise TypeError(
221
+ f"Expected str for {self.format} format, got {type(serialized)}"
222
+ )
223
+ file_path.write_text(serialized, encoding="utf-8")
224
+
225
+ logger.debug(f"Stored key '{key}' at {file_path}")
226
+ except Exception as e:
227
+ logger.error(f"Failed to write key '{key}' to {file_path}: {e}")
228
+ raise
229
+
230
+ async def adelete(self, key: str) -> bool:
231
+ """Delete a key from file storage.
232
+
233
+ Parameters
234
+ ----------
235
+ key : str
236
+ The key to delete
237
+
238
+ Returns
239
+ -------
240
+ bool
241
+ True if key existed and was deleted, False otherwise
242
+ """
243
+ file_path = self._get_file_path(key)
244
+
245
+ if not file_path.exists():
246
+ return False
247
+
248
+ try:
249
+ file_path.unlink()
250
+ logger.debug(f"Deleted key '{key}' from {file_path}")
251
+ return True
252
+ except Exception as e:
253
+ logger.error(f"Failed to delete key '{key}' at {file_path}: {e}")
254
+ return False
255
+
256
+ async def alist_keys(self, prefix: str | None = None) -> list[str]:
257
+ """List all keys in storage, optionally filtered by prefix.
258
+
259
+ Parameters
260
+ ----------
261
+ prefix : str | None
262
+ Optional prefix to filter keys
263
+
264
+ Returns
265
+ -------
266
+ list[str]
267
+ List of matching keys
268
+ """
269
+ if not self.base_path.exists():
270
+ return []
271
+
272
+ keys = []
273
+ pattern = f"*.{self.extension}"
274
+
275
+ for file_path in self.base_path.glob(pattern):
276
+ # Remove extension and reverse sanitization
277
+ key = file_path.stem
278
+
279
+ if prefix is None or key.startswith(prefix):
280
+ keys.append(key)
281
+
282
+ return sorted(keys)
283
+
284
+ async def aclear(self) -> None:
285
+ """Clear all keys from storage."""
286
+ if not self.base_path.exists():
287
+ return
288
+
289
+ pattern = f"*.{self.extension}"
290
+ for file_path in self.base_path.glob(pattern):
291
+ file_path.unlink()
292
+
293
+ logger.info(f"Cleared all files from {self.base_path}")
294
+
295
+ def __repr__(self) -> str:
296
+ """Return string representation."""
297
+ return f"FileMemoryAdapter(path='{self.base_path}', format='{self.format.value}')"
@@ -0,0 +1,216 @@
1
+ """In-memory implementation of Memory for testing purposes."""
2
+
3
+ import asyncio
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ if TYPE_CHECKING:
7
+ from hexdag.core.ports.healthcheck import HealthStatus
8
+
9
+
10
+ from hexdag.core.ports.memory import Memory
11
+
12
+ __all__ = ["InMemoryMemory"]
13
+
14
+
15
+ class InMemoryMemory(Memory):
16
+ """In-memory implementation of Memory for testing.
17
+
18
+ Features:
19
+ - Key-value storage in memory
20
+ - Access history tracking
21
+ - Delay simulation
22
+ - Size limits
23
+ - Reset functionality
24
+ """
25
+
26
+ def __init__(self, **kwargs: Any) -> None:
27
+ """Initialize the in-memory storage.
28
+
29
+ Parameters
30
+ ----------
31
+ **kwargs : Any
32
+ Configuration options (max_size, delay_seconds)
33
+ """
34
+ self.max_size = kwargs.get("max_size", 1000)
35
+ self.delay_seconds = kwargs.get("delay_seconds", 0.0)
36
+
37
+ # State (not from config)
38
+ self.storage: dict[str, Any] = {}
39
+ self.access_history: list[dict[str, Any]] = []
40
+
41
+ async def aget(self, key: str) -> Any:
42
+ """Retrieve a value from memory.
43
+
44
+ Args
45
+ ----
46
+ key: The key to retrieve
47
+
48
+ Returns
49
+ -------
50
+ The stored value, or None if key doesn't exist
51
+ """
52
+ # Simulate access delay
53
+ if self.delay_seconds > 0:
54
+ await asyncio.sleep(self.delay_seconds)
55
+
56
+ result = self.storage.get(key)
57
+
58
+ # Log the access
59
+ self.access_history.append({
60
+ "operation": "get",
61
+ "key": key,
62
+ "found": key in self.storage,
63
+ "timestamp": asyncio.get_event_loop().time(),
64
+ })
65
+
66
+ return result
67
+
68
+ async def aset(self, key: str, value: Any) -> None:
69
+ """Store a value in memory.
70
+
71
+ Args
72
+ ----
73
+ key: The key to store under
74
+ value: The value to store
75
+
76
+ Raises
77
+ ------
78
+ MemoryError
79
+ If max_size is exceeded
80
+ """
81
+ # Simulate access delay
82
+ if self.delay_seconds > 0:
83
+ await asyncio.sleep(self.delay_seconds)
84
+
85
+ # Check size limit
86
+ if (
87
+ self.max_size is not None
88
+ and key not in self.storage
89
+ and len(self.storage) >= self.max_size
90
+ ):
91
+ raise MemoryError(f"Memory limit of {self.max_size} items exceeded")
92
+
93
+ self.storage[key] = value
94
+
95
+ # Log the access
96
+ self.access_history.append({
97
+ "operation": "set",
98
+ "key": key,
99
+ "value_type": type(value).__name__,
100
+ "timestamp": asyncio.get_event_loop().time(),
101
+ })
102
+
103
+ def clear(self) -> None:
104
+ """Clear all stored data."""
105
+ self.storage.clear()
106
+
107
+ def reset(self) -> None:
108
+ """Reset the memory to initial state, clearing both data and history."""
109
+ self.storage.clear()
110
+ self.access_history.clear()
111
+
112
+ def get_access_history(self) -> list[dict[str, Any]]:
113
+ """Get the history of all memory access operations.
114
+
115
+ Returns
116
+ -------
117
+ list[dict[str, Any]]
118
+ List of access history records with operation, key, and timestamp
119
+ """
120
+ return self.access_history.copy()
121
+
122
+ def get_stored_keys(self) -> list[str]:
123
+ """Get list of all stored keys.
124
+
125
+ Returns
126
+ -------
127
+ list[str]
128
+ List of all keys currently stored in memory
129
+ """
130
+ return list(self.storage.keys())
131
+
132
+ def has_key(self, key: str) -> bool:
133
+ """Check if a key exists in storage.
134
+
135
+ Parameters
136
+ ----------
137
+ key : str
138
+ The key to check for existence
139
+
140
+ Returns
141
+ -------
142
+ bool
143
+ True if the key exists, False otherwise
144
+ """
145
+ return key in self.storage
146
+
147
+ def size(self) -> int:
148
+ """Get the number of stored items.
149
+
150
+ Returns
151
+ -------
152
+ int
153
+ Number of items currently stored in memory
154
+ """
155
+ return len(self.storage)
156
+
157
+ async def ahealth_check(self) -> "HealthStatus":
158
+ """Check health of in-memory storage.
159
+
160
+ Returns
161
+ -------
162
+ HealthStatus
163
+ Health status indicating storage is operational
164
+
165
+ Examples
166
+ --------
167
+ Example usage::
168
+
169
+ memory = InMemoryMemory()
170
+ status = await memory.ahealth_check()
171
+ """
172
+ from hexdag.core.ports.healthcheck import HealthStatus
173
+
174
+ try:
175
+ # Test basic operations
176
+ test_key = "__health_check__"
177
+ await self.aset(test_key, "test")
178
+ value = await self.aget(test_key)
179
+
180
+ # Clean up test key
181
+ if test_key in self.storage:
182
+ del self.storage[test_key]
183
+
184
+ if value != "test":
185
+ return HealthStatus(
186
+ status="unhealthy",
187
+ adapter_name="InMemoryMemory",
188
+ error=Exception("Storage read/write verification failed"),
189
+ )
190
+
191
+ details: dict[str, Any] = {"size": len(self.storage)}
192
+ if self.max_size is not None:
193
+ usage_percent = (len(self.storage) / self.max_size) * 100
194
+ details["max_size"] = self.max_size
195
+ details["usage_percent"] = round(usage_percent, 1)
196
+
197
+ if usage_percent > 90:
198
+ return HealthStatus(
199
+ status="degraded",
200
+ adapter_name="InMemoryMemory",
201
+ details=details,
202
+ )
203
+
204
+ return HealthStatus(
205
+ status="healthy",
206
+ adapter_name="InMemoryMemory",
207
+ details=details,
208
+ latency_ms=0.1, # In-memory is always fast
209
+ )
210
+
211
+ except Exception as e:
212
+ return HealthStatus(
213
+ status="unhealthy",
214
+ adapter_name="InMemoryMemory",
215
+ error=e,
216
+ )
@@ -0,0 +1,57 @@
1
+ """Pydantic schemas for memory plugins."""
2
+
3
+ import time
4
+ from typing import Any
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+
9
+ class ConversationHistory(BaseModel):
10
+ """Schema for conversation history in SessionMemoryPlugin."""
11
+
12
+ session_id: str
13
+ messages: list[dict[str, str]] = Field(default_factory=list)
14
+ timestamps: list[float] = Field(default_factory=list)
15
+ token_count: int = 0
16
+ metadata: dict[str, Any] = Field(default_factory=dict)
17
+
18
+
19
+ class EntityState(BaseModel):
20
+ """Schema for entity state in StateMemoryPlugin."""
21
+
22
+ entities: dict[str, dict[str, Any]] = Field(default_factory=dict)
23
+ relationships: list[tuple[str, str, str]] = Field(default_factory=list)
24
+ updated_at: float = Field(default_factory=time.time)
25
+ metadata: dict[str, Any] = Field(default_factory=dict)
26
+
27
+
28
+ class BeliefState(BaseModel):
29
+ """Hinton-style belief state for StateMemoryPlugin.
30
+
31
+ Represents probability distribution over hypotheses with confidence scores.
32
+ """
33
+
34
+ beliefs: dict[str, float] = Field(default_factory=dict)
35
+ confidence: float = 0.0
36
+ evidence: list[str] = Field(default_factory=list)
37
+ updated_at: float = Field(default_factory=time.time)
38
+
39
+
40
+ class ReasoningStep(BaseModel):
41
+ """Schema for reasoning step in EventMemoryPlugin."""
42
+
43
+ step_num: int
44
+ thought: str
45
+ tool_used: str | None = None
46
+ tool_result: Any = None
47
+ timestamp: float = Field(default_factory=time.time)
48
+ metadata: dict[str, Any] = Field(default_factory=dict)
49
+
50
+
51
+ class EventLog(BaseModel):
52
+ """Schema for event log in EventMemoryPlugin."""
53
+
54
+ agent_id: str
55
+ events: list[ReasoningStep] = Field(default_factory=list)
56
+ created_at: float = Field(default_factory=time.time)
57
+ updated_at: float = Field(default_factory=time.time)