aethergraph 0.1.0a1__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 +49 -0
- aethergraph/config/__init__.py +0 -0
- aethergraph/config/config.py +121 -0
- aethergraph/config/context.py +16 -0
- aethergraph/config/llm.py +26 -0
- aethergraph/config/loader.py +60 -0
- aethergraph/config/runtime.py +9 -0
- aethergraph/contracts/errors/errors.py +44 -0
- aethergraph/contracts/services/artifacts.py +142 -0
- aethergraph/contracts/services/channel.py +72 -0
- aethergraph/contracts/services/continuations.py +23 -0
- aethergraph/contracts/services/eventbus.py +12 -0
- aethergraph/contracts/services/kv.py +24 -0
- aethergraph/contracts/services/llm.py +17 -0
- aethergraph/contracts/services/mcp.py +22 -0
- aethergraph/contracts/services/memory.py +108 -0
- aethergraph/contracts/services/resume.py +28 -0
- aethergraph/contracts/services/state_stores.py +33 -0
- aethergraph/contracts/services/wakeup.py +28 -0
- aethergraph/core/execution/base_scheduler.py +77 -0
- aethergraph/core/execution/forward_scheduler.py +777 -0
- aethergraph/core/execution/global_scheduler.py +634 -0
- aethergraph/core/execution/retry_policy.py +22 -0
- aethergraph/core/execution/step_forward.py +411 -0
- aethergraph/core/execution/step_result.py +18 -0
- aethergraph/core/execution/wait_types.py +72 -0
- aethergraph/core/graph/graph_builder.py +192 -0
- aethergraph/core/graph/graph_fn.py +219 -0
- aethergraph/core/graph/graph_io.py +67 -0
- aethergraph/core/graph/graph_refs.py +154 -0
- aethergraph/core/graph/graph_spec.py +115 -0
- aethergraph/core/graph/graph_state.py +59 -0
- aethergraph/core/graph/graphify.py +128 -0
- aethergraph/core/graph/interpreter.py +145 -0
- aethergraph/core/graph/node_handle.py +33 -0
- aethergraph/core/graph/node_spec.py +46 -0
- aethergraph/core/graph/node_state.py +63 -0
- aethergraph/core/graph/task_graph.py +747 -0
- aethergraph/core/graph/task_node.py +82 -0
- aethergraph/core/graph/utils.py +37 -0
- aethergraph/core/graph/visualize.py +239 -0
- aethergraph/core/runtime/ad_hoc_context.py +61 -0
- aethergraph/core/runtime/base_service.py +153 -0
- aethergraph/core/runtime/bind_adapter.py +42 -0
- aethergraph/core/runtime/bound_memory.py +69 -0
- aethergraph/core/runtime/execution_context.py +220 -0
- aethergraph/core/runtime/graph_runner.py +349 -0
- aethergraph/core/runtime/lifecycle.py +26 -0
- aethergraph/core/runtime/node_context.py +203 -0
- aethergraph/core/runtime/node_services.py +30 -0
- aethergraph/core/runtime/recovery.py +159 -0
- aethergraph/core/runtime/run_registration.py +33 -0
- aethergraph/core/runtime/runtime_env.py +157 -0
- aethergraph/core/runtime/runtime_registry.py +32 -0
- aethergraph/core/runtime/runtime_services.py +224 -0
- aethergraph/core/runtime/wakeup_watcher.py +40 -0
- aethergraph/core/tools/__init__.py +10 -0
- aethergraph/core/tools/builtins/channel_tools.py +194 -0
- aethergraph/core/tools/builtins/toolset.py +134 -0
- aethergraph/core/tools/toolkit.py +510 -0
- aethergraph/core/tools/waitable.py +109 -0
- aethergraph/plugins/channel/__init__.py +0 -0
- aethergraph/plugins/channel/adapters/__init__.py +0 -0
- aethergraph/plugins/channel/adapters/console.py +106 -0
- aethergraph/plugins/channel/adapters/file.py +102 -0
- aethergraph/plugins/channel/adapters/slack.py +285 -0
- aethergraph/plugins/channel/adapters/telegram.py +302 -0
- aethergraph/plugins/channel/adapters/webhook.py +104 -0
- aethergraph/plugins/channel/adapters/webui.py +134 -0
- aethergraph/plugins/channel/routes/__init__.py +0 -0
- aethergraph/plugins/channel/routes/console_routes.py +86 -0
- aethergraph/plugins/channel/routes/slack_routes.py +49 -0
- aethergraph/plugins/channel/routes/telegram_routes.py +26 -0
- aethergraph/plugins/channel/routes/webui_routes.py +136 -0
- aethergraph/plugins/channel/utils/__init__.py +0 -0
- aethergraph/plugins/channel/utils/slack_utils.py +278 -0
- aethergraph/plugins/channel/utils/telegram_utils.py +324 -0
- aethergraph/plugins/channel/websockets/slack_ws.py +68 -0
- aethergraph/plugins/channel/websockets/telegram_polling.py +151 -0
- aethergraph/plugins/mcp/fs_server.py +128 -0
- aethergraph/plugins/mcp/http_server.py +101 -0
- aethergraph/plugins/mcp/ws_server.py +180 -0
- aethergraph/plugins/net/http.py +10 -0
- aethergraph/plugins/utils/data_io.py +359 -0
- aethergraph/runner/__init__.py +5 -0
- aethergraph/runtime/__init__.py +62 -0
- aethergraph/server/__init__.py +3 -0
- aethergraph/server/app_factory.py +84 -0
- aethergraph/server/start.py +122 -0
- aethergraph/services/__init__.py +10 -0
- aethergraph/services/artifacts/facade.py +284 -0
- aethergraph/services/artifacts/factory.py +35 -0
- aethergraph/services/artifacts/fs_store.py +656 -0
- aethergraph/services/artifacts/jsonl_index.py +123 -0
- aethergraph/services/artifacts/paths.py +23 -0
- aethergraph/services/artifacts/sqlite_index.py +209 -0
- aethergraph/services/artifacts/utils.py +124 -0
- aethergraph/services/auth/dev.py +16 -0
- aethergraph/services/channel/channel_bus.py +293 -0
- aethergraph/services/channel/factory.py +44 -0
- aethergraph/services/channel/session.py +511 -0
- aethergraph/services/channel/wait_helpers.py +57 -0
- aethergraph/services/clock/clock.py +9 -0
- aethergraph/services/container/default_container.py +320 -0
- aethergraph/services/continuations/continuation.py +56 -0
- aethergraph/services/continuations/factory.py +34 -0
- aethergraph/services/continuations/stores/fs_store.py +264 -0
- aethergraph/services/continuations/stores/inmem_store.py +95 -0
- aethergraph/services/eventbus/inmem.py +21 -0
- aethergraph/services/features/static.py +10 -0
- aethergraph/services/kv/ephemeral.py +90 -0
- aethergraph/services/kv/factory.py +27 -0
- aethergraph/services/kv/layered.py +41 -0
- aethergraph/services/kv/sqlite_kv.py +128 -0
- aethergraph/services/llm/factory.py +157 -0
- aethergraph/services/llm/generic_client.py +542 -0
- aethergraph/services/llm/providers.py +3 -0
- aethergraph/services/llm/service.py +105 -0
- aethergraph/services/logger/base.py +36 -0
- aethergraph/services/logger/compat.py +50 -0
- aethergraph/services/logger/formatters.py +106 -0
- aethergraph/services/logger/std.py +203 -0
- aethergraph/services/mcp/helpers.py +23 -0
- aethergraph/services/mcp/http_client.py +70 -0
- aethergraph/services/mcp/mcp_tools.py +21 -0
- aethergraph/services/mcp/registry.py +14 -0
- aethergraph/services/mcp/service.py +100 -0
- aethergraph/services/mcp/stdio_client.py +70 -0
- aethergraph/services/mcp/ws_client.py +115 -0
- aethergraph/services/memory/bound.py +106 -0
- aethergraph/services/memory/distillers/episode.py +116 -0
- aethergraph/services/memory/distillers/rolling.py +74 -0
- aethergraph/services/memory/facade.py +633 -0
- aethergraph/services/memory/factory.py +78 -0
- aethergraph/services/memory/hotlog_kv.py +27 -0
- aethergraph/services/memory/indices.py +74 -0
- aethergraph/services/memory/io_helpers.py +72 -0
- aethergraph/services/memory/persist_fs.py +40 -0
- aethergraph/services/memory/resolver.py +152 -0
- aethergraph/services/metering/noop.py +4 -0
- aethergraph/services/prompts/file_store.py +41 -0
- aethergraph/services/rag/chunker.py +29 -0
- aethergraph/services/rag/facade.py +593 -0
- aethergraph/services/rag/index/base.py +27 -0
- aethergraph/services/rag/index/faiss_index.py +121 -0
- aethergraph/services/rag/index/sqlite_index.py +134 -0
- aethergraph/services/rag/index_factory.py +52 -0
- aethergraph/services/rag/parsers/md.py +7 -0
- aethergraph/services/rag/parsers/pdf.py +14 -0
- aethergraph/services/rag/parsers/txt.py +7 -0
- aethergraph/services/rag/utils/hybrid.py +39 -0
- aethergraph/services/rag/utils/make_fs_key.py +62 -0
- aethergraph/services/redactor/simple.py +16 -0
- aethergraph/services/registry/key_parsing.py +44 -0
- aethergraph/services/registry/registry_key.py +19 -0
- aethergraph/services/registry/unified_registry.py +185 -0
- aethergraph/services/resume/multi_scheduler_resume_bus.py +65 -0
- aethergraph/services/resume/router.py +73 -0
- aethergraph/services/schedulers/registry.py +41 -0
- aethergraph/services/secrets/base.py +7 -0
- aethergraph/services/secrets/env.py +8 -0
- aethergraph/services/state_stores/externalize.py +135 -0
- aethergraph/services/state_stores/graph_observer.py +131 -0
- aethergraph/services/state_stores/json_store.py +67 -0
- aethergraph/services/state_stores/resume_policy.py +119 -0
- aethergraph/services/state_stores/serialize.py +249 -0
- aethergraph/services/state_stores/utils.py +91 -0
- aethergraph/services/state_stores/validate.py +78 -0
- aethergraph/services/tracing/noop.py +18 -0
- aethergraph/services/waits/wait_registry.py +91 -0
- aethergraph/services/wakeup/memory_queue.py +57 -0
- aethergraph/services/wakeup/scanner_producer.py +56 -0
- aethergraph/services/wakeup/worker.py +31 -0
- aethergraph/tools/__init__.py +25 -0
- aethergraph/utils/optdeps.py +8 -0
- aethergraph-0.1.0a1.dist-info/METADATA +410 -0
- aethergraph-0.1.0a1.dist-info/RECORD +182 -0
- aethergraph-0.1.0a1.dist-info/WHEEL +5 -0
- aethergraph-0.1.0a1.dist-info/entry_points.txt +2 -0
- aethergraph-0.1.0a1.dist-info/licenses/LICENSE +176 -0
- aethergraph-0.1.0a1.dist-info/licenses/NOTICE +31 -0
- aethergraph-0.1.0a1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import websockets
|
|
9
|
+
from websockets.client import WebSocketClientProtocol
|
|
10
|
+
|
|
11
|
+
from aethergraph.contracts.services.mcp import MCPClientProtocol, MCPResource, MCPTool
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class WsMCPClient(MCPClientProtocol):
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
url: str,
|
|
18
|
+
*,
|
|
19
|
+
headers: dict[str, str] | None = None,
|
|
20
|
+
timeout: float = 60.0,
|
|
21
|
+
ping_interval: float = 20.0,
|
|
22
|
+
ping_timeout: float = 10.0,
|
|
23
|
+
):
|
|
24
|
+
self.url = url
|
|
25
|
+
self.headers = headers or {}
|
|
26
|
+
self.timeout = timeout
|
|
27
|
+
self.ping_interval = ping_interval
|
|
28
|
+
self.ping_timeout = ping_timeout
|
|
29
|
+
|
|
30
|
+
self._ws: WebSocketClientProtocol | None = None
|
|
31
|
+
self._id = 0
|
|
32
|
+
self._lock = asyncio.Lock()
|
|
33
|
+
self._ping_task: asyncio.Task | None = None
|
|
34
|
+
|
|
35
|
+
async def open(self):
|
|
36
|
+
if self._ws and not self._ws.closed:
|
|
37
|
+
return
|
|
38
|
+
try:
|
|
39
|
+
# websockets >=14
|
|
40
|
+
self._ws = await websockets.connect(
|
|
41
|
+
self.url, additional_headers=self.headers, open_timeout=self.timeout
|
|
42
|
+
)
|
|
43
|
+
except TypeError:
|
|
44
|
+
# likely on websockets <=13
|
|
45
|
+
self._ws = await websockets.connect(
|
|
46
|
+
self.url, extra_headers=self.headers, open_timeout=self.timeout
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
self._start_ping()
|
|
50
|
+
|
|
51
|
+
async def close(self):
|
|
52
|
+
if self._ping_task:
|
|
53
|
+
self._ping_task.cancel()
|
|
54
|
+
with contextlib.suppress(Exception):
|
|
55
|
+
await self._ping_task
|
|
56
|
+
self._ping_task = None
|
|
57
|
+
if self._ws and not self._ws.closed:
|
|
58
|
+
await self._ws.close()
|
|
59
|
+
self._ws = None
|
|
60
|
+
|
|
61
|
+
def _start_ping(self):
|
|
62
|
+
if self.ping_interval <= 0:
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
async def _pinger():
|
|
66
|
+
try:
|
|
67
|
+
while self._ws and not self._ws.closed:
|
|
68
|
+
await asyncio.sleep(self.ping_interval)
|
|
69
|
+
if not self._ws or self._ws.closed:
|
|
70
|
+
break
|
|
71
|
+
try:
|
|
72
|
+
await asyncio.wait_for(self._ws.ping(), timeout=self.ping_timeout) # type: ignore
|
|
73
|
+
except Exception:
|
|
74
|
+
break
|
|
75
|
+
except asyncio.CancelledError:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
self._ping_task = asyncio.create_task(_pinger())
|
|
79
|
+
|
|
80
|
+
async def _ensure(self):
|
|
81
|
+
if self._ws is None or self._ws.closed:
|
|
82
|
+
await self.open()
|
|
83
|
+
|
|
84
|
+
async def _rpc(self, method: str, params: dict[str, Any] | None = None) -> Any:
|
|
85
|
+
await self._ensure()
|
|
86
|
+
async with self._lock:
|
|
87
|
+
self._id += 1
|
|
88
|
+
req = {"jsonrpc": "2.0", "id": self._id, "method": method, "params": params or {}}
|
|
89
|
+
data = json.dumps(req)
|
|
90
|
+
try:
|
|
91
|
+
assert self._ws is not None
|
|
92
|
+
await asyncio.wait_for(self._ws.send(data), timeout=self.timeout)
|
|
93
|
+
raw = await asyncio.wait_for(self._ws.recv(), timeout=self.timeout)
|
|
94
|
+
except Exception:
|
|
95
|
+
await self.close()
|
|
96
|
+
await self.open()
|
|
97
|
+
assert self._ws is not None
|
|
98
|
+
await asyncio.wait_for(self._ws.send(data), timeout=self.timeout)
|
|
99
|
+
raw = await asyncio.wait_for(self._ws.recv(), timeout=self.timeout)
|
|
100
|
+
resp = json.loads(raw)
|
|
101
|
+
if "error" in resp:
|
|
102
|
+
raise RuntimeError(str(resp["error"]))
|
|
103
|
+
return resp.get("result")
|
|
104
|
+
|
|
105
|
+
async def list_tools(self) -> list[MCPTool]:
|
|
106
|
+
return await self._rpc("tools/list")
|
|
107
|
+
|
|
108
|
+
async def call(self, tool: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
109
|
+
return await self._rpc("tools/call", {"name": tool, "arguments": params or {}})
|
|
110
|
+
|
|
111
|
+
async def list_resources(self) -> list[MCPResource]:
|
|
112
|
+
return await self._rpc("resources/list")
|
|
113
|
+
|
|
114
|
+
async def read_resource(self, uri: str) -> dict[str, Any]:
|
|
115
|
+
return await self._rpc("resources/read", {"uri": uri})
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from aethergraph.contracts.services.memory import Event
|
|
7
|
+
from aethergraph.services.memory.facade import MemoryFacade
|
|
8
|
+
from aethergraph.services.memory.resolver import ResolverContext, resolve_params
|
|
9
|
+
|
|
10
|
+
Value = dict[str, Any]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class BoundMemory:
|
|
15
|
+
mem: MemoryFacade
|
|
16
|
+
defaults: dict[str, Any] = (
|
|
17
|
+
None # run_id, graph_id, node_id, agent_id (usually injected by NodeContext)
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
async def record(
|
|
21
|
+
self,
|
|
22
|
+
*,
|
|
23
|
+
kind: str,
|
|
24
|
+
text: str | None = None,
|
|
25
|
+
severity: int = 2,
|
|
26
|
+
stage: str | None = None,
|
|
27
|
+
tags: list[str] | None = None,
|
|
28
|
+
entities: list[str] | None = None,
|
|
29
|
+
metrics: dict[str, Any] | None = None,
|
|
30
|
+
inputs_ref: dict[str, Any] | None = None,
|
|
31
|
+
outputs_ref: dict[str, Any] | None = None,
|
|
32
|
+
sources: list[str] | None = None,
|
|
33
|
+
signal: float | None = None,
|
|
34
|
+
) -> Event:
|
|
35
|
+
base = {
|
|
36
|
+
**(self.defaults or {}),
|
|
37
|
+
"kind": kind,
|
|
38
|
+
"stage": stage,
|
|
39
|
+
"severity": severity,
|
|
40
|
+
"tags": tags or [],
|
|
41
|
+
"entities": entities or [],
|
|
42
|
+
"inputs_ref": inputs_ref,
|
|
43
|
+
"outputs_ref": outputs_ref,
|
|
44
|
+
"sources": sources,
|
|
45
|
+
"signal": float(
|
|
46
|
+
signal
|
|
47
|
+
if signal is not None
|
|
48
|
+
else self._estimate_signal(text=text, metrics=metrics, severity=severity)
|
|
49
|
+
),
|
|
50
|
+
}
|
|
51
|
+
return await self.mem.record_raw(base=base, text=text, metrics=metrics)
|
|
52
|
+
|
|
53
|
+
async def user(self, text: str):
|
|
54
|
+
return await self.record(kind="user_msg", text=text, stage="observe")
|
|
55
|
+
|
|
56
|
+
async def assistant(self, text: str):
|
|
57
|
+
return await self.record(kind="assistant_msg", text=text, stage="act")
|
|
58
|
+
|
|
59
|
+
async def tool_start(self, note=None):
|
|
60
|
+
return await self.record(kind="tool_start", text=note, stage="act")
|
|
61
|
+
|
|
62
|
+
async def tool_ok(self, note=None, metrics=None):
|
|
63
|
+
return await self.record(
|
|
64
|
+
kind="tool_result", text=note, metrics=metrics, stage="observe", severity=3
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
async def tool_error(self, err: Exception):
|
|
68
|
+
return await self.record(
|
|
69
|
+
kind="error", text=f"{type(err).__name__}: {err}", severity=5, stage="observe"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
async def write_result(
|
|
73
|
+
self,
|
|
74
|
+
*,
|
|
75
|
+
topic: str,
|
|
76
|
+
inputs: list[Value] | None = None,
|
|
77
|
+
outputs: list[Value] | None = None,
|
|
78
|
+
tags: list[str] | None = None,
|
|
79
|
+
metrics: dict[str, float] | None = None,
|
|
80
|
+
message: str | None = None,
|
|
81
|
+
severity: int = 3,
|
|
82
|
+
) -> Event:
|
|
83
|
+
return await self.mem.write_result(
|
|
84
|
+
topic=topic,
|
|
85
|
+
inputs=inputs or [],
|
|
86
|
+
outputs=outputs or [],
|
|
87
|
+
tags=tags,
|
|
88
|
+
metrics=metrics,
|
|
89
|
+
message=message,
|
|
90
|
+
severity=severity,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
async def resolve(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
94
|
+
rctx = ResolverContext(mem=self.mem)
|
|
95
|
+
return await resolve_params(params, rctx)
|
|
96
|
+
|
|
97
|
+
# ---- helper ----
|
|
98
|
+
def _estimate_signal(
|
|
99
|
+
self, *, text: str | None, metrics: dict[str, Any] | None, severity: int
|
|
100
|
+
) -> float:
|
|
101
|
+
score = 0.15 + 0.1 * severity
|
|
102
|
+
if text:
|
|
103
|
+
score += min(len(text) / 400.0, 0.4)
|
|
104
|
+
if metrics:
|
|
105
|
+
score += 0.2
|
|
106
|
+
return max(0.0, min(1.0, score))
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from aethergraph.contracts.services.memory import Distiller, Event, HotLog, Indices, Persistence
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _now_iso() -> str:
|
|
12
|
+
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _stable_event_id(parts: dict[str, Any]) -> str:
|
|
16
|
+
blob = json.dumps(parts, sort_keys=True, ensure_ascii=False).encode("utf-8")
|
|
17
|
+
return hashlib.sha256(blob).hexdigest()[:24]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _episode_uri(sessiorun_idn_id: str, tool: str, run_id: str) -> str:
|
|
21
|
+
safe = tool.replace("/", "_")
|
|
22
|
+
return f"file://mem/{run_id}/episodes/{safe}/{run_id}.json"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class EpisodeSummarizer(Distiller):
|
|
26
|
+
"""
|
|
27
|
+
Aggregate all events for (tool, run_id) into a compact episode summary:
|
|
28
|
+
- sources: event_ids
|
|
29
|
+
- merged metrics (last-write-wins)
|
|
30
|
+
- notes: last N textual notes
|
|
31
|
+
Writes a JSON summary artifact and emits a lightweight run_summary event.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self, *, include_metrics: bool = True, note_limit: int = 8, note_chars: int = 2000
|
|
36
|
+
):
|
|
37
|
+
self.include_metrics = include_metrics
|
|
38
|
+
self.note_limit = note_limit
|
|
39
|
+
self.note_chars = note_chars
|
|
40
|
+
|
|
41
|
+
async def distill(
|
|
42
|
+
self,
|
|
43
|
+
run_id: str,
|
|
44
|
+
*,
|
|
45
|
+
hotlog: HotLog,
|
|
46
|
+
persistence: Persistence,
|
|
47
|
+
indices: Indices,
|
|
48
|
+
tool: str,
|
|
49
|
+
**kw,
|
|
50
|
+
) -> dict[str, Any]:
|
|
51
|
+
# Pull a reasonable window from hot memory; filter in-process.
|
|
52
|
+
# (If needed later, add a Persistence scan by day.)
|
|
53
|
+
events = await hotlog.recent(
|
|
54
|
+
run_id, kinds=["tool_start", "tool_result", "error", "run_summary"], limit=400
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
eps = [e for e in events if e.run_id == run_id and (e.tool or "") == tool]
|
|
58
|
+
if not eps:
|
|
59
|
+
return {}
|
|
60
|
+
|
|
61
|
+
srcs: list[str] = []
|
|
62
|
+
notes: list[str] = []
|
|
63
|
+
metrics: dict[str, float] = {}
|
|
64
|
+
|
|
65
|
+
for e in eps:
|
|
66
|
+
if e.event_id:
|
|
67
|
+
srcs.append(e.event_id)
|
|
68
|
+
if self.include_metrics and e.metrics:
|
|
69
|
+
metrics.update(e.metrics) # simple merge; last-write-wins
|
|
70
|
+
if e.text:
|
|
71
|
+
notes.append(e.text)
|
|
72
|
+
|
|
73
|
+
ts = _now_iso()
|
|
74
|
+
summary = {
|
|
75
|
+
"kind": "episode_summary",
|
|
76
|
+
"run_id": run_id,
|
|
77
|
+
"tool": tool,
|
|
78
|
+
"ts": ts,
|
|
79
|
+
"sources": srcs,
|
|
80
|
+
"metrics": metrics,
|
|
81
|
+
"notes": notes[-self.note_limit :],
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
uri = _episode_uri(run_id, tool, run_id)
|
|
85
|
+
await persistence.save_json(uri, summary)
|
|
86
|
+
|
|
87
|
+
# Emit a compact run_summary event
|
|
88
|
+
compact_text = "\n".join(summary["notes"][-self.note_limit :])[: self.note_chars]
|
|
89
|
+
evt_base = {
|
|
90
|
+
"run_id": run_id,
|
|
91
|
+
"tool": tool,
|
|
92
|
+
"kind": "run_summary",
|
|
93
|
+
"severity": 1,
|
|
94
|
+
"tags": ["summary", "episode"],
|
|
95
|
+
}
|
|
96
|
+
eid = _stable_event_id(
|
|
97
|
+
{
|
|
98
|
+
"ts": ts,
|
|
99
|
+
"run_id": run_id,
|
|
100
|
+
"tool": tool,
|
|
101
|
+
"kind": "run_summary",
|
|
102
|
+
"text": compact_text[:200],
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
evt = Event(
|
|
106
|
+
event_id=eid,
|
|
107
|
+
ts=ts,
|
|
108
|
+
text=compact_text,
|
|
109
|
+
metrics={"notes": len(notes)},
|
|
110
|
+
signal=0.5,
|
|
111
|
+
**evt_base,
|
|
112
|
+
)
|
|
113
|
+
await hotlog.append(run_id, evt, ttl_s=7 * 24 * 3600, limit=1000)
|
|
114
|
+
await persistence.append_event(run_id, evt)
|
|
115
|
+
|
|
116
|
+
return {"uri": uri, "sources": srcs, "metrics": metrics}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from aethergraph.contracts.services.memory import Distiller, Event, HotLog, Indices, Persistence
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _now_iso():
|
|
8
|
+
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def ar_summary_uri(run_id: str, tag: str, ts: str) -> str:
|
|
12
|
+
# Save summaries under the same base "mem/<run_id>/..." tree as append_event,
|
|
13
|
+
# but using a file:// URI so FSPersistence can handle it.
|
|
14
|
+
safe_ts = ts.replace(":", "-")
|
|
15
|
+
return f"file://mem/{run_id}/summaries/{tag}/{safe_ts}.json"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RollingSummarizer(Distiller):
|
|
19
|
+
def __init__(
|
|
20
|
+
self, *, max_turns: int = 20, min_signal: float = 0.25, turn_kinds: list[str] | None = None
|
|
21
|
+
):
|
|
22
|
+
self.max_turns = max_turns
|
|
23
|
+
self.min_signal = min_signal
|
|
24
|
+
self.turn_kinds = turn_kinds or ["user_msg", "assistant_msg"]
|
|
25
|
+
|
|
26
|
+
async def distill(
|
|
27
|
+
self, run_id: str, *, hotlog: HotLog, persistence: Persistence, indices: Indices, **kw
|
|
28
|
+
) -> dict[str, Any]:
|
|
29
|
+
turns = await hotlog.recent(run_id, kinds=self.turn_kinds, limit=self.max_turns * 2)
|
|
30
|
+
kept = [t for t in turns if (t.signal or 0.0) >= self.min_signal]
|
|
31
|
+
if not kept:
|
|
32
|
+
return {}
|
|
33
|
+
|
|
34
|
+
lines = []
|
|
35
|
+
srcs: list[str] = []
|
|
36
|
+
for t in kept[-self.max_turns :]:
|
|
37
|
+
role = "User" if t.kind == "user_msg" else "Assistant"
|
|
38
|
+
if t.text:
|
|
39
|
+
lines.append(f"{role}: {t.text}")
|
|
40
|
+
srcs.append(t.event_id)
|
|
41
|
+
digest_text = "\n".join(lines)
|
|
42
|
+
ts = _now_iso()
|
|
43
|
+
summary = {
|
|
44
|
+
"kind": "rolling_summary",
|
|
45
|
+
"run_id": run_id,
|
|
46
|
+
"ts": ts,
|
|
47
|
+
"sources": srcs,
|
|
48
|
+
"text": digest_text,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
uri = ar_summary_uri(run_id, "rolling", ts)
|
|
52
|
+
|
|
53
|
+
await persistence.save_json(uri=uri, obj=summary)
|
|
54
|
+
|
|
55
|
+
evt = Event(
|
|
56
|
+
event_id="",
|
|
57
|
+
ts=ts,
|
|
58
|
+
run_id=run_id,
|
|
59
|
+
kind="rolling_summary",
|
|
60
|
+
severity=1,
|
|
61
|
+
signal=0.5,
|
|
62
|
+
text=digest_text,
|
|
63
|
+
metrics={"num_turns": len(kept)},
|
|
64
|
+
tags=["summary"],
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
from aethergraph.services.memory.facade import stable_event_id
|
|
68
|
+
|
|
69
|
+
evt.event_id = stable_event_id(
|
|
70
|
+
{"ts": ts, "run_id": run_id, "kind": "rolling_summary", "text": digest_text[:200]}
|
|
71
|
+
)
|
|
72
|
+
await hotlog.append(run_id, evt, ttl_s=7 * 24 * 3600, limit=1000)
|
|
73
|
+
await persistence.append_event(run_id, evt)
|
|
74
|
+
return {"uri": uri, "sources": srcs}
|