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,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from aethergraph.contracts.services.artifacts import AsyncArtifactStore # generic protocol
|
|
7
|
+
from aethergraph.contracts.services.memory import HotLog, Indices, Persistence
|
|
8
|
+
from aethergraph.services.memory.facade import MemoryFacade
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
# --- Artifacts (async FS store)
|
|
12
|
+
artifacts = FSArtifactStore(artifacts_dir)
|
|
13
|
+
|
|
14
|
+
# --- KV for hotlog/indices (choose EphemeralKV or SQLiteKV)
|
|
15
|
+
kv = SQLiteKV(f"{artifacts_dir}/kv.sqlite") if durable else EphemeralKV()
|
|
16
|
+
|
|
17
|
+
# --- HotLog + Indices
|
|
18
|
+
hotlog = KVHotLog(kv, default_ttl_s=7*24*3600, default_limit=1000)
|
|
19
|
+
indices = KVIndices(kv, ttl_s=7*24*3600)
|
|
20
|
+
|
|
21
|
+
# --- Persistence (JSONL under artifacts_dir/mem/<session>/events/...)
|
|
22
|
+
persistence = FSPersistence(base_dir=artifacts_dir)
|
|
23
|
+
|
|
24
|
+
# --- Factory
|
|
25
|
+
factory = MemoryFactory(
|
|
26
|
+
hotlog=hotlog,
|
|
27
|
+
persistence=persistence,
|
|
28
|
+
indices=indices,
|
|
29
|
+
artifacts=artifacts,
|
|
30
|
+
hot_limit=1000,
|
|
31
|
+
hot_ttl_s=7*24*3600,
|
|
32
|
+
default_signal_threshold=0.25,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# --- Global session handle (optional convenience)
|
|
36
|
+
global_mem = factory.for_session("global", run_id="global")
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class MemoryFactory:
|
|
42
|
+
"""Factory for creating MemoryFacade instances with shared components."""
|
|
43
|
+
|
|
44
|
+
hotlog: HotLog
|
|
45
|
+
persistence: Persistence
|
|
46
|
+
indices: Indices # key-value backed indices for fast lookups, not artifact storage index
|
|
47
|
+
artifacts: AsyncArtifactStore
|
|
48
|
+
hot_limit: int = 1000
|
|
49
|
+
hot_ttl_s: int = 7 * 24 * 3600
|
|
50
|
+
default_signal_threshold: float = 0.25
|
|
51
|
+
logger: Any | None = None
|
|
52
|
+
llm_service: Any | None = None # LLMService
|
|
53
|
+
rag_facade: Any | None = None # RAGFacade
|
|
54
|
+
|
|
55
|
+
def for_session(
|
|
56
|
+
self,
|
|
57
|
+
run_id: str,
|
|
58
|
+
*,
|
|
59
|
+
graph_id: str | None = None,
|
|
60
|
+
node_id: str | None = None,
|
|
61
|
+
agent_id: str | None = None,
|
|
62
|
+
) -> MemoryFacade:
|
|
63
|
+
return MemoryFacade(
|
|
64
|
+
run_id=run_id,
|
|
65
|
+
graph_id=graph_id,
|
|
66
|
+
node_id=node_id,
|
|
67
|
+
agent_id=agent_id,
|
|
68
|
+
hotlog=self.hotlog,
|
|
69
|
+
persistence=self.persistence,
|
|
70
|
+
indices=self.indices,
|
|
71
|
+
artifact_store=self.artifacts,
|
|
72
|
+
hot_limit=self.hot_limit,
|
|
73
|
+
hot_ttl_s=self.hot_ttl_s,
|
|
74
|
+
default_signal_threshold=self.default_signal_threshold,
|
|
75
|
+
logger=self.logger,
|
|
76
|
+
rag=self.rag_facade,
|
|
77
|
+
llm=self.llm_service,
|
|
78
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from aethergraph.contracts.services.kv import AsyncKV
|
|
2
|
+
from aethergraph.contracts.services.memory import Event, HotLog
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def kv_hot_key(run_id: str) -> str:
|
|
6
|
+
return f"mem:{run_id}:hot"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class KVHotLog(HotLog):
|
|
10
|
+
def __init__(self, kv: AsyncKV):
|
|
11
|
+
self.kv = kv
|
|
12
|
+
|
|
13
|
+
async def append(self, run_id: str, evt: Event, *, ttl_s: int, limit: int) -> None:
|
|
14
|
+
key = kv_hot_key(run_id)
|
|
15
|
+
buf = list((await self.kv.get(key, default=[])) or [])
|
|
16
|
+
buf.append(evt.__dict__) # store as dict for JSON serializability
|
|
17
|
+
if len(buf) > limit:
|
|
18
|
+
buf = buf[-limit:]
|
|
19
|
+
await self.kv.set(key, buf, ttl_s=ttl_s)
|
|
20
|
+
|
|
21
|
+
async def recent(
|
|
22
|
+
self, run_id: str, *, kinds: list[str] | None = None, limit: int = 50
|
|
23
|
+
) -> list[Event]:
|
|
24
|
+
buf = (await self.kv.get(kv_hot_key(run_id), default=[])) or []
|
|
25
|
+
if kinds:
|
|
26
|
+
buf = [e for e in buf if e.get("kind") in kinds]
|
|
27
|
+
return [Event(**e) for e in buf[-limit:]]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from aethergraph.contracts.services.kv import AsyncKV
|
|
4
|
+
from aethergraph.contracts.services.memory import Event, Indices
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def idx_by_ref_kind(run_id: str) -> str:
|
|
8
|
+
return f"mem:{run_id}:idx2:ref_kind"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def idx_by_name(run_id: str) -> str:
|
|
12
|
+
return f"mem:{run_id}:idx2:name"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def idx_by_topic(run_id: str) -> str:
|
|
16
|
+
return f"mem:{run_id}:idx2:topic"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class KVIndices(Indices):
|
|
20
|
+
def __init__(self, kv: AsyncKV, hot_ttl_s: int):
|
|
21
|
+
self.kv = kv
|
|
22
|
+
self.ttl = hot_ttl_s
|
|
23
|
+
|
|
24
|
+
async def update(self, run_id: str, evt: Event) -> None:
|
|
25
|
+
ts, eid, tool = evt.ts, evt.event_id, (evt.tool or "")
|
|
26
|
+
outs = evt.outputs or []
|
|
27
|
+
|
|
28
|
+
by_kind = (await self.kv.get(idx_by_ref_kind(run_id), {})) or {}
|
|
29
|
+
by_name = (await self.kv.get(idx_by_name(run_id), {})) or {}
|
|
30
|
+
by_topic = (await self.kv.get(idx_by_topic(run_id), {})) or {}
|
|
31
|
+
|
|
32
|
+
for v in outs:
|
|
33
|
+
nm = v.get("name")
|
|
34
|
+
if not nm:
|
|
35
|
+
continue
|
|
36
|
+
by_name[nm] = {
|
|
37
|
+
"ts": ts,
|
|
38
|
+
"event_id": eid,
|
|
39
|
+
"vtype": v.get("vtype"),
|
|
40
|
+
"value": v.get("value"),
|
|
41
|
+
}
|
|
42
|
+
if v.get("vtype") == "ref" and isinstance(v.get("value"), dict):
|
|
43
|
+
kind = v["value"].get("kind")
|
|
44
|
+
uri = v["value"].get("uri")
|
|
45
|
+
if kind and uri:
|
|
46
|
+
lst = by_kind.setdefault(kind, [])
|
|
47
|
+
lst.append({"ts": ts, "event_id": eid, "name": nm, "uri": uri, "topic": tool})
|
|
48
|
+
if len(lst) > 200:
|
|
49
|
+
del lst[:-200]
|
|
50
|
+
|
|
51
|
+
if tool:
|
|
52
|
+
last = by_topic.get(tool, {}) or {}
|
|
53
|
+
last["ts"] = ts
|
|
54
|
+
last["event_id"] = eid
|
|
55
|
+
last["last_outputs"] = {v["name"]: v.get("value") for v in outs if v.get("name")}
|
|
56
|
+
by_topic[tool] = last
|
|
57
|
+
|
|
58
|
+
await self.kv.set(idx_by_ref_kind(run_id), by_kind, ttl_s=self.ttl)
|
|
59
|
+
await self.kv.set(idx_by_name(run_id), by_name, ttl_s=self.ttl)
|
|
60
|
+
await self.kv.set(idx_by_topic(run_id), by_topic, ttl_s=self.ttl)
|
|
61
|
+
|
|
62
|
+
async def last_by_name(self, run_id: str, name: str) -> dict[str, Any] | None:
|
|
63
|
+
by_name = await self.kv.get(idx_by_name(run_id), {}) or {}
|
|
64
|
+
return by_name.get(name)
|
|
65
|
+
|
|
66
|
+
async def latest_refs_by_kind(
|
|
67
|
+
self, run_id: str, kind: str, *, limit: int = 50
|
|
68
|
+
) -> list[dict[str, Any]]:
|
|
69
|
+
by_kind = await self.kv.get(idx_by_ref_kind(run_id), {}) or {}
|
|
70
|
+
return list(reversed((by_kind.get(kind) or [])[-limit:]))
|
|
71
|
+
|
|
72
|
+
async def last_outputs_by_topic(self, run_id: str, topic: str) -> dict[str, Any] | None:
|
|
73
|
+
by_topic = await self.kv.get(idx_by_topic(run_id), {}) or {}
|
|
74
|
+
return by_topic.get(topic)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from aethergraph.contracts.services.memory import Value
|
|
4
|
+
|
|
5
|
+
"""Create a Value of vtype 'ref' pointing to the given kind and uri.
|
|
6
|
+
Args:
|
|
7
|
+
name: name of the Value slot
|
|
8
|
+
kind: kind of the referenced artifact, e.g. "spec", "design", "output", "tool_result"
|
|
9
|
+
uri: URI of the referenced artifact, e.g. "file://...", "mem://...", "db://..."
|
|
10
|
+
meta: optional additional metadata for the Ref
|
|
11
|
+
Returns:
|
|
12
|
+
Value dict with vtype 'ref'
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
v = ref(
|
|
16
|
+
name="my_ref",
|
|
17
|
+
kind="spec",
|
|
18
|
+
uri="file://path/to/spec",
|
|
19
|
+
title="My Spec",
|
|
20
|
+
mime="application/json"
|
|
21
|
+
)
|
|
22
|
+
print(v)
|
|
23
|
+
# Output: {
|
|
24
|
+
# "name": "my_ref",
|
|
25
|
+
# "vtype": "ref",
|
|
26
|
+
# "value": {
|
|
27
|
+
# "kind": "spec",
|
|
28
|
+
# "uri": "file://path/to/spec",
|
|
29
|
+
# "title": "My Spec",
|
|
30
|
+
# "mime": "application/json"
|
|
31
|
+
# }
|
|
32
|
+
# }
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def ref(name: str, kind: str, uri: str, **meta) -> Value:
|
|
37
|
+
v: Value = {
|
|
38
|
+
"name": name,
|
|
39
|
+
"vtype": "ref",
|
|
40
|
+
"value": {"kind": kind, "uri": uri, **meta},
|
|
41
|
+
}
|
|
42
|
+
return v
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def num(name: str, x: float) -> Value:
|
|
46
|
+
"""Create a Value of vtype 'number'."""
|
|
47
|
+
return {"name": name, "vtype": "number", "value": float(x)}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def text(name: str, s: str) -> Value:
|
|
51
|
+
"""Create a Value of vtype 'string'."""
|
|
52
|
+
return {"name": name, "vtype": "string", "value": str(s)}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def flag(name: str, b: bool) -> Value:
|
|
56
|
+
"""Create a Value of vtype 'boolean'."""
|
|
57
|
+
return {"name": name, "vtype": "boolean", "value": bool(b)}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def obj(name: str, d: dict) -> Value:
|
|
61
|
+
"""Create a Value of vtype 'object'."""
|
|
62
|
+
return {"name": name, "vtype": "object", "value": dict(d)}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def arr(name: str, lst: list) -> Value:
|
|
66
|
+
"""Create a Value of vtype 'array'."""
|
|
67
|
+
return {"name": name, "vtype": "array", "value": list(lst)}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def null(name: str) -> Value:
|
|
71
|
+
"""Create a Value of vtype 'null'."""
|
|
72
|
+
return {"name": name, "vtype": "null", "value": None}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from dataclasses import asdict
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
from aethergraph.contracts.services.memory import Event, Persistence
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FSPersistence(Persistence):
|
|
13
|
+
def __init__(self, *, base_dir: str):
|
|
14
|
+
self.base_dir = os.path.abspath(base_dir)
|
|
15
|
+
|
|
16
|
+
async def append_event(self, run_id: str, evt: Event) -> None:
|
|
17
|
+
day = time.strftime("%Y-%m-%d", time.gmtime())
|
|
18
|
+
rel = os.path.join("mem", run_id, "events", f"{day}.jsonl")
|
|
19
|
+
path = os.path.join(self.base_dir, rel)
|
|
20
|
+
|
|
21
|
+
def _write():
|
|
22
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
23
|
+
with open(path, "a", encoding="utf-8") as f:
|
|
24
|
+
f.write(json.dumps(asdict(evt), ensure_ascii=False) + "\n")
|
|
25
|
+
|
|
26
|
+
await asyncio.to_thread(_write)
|
|
27
|
+
|
|
28
|
+
async def save_json(self, uri: str, obj: dict[str, any]) -> None:
|
|
29
|
+
assert uri.startswith("file://"), f"FSPersistence only supports file://, got {uri!r}"
|
|
30
|
+
rel = uri[len("file://") :].lstrip("/\\")
|
|
31
|
+
path = os.path.join(self.base_dir, rel)
|
|
32
|
+
|
|
33
|
+
def _write():
|
|
34
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
35
|
+
tmp = path + ".tmp"
|
|
36
|
+
with open(tmp, "w", encoding="utf-8") as f:
|
|
37
|
+
json.dump(obj, f, ensure_ascii=False, indent=2)
|
|
38
|
+
os.replace(tmp, path)
|
|
39
|
+
|
|
40
|
+
await asyncio.to_thread(_write)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from aethergraph.services.memory.facade import MemoryFacade
|
|
7
|
+
|
|
8
|
+
# -------- Regexes (unchanged) --------
|
|
9
|
+
STEP_RE = re.compile(r"^\s*\$step\[(?P<idx>-?\d+)\]\.refs\.(?P<key>\w+)\s*$")
|
|
10
|
+
FROM_RE = re.compile(r"^\s*\$from:(\w+)\s*$")
|
|
11
|
+
VAR_RE = re.compile(r"^\s*\$var:(\w+)\s*$")
|
|
12
|
+
|
|
13
|
+
REF_KIND_RE = re.compile(r"^\s*\$resolve\s*:\s*ref\.kind\s*=\s*(\w+)\s*\|\s*last\s*$", re.I)
|
|
14
|
+
NAME_RE = re.compile(r"^\s*\$resolve\s*:\s*name\s*=\s*(\w+)\s*\|\s*last\s*$", re.I)
|
|
15
|
+
TOPIC_NAME_RE = re.compile(
|
|
16
|
+
r"^\s*\$resolve\s*:\s*topic\s*=\s*([\w\.\-\/]+)\s*\|\s*name\s*=\s*(\w+)\s*$", re.I
|
|
17
|
+
)
|
|
18
|
+
LEGACY_KIND_RE = re.compile(r"^\s*\$resolve\s*:\s*kind\s*=\s*(\w+)\s*\|\s*last\s*$", re.I)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ResolverContext:
|
|
22
|
+
def __init__(
|
|
23
|
+
self, mem: MemoryFacade, seq_ctx: dict | None = None, vars: dict[str, Any] | None = None
|
|
24
|
+
):
|
|
25
|
+
self.mem = mem
|
|
26
|
+
self.seq_ctx = seq_ctx or {}
|
|
27
|
+
self.vars = vars or {}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _get_step_outputs(seq_ctx: dict, j: int) -> dict[str, Any] | None:
|
|
31
|
+
steps = seq_ctx.get("steps") or []
|
|
32
|
+
if 0 <= j < len(steps):
|
|
33
|
+
return steps[j].get("outputs") or {}
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def _latest_ref_by_kind(mem: MemoryFacade, kind: str) -> str | None:
|
|
38
|
+
arr = await mem.latest_refs_by_kind(kind, limit=1)
|
|
39
|
+
if arr:
|
|
40
|
+
return arr[0].get("uri")
|
|
41
|
+
# Fallback scan
|
|
42
|
+
events = await mem.recent(kinds=["tool_result", "checkpoint"], limit=400)
|
|
43
|
+
for e in reversed(events):
|
|
44
|
+
outs = e.outputs or []
|
|
45
|
+
for v in outs:
|
|
46
|
+
if (
|
|
47
|
+
v.get("vtype") == "ref"
|
|
48
|
+
and isinstance(v.get("value"), dict)
|
|
49
|
+
and v["value"].get("kind") == kind
|
|
50
|
+
):
|
|
51
|
+
return v["value"].get("uri")
|
|
52
|
+
# legacy
|
|
53
|
+
if e.outputs_ref and f"{kind}_ref" in e.outputs_ref:
|
|
54
|
+
return e.outputs_ref.get(f"{kind}_ref")
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def _latest_value_by_name(mem: MemoryFacade, name: str) -> Any | None:
|
|
59
|
+
ent = await mem.last_by_name(name)
|
|
60
|
+
if ent:
|
|
61
|
+
return ent.get("value")
|
|
62
|
+
# Fallback scan
|
|
63
|
+
events = await mem.recent(kinds=["tool_result", "checkpoint"], limit=400)
|
|
64
|
+
for e in reversed(events):
|
|
65
|
+
outs = e.outputs or []
|
|
66
|
+
for v in outs:
|
|
67
|
+
if v.get("name") == name:
|
|
68
|
+
return v.get("value")
|
|
69
|
+
if e.outputs_ref and name in e.outputs_ref:
|
|
70
|
+
return e.outputs_ref.get(name)
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def _latest_value_by_topic_name(mem: MemoryFacade, topic: str, name: str) -> Any | None:
|
|
75
|
+
ent = await mem.last_outputs_by_topic(topic)
|
|
76
|
+
if ent:
|
|
77
|
+
last = ent.get("last_outputs") or {}
|
|
78
|
+
if name in last:
|
|
79
|
+
return last[name]
|
|
80
|
+
# Fallback scan
|
|
81
|
+
events = await mem.recent(kinds=["tool_result"], limit=400)
|
|
82
|
+
for e in reversed(events):
|
|
83
|
+
if (e.tool or "") != topic:
|
|
84
|
+
continue
|
|
85
|
+
outs = e.outputs or []
|
|
86
|
+
for v in outs:
|
|
87
|
+
if v.get("name") == name:
|
|
88
|
+
return v.get("value")
|
|
89
|
+
if e.outputs_ref and name in e.outputs_ref:
|
|
90
|
+
return e.outputs_ref.get(name)
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
async def resolve_params(raw: dict[str, Any], ctx: ResolverContext) -> dict[str, Any]:
|
|
95
|
+
out = dict(raw)
|
|
96
|
+
|
|
97
|
+
# 1) $step[i].refs.key
|
|
98
|
+
for k, v in list(out.items()):
|
|
99
|
+
if not isinstance(v, str):
|
|
100
|
+
continue
|
|
101
|
+
m = STEP_RE.match(v)
|
|
102
|
+
if not m:
|
|
103
|
+
continue
|
|
104
|
+
idx = int(m.group("idx"))
|
|
105
|
+
key = m.group("key")
|
|
106
|
+
steps = ctx.seq_ctx.get("steps") or []
|
|
107
|
+
j = idx if idx >= 0 else len(steps) + idx
|
|
108
|
+
refs = _get_step_outputs(ctx.seq_ctx, j)
|
|
109
|
+
if refs and key in refs:
|
|
110
|
+
out[k] = refs[key]
|
|
111
|
+
else:
|
|
112
|
+
if key.endswith("_ref"):
|
|
113
|
+
kind = key[:-4]
|
|
114
|
+
out[k] = await _latest_ref_by_kind(ctx.mem, kind)
|
|
115
|
+
else:
|
|
116
|
+
out[k] = None
|
|
117
|
+
|
|
118
|
+
# 2) $from:TAG (example strategy slot)
|
|
119
|
+
for k, v in list(out.items()):
|
|
120
|
+
if not isinstance(v, str):
|
|
121
|
+
continue
|
|
122
|
+
m = FROM_RE.match(v)
|
|
123
|
+
if m:
|
|
124
|
+
tag = m.group(1)
|
|
125
|
+
resolved = None
|
|
126
|
+
if tag == "last_opt_top1":
|
|
127
|
+
resolved = await _latest_value_by_topic_name(ctx.mem, "optimize.flow", "top1_ref")
|
|
128
|
+
out[k] = resolved
|
|
129
|
+
|
|
130
|
+
# 3) New selectors + legacy
|
|
131
|
+
for k, v in list(out.items()):
|
|
132
|
+
if not isinstance(v, str):
|
|
133
|
+
continue
|
|
134
|
+
if m := REF_KIND_RE.match(v):
|
|
135
|
+
out[k] = await _latest_ref_by_kind(ctx.mem, m.group(1).lower())
|
|
136
|
+
continue
|
|
137
|
+
if m := NAME_RE.match(v):
|
|
138
|
+
out[k] = await _latest_value_by_name(ctx.mem, m.group(1))
|
|
139
|
+
continue
|
|
140
|
+
if m := TOPIC_NAME_RE.match(v):
|
|
141
|
+
out[k] = await _latest_value_by_topic_name(ctx.mem, m.group(1), m.group(2))
|
|
142
|
+
continue
|
|
143
|
+
if m := LEGACY_KIND_RE.match(v):
|
|
144
|
+
out[k] = await _latest_ref_by_kind(ctx.mem, m.group(1).lower())
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
# 4) $var:NAME
|
|
148
|
+
for k, v in list(out.items()):
|
|
149
|
+
if isinstance(v, str) and (m := VAR_RE.match(v)):
|
|
150
|
+
out[k] = ctx.vars.get(m.group(1))
|
|
151
|
+
|
|
152
|
+
return out
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# services/prompts/file_store.py
|
|
2
|
+
# Simple file-based prompt store
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FilePromptStore:
|
|
7
|
+
def __init__(self, root: str = "./prompts"):
|
|
8
|
+
self.root = Path(root)
|
|
9
|
+
|
|
10
|
+
async def get(self, name: str, version: str | None = None) -> str:
|
|
11
|
+
"""Get prompt by name and optional version.
|
|
12
|
+
If version is None, get the latest (unversioned) prompt.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
name: Prompt name (filename without extension)
|
|
16
|
+
version: Optional version string
|
|
17
|
+
Returns:
|
|
18
|
+
Prompt content as string
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
prompt = await store.get("welcome_message", version="v1")
|
|
22
|
+
print(prompt)
|
|
23
|
+
# ./prompts/welcome_message@v1.md
|
|
24
|
+
"""
|
|
25
|
+
p = self.root / (f"{name}@{version}.md" if version else f"{name}.md")
|
|
26
|
+
return p.read_text(encoding="utf-8")
|
|
27
|
+
|
|
28
|
+
async def render(self, name: str, **vars) -> str:
|
|
29
|
+
"""Get and render prompt with variable substitution.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
name: Prompt name (filename without extension)
|
|
33
|
+
**vars: Variables to substitute in the prompt
|
|
34
|
+
Returns:
|
|
35
|
+
Rendered prompt content as string
|
|
36
|
+
"""
|
|
37
|
+
# Tiny {{var}} replacement; swap later for jinja/mustache
|
|
38
|
+
txt = await self.get(name)
|
|
39
|
+
for k, v in vars.items():
|
|
40
|
+
txt = txt.replace(f"{{{{{k}}}}}", str(v))
|
|
41
|
+
return txt
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TextSplitter:
|
|
5
|
+
"""A simple text splitter that splits text into chunks of approximately target_tokens,
|
|
6
|
+
with a specified overlap in tokens.
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
splitter = TextSplitter(target_tokens=400, overlap_tokens=60)
|
|
10
|
+
chunks = splitter.split(long_text)
|
|
11
|
+
for chunk in chunks:
|
|
12
|
+
print(chunk)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, target_tokens: int = 400, overlap_tokens: int = 60):
|
|
16
|
+
self.n = max(50, target_tokens)
|
|
17
|
+
self.o = max(0, min(self.n - 1, overlap_tokens))
|
|
18
|
+
|
|
19
|
+
def split(self, text: str) -> list[str]:
|
|
20
|
+
words = text.split()
|
|
21
|
+
if not words:
|
|
22
|
+
return []
|
|
23
|
+
step = self.n - self.o
|
|
24
|
+
chunks = []
|
|
25
|
+
for i in range(0, len(words), step):
|
|
26
|
+
chunk = " ".join(words[i : i + self.n])
|
|
27
|
+
if chunk.strip():
|
|
28
|
+
chunks.append(chunk)
|
|
29
|
+
return chunks
|