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,413 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from aethergraph.contracts.services.viz import VizEvent, VizMode
|
|
8
|
+
from aethergraph.services.artifacts.facade import Artifact, ArtifactFacade
|
|
9
|
+
from aethergraph.services.scope.scope import Scope
|
|
10
|
+
from aethergraph.services.viz.viz_service import VizService
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class VizFacade:
|
|
15
|
+
"""
|
|
16
|
+
High-level facade for visualization operations within a given Scope.
|
|
17
|
+
|
|
18
|
+
- Wraps VizService and ArtifactFacade.
|
|
19
|
+
- Knows about Scope to auto-fill provenance and tenant fields.
|
|
20
|
+
|
|
21
|
+
Usage pattern in ctx.viz:
|
|
22
|
+
# Scalars
|
|
23
|
+
await ctx.viz.scalar("loss", step=iter, value=float(loss), figure_id="metrics")
|
|
24
|
+
|
|
25
|
+
# Matrix (small heatmap)
|
|
26
|
+
await ctx.viz.matrix("field_map", step=iter, matrix=field_2d, figure_id="fields")
|
|
27
|
+
|
|
28
|
+
# Image (pre-rendered PNG)
|
|
29
|
+
artifact = await ctx.artifacts.save_file(path="frame_17.png", kind="image")
|
|
30
|
+
await ctx.viz.image_from_artifact("design_shape", step=17, artifact=artifact, figure_id="design")
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
run_id: str
|
|
34
|
+
graph_id: str
|
|
35
|
+
node_id: str
|
|
36
|
+
tool_name: str
|
|
37
|
+
tool_version: str
|
|
38
|
+
|
|
39
|
+
viz_service: VizService
|
|
40
|
+
scope: Scope | None = None
|
|
41
|
+
artifacts: ArtifactFacade | None = None
|
|
42
|
+
|
|
43
|
+
# ------- internal helpers -------
|
|
44
|
+
def _scope_dims(self) -> dict[str, Any]:
|
|
45
|
+
if not self.scope:
|
|
46
|
+
return {}
|
|
47
|
+
return self.scope.metering_dimensions()
|
|
48
|
+
|
|
49
|
+
def _apply_scope(self, evt: VizEvent) -> VizEvent:
|
|
50
|
+
dims = self._scope_dims()
|
|
51
|
+
evt.org_id = evt.org_id or dims.get("org_id")
|
|
52
|
+
evt.user_id = evt.user_id or dims.get("user_id")
|
|
53
|
+
evt.client_id = evt.client_id or dims.get("client_id")
|
|
54
|
+
evt.app_id = evt.app_id or dims.get("app_id")
|
|
55
|
+
evt.session_id = evt.session_id or dims.get("session_id")
|
|
56
|
+
return evt
|
|
57
|
+
|
|
58
|
+
# ------- public API -------
|
|
59
|
+
async def scalar(
|
|
60
|
+
self,
|
|
61
|
+
track_id: str,
|
|
62
|
+
*,
|
|
63
|
+
step: int,
|
|
64
|
+
value: float,
|
|
65
|
+
figure_id: str | None = None,
|
|
66
|
+
mode: VizMode = "append",
|
|
67
|
+
meta: dict[str, Any] | None = None,
|
|
68
|
+
tags: list[str] | None = None,
|
|
69
|
+
) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Record a single scalar value for visualization in the Aethergraph UI.
|
|
72
|
+
|
|
73
|
+
This method standardizes the event format, auto-fills provenance fields,
|
|
74
|
+
and dispatches the scalar data to the configured storage backend.
|
|
75
|
+
|
|
76
|
+
Examples:
|
|
77
|
+
Basic usage to log a loss metric:
|
|
78
|
+
```python
|
|
79
|
+
await context.viz().scalar("loss", step=iteration, value=loss)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Logging a scalar with extra metadata and custom tags:
|
|
83
|
+
```python
|
|
84
|
+
await context.viz().scalar(
|
|
85
|
+
"accuracy",
|
|
86
|
+
step=42,
|
|
87
|
+
value=0.98,
|
|
88
|
+
figure_id="metrics",
|
|
89
|
+
meta={"model": "resnet"},
|
|
90
|
+
tags=["experiment:baseline"]
|
|
91
|
+
)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
track_id: Unique identifier for the scalar track (e.g., "loss").
|
|
96
|
+
step: Integer step or iteration number for the data point.
|
|
97
|
+
value: The scalar value to record (float).
|
|
98
|
+
figure_id: Optional figure grouping for UI display.
|
|
99
|
+
mode: Storage mode, typically "append".
|
|
100
|
+
meta: Optional dictionary of extra metadata.
|
|
101
|
+
tags: Optional list of string labels. The tag "type:scalar" is automatically appended.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
None. The event is persisted for later visualization.
|
|
105
|
+
"""
|
|
106
|
+
evt = VizEvent(
|
|
107
|
+
run_id=self.run_id,
|
|
108
|
+
graph_id=self.graph_id,
|
|
109
|
+
node_id=self.node_id,
|
|
110
|
+
tool_name=self.tool_name,
|
|
111
|
+
tool_version=self.tool_version,
|
|
112
|
+
track_id=track_id,
|
|
113
|
+
figure_id=figure_id,
|
|
114
|
+
viz_kind="scalar",
|
|
115
|
+
step=step,
|
|
116
|
+
mode=mode,
|
|
117
|
+
value=float(value),
|
|
118
|
+
meta=meta,
|
|
119
|
+
tags=(tags or []) + ["type:scalar"],
|
|
120
|
+
)
|
|
121
|
+
evt = self._apply_scope(evt)
|
|
122
|
+
await self.viz_service.append(evt)
|
|
123
|
+
|
|
124
|
+
async def vector(
|
|
125
|
+
self,
|
|
126
|
+
track_id: str,
|
|
127
|
+
*,
|
|
128
|
+
step: int,
|
|
129
|
+
values: Sequence[float],
|
|
130
|
+
figure_id: str | None = None,
|
|
131
|
+
mode: VizMode = "append",
|
|
132
|
+
meta: dict[str, Any] | None = None,
|
|
133
|
+
tags: list[str] | None = None,
|
|
134
|
+
) -> None:
|
|
135
|
+
"""
|
|
136
|
+
Record a single vector (1D array) for visualization in the Aethergraph UI.
|
|
137
|
+
|
|
138
|
+
This method standardizes the event format, auto-fills provenance fields,
|
|
139
|
+
and dispatches the vector data to the configured storage backend.
|
|
140
|
+
|
|
141
|
+
Examples:
|
|
142
|
+
Basic usage to log a vector:
|
|
143
|
+
```python
|
|
144
|
+
await context.viz().vector("embedding", step=iteration, values=[0.1, 0.2, 0.3])
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Logging a vector with extra metadata and custom tags:
|
|
148
|
+
```python
|
|
149
|
+
await context.viz().vector(
|
|
150
|
+
"features",
|
|
151
|
+
step=42,
|
|
152
|
+
values=[1.0, 2.5, 3.7],
|
|
153
|
+
figure_id="feature_tracks",
|
|
154
|
+
meta={"source": "encoder"},
|
|
155
|
+
tags=["experiment:baseline"]
|
|
156
|
+
)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
track_id: Unique identifier for the vector track (e.g., "embedding").
|
|
161
|
+
step: Integer step or iteration number for the data point.
|
|
162
|
+
values: Sequence of float values representing the vector.
|
|
163
|
+
figure_id: Optional figure grouping for UI display.
|
|
164
|
+
mode: Storage mode, typically "append".
|
|
165
|
+
meta: Optional dictionary of extra metadata.
|
|
166
|
+
tags: Optional list of string labels. The tag "type:vector" is automatically appended.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
None. The event is persisted for later visualization.
|
|
170
|
+
"""
|
|
171
|
+
evt = VizEvent(
|
|
172
|
+
run_id=self.run_id,
|
|
173
|
+
graph_id=self.graph_id,
|
|
174
|
+
node_id=self.node_id,
|
|
175
|
+
tool_name=self.tool_name,
|
|
176
|
+
tool_version=self.tool_version,
|
|
177
|
+
track_id=track_id,
|
|
178
|
+
figure_id=figure_id,
|
|
179
|
+
viz_kind="vector",
|
|
180
|
+
step=step,
|
|
181
|
+
mode=mode,
|
|
182
|
+
vector=[float(v) for v in values],
|
|
183
|
+
meta=meta,
|
|
184
|
+
tags=(tags or []) + ["type:vector"],
|
|
185
|
+
)
|
|
186
|
+
evt = self._apply_scope(evt)
|
|
187
|
+
await self.viz_service.append(evt)
|
|
188
|
+
|
|
189
|
+
async def matrix(
|
|
190
|
+
self,
|
|
191
|
+
track_id: str,
|
|
192
|
+
*,
|
|
193
|
+
step: int,
|
|
194
|
+
matrix: Sequence[Sequence[float]],
|
|
195
|
+
figure_id: str | None = None,
|
|
196
|
+
mode: VizMode = "append",
|
|
197
|
+
meta: dict[str, Any] | None = None,
|
|
198
|
+
tags: list[str] | None = None,
|
|
199
|
+
) -> None:
|
|
200
|
+
"""
|
|
201
|
+
Record a single matrix (2D array) for visualization in the Aethergraph UI.
|
|
202
|
+
|
|
203
|
+
This method standardizes the event format, auto-fills provenance fields,
|
|
204
|
+
and dispatches the matrix data to the configured storage backend.
|
|
205
|
+
|
|
206
|
+
Examples:
|
|
207
|
+
Basic usage to log a matrix:
|
|
208
|
+
```python
|
|
209
|
+
await context.viz().matrix("confusion", step=iteration, matrix=[[1, 2], [3, 4]])
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Logging a matrix with extra metadata and custom tags:
|
|
213
|
+
```python
|
|
214
|
+
await context.viz().matrix(
|
|
215
|
+
"heatmap",
|
|
216
|
+
step=42,
|
|
217
|
+
matrix=[[0.1, 0.2], [0.3, 0.4]],
|
|
218
|
+
figure_id="metrics",
|
|
219
|
+
meta={"source": "model"},
|
|
220
|
+
tags=["experiment:baseline"]
|
|
221
|
+
)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
track_id: Unique identifier for the matrix track (e.g., "confusion").
|
|
226
|
+
step: Integer step or iteration number for the data point.
|
|
227
|
+
matrix: Sequence of sequences of float values representing the 2D matrix.
|
|
228
|
+
figure_id: Optional figure grouping for UI display.
|
|
229
|
+
mode: Storage mode, typically "append".
|
|
230
|
+
meta: Optional dictionary of extra metadata.
|
|
231
|
+
tags: Optional list of string labels. The tag "matrix" is automatically appended.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
None. The event is persisted for later visualization.
|
|
235
|
+
"""
|
|
236
|
+
# Convert to plain list[list[float]]
|
|
237
|
+
m = [[float(x) for x in row] for row in matrix]
|
|
238
|
+
evt = VizEvent(
|
|
239
|
+
run_id=self.run_id,
|
|
240
|
+
graph_id=self.graph_id,
|
|
241
|
+
node_id=self.node_id,
|
|
242
|
+
tool_name=self.tool_name,
|
|
243
|
+
tool_version=self.tool_version,
|
|
244
|
+
track_id=track_id,
|
|
245
|
+
figure_id=figure_id,
|
|
246
|
+
viz_kind="matrix",
|
|
247
|
+
step=step,
|
|
248
|
+
mode=mode,
|
|
249
|
+
matrix=m,
|
|
250
|
+
meta=meta,
|
|
251
|
+
tags=(tags or []) + ["matrix"],
|
|
252
|
+
)
|
|
253
|
+
evt = self._apply_scope(evt)
|
|
254
|
+
await self.viz_service.append(evt)
|
|
255
|
+
|
|
256
|
+
async def image_from_artifact(
|
|
257
|
+
self,
|
|
258
|
+
track_id: str,
|
|
259
|
+
*,
|
|
260
|
+
step: int,
|
|
261
|
+
artifact: Artifact,
|
|
262
|
+
figure_id: str | None = None,
|
|
263
|
+
mode: VizMode = "append",
|
|
264
|
+
meta: dict[str, Any] | None = None,
|
|
265
|
+
tags: list[str] | None = None,
|
|
266
|
+
) -> None:
|
|
267
|
+
"""
|
|
268
|
+
Record a reference to an existing image Artifact for visualization in the Aethergraph UI.
|
|
269
|
+
|
|
270
|
+
This method standardizes the event format, auto-fills provenance fields,
|
|
271
|
+
and dispatches the image reference to the configured storage backend.
|
|
272
|
+
|
|
273
|
+
Examples:
|
|
274
|
+
Basic usage to log an image artifact:
|
|
275
|
+
```python
|
|
276
|
+
await context.viz().image_from_artifact(
|
|
277
|
+
"design_shape",
|
|
278
|
+
step=17,
|
|
279
|
+
artifact=artifact,
|
|
280
|
+
figure_id="design"
|
|
281
|
+
)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Logging an image with extra metadata and custom tags:
|
|
285
|
+
```python
|
|
286
|
+
await context.viz().image_from_artifact(
|
|
287
|
+
"output_frame",
|
|
288
|
+
step=42,
|
|
289
|
+
artifact=artifact,
|
|
290
|
+
figure_id="frames",
|
|
291
|
+
meta={"source": "simulation"},
|
|
292
|
+
tags=["experiment:baseline"]
|
|
293
|
+
)
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
track_id: Unique identifier for the image track (e.g., "design_shape").
|
|
298
|
+
step: Integer step or iteration number for the data point.
|
|
299
|
+
artifact: The Artifact object referencing the stored image.
|
|
300
|
+
figure_id: Optional figure grouping for UI display.
|
|
301
|
+
mode: Storage mode, typically "append".
|
|
302
|
+
meta: Optional dictionary of extra metadata.
|
|
303
|
+
tags: Optional list of string labels. The tag "image" is automatically appended.
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
None. The event is persisted for later visualization.
|
|
307
|
+
"""
|
|
308
|
+
evt = VizEvent(
|
|
309
|
+
run_id=self.run_id,
|
|
310
|
+
graph_id=self.graph_id,
|
|
311
|
+
node_id=self.node_id,
|
|
312
|
+
tool_name=self.tool_name,
|
|
313
|
+
tool_version=self.tool_version,
|
|
314
|
+
track_id=track_id,
|
|
315
|
+
figure_id=figure_id,
|
|
316
|
+
viz_kind="image",
|
|
317
|
+
step=step,
|
|
318
|
+
mode=mode,
|
|
319
|
+
artifact_id=artifact.artifact_id,
|
|
320
|
+
meta=meta,
|
|
321
|
+
tags=(tags or []) + ["image"],
|
|
322
|
+
)
|
|
323
|
+
evt = self._apply_scope(evt)
|
|
324
|
+
await self.viz_service.append(evt)
|
|
325
|
+
|
|
326
|
+
async def image_from_bytes(
|
|
327
|
+
self,
|
|
328
|
+
track_id: str,
|
|
329
|
+
*,
|
|
330
|
+
step: int,
|
|
331
|
+
data: bytes,
|
|
332
|
+
mime: str = "image/png",
|
|
333
|
+
kind: str = "image",
|
|
334
|
+
figure_id: str | None = None,
|
|
335
|
+
mode: VizMode = "append",
|
|
336
|
+
labels: dict[str, Any] | None = None,
|
|
337
|
+
meta: dict[str, Any] | None = None,
|
|
338
|
+
tags: list[str] | None = None,
|
|
339
|
+
) -> Artifact:
|
|
340
|
+
"""
|
|
341
|
+
Save image bytes as an Artifact and log a visualization event.
|
|
342
|
+
|
|
343
|
+
This convenience method is accessed via `context.viz().image_from_bytes(...)` and is used by the Aethergraph UI to persist image data to storage. It stores the image as an Artifact using the configured ArtifactFacade, then logs a visualization event referencing the saved artifact.
|
|
344
|
+
|
|
345
|
+
Examples:
|
|
346
|
+
Saving a PNG image to the current visualization track:
|
|
347
|
+
```python
|
|
348
|
+
await context.viz().image_from_bytes(
|
|
349
|
+
track_id="experiment-123",
|
|
350
|
+
step=42,
|
|
351
|
+
data=image_bytes,
|
|
352
|
+
mime="image/png",
|
|
353
|
+
labels={"type": "output", "stage": "inference"},
|
|
354
|
+
tags=["result", "png"]
|
|
355
|
+
)
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Saving an image with custom metadata and figure association:
|
|
359
|
+
```python
|
|
360
|
+
await context.viz().image_from_bytes(
|
|
361
|
+
track_id="demo-track",
|
|
362
|
+
step=7,
|
|
363
|
+
data=img_bytes,
|
|
364
|
+
figure_id="fig-1",
|
|
365
|
+
meta={"caption": "Sample output"},
|
|
366
|
+
mode="replace"
|
|
367
|
+
)
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
track_id: The identifier for the visualization track to associate with the image.
|
|
372
|
+
step: The step or index within the track for this image.
|
|
373
|
+
data: Raw image bytes to be saved.
|
|
374
|
+
mime: The MIME type of the image (default: "image/png").
|
|
375
|
+
kind: The artifact kind (default: "image").
|
|
376
|
+
figure_id: Optional identifier for the figure this image belongs to.
|
|
377
|
+
mode: Visualization mode, e.g., "append" or "replace".
|
|
378
|
+
labels: Optional dictionary of labels to attach to the artifact.
|
|
379
|
+
meta: Optional dictionary of metadata for the visualization event.
|
|
380
|
+
tags: Optional list of string tags for categorization.
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
Artifact: The persisted `Artifact` object representing the saved image.
|
|
384
|
+
|
|
385
|
+
Notes:
|
|
386
|
+
- This method requires that `self.artifacts` is set to an `ArtifactFacade` instance.
|
|
387
|
+
- The saved artifact is automatically linked to the visualization event.
|
|
388
|
+
- To change the name of the saved artifact in UI, use `labels` to set a "filename" label.
|
|
389
|
+
"""
|
|
390
|
+
if not self.artifacts:
|
|
391
|
+
raise RuntimeError("VizFacade.image_from_bytes requires an ArtifactFacade")
|
|
392
|
+
|
|
393
|
+
# Save artifact using writer() so we get proper metering + labels
|
|
394
|
+
|
|
395
|
+
# Use ArtifactFacade.writer to store the image
|
|
396
|
+
async with self.artifacts.writer(kind=kind, planned_ext=".png") as w:
|
|
397
|
+
w.write(data)
|
|
398
|
+
if labels:
|
|
399
|
+
w.add_labels(labels)
|
|
400
|
+
art = self.artifacts.last_artifact
|
|
401
|
+
if not art:
|
|
402
|
+
raise RuntimeError("Artifact writer did not produce an artifact")
|
|
403
|
+
|
|
404
|
+
await self.image_from_artifact(
|
|
405
|
+
track_id=track_id,
|
|
406
|
+
step=step,
|
|
407
|
+
artifact=art,
|
|
408
|
+
figure_id=figure_id,
|
|
409
|
+
mode=mode,
|
|
410
|
+
meta=meta,
|
|
411
|
+
tags=tags,
|
|
412
|
+
)
|
|
413
|
+
return art
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from aethergraph.contracts.services.viz import VizEvent, VizKind
|
|
8
|
+
from aethergraph.contracts.storage.event_log import EventLog
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class VizService:
|
|
12
|
+
"""
|
|
13
|
+
Low-level service to append/query visualization events.
|
|
14
|
+
|
|
15
|
+
- Uses EventLog as the underlying storage.
|
|
16
|
+
- Does NOT know about NodeContext or Scope; that's the Facade's job.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, event_log: EventLog):
|
|
20
|
+
self._log = event_log
|
|
21
|
+
|
|
22
|
+
async def append(self, evt: VizEvent) -> None:
|
|
23
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
24
|
+
if not evt.created_at:
|
|
25
|
+
evt.created_at = now
|
|
26
|
+
|
|
27
|
+
payload = asdict(evt)
|
|
28
|
+
await self._log.append(
|
|
29
|
+
{
|
|
30
|
+
"kind": "viz",
|
|
31
|
+
"scope_id": evt.run_id,
|
|
32
|
+
"ts": evt.created_at,
|
|
33
|
+
"data": payload,
|
|
34
|
+
"tags": (evt.tags or []) + [f"track:{evt.track_id}"],
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
async def query_run(
|
|
39
|
+
self,
|
|
40
|
+
run_id: str,
|
|
41
|
+
*,
|
|
42
|
+
kinds: list[VizKind] | None = None,
|
|
43
|
+
since: datetime | None = None,
|
|
44
|
+
until: datetime | None = None,
|
|
45
|
+
limit: int | None = None,
|
|
46
|
+
offset: int = 0,
|
|
47
|
+
) -> list[dict[str, Any]]:
|
|
48
|
+
"""
|
|
49
|
+
Raw fetch of viz events for a given run.
|
|
50
|
+
Returns raw event dicts as stored in EventLog.
|
|
51
|
+
The API layer can normalize/group them for the frontend.
|
|
52
|
+
"""
|
|
53
|
+
rows = await self._log.query(
|
|
54
|
+
scope_id=run_id,
|
|
55
|
+
since=since,
|
|
56
|
+
until=until,
|
|
57
|
+
kinds=["viz"],
|
|
58
|
+
limit=limit,
|
|
59
|
+
offset=offset,
|
|
60
|
+
)
|
|
61
|
+
# Optionally filter by viz_kind inside data
|
|
62
|
+
if kinds:
|
|
63
|
+
out: list[dict[str, Any]] = []
|
|
64
|
+
for r in rows:
|
|
65
|
+
data = r.get("data") or {}
|
|
66
|
+
if data.get("viz_kind") in kinds:
|
|
67
|
+
out.append(r)
|
|
68
|
+
return out
|
|
69
|
+
return rows
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import threading
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
from aethergraph.contracts.services.artifacts import Artifact
|
|
10
|
+
from aethergraph.contracts.storage.artifact_index import AsyncArtifactIndex
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class JsonlArtifactIndexSync:
|
|
14
|
+
"""
|
|
15
|
+
Simple JSONL-based artifact index for small/medium scale.
|
|
16
|
+
Not suitable for millions of artifacts due to linear scans.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, path: str, occurrences_path: str | None = None):
|
|
20
|
+
self.path = path
|
|
21
|
+
self.occ_path = occurrences_path or (os.path.splitext(path)[0] + "_occurrences.jsonl")
|
|
22
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
23
|
+
|
|
24
|
+
self._by_id: dict[str, dict[str, Any]] = {}
|
|
25
|
+
self._lock = threading.Lock()
|
|
26
|
+
|
|
27
|
+
if os.path.exists(self.path):
|
|
28
|
+
with open(self.path, encoding="utf-8") as f:
|
|
29
|
+
for line in f:
|
|
30
|
+
if not line.strip():
|
|
31
|
+
continue
|
|
32
|
+
rec = json.loads(line)
|
|
33
|
+
self._by_id[rec["artifact_id"]] = rec
|
|
34
|
+
|
|
35
|
+
# -------- core operations --------
|
|
36
|
+
|
|
37
|
+
def upsert(self, a: Artifact) -> None:
|
|
38
|
+
with self._lock:
|
|
39
|
+
rec = a.to_dict()
|
|
40
|
+
self._by_id[a.artifact_id] = rec
|
|
41
|
+
with open(self.path, "a", encoding="utf-8") as f:
|
|
42
|
+
f.write(json.dumps(rec) + "\n")
|
|
43
|
+
|
|
44
|
+
def list_for_run(self, run_id: str) -> list[Artifact]:
|
|
45
|
+
return [Artifact(**r) for r in self._by_id.values() if r.get("run_id") == run_id]
|
|
46
|
+
|
|
47
|
+
def search(
|
|
48
|
+
self,
|
|
49
|
+
*,
|
|
50
|
+
kind: str | None = None,
|
|
51
|
+
labels: dict[str, Any] | None = None,
|
|
52
|
+
metric: str | None = None,
|
|
53
|
+
mode: Literal["max", "min"] | None = None,
|
|
54
|
+
limit: int | None = None,
|
|
55
|
+
offset: int = 0,
|
|
56
|
+
) -> list[Artifact]:
|
|
57
|
+
# NOTE: JSONL index keeps all artifacts in memory (_by_id.values()) and
|
|
58
|
+
# performs filtering / sorting in Python, then applies offset + limit.
|
|
59
|
+
# This is intended for small/medium local installs and tests only.
|
|
60
|
+
rows = list(self._by_id.values())
|
|
61
|
+
|
|
62
|
+
if kind:
|
|
63
|
+
rows = [r for r in rows if r.get("kind") == kind]
|
|
64
|
+
|
|
65
|
+
# Treat tenant keys as top-level fields, not labels
|
|
66
|
+
TENANT_KEYS = {
|
|
67
|
+
"org_id",
|
|
68
|
+
"user_id",
|
|
69
|
+
"client_id",
|
|
70
|
+
"app_id",
|
|
71
|
+
"session_id",
|
|
72
|
+
"run_id",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if labels:
|
|
76
|
+
for k, v in labels.items():
|
|
77
|
+
if k in TENANT_KEYS:
|
|
78
|
+
# Match against top-level JSON fields
|
|
79
|
+
rows = [r for r in rows if r.get(k) == v]
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
# Normal label filters
|
|
83
|
+
if isinstance(v, list):
|
|
84
|
+
rows = [
|
|
85
|
+
r
|
|
86
|
+
for r in rows
|
|
87
|
+
if isinstance(r.get("labels", {}).get(k), list)
|
|
88
|
+
and set(v).issubset(set(r["labels"][k]))
|
|
89
|
+
]
|
|
90
|
+
else:
|
|
91
|
+
rows = [r for r in rows if r.get("labels", {}).get(k) == v]
|
|
92
|
+
|
|
93
|
+
if metric and mode:
|
|
94
|
+
rows = [r for r in rows if metric in r.get("metrics", {})]
|
|
95
|
+
rows.sort(
|
|
96
|
+
key=lambda r: r["metrics"][metric],
|
|
97
|
+
reverse=(mode == "max"),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if offset > 0:
|
|
101
|
+
rows = rows[offset:]
|
|
102
|
+
|
|
103
|
+
if limit is not None:
|
|
104
|
+
rows = rows[:limit]
|
|
105
|
+
|
|
106
|
+
return [Artifact(**r) for r in rows]
|
|
107
|
+
|
|
108
|
+
def best(
|
|
109
|
+
self,
|
|
110
|
+
*,
|
|
111
|
+
kind: str,
|
|
112
|
+
metric: str,
|
|
113
|
+
mode: Literal["max", "min"],
|
|
114
|
+
filters: dict[str, Any] | None = None,
|
|
115
|
+
) -> Artifact | None:
|
|
116
|
+
rows = self.search(
|
|
117
|
+
kind=kind,
|
|
118
|
+
labels=filters,
|
|
119
|
+
metric=metric,
|
|
120
|
+
mode=mode,
|
|
121
|
+
limit=1,
|
|
122
|
+
)
|
|
123
|
+
return rows[0] if rows else None
|
|
124
|
+
|
|
125
|
+
def pin(self, artifact_id: str, pinned: bool = True) -> None:
|
|
126
|
+
with self._lock:
|
|
127
|
+
if artifact_id not in self._by_id:
|
|
128
|
+
return
|
|
129
|
+
rec = self._by_id[artifact_id]
|
|
130
|
+
rec["pinned"] = bool(pinned)
|
|
131
|
+
self._by_id[artifact_id] = rec
|
|
132
|
+
with open(self.path, "a", encoding="utf-8") as f:
|
|
133
|
+
f.write(json.dumps(rec) + "\n")
|
|
134
|
+
|
|
135
|
+
def record_occurrence(self, a: Artifact, extra_labels: dict | None = None) -> None:
|
|
136
|
+
row = {
|
|
137
|
+
"artifact_id": a.artifact_id,
|
|
138
|
+
"run_id": a.run_id,
|
|
139
|
+
"graph_id": a.graph_id,
|
|
140
|
+
"node_id": a.node_id,
|
|
141
|
+
"tool_name": a.tool_name,
|
|
142
|
+
"tool_version": a.tool_version,
|
|
143
|
+
"created_at": a.created_at,
|
|
144
|
+
"labels": {**(a.labels or {}), **(extra_labels or {})},
|
|
145
|
+
}
|
|
146
|
+
with open(self.occ_path, "a", encoding="utf-8") as f:
|
|
147
|
+
f.write(json.dumps(row) + "\n")
|
|
148
|
+
|
|
149
|
+
def get(self, artifact_id: str) -> Artifact | None:
|
|
150
|
+
if artifact_id in self._by_id:
|
|
151
|
+
return Artifact(**self._by_id[artifact_id])
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class JsonlArtifactIndex(AsyncArtifactIndex):
|
|
156
|
+
"""Async wrapper for JsonlArtifactIndexSync using asyncio.to_thread."""
|
|
157
|
+
|
|
158
|
+
def __init__(self, path: str, occurrences_path: str | None = None):
|
|
159
|
+
self._sync = JsonlArtifactIndexSync(path, occurrences_path)
|
|
160
|
+
|
|
161
|
+
async def upsert(self, a: Artifact) -> None:
|
|
162
|
+
await asyncio.to_thread(self._sync.upsert, a)
|
|
163
|
+
|
|
164
|
+
async def list_for_run(self, run_id: str) -> list[Artifact]:
|
|
165
|
+
return await asyncio.to_thread(self._sync.list_for_run, run_id)
|
|
166
|
+
|
|
167
|
+
async def search(self, **kwargs) -> list[Artifact]:
|
|
168
|
+
return await asyncio.to_thread(self._sync.search, **kwargs)
|
|
169
|
+
|
|
170
|
+
async def best(self, **kwargs) -> Artifact | None:
|
|
171
|
+
return await asyncio.to_thread(self._sync.best, **kwargs)
|
|
172
|
+
|
|
173
|
+
async def pin(self, artifact_id: str, pinned: bool = True) -> None:
|
|
174
|
+
await asyncio.to_thread(self._sync.pin, artifact_id, pinned)
|
|
175
|
+
|
|
176
|
+
async def record_occurrence(self, a: Artifact, extra_labels: dict | None = None) -> None:
|
|
177
|
+
await asyncio.to_thread(self._sync.record_occurrence, a, extra_labels)
|
|
178
|
+
|
|
179
|
+
async def get(self, artifact_id: str) -> Artifact | None:
|
|
180
|
+
return await asyncio.to_thread(self._sync.get, artifact_id)
|