aethergraph 0.1.0a1__py3-none-any.whl → 0.1.0a3__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.
- aethergraph/__init__.py +4 -10
- aethergraph/__main__.py +296 -0
- aethergraph/api/v1/__init__.py +0 -0
- aethergraph/api/v1/agents.py +46 -0
- aethergraph/api/v1/apps.py +70 -0
- aethergraph/api/v1/artifacts.py +415 -0
- aethergraph/api/v1/channels.py +89 -0
- aethergraph/api/v1/deps.py +168 -0
- aethergraph/api/v1/graphs.py +259 -0
- aethergraph/api/v1/identity.py +25 -0
- aethergraph/api/v1/memory.py +353 -0
- aethergraph/api/v1/misc.py +47 -0
- aethergraph/api/v1/pagination.py +29 -0
- aethergraph/api/v1/runs.py +568 -0
- aethergraph/api/v1/schemas.py +535 -0
- aethergraph/api/v1/session.py +323 -0
- aethergraph/api/v1/stats.py +201 -0
- aethergraph/api/v1/viz.py +152 -0
- aethergraph/config/config.py +22 -0
- aethergraph/config/loader.py +3 -2
- aethergraph/config/storage.py +209 -0
- aethergraph/contracts/__init__.py +0 -0
- aethergraph/contracts/services/__init__.py +0 -0
- aethergraph/contracts/services/artifacts.py +27 -14
- aethergraph/contracts/services/memory.py +45 -17
- aethergraph/contracts/services/metering.py +129 -0
- aethergraph/contracts/services/runs.py +50 -0
- aethergraph/contracts/services/sessions.py +87 -0
- aethergraph/contracts/services/state_stores.py +3 -0
- aethergraph/contracts/services/viz.py +44 -0
- aethergraph/contracts/storage/artifact_index.py +88 -0
- aethergraph/contracts/storage/artifact_store.py +99 -0
- aethergraph/contracts/storage/async_kv.py +34 -0
- aethergraph/contracts/storage/blob_store.py +50 -0
- aethergraph/contracts/storage/doc_store.py +35 -0
- aethergraph/contracts/storage/event_log.py +31 -0
- aethergraph/contracts/storage/vector_index.py +48 -0
- aethergraph/core/__init__.py +0 -0
- aethergraph/core/execution/forward_scheduler.py +13 -2
- aethergraph/core/execution/global_scheduler.py +21 -15
- aethergraph/core/execution/step_forward.py +10 -1
- aethergraph/core/graph/__init__.py +0 -0
- aethergraph/core/graph/graph_builder.py +8 -4
- aethergraph/core/graph/graph_fn.py +156 -15
- aethergraph/core/graph/graph_spec.py +8 -0
- aethergraph/core/graph/graphify.py +146 -27
- aethergraph/core/graph/node_spec.py +0 -2
- aethergraph/core/graph/node_state.py +3 -0
- aethergraph/core/graph/task_graph.py +39 -1
- aethergraph/core/runtime/__init__.py +0 -0
- aethergraph/core/runtime/ad_hoc_context.py +64 -4
- aethergraph/core/runtime/base_service.py +28 -4
- aethergraph/core/runtime/execution_context.py +13 -15
- aethergraph/core/runtime/graph_runner.py +222 -37
- aethergraph/core/runtime/node_context.py +510 -6
- aethergraph/core/runtime/node_services.py +12 -5
- aethergraph/core/runtime/recovery.py +15 -1
- aethergraph/core/runtime/run_manager.py +783 -0
- aethergraph/core/runtime/run_manager_local.py +204 -0
- aethergraph/core/runtime/run_registration.py +2 -2
- aethergraph/core/runtime/run_types.py +89 -0
- aethergraph/core/runtime/runtime_env.py +136 -7
- aethergraph/core/runtime/runtime_metering.py +71 -0
- aethergraph/core/runtime/runtime_registry.py +36 -13
- aethergraph/core/runtime/runtime_services.py +194 -6
- aethergraph/core/tools/builtins/toolset.py +1 -1
- aethergraph/core/tools/toolkit.py +5 -0
- aethergraph/plugins/agents/default_chat_agent copy.py +90 -0
- aethergraph/plugins/agents/default_chat_agent.py +171 -0
- aethergraph/plugins/agents/shared.py +81 -0
- aethergraph/plugins/channel/adapters/webui.py +112 -112
- aethergraph/plugins/channel/routes/webui_routes.py +367 -102
- aethergraph/plugins/channel/utils/slack_utils.py +115 -59
- aethergraph/plugins/channel/utils/telegram_utils.py +88 -47
- aethergraph/plugins/channel/websockets/weibui_ws.py +172 -0
- aethergraph/runtime/__init__.py +15 -0
- aethergraph/server/app_factory.py +196 -34
- aethergraph/server/clients/channel_client.py +202 -0
- aethergraph/server/http/channel_http_routes.py +116 -0
- aethergraph/server/http/channel_ws_routers.py +45 -0
- aethergraph/server/loading.py +117 -0
- aethergraph/server/server.py +131 -0
- aethergraph/server/server_state.py +240 -0
- aethergraph/server/start.py +227 -66
- aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- aethergraph/server/ui_static/assets/index-BR5GtXcZ.css +1 -0
- aethergraph/server/ui_static/assets/index-CQ0HZZ83.js +400 -0
- aethergraph/server/ui_static/index.html +15 -0
- aethergraph/server/ui_static/logo.png +0 -0
- aethergraph/services/artifacts/__init__.py +0 -0
- aethergraph/services/artifacts/facade.py +1239 -132
- aethergraph/services/auth/{dev.py → authn.py} +0 -8
- aethergraph/services/auth/authz.py +100 -0
- aethergraph/services/channel/__init__.py +0 -0
- aethergraph/services/channel/channel_bus.py +19 -1
- aethergraph/services/channel/factory.py +13 -1
- aethergraph/services/channel/ingress.py +311 -0
- aethergraph/services/channel/queue_adapter.py +75 -0
- aethergraph/services/channel/session.py +502 -19
- aethergraph/services/container/default_container.py +122 -43
- aethergraph/services/continuations/continuation.py +6 -0
- aethergraph/services/continuations/stores/fs_store.py +19 -0
- aethergraph/services/eventhub/event_hub.py +76 -0
- aethergraph/services/kv/__init__.py +0 -0
- aethergraph/services/kv/ephemeral.py +244 -0
- aethergraph/services/llm/__init__.py +0 -0
- aethergraph/services/llm/generic_client copy.py +691 -0
- aethergraph/services/llm/generic_client.py +1288 -187
- aethergraph/services/llm/providers.py +3 -1
- aethergraph/services/llm/types.py +47 -0
- aethergraph/services/llm/utils.py +284 -0
- aethergraph/services/logger/std.py +3 -0
- aethergraph/services/mcp/__init__.py +9 -0
- aethergraph/services/mcp/http_client.py +38 -0
- aethergraph/services/mcp/service.py +225 -1
- aethergraph/services/mcp/stdio_client.py +41 -6
- aethergraph/services/mcp/ws_client.py +44 -2
- aethergraph/services/memory/__init__.py +0 -0
- aethergraph/services/memory/distillers/llm_long_term.py +234 -0
- aethergraph/services/memory/distillers/llm_meta_summary.py +398 -0
- aethergraph/services/memory/distillers/long_term.py +225 -0
- aethergraph/services/memory/facade/__init__.py +3 -0
- aethergraph/services/memory/facade/chat.py +440 -0
- aethergraph/services/memory/facade/core.py +447 -0
- aethergraph/services/memory/facade/distillation.py +424 -0
- aethergraph/services/memory/facade/rag.py +410 -0
- aethergraph/services/memory/facade/results.py +315 -0
- aethergraph/services/memory/facade/retrieval.py +139 -0
- aethergraph/services/memory/facade/types.py +77 -0
- aethergraph/services/memory/facade/utils.py +43 -0
- aethergraph/services/memory/facade_dep.py +1539 -0
- aethergraph/services/memory/factory.py +9 -3
- aethergraph/services/memory/utils.py +10 -0
- aethergraph/services/metering/eventlog_metering.py +470 -0
- aethergraph/services/metering/noop.py +25 -4
- aethergraph/services/rag/__init__.py +0 -0
- aethergraph/services/rag/facade.py +279 -23
- aethergraph/services/rag/index_factory.py +2 -2
- aethergraph/services/rag/node_rag.py +317 -0
- aethergraph/services/rate_limit/inmem_rate_limit.py +24 -0
- aethergraph/services/registry/__init__.py +0 -0
- aethergraph/services/registry/agent_app_meta.py +419 -0
- aethergraph/services/registry/registry_key.py +1 -1
- aethergraph/services/registry/unified_registry.py +74 -6
- aethergraph/services/scope/scope.py +159 -0
- aethergraph/services/scope/scope_factory.py +164 -0
- aethergraph/services/state_stores/serialize.py +5 -0
- aethergraph/services/state_stores/utils.py +2 -1
- aethergraph/services/viz/__init__.py +0 -0
- aethergraph/services/viz/facade.py +413 -0
- aethergraph/services/viz/viz_service.py +69 -0
- aethergraph/storage/artifacts/artifact_index_jsonl.py +180 -0
- aethergraph/storage/artifacts/artifact_index_sqlite.py +426 -0
- aethergraph/storage/artifacts/cas_store.py +422 -0
- aethergraph/storage/artifacts/fs_cas.py +18 -0
- aethergraph/storage/artifacts/s3_cas.py +14 -0
- aethergraph/storage/artifacts/utils.py +124 -0
- aethergraph/storage/blob/fs_blob.py +86 -0
- aethergraph/storage/blob/s3_blob.py +115 -0
- aethergraph/storage/continuation_store/fs_cont.py +283 -0
- aethergraph/storage/continuation_store/inmem_cont.py +146 -0
- aethergraph/storage/continuation_store/kvdoc_cont.py +261 -0
- aethergraph/storage/docstore/fs_doc.py +63 -0
- aethergraph/storage/docstore/sqlite_doc.py +31 -0
- aethergraph/storage/docstore/sqlite_doc_sync.py +90 -0
- aethergraph/storage/eventlog/fs_event.py +136 -0
- aethergraph/storage/eventlog/sqlite_event.py +47 -0
- aethergraph/storage/eventlog/sqlite_event_sync.py +178 -0
- aethergraph/storage/factory.py +432 -0
- aethergraph/storage/fs_utils.py +28 -0
- aethergraph/storage/graph_state_store/state_store.py +64 -0
- aethergraph/storage/kv/inmem_kv.py +103 -0
- aethergraph/storage/kv/layered_kv.py +52 -0
- aethergraph/storage/kv/sqlite_kv.py +39 -0
- aethergraph/storage/kv/sqlite_kv_sync.py +98 -0
- aethergraph/storage/memory/event_persist.py +68 -0
- aethergraph/storage/memory/fs_persist.py +118 -0
- aethergraph/{services/memory/hotlog_kv.py → storage/memory/hotlog.py} +8 -2
- aethergraph/{services → storage}/memory/indices.py +31 -7
- aethergraph/storage/metering/meter_event.py +55 -0
- aethergraph/storage/runs/doc_store.py +280 -0
- aethergraph/storage/runs/inmen_store.py +82 -0
- aethergraph/storage/runs/sqlite_run_store.py +403 -0
- aethergraph/storage/sessions/doc_store.py +183 -0
- aethergraph/storage/sessions/inmem_store.py +110 -0
- aethergraph/storage/sessions/sqlite_session_store.py +399 -0
- aethergraph/storage/vector_index/chroma_index.py +138 -0
- aethergraph/storage/vector_index/faiss_index.py +179 -0
- aethergraph/storage/vector_index/sqlite_index.py +187 -0
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a3.dist-info}/METADATA +138 -31
- aethergraph-0.1.0a3.dist-info/RECORD +356 -0
- aethergraph-0.1.0a3.dist-info/entry_points.txt +3 -0
- aethergraph/services/artifacts/factory.py +0 -35
- aethergraph/services/artifacts/fs_store.py +0 -656
- aethergraph/services/artifacts/jsonl_index.py +0 -123
- aethergraph/services/artifacts/sqlite_index.py +0 -209
- aethergraph/services/memory/distillers/episode.py +0 -116
- aethergraph/services/memory/distillers/rolling.py +0 -74
- aethergraph/services/memory/facade.py +0 -633
- aethergraph/services/memory/persist_fs.py +0 -40
- aethergraph/services/rag/index/base.py +0 -27
- aethergraph/services/rag/index/faiss_index.py +0 -121
- aethergraph/services/rag/index/sqlite_index.py +0 -134
- aethergraph-0.1.0a1.dist-info/RECORD +0 -182
- aethergraph-0.1.0a1.dist-info/entry_points.txt +0 -2
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a3.dist-info}/WHEEL +0 -0
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a3.dist-info}/licenses/LICENSE +0 -0
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a3.dist-info}/licenses/NOTICE +0 -0
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from aethergraph.contracts.services.memory import Event
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .types import MemoryFacadeInterface
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ResultMixin:
|
|
12
|
+
"""Methods for recording tool execution results.
|
|
13
|
+
NOTE: there are many potentially overlapping methods here. We will deprecate most of them
|
|
14
|
+
over time in favor of a smaller, clearer set.
|
|
15
|
+
|
|
16
|
+
Include methods:
|
|
17
|
+
- write_result (general)
|
|
18
|
+
- write_tool_result (deprecated, use record_tool_result)
|
|
19
|
+
- record_result (general alias)
|
|
20
|
+
- record_tool_result (dedicated to tools)
|
|
21
|
+
- last_tool_result
|
|
22
|
+
- recent_tool_result_data
|
|
23
|
+
|
|
24
|
+
The following are convenience wrappers. TODO: standardize naming
|
|
25
|
+
- last_by_name
|
|
26
|
+
- last_output_by_name
|
|
27
|
+
- last_outputs_by_topic
|
|
28
|
+
- last_tool_result_outputs
|
|
29
|
+
- latest_refs_by_kind
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
async def record_tool_result(
|
|
33
|
+
self: MemoryFacadeInterface,
|
|
34
|
+
*,
|
|
35
|
+
tool: str,
|
|
36
|
+
inputs: list[dict[str, Any]] | None = None,
|
|
37
|
+
outputs: list[dict[str, Any]] | None = None,
|
|
38
|
+
tags: list[str] | None = None,
|
|
39
|
+
metrics: dict[str, float] | None = None,
|
|
40
|
+
message: str | None = None,
|
|
41
|
+
severity: int = 3,
|
|
42
|
+
) -> Event:
|
|
43
|
+
"""
|
|
44
|
+
Record the result of a tool execution in a normalized format.
|
|
45
|
+
|
|
46
|
+
This method provides the method to log tool execution results with standardized metadata.
|
|
47
|
+
Interally, it constructs an `Event` object encapsulating details about the tool execution,
|
|
48
|
+
including inputs, outputs, tags, metrics, and a descriptive message.
|
|
49
|
+
|
|
50
|
+
Examples:
|
|
51
|
+
Recording a tool result with inputs and outputs:
|
|
52
|
+
```python
|
|
53
|
+
await context.memory().record_tool_result(
|
|
54
|
+
tool="data_cleaner",
|
|
55
|
+
inputs=[{"raw_data": "some raw input"}],
|
|
56
|
+
outputs=[{"cleaned_data": "processed output"}],
|
|
57
|
+
tags=["data", "cleaning"],
|
|
58
|
+
metrics={"execution_time": 1.23},
|
|
59
|
+
message="Tool executed successfully.",
|
|
60
|
+
severity=2,
|
|
61
|
+
)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Logging a tool result with minimal metadata:
|
|
65
|
+
```python
|
|
66
|
+
await context.memory().record_tool_result(
|
|
67
|
+
tool="simple_logger",
|
|
68
|
+
message="Logged an event.",
|
|
69
|
+
)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
tool: The name of the tool that generated the result.
|
|
74
|
+
inputs: A list of dictionaries representing the tool's input data.
|
|
75
|
+
outputs: A list of dictionaries representing the tool's output data.
|
|
76
|
+
tags: A list of string labels for categorization.
|
|
77
|
+
metrics: A dictionary of numerical metrics (e.g., execution time, accuracy).
|
|
78
|
+
message: A descriptive message about the tool's execution or result.
|
|
79
|
+
severity: An integer (1-5) indicating the importance or severity of the result.
|
|
80
|
+
(1=Lowest, 5=Highest).
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Event: The fully persisted `Event` object containing the generated ID and timestamp.
|
|
84
|
+
"""
|
|
85
|
+
return await self.write_tool_result(
|
|
86
|
+
tool=tool,
|
|
87
|
+
inputs=inputs,
|
|
88
|
+
outputs=outputs,
|
|
89
|
+
tags=tags,
|
|
90
|
+
metrics=metrics,
|
|
91
|
+
message=message,
|
|
92
|
+
severity=severity,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
async def recent_tool_results(
|
|
96
|
+
self,
|
|
97
|
+
*,
|
|
98
|
+
tool: str,
|
|
99
|
+
limit: int = 10,
|
|
100
|
+
) -> list[Event]:
|
|
101
|
+
"""
|
|
102
|
+
Retrieve recent tool execution results for a specific tool.
|
|
103
|
+
|
|
104
|
+
This method filters and returns the most recent `tool_result` events
|
|
105
|
+
associated with the specified tool, allowing you to analyze or process
|
|
106
|
+
the results of tool executions.
|
|
107
|
+
|
|
108
|
+
Examples:
|
|
109
|
+
Fetching the 5 most recent results for a tool:
|
|
110
|
+
```python
|
|
111
|
+
recent_results = await context.memory().recent_tool_results(
|
|
112
|
+
tool="data_cleaner",
|
|
113
|
+
limit=5,
|
|
114
|
+
)
|
|
115
|
+
for result in recent_results:
|
|
116
|
+
print(result)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Retrieving all available results for a tool (up to the default limit):
|
|
120
|
+
```python
|
|
121
|
+
recent_results = await context.memory().recent_tool_results(
|
|
122
|
+
tool="simple_logger",
|
|
123
|
+
)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
tool: The name of the tool whose results are being queried.
|
|
128
|
+
limit: The maximum number of results to return (default is 10).
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
list[Event]: A list of `Event` objects representing the recent
|
|
132
|
+
`tool_result` events for the specified tool, ordered by recency.
|
|
133
|
+
"""
|
|
134
|
+
events = await self.recent(kinds=["tool_result"], limit=100)
|
|
135
|
+
tool_events = [e for e in events if e.tool == tool]
|
|
136
|
+
return tool_events[:limit]
|
|
137
|
+
|
|
138
|
+
async def recent_tool_result_data(
|
|
139
|
+
self,
|
|
140
|
+
*,
|
|
141
|
+
tool: str,
|
|
142
|
+
limit: int = 10,
|
|
143
|
+
) -> list[dict[str, Any]]:
|
|
144
|
+
"""
|
|
145
|
+
Return a simplified view over recent tool_result events.
|
|
146
|
+
|
|
147
|
+
This method provides a developer-friendly way to retrieve recent tool execution results
|
|
148
|
+
in a normalized format, including metadata such as timestamps, inputs, outputs, and tags.
|
|
149
|
+
|
|
150
|
+
Examples:
|
|
151
|
+
Fetching recent tool result data:
|
|
152
|
+
```python
|
|
153
|
+
recent_data = await context.memory().recent_tool_result_data(
|
|
154
|
+
tool="data_cleaner",
|
|
155
|
+
limit=5,
|
|
156
|
+
)
|
|
157
|
+
for entry in recent_data:
|
|
158
|
+
print(entry)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
tool: The name of the tool whose results are being queried.
|
|
163
|
+
limit: The maximum number of recent results to retrieve.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
list[dict[str, Any]]: A list of dictionaries, each containing:
|
|
167
|
+
- "ts": The timestamp of the event.
|
|
168
|
+
- "tool": The name of the tool.
|
|
169
|
+
- "message": A descriptive message about the tool's execution.
|
|
170
|
+
- "inputs": The input data provided to the tool.
|
|
171
|
+
- "outputs": The output data generated by the tool.
|
|
172
|
+
- "tags": A list of string labels associated with the event.
|
|
173
|
+
"""
|
|
174
|
+
events = await self.recent_tool_results(tool=tool, limit=limit)
|
|
175
|
+
out: list[dict[str, Any]] = []
|
|
176
|
+
for e in events:
|
|
177
|
+
out.append(
|
|
178
|
+
{
|
|
179
|
+
"ts": getattr(e, "ts", None),
|
|
180
|
+
"tool": e.tool,
|
|
181
|
+
"message": e.text,
|
|
182
|
+
"inputs": getattr(e, "inputs", None),
|
|
183
|
+
"outputs": getattr(e, "outputs", None),
|
|
184
|
+
"tags": list(e.tags or []),
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
return out
|
|
188
|
+
|
|
189
|
+
async def write_result(
|
|
190
|
+
self: MemoryFacadeInterface,
|
|
191
|
+
*,
|
|
192
|
+
tool: str | None = None, # back compatibility with 'topic'
|
|
193
|
+
inputs: list[dict[str, Any]] | None = None,
|
|
194
|
+
outputs: list[dict[str, Any]] | None = None,
|
|
195
|
+
tags: list[str] | None = None,
|
|
196
|
+
metrics: dict[str, float] | None = None,
|
|
197
|
+
message: str | None = None,
|
|
198
|
+
severity: int = 3,
|
|
199
|
+
topic: str | None = None, # alias for tool, backwards compatibility
|
|
200
|
+
) -> Event:
|
|
201
|
+
"""
|
|
202
|
+
Convenience for recording a “tool/agent/flow result” with typed I/O.
|
|
203
|
+
|
|
204
|
+
`tool` : tool/agent/flow identifier (also used by KVIndices.last_outputs_by_topic)
|
|
205
|
+
`inputs` : List[Value]-like dicts
|
|
206
|
+
`outputs` : List[Value]-like dicts
|
|
207
|
+
`tags` : labels like ["rag","qa"] for filtering/search
|
|
208
|
+
"""
|
|
209
|
+
if tool is None and topic is not None:
|
|
210
|
+
tool = topic
|
|
211
|
+
if tool is None:
|
|
212
|
+
raise ValueError("write_result requires a 'tool' (or legacy 'topic') name")
|
|
213
|
+
|
|
214
|
+
inputs = inputs or []
|
|
215
|
+
outputs = outputs or []
|
|
216
|
+
|
|
217
|
+
evt = await self.record_raw(
|
|
218
|
+
base=dict(
|
|
219
|
+
tool=tool,
|
|
220
|
+
kind="tool_result",
|
|
221
|
+
severity=severity,
|
|
222
|
+
tags=tags or [],
|
|
223
|
+
inputs=inputs,
|
|
224
|
+
outputs=outputs,
|
|
225
|
+
),
|
|
226
|
+
text=message,
|
|
227
|
+
metrics=metrics,
|
|
228
|
+
)
|
|
229
|
+
await self.indices.update(self.timeline_id, evt)
|
|
230
|
+
return evt
|
|
231
|
+
|
|
232
|
+
async def write_tool_result(
|
|
233
|
+
self: MemoryFacadeInterface,
|
|
234
|
+
*,
|
|
235
|
+
tool: str,
|
|
236
|
+
inputs: list[dict[str, Any]] | None = None,
|
|
237
|
+
outputs: list[dict[str, Any]] | None = None,
|
|
238
|
+
tags: list[str] | None = None,
|
|
239
|
+
metrics: dict[str, float] | None = None,
|
|
240
|
+
message: str | None = None,
|
|
241
|
+
severity: int = 3,
|
|
242
|
+
) -> Event:
|
|
243
|
+
"""
|
|
244
|
+
Convenience wrapper around write_result() for tool results.
|
|
245
|
+
"""
|
|
246
|
+
return await self.write_result(
|
|
247
|
+
tool=tool,
|
|
248
|
+
inputs=inputs,
|
|
249
|
+
outputs=outputs,
|
|
250
|
+
tags=tags,
|
|
251
|
+
metrics=metrics,
|
|
252
|
+
message=message,
|
|
253
|
+
severity=severity,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
async def record_result(
|
|
257
|
+
self: MemoryFacadeInterface,
|
|
258
|
+
*,
|
|
259
|
+
tool: str | None = None,
|
|
260
|
+
inputs: list[dict[str, Any]] | None = None,
|
|
261
|
+
outputs: list[dict[str, Any]] | None = None,
|
|
262
|
+
tags: list[str] | None = None,
|
|
263
|
+
metrics: dict[str, float] | None = None,
|
|
264
|
+
message: str | None = None,
|
|
265
|
+
severity: int = 3,
|
|
266
|
+
) -> Event:
|
|
267
|
+
"""
|
|
268
|
+
Alias for write_result(); symmetric with record_tool_result().
|
|
269
|
+
|
|
270
|
+
Use this when you conceptually have a "result" but don't care whether
|
|
271
|
+
it's a tool vs agent vs flow.
|
|
272
|
+
"""
|
|
273
|
+
return await self.write_result(
|
|
274
|
+
tool=tool,
|
|
275
|
+
inputs=inputs,
|
|
276
|
+
outputs=outputs,
|
|
277
|
+
tags=tags,
|
|
278
|
+
metrics=metrics,
|
|
279
|
+
message=message,
|
|
280
|
+
severity=severity,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
async def last_tool_result(self, tool: str) -> Event | None:
|
|
284
|
+
"""
|
|
285
|
+
Convenience: return the most recent tool_result Event for a given tool.
|
|
286
|
+
"""
|
|
287
|
+
events = await self.recent_tool_results(tool=tool, limit=1)
|
|
288
|
+
return events[-1] if events else None
|
|
289
|
+
|
|
290
|
+
async def last_by_name(self, name: str):
|
|
291
|
+
"""Return the last output value by `name` from Indices (fast path)."""
|
|
292
|
+
return await self.indices.last_by_name(self.timeline_id, name)
|
|
293
|
+
|
|
294
|
+
async def last_output_by_name(self, name: str):
|
|
295
|
+
"""Return the last output value (Value.value) by `name` from Indices (fast path)."""
|
|
296
|
+
out = await self.indices.last_by_name(self.timeline_id, name)
|
|
297
|
+
if out is None:
|
|
298
|
+
return None
|
|
299
|
+
return out.get("value") # type: ignore
|
|
300
|
+
|
|
301
|
+
async def last_outputs_by_topic(self, topic: str):
|
|
302
|
+
"""Return the last output map for a given topic (tool/flow/agent) from Indices."""
|
|
303
|
+
return await self.indices.last_outputs_by_topic(self.timeline_id, topic)
|
|
304
|
+
|
|
305
|
+
# replace last_tool_result_outputs
|
|
306
|
+
async def last_tool_result_outputs(self, tool: str) -> dict[str, Any] | None:
|
|
307
|
+
"""
|
|
308
|
+
Convenience wrapper around KVIndices.last_outputs_by_topic for this run.
|
|
309
|
+
Returns the last outputs map for a given tool, or None.
|
|
310
|
+
"""
|
|
311
|
+
return await self.indices.last_outputs_by_topic(self.timeline_id, tool)
|
|
312
|
+
|
|
313
|
+
async def latest_refs_by_kind(self, kind: str, *, limit: int = 50):
|
|
314
|
+
"""Return latest ref outputs by ref.kind (fast path, KV-backed)."""
|
|
315
|
+
return await self.indices.latest_refs_by_kind(self.timeline_id, kind, limit=limit)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from aethergraph.contracts.services.memory import Event
|
|
8
|
+
|
|
9
|
+
from .types import MemoryFacadeInterface
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RetrievalMixin:
|
|
13
|
+
"""Methods for retrieving events and values."""
|
|
14
|
+
|
|
15
|
+
async def recent(
|
|
16
|
+
self: MemoryFacadeInterface, *, kinds: list[str] | None = None, limit: int = 50
|
|
17
|
+
) -> list[Event]:
|
|
18
|
+
"""
|
|
19
|
+
Retrieve recent events.
|
|
20
|
+
|
|
21
|
+
This method fetches a list of recent events, optionally filtered by kinds.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
kinds: A list of event kinds to filter by. Defaults to None.
|
|
25
|
+
limit: The maximum number of events to retrieve. Defaults to 50.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
list[Event]: A list of recent events.
|
|
29
|
+
|
|
30
|
+
Notes:
|
|
31
|
+
This method interacts with the underlying HotLog service to fetch events
|
|
32
|
+
associated with the current timeline. The events are returned in chronological order,
|
|
33
|
+
with the most recent events appearing last in the list. Memory out of the limit will be discarded
|
|
34
|
+
in the HotLog layer (but persistent in the Persistence layer). Memory in persistence cannot be retrieved
|
|
35
|
+
via this method.
|
|
36
|
+
"""
|
|
37
|
+
return await self.hotlog.recent(self.timeline_id, kinds=kinds, limit=limit)
|
|
38
|
+
|
|
39
|
+
async def recent_data(
|
|
40
|
+
self: MemoryFacadeInterface,
|
|
41
|
+
*,
|
|
42
|
+
kinds: list[str] | None = None,
|
|
43
|
+
tags: list[str] | None = None,
|
|
44
|
+
limit: int = 50,
|
|
45
|
+
) -> list[Any]:
|
|
46
|
+
"""
|
|
47
|
+
Retrieve recent event data.
|
|
48
|
+
|
|
49
|
+
This method fetches the data or text of recent events, optionally filtered by kinds and tags.
|
|
50
|
+
Unlike `recent()`, which returns full Event objects, this method extracts and returns only the
|
|
51
|
+
data or text content of the events. This is useful for scenarios where only the event payloads are needed.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
kinds: A list of event kinds to filter by. Defaults to None.
|
|
55
|
+
tags: A list of tags to filter events by. Defaults to None.
|
|
56
|
+
limit: The maximum number of events to retrieve. Defaults to 50.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
list[Any]: A list of event data or text.
|
|
60
|
+
|
|
61
|
+
Notes:
|
|
62
|
+
This method first retrieves recent events using the `recent()` method and then filters them
|
|
63
|
+
based on the provided tags. It extracts the `data` attribute if available; otherwise, it
|
|
64
|
+
attempts to parse the `text` attribute as JSON. If parsing fails, the raw text is returned.
|
|
65
|
+
|
|
66
|
+
Memory out of the limit will be discarded in the HotLog layer (but persistent in the Persistence layer).
|
|
67
|
+
Memory in persistence cannot be retrieved via this method.
|
|
68
|
+
"""
|
|
69
|
+
evts = await self.recent(kinds=kinds, limit=limit)
|
|
70
|
+
if tags:
|
|
71
|
+
want = set(tags)
|
|
72
|
+
evts = [e for e in evts if want.issubset(set(e.tags or []))]
|
|
73
|
+
|
|
74
|
+
out: list[Any] = []
|
|
75
|
+
for e in evts:
|
|
76
|
+
if e.data is not None:
|
|
77
|
+
out.append(e.data)
|
|
78
|
+
elif e.text:
|
|
79
|
+
t = e.text.strip()
|
|
80
|
+
if (t.startswith("{") and t.endswith("}")) or (
|
|
81
|
+
t.startswith("[") and t.endswith("]")
|
|
82
|
+
):
|
|
83
|
+
try:
|
|
84
|
+
out.append(json.loads(t))
|
|
85
|
+
continue
|
|
86
|
+
except Exception:
|
|
87
|
+
pass
|
|
88
|
+
out.append(e.text)
|
|
89
|
+
return out
|
|
90
|
+
|
|
91
|
+
async def search(
|
|
92
|
+
self: MemoryFacadeInterface,
|
|
93
|
+
*,
|
|
94
|
+
query: str,
|
|
95
|
+
kinds: list[str] | None = None,
|
|
96
|
+
tags: list[str] | None = None,
|
|
97
|
+
limit: int = 100,
|
|
98
|
+
use_embedding: bool = True,
|
|
99
|
+
) -> list[Event]:
|
|
100
|
+
"""
|
|
101
|
+
Search for events based on a query.
|
|
102
|
+
|
|
103
|
+
This method searches for events that match a query, optionally filtered by kinds and tags.
|
|
104
|
+
Note that this implementation currently performs a lexical search. Embedding-based search
|
|
105
|
+
is planned for future development.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
query: The search query string.
|
|
109
|
+
kinds: A list of event kinds to filter by. Defaults to None.
|
|
110
|
+
tags: A list of tags to filter events by. Defaults to None.
|
|
111
|
+
limit: The maximum number of events to retrieve. Defaults to 100.
|
|
112
|
+
use_embedding: Whether to use embedding-based search. Defaults to True.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
list[Event]: A list of events matching the query.
|
|
116
|
+
|
|
117
|
+
Notes:
|
|
118
|
+
This method retrieves recent events using the `recent()` method and filters them
|
|
119
|
+
based on the provided tags. It performs a simple lexical search on the event text.
|
|
120
|
+
Embedding-based search functionality is not yet implemented.
|
|
121
|
+
|
|
122
|
+
Memory out of the limit will be discarded in the HotLog layer (but persistent in the Persistence layer).
|
|
123
|
+
Memory in persistence cannot be retrieved via this method.
|
|
124
|
+
"""
|
|
125
|
+
events = await self.recent(kinds=kinds, limit=limit)
|
|
126
|
+
if tags:
|
|
127
|
+
want = set(tags)
|
|
128
|
+
events = [e for e in events if want.issubset(set(e.tags or []))]
|
|
129
|
+
|
|
130
|
+
query_l = query.lower()
|
|
131
|
+
lexical_hits = [e for e in events if (e.text or "").lower().find(query_l) >= 0]
|
|
132
|
+
|
|
133
|
+
if not use_embedding:
|
|
134
|
+
return lexical_hits or events
|
|
135
|
+
|
|
136
|
+
# Placeholder for future embedding search logic
|
|
137
|
+
# if not (self.llm and any(e.embedding for e in events)): return lexical_hits or events
|
|
138
|
+
# ... logic ...
|
|
139
|
+
return lexical_hits or events
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Protocol
|
|
4
|
+
|
|
5
|
+
from aethergraph.contracts.services.llm import LLMClientProtocol
|
|
6
|
+
from aethergraph.contracts.services.memory import Event, HotLog, Indices, Persistence
|
|
7
|
+
from aethergraph.contracts.storage.artifact_store import AsyncArtifactStore
|
|
8
|
+
from aethergraph.contracts.storage.doc_store import DocStore
|
|
9
|
+
from aethergraph.services.rag.facade import RAGFacade
|
|
10
|
+
from aethergraph.services.scope.scope import Scope
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MemoryFacadeInterface(Protocol):
|
|
14
|
+
"""
|
|
15
|
+
Protocol defining the state and core methods available on the MemoryFacade.
|
|
16
|
+
Mixins use this to type-hint 'self'.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
run_id: str
|
|
20
|
+
timeline_id: str
|
|
21
|
+
memory_scope_id: str
|
|
22
|
+
|
|
23
|
+
hotlog: HotLog
|
|
24
|
+
persistence: Persistence
|
|
25
|
+
indices: Indices
|
|
26
|
+
docs: DocStore
|
|
27
|
+
artifacts: AsyncArtifactStore
|
|
28
|
+
scope: Scope | None
|
|
29
|
+
|
|
30
|
+
rag: RAGFacade | None
|
|
31
|
+
llm: LLMClientProtocol | None
|
|
32
|
+
logger: Any
|
|
33
|
+
|
|
34
|
+
default_signal_threshold: float
|
|
35
|
+
hot_limit: int
|
|
36
|
+
hot_ttl_s: int
|
|
37
|
+
|
|
38
|
+
async def record_raw(
|
|
39
|
+
self,
|
|
40
|
+
*,
|
|
41
|
+
base: dict[str, Any],
|
|
42
|
+
text: str | None = None,
|
|
43
|
+
metrics: dict[str, float] | None = None,
|
|
44
|
+
) -> Event: ...
|
|
45
|
+
|
|
46
|
+
async def record(
|
|
47
|
+
self,
|
|
48
|
+
kind: str,
|
|
49
|
+
data: Any,
|
|
50
|
+
tags: list[str] | None = None,
|
|
51
|
+
severity: int = 2,
|
|
52
|
+
stage: str | None = None,
|
|
53
|
+
inputs_ref=None,
|
|
54
|
+
outputs_ref=None,
|
|
55
|
+
metrics: dict[str, float] | None = None,
|
|
56
|
+
signal: float | None = None,
|
|
57
|
+
) -> Event: ...
|
|
58
|
+
|
|
59
|
+
async def write_result(
|
|
60
|
+
self,
|
|
61
|
+
*,
|
|
62
|
+
tool: str,
|
|
63
|
+
inputs: list[dict[str, Any]] | None = None,
|
|
64
|
+
outputs: list[dict[str, Any]] | None = None,
|
|
65
|
+
tags: list[str] | None = None,
|
|
66
|
+
metrics: dict[str, float] | None = None,
|
|
67
|
+
message: str | None = None,
|
|
68
|
+
severity: int = 3,
|
|
69
|
+
) -> Event: ...
|
|
70
|
+
|
|
71
|
+
# Required for RetrievalMixin to expose 'recent' to other mixins
|
|
72
|
+
async def recent(self, *, kinds: list[str] | None = None, limit: int = 50) -> list[Event]: ...
|
|
73
|
+
|
|
74
|
+
# Required for RAGMixin to expose 'rag_bind'
|
|
75
|
+
async def rag_bind(
|
|
76
|
+
self, *, key: str = "default", create_if_missing: bool = True, labels: dict | None = None
|
|
77
|
+
) -> str: ...
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any
|
|
7
|
+
import unicodedata
|
|
8
|
+
|
|
9
|
+
_SAFE = re.compile(r"[^A-Za-z0-9._-]+")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def now_iso() -> str:
|
|
13
|
+
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def stable_event_id(parts: dict[str, Any]) -> str:
|
|
17
|
+
blob = json.dumps(parts, sort_keys=True, ensure_ascii=False).encode("utf-8")
|
|
18
|
+
return hashlib.sha256(blob).hexdigest()[:24]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def short_hash(s: str, n: int = 8) -> str:
|
|
22
|
+
return hashlib.sha256(s.encode("utf-8")).hexdigest()[:n]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def slug(s: str) -> str:
|
|
26
|
+
s = unicodedata.normalize("NFKC", str(s)).strip()
|
|
27
|
+
s = s.replace(" ", "-")
|
|
28
|
+
s = _SAFE.sub("-", s)
|
|
29
|
+
return s.strip("-") or "default"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def load_sticky(path: str) -> dict:
|
|
33
|
+
try:
|
|
34
|
+
with open(path, encoding="utf-8") as f:
|
|
35
|
+
return json.load(f)
|
|
36
|
+
except Exception:
|
|
37
|
+
return {}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def save_sticky(path: str, m: dict):
|
|
41
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
42
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
43
|
+
json.dump(m, f, ensure_ascii=False, indent=2)
|