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,178 @@
1
+ """Session Memory Plugin for conversation history."""
2
+
3
+ import time
4
+ from typing import Any
5
+
6
+ from hexdag.builtin.adapters.memory.schemas import ConversationHistory
7
+ from hexdag.core.logging import get_logger
8
+ from hexdag.core.ports.memory import Memory
9
+
10
+ logger = get_logger(__name__)
11
+
12
+
13
+ class SessionMemoryPlugin:
14
+ """Memory plugin for conversation history and session context.
15
+
16
+ Wraps a base Memory adapter (InMemoryMemory, SQLiteMemoryAdapter, etc.)
17
+ and provides domain-specific operations for managing conversation history.
18
+
19
+ This plugin does NOT store data itself - it delegates to the underlying
20
+ Memory port implementation and adds session-specific logic.
21
+
22
+ Example
23
+ -------
24
+ from hexdag.builtin.adapters.memory import InMemoryMemory
25
+
26
+ # Use existing storage adapter
27
+ storage = InMemoryMemory()
28
+ session_memory = SessionMemoryPlugin(storage=storage, max_messages=100)
29
+
30
+ # Append messages
31
+ await session_memory.append_message("session123", "user", "Hello!")
32
+ await session_memory.append_message("session123", "assistant", "Hi there!")
33
+
34
+ # Get recent history
35
+ recent = await session_memory.get_recent_messages("session123", count=10)
36
+
37
+ # Get full history
38
+ history = await session_memory.get_history("session123")
39
+ """
40
+
41
+ plugin_type = "session"
42
+
43
+ def __init__(self, storage: Memory, max_messages: int = 100):
44
+ """Initialize session memory plugin.
45
+
46
+ Args
47
+ ----
48
+ storage: Base Memory port implementation (InMemoryMemory, SQLiteMemoryAdapter, etc.)
49
+ max_messages: Maximum messages to keep per session (auto-truncates)
50
+ """
51
+ self.storage = storage
52
+ self.max_messages = max_messages
53
+
54
+ async def aget(self, key: str) -> Any:
55
+ """Get value from session scope.
56
+
57
+ Delegates to underlying storage with session:: prefix.
58
+ """
59
+ return await self.storage.aget(f"session::{key}")
60
+
61
+ async def aset(self, key: str, value: Any) -> None:
62
+ """Set value in session scope.
63
+
64
+ Delegates to underlying storage with session:: prefix.
65
+ """
66
+ await self.storage.aset(f"session::{key}", value)
67
+
68
+ # Specialized methods for session memory
69
+
70
+ async def get_history(self, session_id: str) -> ConversationHistory:
71
+ """Get conversation history for session.
72
+
73
+ Args
74
+ ----
75
+ session_id: Session identifier
76
+
77
+ Returns
78
+ -------
79
+ ConversationHistory with messages and timestamps
80
+ """
81
+ data = await self.aget(session_id)
82
+ if data is None:
83
+ return ConversationHistory(
84
+ session_id=session_id,
85
+ messages=[],
86
+ timestamps=[],
87
+ )
88
+ return ConversationHistory.model_validate(data)
89
+
90
+ async def append_message(
91
+ self,
92
+ session_id: str,
93
+ role: str,
94
+ content: str,
95
+ metadata: dict[str, Any] | None = None,
96
+ ) -> None:
97
+ """Append message to conversation history with auto-truncation.
98
+
99
+ Args
100
+ ----
101
+ session_id: Session identifier
102
+ role: Message role ("user", "assistant", "system")
103
+ content: Message content
104
+ metadata: Optional metadata for this message
105
+ """
106
+ history = await self.get_history(session_id)
107
+
108
+ # Append new message
109
+ history.messages.append({"role": role, "content": content})
110
+ history.timestamps.append(time.time())
111
+
112
+ # Update token count (rough estimate: 4 chars per token)
113
+ history.token_count += len(content) // 4
114
+
115
+ # Auto-truncate to max_messages
116
+ if len(history.messages) > self.max_messages:
117
+ overflow = len(history.messages) - self.max_messages
118
+ history.messages = history.messages[overflow:]
119
+ history.timestamps = history.timestamps[overflow:]
120
+ logger.debug(
121
+ "Truncated session %s: removed %d old messages",
122
+ session_id,
123
+ overflow,
124
+ )
125
+
126
+ # Update metadata if provided
127
+ if metadata:
128
+ history.metadata.update(metadata)
129
+
130
+ await self.aset(session_id, history.model_dump())
131
+
132
+ async def get_recent_messages(
133
+ self,
134
+ session_id: str,
135
+ count: int = 10,
136
+ ) -> list[dict[str, str]]:
137
+ """Get recent N messages from conversation.
138
+
139
+ Args
140
+ ----
141
+ session_id: Session identifier
142
+ count: Number of recent messages to return
143
+
144
+ Returns
145
+ -------
146
+ List of recent message dicts
147
+ """
148
+ history = await self.get_history(session_id)
149
+ return history.messages[-count:]
150
+
151
+ async def clear_session(self, session_id: str) -> None:
152
+ """Clear conversation history for session.
153
+
154
+ Args
155
+ ----
156
+ session_id: Session identifier
157
+ """
158
+ empty_history = ConversationHistory(
159
+ session_id=session_id,
160
+ messages=[],
161
+ timestamps=[],
162
+ )
163
+ await self.aset(session_id, empty_history.model_dump())
164
+ logger.info("Cleared session %s", session_id)
165
+
166
+ async def get_token_count(self, session_id: str) -> int:
167
+ """Get approximate token count for session.
168
+
169
+ Args
170
+ ----
171
+ session_id: Session identifier
172
+
173
+ Returns
174
+ -------
175
+ Approximate token count
176
+ """
177
+ history = await self.get_history(session_id)
178
+ return history.token_count
@@ -0,0 +1,215 @@
1
+ """SQLite-backed Memory adapter that bridges DatabasePort to Memory Port.
2
+
3
+ This adapter allows using SQLite databases as a Memory storage backend,
4
+ providing persistent key-value storage with SQL database benefits.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ from hexdag.core.logging import get_logger
10
+ from hexdag.core.ports.database import DatabasePort
11
+ from hexdag.core.utils.sql_validation import validate_sql_identifier
12
+
13
+ logger = get_logger(__name__)
14
+
15
+
16
+ class SQLiteMemoryAdapter:
17
+ """Memory adapter backed by SQLite database.
18
+
19
+ Provides persistent key-value storage using SQLite, bridging the
20
+ DatabasePort and Memory Port. Automatically creates a key-value table
21
+ and provides async get/set operations.
22
+
23
+ This adapter is ideal for:
24
+ - Checkpoint persistence
25
+ - Configuration storage
26
+ - Any key-value data that needs SQL database benefits
27
+
28
+ Parameters
29
+ ----------
30
+ database : DatabasePort
31
+ SQLite database adapter (typically SQLiteAdapter)
32
+ table_name : str, default="memory_store"
33
+ Name of the key-value table
34
+ auto_init : bool, default=True
35
+ Automatically create table on first use
36
+
37
+ Examples
38
+ --------
39
+ Example usage::
40
+
41
+ from hexdag.builtin.adapters.database.sqlite import SQLiteAdapter
42
+ db = SQLiteAdapter(db_path="memory.db")
43
+ memory = SQLiteMemoryAdapter(database=db, table_name="memory_store")
44
+ """
45
+
46
+ # Type annotations for attributes
47
+ database: DatabasePort
48
+ table_name: str
49
+ auto_init: bool
50
+ _initialized: bool
51
+
52
+ def __init__(
53
+ self, database: DatabasePort, table_name: str = "memory_store", auto_init: bool = True
54
+ ) -> None:
55
+ """Initialize SQLite memory adapter.
56
+
57
+ Parameters
58
+ ----------
59
+ database : DatabasePort
60
+ SQLite database adapter
61
+ table_name : str
62
+ Name of the key-value table
63
+ auto_init : bool
64
+ Automatically create table if it doesn't exist
65
+
66
+ """
67
+ self.table_name = table_name
68
+ self.auto_init = auto_init
69
+ self.database = database
70
+ self._initialized = False
71
+
72
+ # Validate table name to prevent SQL injection
73
+ self._validate_table_name(self.table_name)
74
+
75
+ @staticmethod
76
+ def _validate_table_name(table_name: str) -> None:
77
+ """Validate table name to prevent SQL injection.
78
+
79
+ Parameters
80
+ ----------
81
+ table_name : str
82
+ Table name to validate
83
+ """
84
+ validate_sql_identifier(table_name, identifier_type="table", raise_on_invalid=True)
85
+
86
+ async def _ensure_table(self) -> None:
87
+ """Create key-value table if it doesn't exist."""
88
+ if self._initialized:
89
+ return
90
+
91
+ # Table name is validated in __init__, safe to use in f-string
92
+ sql = f"""
93
+ CREATE TABLE IF NOT EXISTS {self.table_name} (
94
+ key TEXT PRIMARY KEY,
95
+ value TEXT NOT NULL,
96
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP
97
+ )
98
+ """ # nosec B608 - table_name is validated
99
+ await self.database.aexecute_query(sql)
100
+ self._initialized = True
101
+ logger.debug(f"Initialized table '{self.table_name}' for memory storage")
102
+
103
+ async def aget(self, key: str) -> Any:
104
+ """Retrieve a value from memory.
105
+
106
+ Parameters
107
+ ----------
108
+ key : str
109
+ The key to retrieve
110
+
111
+ Returns
112
+ -------
113
+ Any
114
+ The stored value, or None if key doesn't exist
115
+ """
116
+ if self.auto_init:
117
+ await self._ensure_table()
118
+
119
+ # Table name is validated, user data in parameters
120
+ sql = f"SELECT value FROM {self.table_name} WHERE key = :key" # nosec B608
121
+ rows = await self.database.aexecute_query(sql, {"key": key})
122
+
123
+ if not rows:
124
+ return None
125
+
126
+ return rows[0]["value"]
127
+
128
+ async def aset(self, key: str, value: Any) -> None:
129
+ """Store a value in memory.
130
+
131
+ Parameters
132
+ ----------
133
+ key : str
134
+ The key to store under
135
+ value : Any
136
+ The value to store (must be serializable to string)
137
+ """
138
+ if self.auto_init:
139
+ await self._ensure_table()
140
+
141
+ # SQLite doesn't support standard UPSERT, use INSERT OR REPLACE
142
+ # Table name is validated, user data in parameters
143
+ sql = f"""
144
+ INSERT OR REPLACE INTO {self.table_name} (key, value, updated_at)
145
+ VALUES (:key, :value, CURRENT_TIMESTAMP)
146
+ """ # nosec B608
147
+ await self.database.aexecute_query(sql, {"key": key, "value": str(value)})
148
+ logger.debug(f"Stored key '{key}' in table '{self.table_name}'")
149
+
150
+ async def adelete(self, key: str) -> bool:
151
+ """Delete a key from memory.
152
+
153
+ Parameters
154
+ ----------
155
+ key : str
156
+ The key to delete
157
+
158
+ Returns
159
+ -------
160
+ bool
161
+ True if key existed and was deleted, False otherwise
162
+ """
163
+ if self.auto_init:
164
+ await self._ensure_table()
165
+
166
+ exists = await self.aget(key)
167
+ if exists is None:
168
+ return False
169
+
170
+ # Table name is validated, user data in parameters
171
+ sql = f"DELETE FROM {self.table_name} WHERE key = :key" # nosec B608
172
+ await self.database.aexecute_query(sql, {"key": key})
173
+ logger.debug(f"Deleted key '{key}' from table '{self.table_name}'")
174
+ return True
175
+
176
+ async def alist_keys(self, prefix: str | None = None) -> list[str]:
177
+ """List all keys in memory, optionally filtered by prefix.
178
+
179
+ Parameters
180
+ ----------
181
+ prefix : str | None
182
+ Optional prefix to filter keys
183
+
184
+ Returns
185
+ -------
186
+ list[str]
187
+ List of matching keys
188
+ """
189
+ if self.auto_init:
190
+ await self._ensure_table()
191
+
192
+ if prefix:
193
+ # Table name is validated, user data in parameters
194
+ sql = f"SELECT key FROM {self.table_name} WHERE key LIKE :prefix" # nosec B608
195
+ rows = await self.database.aexecute_query(sql, {"prefix": f"{prefix}%"})
196
+ else:
197
+ # Table name is validated
198
+ sql = f"SELECT key FROM {self.table_name}" # nosec B608
199
+ rows = await self.database.aexecute_query(sql)
200
+
201
+ return [row["key"] for row in rows]
202
+
203
+ async def aclear(self) -> None:
204
+ """Clear all keys from memory."""
205
+ if self.auto_init:
206
+ await self._ensure_table()
207
+
208
+ # Table name is validated
209
+ sql = f"DELETE FROM {self.table_name}" # nosec B608
210
+ await self.database.aexecute_query(sql)
211
+ logger.info(f"Cleared all keys from table '{self.table_name}'")
212
+
213
+ def __repr__(self) -> str:
214
+ """Return string representation."""
215
+ return f"SQLiteMemoryAdapter(table='{self.table_name}')"
@@ -0,0 +1,280 @@
1
+ """State Memory Plugin for structured entities and belief states."""
2
+
3
+ import time
4
+ from typing import Any
5
+
6
+ from hexdag.builtin.adapters.memory.schemas import BeliefState, EntityState
7
+ from hexdag.core.logging import get_logger
8
+ from hexdag.core.ports.memory import Memory
9
+
10
+ logger = get_logger(__name__)
11
+
12
+
13
+ class StateMemoryPlugin:
14
+ """Memory plugin for structured state (entities, relationships, beliefs).
15
+
16
+ Wraps a base Memory adapter and provides domain-specific operations for:
17
+ - Entity state: entities with properties and relationships
18
+ - Belief state: Hinton-style probability distributions over hypotheses
19
+
20
+ This plugin does NOT store data itself - it delegates to the underlying
21
+ Memory port implementation and adds state management logic.
22
+
23
+ Example
24
+ -------
25
+ from hexdag.builtin.adapters.memory import InMemoryMemory
26
+
27
+ storage = InMemoryMemory()
28
+ state_memory = StateMemoryPlugin(storage=storage)
29
+
30
+ # Entity operations
31
+ await state_memory.update_entity("agent1", "user_123", {"name": "Alice"})
32
+ await state_memory.add_relationship("agent1", "user_123", "knows", "user_456")
33
+ entities = await state_memory.get_entities("agent1")
34
+
35
+ # Belief operations (Hinton-style Bayesian updates)
36
+ await state_memory.update_beliefs("agent1", {"hypothesis_a": 0.7}, "new evidence")
37
+ belief = await state_memory.get_belief_state("agent1")
38
+ """
39
+
40
+ plugin_type = "state"
41
+
42
+ def __init__(self, storage: Memory):
43
+ """Initialize state memory plugin.
44
+
45
+ Args
46
+ ----
47
+ storage: Base Memory port implementation (InMemoryMemory, SQLiteMemoryAdapter, etc.)
48
+ """
49
+ self.storage = storage
50
+
51
+ async def aget(self, key: str) -> Any:
52
+ """Get value from state scope.
53
+
54
+ Delegates to underlying storage with state:: prefix.
55
+ """
56
+ return await self.storage.aget(f"state::{key}")
57
+
58
+ async def aset(self, key: str, value: Any) -> None:
59
+ """Set value in state scope.
60
+
61
+ Delegates to underlying storage with state:: prefix.
62
+ """
63
+ await self.storage.aset(f"state::{key}", value)
64
+
65
+ # Entity state operations
66
+
67
+ async def get_entities(self, agent_id: str) -> EntityState:
68
+ """Get entity state for agent.
69
+
70
+ Args
71
+ ----
72
+ agent_id: Agent identifier
73
+
74
+ Returns
75
+ -------
76
+ EntityState with entities and relationships
77
+ """
78
+ data = await self.aget(f"entities:{agent_id}")
79
+ if data is None:
80
+ return EntityState(
81
+ entities={},
82
+ relationships=[],
83
+ updated_at=time.time(),
84
+ )
85
+ return EntityState.model_validate(data)
86
+
87
+ async def update_entity(
88
+ self,
89
+ agent_id: str,
90
+ entity_id: str,
91
+ properties: dict[str, Any],
92
+ ) -> None:
93
+ """Update or create entity with properties.
94
+
95
+ Args
96
+ ----
97
+ agent_id: Agent identifier
98
+ entity_id: Entity identifier
99
+ properties: Entity properties to set/update
100
+ """
101
+ state = await self.get_entities(agent_id)
102
+ state.entities[entity_id] = properties
103
+ state.updated_at = time.time()
104
+ await self.aset(f"entities:{agent_id}", state.model_dump())
105
+ logger.debug("Updated entity %s for agent %s", entity_id, agent_id)
106
+
107
+ async def get_entity(
108
+ self,
109
+ agent_id: str,
110
+ entity_id: str,
111
+ ) -> dict[str, Any] | None:
112
+ """Get single entity by ID.
113
+
114
+ Args
115
+ ----
116
+ agent_id: Agent identifier
117
+ entity_id: Entity identifier
118
+
119
+ Returns
120
+ -------
121
+ Entity properties or None if not found
122
+ """
123
+ state = await self.get_entities(agent_id)
124
+ return state.entities.get(entity_id)
125
+
126
+ async def add_relationship(
127
+ self,
128
+ agent_id: str,
129
+ subject: str,
130
+ predicate: str,
131
+ object: str,
132
+ ) -> None:
133
+ """Add relationship between entities.
134
+
135
+ Args
136
+ ----
137
+ agent_id: Agent identifier
138
+ subject: Subject entity ID
139
+ predicate: Relationship type
140
+ object: Object entity ID
141
+ """
142
+ state = await self.get_entities(agent_id)
143
+ relationship = (subject, predicate, object)
144
+
145
+ # Avoid duplicates
146
+ if relationship not in state.relationships:
147
+ state.relationships.append(relationship)
148
+ state.updated_at = time.time()
149
+ await self.aset(f"entities:{agent_id}", state.model_dump())
150
+ logger.debug(
151
+ "Added relationship (%s, %s, %s) for agent %s",
152
+ subject,
153
+ predicate,
154
+ object,
155
+ agent_id,
156
+ )
157
+
158
+ async def query_relationships(
159
+ self,
160
+ agent_id: str,
161
+ subject: str | None = None,
162
+ predicate: str | None = None,
163
+ object: str | None = None,
164
+ ) -> list[tuple[str, str, str]]:
165
+ """Query relationships by subject, predicate, or object.
166
+
167
+ Args
168
+ ----
169
+ agent_id: Agent identifier
170
+ subject: Optional subject to filter by
171
+ predicate: Optional predicate to filter by
172
+ object: Optional object to filter by
173
+
174
+ Returns
175
+ -------
176
+ List of matching relationships
177
+ """
178
+ state = await self.get_entities(agent_id)
179
+ results = []
180
+
181
+ for rel in state.relationships:
182
+ s, p, o = rel
183
+ if subject is not None and s != subject:
184
+ continue
185
+ if predicate is not None and p != predicate:
186
+ continue
187
+ if object is not None and o != object:
188
+ continue
189
+ results.append(rel)
190
+
191
+ return results
192
+
193
+ # Belief state operations (Hinton-style)
194
+
195
+ async def get_belief_state(self, agent_id: str) -> BeliefState:
196
+ """Get Hinton-style belief state for agent.
197
+
198
+ Args
199
+ ----
200
+ agent_id: Agent identifier
201
+
202
+ Returns
203
+ -------
204
+ BeliefState with probability distribution over hypotheses
205
+ """
206
+ data = await self.aget(f"belief:{agent_id}")
207
+ if data is None:
208
+ return BeliefState(
209
+ beliefs={},
210
+ confidence=0.0,
211
+ evidence=[],
212
+ updated_at=time.time(),
213
+ )
214
+ return BeliefState.model_validate(data)
215
+
216
+ async def update_beliefs(
217
+ self,
218
+ agent_id: str,
219
+ new_beliefs: dict[str, float],
220
+ evidence: str,
221
+ ) -> None:
222
+ """Bayesian belief update with new evidence.
223
+
224
+ Args
225
+ ----
226
+ agent_id: Agent identifier
227
+ new_beliefs: New belief values (likelihoods)
228
+ evidence: Description of the evidence
229
+ """
230
+ state = await self.get_belief_state(agent_id)
231
+
232
+ # Bayesian update: P(H|E) ∝ P(E|H) * P(H)
233
+ for hypothesis, prior in list(state.beliefs.items()):
234
+ if hypothesis in new_beliefs:
235
+ likelihood = new_beliefs[hypothesis]
236
+ state.beliefs[hypothesis] = likelihood * prior
237
+
238
+ # Add new hypotheses
239
+ for hypothesis, prob in new_beliefs.items():
240
+ if hypothesis not in state.beliefs:
241
+ state.beliefs[hypothesis] = prob
242
+
243
+ # Normalize to sum to 1.0
244
+ total = sum(state.beliefs.values())
245
+ if total > 0:
246
+ state.beliefs = {h: p / total for h, p in state.beliefs.items()}
247
+
248
+ # Update confidence (max posterior probability)
249
+ state.confidence = max(state.beliefs.values()) if state.beliefs else 0.0
250
+
251
+ # Record evidence
252
+ state.evidence.append(evidence)
253
+ state.updated_at = time.time()
254
+
255
+ await self.aset(f"belief:{agent_id}", state.model_dump())
256
+ logger.debug(
257
+ "Updated beliefs for agent %s: confidence=%.2f",
258
+ agent_id,
259
+ state.confidence,
260
+ )
261
+
262
+ async def set_belief(
263
+ self,
264
+ agent_id: str,
265
+ hypothesis: str,
266
+ probability: float,
267
+ ) -> None:
268
+ """Set belief probability for single hypothesis.
269
+
270
+ Args
271
+ ----
272
+ agent_id: Agent identifier
273
+ hypothesis: Hypothesis name
274
+ probability: Belief probability (0.0-1.0)
275
+ """
276
+ state = await self.get_belief_state(agent_id)
277
+ state.beliefs[hypothesis] = max(0.0, min(1.0, probability))
278
+ state.confidence = max(state.beliefs.values()) if state.beliefs else 0.0
279
+ state.updated_at = time.time()
280
+ await self.aset(f"belief:{agent_id}", state.model_dump())