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,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)
|