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.
- hexdag/__init__.py +116 -0
- hexdag/__main__.py +30 -0
- hexdag/adapters/executors/__init__.py +5 -0
- hexdag/adapters/executors/local_executor.py +316 -0
- hexdag/builtin/__init__.py +6 -0
- hexdag/builtin/adapters/__init__.py +51 -0
- hexdag/builtin/adapters/anthropic/__init__.py +5 -0
- hexdag/builtin/adapters/anthropic/anthropic_adapter.py +151 -0
- hexdag/builtin/adapters/database/__init__.py +6 -0
- hexdag/builtin/adapters/database/csv/csv_adapter.py +249 -0
- hexdag/builtin/adapters/database/pgvector/__init__.py +5 -0
- hexdag/builtin/adapters/database/pgvector/pgvector_adapter.py +478 -0
- hexdag/builtin/adapters/database/sqlalchemy/sqlalchemy_adapter.py +252 -0
- hexdag/builtin/adapters/database/sqlite/__init__.py +5 -0
- hexdag/builtin/adapters/database/sqlite/sqlite_adapter.py +410 -0
- hexdag/builtin/adapters/local/README.md +59 -0
- hexdag/builtin/adapters/local/__init__.py +7 -0
- hexdag/builtin/adapters/local/local_observer_manager.py +696 -0
- hexdag/builtin/adapters/memory/__init__.py +47 -0
- hexdag/builtin/adapters/memory/file_memory_adapter.py +297 -0
- hexdag/builtin/adapters/memory/in_memory_memory.py +216 -0
- hexdag/builtin/adapters/memory/schemas.py +57 -0
- hexdag/builtin/adapters/memory/session_memory.py +178 -0
- hexdag/builtin/adapters/memory/sqlite_memory_adapter.py +215 -0
- hexdag/builtin/adapters/memory/state_memory.py +280 -0
- hexdag/builtin/adapters/mock/README.md +89 -0
- hexdag/builtin/adapters/mock/__init__.py +15 -0
- hexdag/builtin/adapters/mock/hexdag.toml +50 -0
- hexdag/builtin/adapters/mock/mock_database.py +225 -0
- hexdag/builtin/adapters/mock/mock_embedding.py +223 -0
- hexdag/builtin/adapters/mock/mock_llm.py +177 -0
- hexdag/builtin/adapters/mock/mock_tool_adapter.py +192 -0
- hexdag/builtin/adapters/mock/mock_tool_router.py +232 -0
- hexdag/builtin/adapters/openai/__init__.py +5 -0
- hexdag/builtin/adapters/openai/openai_adapter.py +634 -0
- hexdag/builtin/adapters/secret/__init__.py +7 -0
- hexdag/builtin/adapters/secret/local_secret_adapter.py +248 -0
- hexdag/builtin/adapters/unified_tool_router.py +280 -0
- hexdag/builtin/macros/__init__.py +17 -0
- hexdag/builtin/macros/conversation_agent.py +390 -0
- hexdag/builtin/macros/llm_macro.py +151 -0
- hexdag/builtin/macros/reasoning_agent.py +423 -0
- hexdag/builtin/macros/tool_macro.py +380 -0
- hexdag/builtin/nodes/__init__.py +38 -0
- hexdag/builtin/nodes/_discovery.py +123 -0
- hexdag/builtin/nodes/agent_node.py +696 -0
- hexdag/builtin/nodes/base_node_factory.py +242 -0
- hexdag/builtin/nodes/composite_node.py +926 -0
- hexdag/builtin/nodes/data_node.py +201 -0
- hexdag/builtin/nodes/expression_node.py +487 -0
- hexdag/builtin/nodes/function_node.py +454 -0
- hexdag/builtin/nodes/llm_node.py +491 -0
- hexdag/builtin/nodes/loop_node.py +920 -0
- hexdag/builtin/nodes/mapped_input.py +518 -0
- hexdag/builtin/nodes/port_call_node.py +269 -0
- hexdag/builtin/nodes/tool_call_node.py +195 -0
- hexdag/builtin/nodes/tool_utils.py +390 -0
- hexdag/builtin/prompts/__init__.py +68 -0
- hexdag/builtin/prompts/base.py +422 -0
- hexdag/builtin/prompts/chat_prompts.py +303 -0
- hexdag/builtin/prompts/error_correction_prompts.py +320 -0
- hexdag/builtin/prompts/tool_prompts.py +160 -0
- hexdag/builtin/tools/builtin_tools.py +84 -0
- hexdag/builtin/tools/database_tools.py +164 -0
- hexdag/cli/__init__.py +17 -0
- hexdag/cli/__main__.py +7 -0
- hexdag/cli/commands/__init__.py +27 -0
- hexdag/cli/commands/build_cmd.py +812 -0
- hexdag/cli/commands/create_cmd.py +208 -0
- hexdag/cli/commands/docs_cmd.py +293 -0
- hexdag/cli/commands/generate_types_cmd.py +252 -0
- hexdag/cli/commands/init_cmd.py +188 -0
- hexdag/cli/commands/pipeline_cmd.py +494 -0
- hexdag/cli/commands/plugin_dev_cmd.py +529 -0
- hexdag/cli/commands/plugins_cmd.py +441 -0
- hexdag/cli/commands/studio_cmd.py +101 -0
- hexdag/cli/commands/validate_cmd.py +221 -0
- hexdag/cli/main.py +84 -0
- hexdag/core/__init__.py +83 -0
- hexdag/core/config/__init__.py +20 -0
- hexdag/core/config/loader.py +479 -0
- hexdag/core/config/models.py +150 -0
- hexdag/core/configurable.py +294 -0
- hexdag/core/context/__init__.py +37 -0
- hexdag/core/context/execution_context.py +378 -0
- hexdag/core/docs/__init__.py +26 -0
- hexdag/core/docs/extractors.py +678 -0
- hexdag/core/docs/generators.py +890 -0
- hexdag/core/docs/models.py +120 -0
- hexdag/core/domain/__init__.py +10 -0
- hexdag/core/domain/dag.py +1225 -0
- hexdag/core/exceptions.py +234 -0
- hexdag/core/expression_parser.py +569 -0
- hexdag/core/logging.py +449 -0
- hexdag/core/models/__init__.py +17 -0
- hexdag/core/models/base.py +138 -0
- hexdag/core/orchestration/__init__.py +46 -0
- hexdag/core/orchestration/body_executor.py +481 -0
- hexdag/core/orchestration/components/__init__.py +97 -0
- hexdag/core/orchestration/components/adapter_lifecycle_manager.py +113 -0
- hexdag/core/orchestration/components/checkpoint_manager.py +134 -0
- hexdag/core/orchestration/components/execution_coordinator.py +360 -0
- hexdag/core/orchestration/components/health_check_manager.py +176 -0
- hexdag/core/orchestration/components/input_mapper.py +143 -0
- hexdag/core/orchestration/components/lifecycle_manager.py +583 -0
- hexdag/core/orchestration/components/node_executor.py +377 -0
- hexdag/core/orchestration/components/secret_manager.py +202 -0
- hexdag/core/orchestration/components/wave_executor.py +158 -0
- hexdag/core/orchestration/constants.py +17 -0
- hexdag/core/orchestration/events/README.md +312 -0
- hexdag/core/orchestration/events/__init__.py +104 -0
- hexdag/core/orchestration/events/batching.py +330 -0
- hexdag/core/orchestration/events/decorators.py +139 -0
- hexdag/core/orchestration/events/events.py +573 -0
- hexdag/core/orchestration/events/observers/__init__.py +30 -0
- hexdag/core/orchestration/events/observers/core_observers.py +690 -0
- hexdag/core/orchestration/events/observers/models.py +111 -0
- hexdag/core/orchestration/events/taxonomy.py +269 -0
- hexdag/core/orchestration/hook_context.py +237 -0
- hexdag/core/orchestration/hooks.py +437 -0
- hexdag/core/orchestration/models.py +418 -0
- hexdag/core/orchestration/orchestrator.py +910 -0
- hexdag/core/orchestration/orchestrator_factory.py +275 -0
- hexdag/core/orchestration/port_wrappers.py +327 -0
- hexdag/core/orchestration/prompt/__init__.py +32 -0
- hexdag/core/orchestration/prompt/template.py +332 -0
- hexdag/core/pipeline_builder/__init__.py +21 -0
- hexdag/core/pipeline_builder/component_instantiator.py +386 -0
- hexdag/core/pipeline_builder/include_tag.py +265 -0
- hexdag/core/pipeline_builder/pipeline_config.py +133 -0
- hexdag/core/pipeline_builder/py_tag.py +223 -0
- hexdag/core/pipeline_builder/tag_discovery.py +268 -0
- hexdag/core/pipeline_builder/yaml_builder.py +1196 -0
- hexdag/core/pipeline_builder/yaml_validator.py +569 -0
- hexdag/core/ports/__init__.py +65 -0
- hexdag/core/ports/api_call.py +133 -0
- hexdag/core/ports/database.py +489 -0
- hexdag/core/ports/embedding.py +215 -0
- hexdag/core/ports/executor.py +237 -0
- hexdag/core/ports/file_storage.py +117 -0
- hexdag/core/ports/healthcheck.py +87 -0
- hexdag/core/ports/llm.py +551 -0
- hexdag/core/ports/memory.py +70 -0
- hexdag/core/ports/observer_manager.py +130 -0
- hexdag/core/ports/secret.py +145 -0
- hexdag/core/ports/tool_router.py +94 -0
- hexdag/core/ports_builder.py +623 -0
- hexdag/core/protocols.py +273 -0
- hexdag/core/resolver.py +304 -0
- hexdag/core/schema/__init__.py +9 -0
- hexdag/core/schema/generator.py +742 -0
- hexdag/core/secrets.py +242 -0
- hexdag/core/types.py +413 -0
- hexdag/core/utils/async_warnings.py +206 -0
- hexdag/core/utils/schema_conversion.py +78 -0
- hexdag/core/utils/sql_validation.py +86 -0
- hexdag/core/validation/secure_json.py +148 -0
- hexdag/core/yaml_macro.py +517 -0
- hexdag/mcp_server.py +3120 -0
- hexdag/studio/__init__.py +10 -0
- hexdag/studio/build_ui.py +92 -0
- hexdag/studio/server/__init__.py +1 -0
- hexdag/studio/server/main.py +100 -0
- hexdag/studio/server/routes/__init__.py +9 -0
- hexdag/studio/server/routes/execute.py +208 -0
- hexdag/studio/server/routes/export.py +558 -0
- hexdag/studio/server/routes/files.py +207 -0
- hexdag/studio/server/routes/plugins.py +419 -0
- hexdag/studio/server/routes/validate.py +220 -0
- hexdag/studio/ui/index.html +13 -0
- hexdag/studio/ui/package-lock.json +2992 -0
- hexdag/studio/ui/package.json +31 -0
- hexdag/studio/ui/postcss.config.js +6 -0
- hexdag/studio/ui/public/hexdag.svg +5 -0
- hexdag/studio/ui/src/App.tsx +251 -0
- hexdag/studio/ui/src/components/Canvas.tsx +408 -0
- hexdag/studio/ui/src/components/ContextMenu.tsx +187 -0
- hexdag/studio/ui/src/components/FileBrowser.tsx +123 -0
- hexdag/studio/ui/src/components/Header.tsx +181 -0
- hexdag/studio/ui/src/components/HexdagNode.tsx +193 -0
- hexdag/studio/ui/src/components/NodeInspector.tsx +512 -0
- hexdag/studio/ui/src/components/NodePalette.tsx +262 -0
- hexdag/studio/ui/src/components/NodePortsSection.tsx +403 -0
- hexdag/studio/ui/src/components/PluginManager.tsx +347 -0
- hexdag/studio/ui/src/components/PortsEditor.tsx +481 -0
- hexdag/studio/ui/src/components/PythonEditor.tsx +195 -0
- hexdag/studio/ui/src/components/ValidationPanel.tsx +105 -0
- hexdag/studio/ui/src/components/YamlEditor.tsx +196 -0
- hexdag/studio/ui/src/components/index.ts +8 -0
- hexdag/studio/ui/src/index.css +92 -0
- hexdag/studio/ui/src/main.tsx +10 -0
- hexdag/studio/ui/src/types/index.ts +123 -0
- hexdag/studio/ui/src/vite-env.d.ts +1 -0
- hexdag/studio/ui/tailwind.config.js +29 -0
- hexdag/studio/ui/tsconfig.json +37 -0
- hexdag/studio/ui/tsconfig.node.json +13 -0
- hexdag/studio/ui/vite.config.ts +35 -0
- hexdag/visualization/__init__.py +69 -0
- hexdag/visualization/dag_visualizer.py +1020 -0
- hexdag-0.5.0.dev1.dist-info/METADATA +369 -0
- hexdag-0.5.0.dev1.dist-info/RECORD +261 -0
- hexdag-0.5.0.dev1.dist-info/WHEEL +4 -0
- hexdag-0.5.0.dev1.dist-info/entry_points.txt +4 -0
- hexdag-0.5.0.dev1.dist-info/licenses/LICENSE +190 -0
- hexdag_plugins/.gitignore +43 -0
- hexdag_plugins/README.md +73 -0
- hexdag_plugins/__init__.py +1 -0
- hexdag_plugins/azure/LICENSE +21 -0
- hexdag_plugins/azure/README.md +414 -0
- hexdag_plugins/azure/__init__.py +21 -0
- hexdag_plugins/azure/azure_blob_adapter.py +450 -0
- hexdag_plugins/azure/azure_cosmos_adapter.py +383 -0
- hexdag_plugins/azure/azure_keyvault_adapter.py +314 -0
- hexdag_plugins/azure/azure_openai_adapter.py +415 -0
- hexdag_plugins/azure/pyproject.toml +107 -0
- hexdag_plugins/azure/tests/__init__.py +1 -0
- hexdag_plugins/azure/tests/test_azure_blob_adapter.py +350 -0
- hexdag_plugins/azure/tests/test_azure_cosmos_adapter.py +323 -0
- hexdag_plugins/azure/tests/test_azure_keyvault_adapter.py +330 -0
- hexdag_plugins/azure/tests/test_azure_openai_adapter.py +329 -0
- hexdag_plugins/hexdag_etl/README.md +168 -0
- hexdag_plugins/hexdag_etl/__init__.py +53 -0
- hexdag_plugins/hexdag_etl/examples/01_simple_pandas_transform.py +270 -0
- hexdag_plugins/hexdag_etl/examples/02_simple_pandas_only.py +149 -0
- hexdag_plugins/hexdag_etl/examples/03_file_io_pipeline.py +109 -0
- hexdag_plugins/hexdag_etl/examples/test_pandas_transform.py +84 -0
- hexdag_plugins/hexdag_etl/hexdag.toml +25 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/__init__.py +48 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/__init__.py +13 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/api_extract.py +230 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/base_node_factory.py +181 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/file_io.py +415 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/outlook.py +492 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/pandas_transform.py +563 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/sql_extract_load.py +112 -0
- hexdag_plugins/hexdag_etl/pyproject.toml +82 -0
- hexdag_plugins/hexdag_etl/test_transform.py +54 -0
- hexdag_plugins/hexdag_etl/tests/test_plugin_integration.py +62 -0
- hexdag_plugins/mysql_adapter/LICENSE +21 -0
- hexdag_plugins/mysql_adapter/README.md +224 -0
- hexdag_plugins/mysql_adapter/__init__.py +6 -0
- hexdag_plugins/mysql_adapter/mysql_adapter.py +408 -0
- hexdag_plugins/mysql_adapter/pyproject.toml +93 -0
- hexdag_plugins/mysql_adapter/tests/test_mysql_adapter.py +259 -0
- hexdag_plugins/storage/README.md +184 -0
- hexdag_plugins/storage/__init__.py +19 -0
- hexdag_plugins/storage/file/__init__.py +5 -0
- hexdag_plugins/storage/file/local.py +325 -0
- hexdag_plugins/storage/ports/__init__.py +5 -0
- hexdag_plugins/storage/ports/vector_store.py +236 -0
- hexdag_plugins/storage/sql/__init__.py +7 -0
- hexdag_plugins/storage/sql/base.py +187 -0
- hexdag_plugins/storage/sql/mysql.py +27 -0
- hexdag_plugins/storage/sql/postgresql.py +27 -0
- hexdag_plugins/storage/tests/__init__.py +1 -0
- hexdag_plugins/storage/tests/test_local_file_storage.py +161 -0
- hexdag_plugins/storage/tests/test_sql_adapters.py +212 -0
- hexdag_plugins/storage/vector/__init__.py +7 -0
- hexdag_plugins/storage/vector/chromadb.py +223 -0
- hexdag_plugins/storage/vector/in_memory.py +285 -0
- 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())
|