aethergraph 0.1.0a1__py3-none-any.whl → 0.1.0a2__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 +293 -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 +190 -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.0a2.dist-info}/METADATA +138 -31
- aethergraph-0.1.0a2.dist-info/RECORD +356 -0
- aethergraph-0.1.0a2.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.0a2.dist-info}/WHEEL +0 -0
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/licenses/LICENSE +0 -0
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/licenses/NOTICE +0 -0
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# /graphs
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from typing import Annotated, Any
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
7
|
+
|
|
8
|
+
from aethergraph.core.graph.graph_fn import GraphFunction
|
|
9
|
+
from aethergraph.core.graph.task_graph import TaskGraph
|
|
10
|
+
from aethergraph.core.runtime.runtime_registry import current_registry
|
|
11
|
+
from aethergraph.services.registry.unified_registry import UnifiedRegistry
|
|
12
|
+
|
|
13
|
+
from .deps import RequestIdentity, get_identity
|
|
14
|
+
from .schemas import GraphDetail, GraphListItem
|
|
15
|
+
|
|
16
|
+
router = APIRouter(tags=["graphs"])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
GRAPH_NS = "graph"
|
|
20
|
+
GRAPHFN_NS = "graphfn"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _is_task_graph(obj: Any) -> bool:
|
|
24
|
+
if isinstance(obj, TaskGraph):
|
|
25
|
+
return True
|
|
26
|
+
# Fallback check -- used in tests
|
|
27
|
+
return hasattr(obj, "spec")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _is_graph_function(obj: Any) -> bool:
|
|
31
|
+
if isinstance(obj, GraphFunction):
|
|
32
|
+
return True
|
|
33
|
+
# Fallback check -- used in tests
|
|
34
|
+
return hasattr(obj, "fn") and hasattr(obj, "name")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@router.get("/graphs", response_model=list[GraphListItem])
|
|
38
|
+
async def list_graphs(
|
|
39
|
+
flow_id: Annotated[str | None, Query()] = None,
|
|
40
|
+
identity: RequestIdentity = Depends(get_identity), # noqa: B008
|
|
41
|
+
) -> list[GraphListItem]:
|
|
42
|
+
"""
|
|
43
|
+
List available graphs (TaskGraphs and GraphFunctions).
|
|
44
|
+
|
|
45
|
+
Optional:
|
|
46
|
+
- flow_id: filter to graphs whose registry metadata has this flow_id.
|
|
47
|
+
"""
|
|
48
|
+
reg: UnifiedRegistry = current_registry()
|
|
49
|
+
|
|
50
|
+
items: list[GraphListItem] = []
|
|
51
|
+
|
|
52
|
+
# ---- 1) Static TaskGraphs (ns="graph") ----
|
|
53
|
+
latest_graphs = reg.list(nspace=GRAPH_NS)
|
|
54
|
+
for key, version in latest_graphs.items():
|
|
55
|
+
ns, name = key.split(":", 1)
|
|
56
|
+
if ns != GRAPH_NS:
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
graph_obj = reg.get_graph(name=name, version=version)
|
|
60
|
+
spec = getattr(graph_obj, "spec", None)
|
|
61
|
+
|
|
62
|
+
meta = reg.get_meta(nspace=GRAPH_NS, name=name, version=version) or {}
|
|
63
|
+
meta_flow_id: str | None = meta.get("flow_id")
|
|
64
|
+
meta_entrypoint: bool = bool(meta.get("entrypoint", False))
|
|
65
|
+
meta_tags = list(meta.get("tags", []))
|
|
66
|
+
|
|
67
|
+
# flow filter
|
|
68
|
+
if flow_id is not None and meta_flow_id != flow_id:
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
if spec is None:
|
|
72
|
+
items.append(
|
|
73
|
+
GraphListItem(
|
|
74
|
+
graph_id=name,
|
|
75
|
+
name=name,
|
|
76
|
+
description=None,
|
|
77
|
+
inputs=[],
|
|
78
|
+
outputs=[],
|
|
79
|
+
tags=meta_tags or ["graph"],
|
|
80
|
+
kind="graph",
|
|
81
|
+
flow_id=meta_flow_id,
|
|
82
|
+
entrypoint=meta_entrypoint,
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
inputs = list(spec.io.required.keys()) + list(spec.io.optional.keys())
|
|
88
|
+
outputs = list(spec.io.outputs.keys())
|
|
89
|
+
|
|
90
|
+
desc = spec.meta.get("description") if hasattr(spec, "meta") else None
|
|
91
|
+
spec_tags = list(spec.meta.get("tags", [])) if hasattr(spec, "meta") else []
|
|
92
|
+
|
|
93
|
+
tags = meta_tags or spec_tags or ["graph"]
|
|
94
|
+
|
|
95
|
+
items.append(
|
|
96
|
+
GraphListItem(
|
|
97
|
+
graph_id=name,
|
|
98
|
+
name=name,
|
|
99
|
+
description=desc,
|
|
100
|
+
inputs=inputs,
|
|
101
|
+
outputs=outputs,
|
|
102
|
+
tags=tags,
|
|
103
|
+
kind="graph",
|
|
104
|
+
flow_id=meta_flow_id,
|
|
105
|
+
entrypoint=meta_entrypoint,
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# ---- 2) Imperative GraphFunctions (ns="graphfn") ----
|
|
110
|
+
latest_graphfns = reg.list(nspace=GRAPHFN_NS)
|
|
111
|
+
for key, version in latest_graphfns.items():
|
|
112
|
+
ns, name = key.split(":", 1)
|
|
113
|
+
if ns != GRAPHFN_NS:
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
gf = reg.get_graphfn(name=name, version=version)
|
|
117
|
+
if not _is_graph_function(gf):
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
meta = reg.get_meta(nspace=GRAPHFN_NS, name=name, version=version) or {}
|
|
121
|
+
meta_flow_id: str | None = meta.get("flow_id")
|
|
122
|
+
meta_entrypoint: bool = bool(meta.get("entrypoint", False))
|
|
123
|
+
meta_tags = list(meta.get("tags", []))
|
|
124
|
+
|
|
125
|
+
if flow_id is not None and meta_flow_id != flow_id:
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
inputs = list(getattr(gf, "inputs", []) or [])
|
|
129
|
+
outputs = list(getattr(gf, "outputs", []) or [])
|
|
130
|
+
desc = getattr(gf, "description", None)
|
|
131
|
+
|
|
132
|
+
items.append(
|
|
133
|
+
GraphListItem(
|
|
134
|
+
graph_id=name,
|
|
135
|
+
name=name,
|
|
136
|
+
description=desc,
|
|
137
|
+
inputs=inputs,
|
|
138
|
+
outputs=outputs,
|
|
139
|
+
tags=meta_tags or ["graphfn"],
|
|
140
|
+
kind="graphfn",
|
|
141
|
+
flow_id=meta_flow_id,
|
|
142
|
+
entrypoint=meta_entrypoint,
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return items
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@router.get("/graphs/{graph_id}", response_model=GraphDetail)
|
|
150
|
+
async def get_graph_detail(
|
|
151
|
+
graph_id: str,
|
|
152
|
+
identity: RequestIdentity = Depends(get_identity), # noqa: B008
|
|
153
|
+
) -> GraphDetail:
|
|
154
|
+
"""
|
|
155
|
+
Get detailed information about a specific graph (structure only).
|
|
156
|
+
"""
|
|
157
|
+
reg: UnifiedRegistry = current_registry()
|
|
158
|
+
|
|
159
|
+
# 1) Try TaskGraph
|
|
160
|
+
try:
|
|
161
|
+
graph_obj = reg.get_graph(name=graph_id, version=None)
|
|
162
|
+
spec = getattr(graph_obj, "spec", None)
|
|
163
|
+
meta = reg.get_meta(nspace=GRAPH_NS, name=graph_id, version=None) or {}
|
|
164
|
+
|
|
165
|
+
flow_id = meta.get("flow_id")
|
|
166
|
+
entrypoint = bool(meta.get("entrypoint", False))
|
|
167
|
+
meta_tags = list(meta.get("tags", []))
|
|
168
|
+
|
|
169
|
+
if spec is None:
|
|
170
|
+
return GraphDetail(
|
|
171
|
+
graph_id=graph_id,
|
|
172
|
+
name=graph_id,
|
|
173
|
+
description=None,
|
|
174
|
+
inputs=[],
|
|
175
|
+
outputs=[],
|
|
176
|
+
tags=meta_tags or ["graph"],
|
|
177
|
+
kind="graph",
|
|
178
|
+
flow_id=flow_id,
|
|
179
|
+
entrypoint=entrypoint,
|
|
180
|
+
nodes=[],
|
|
181
|
+
edges=[],
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# ---- Nodes from TaskNodeSpec ----
|
|
185
|
+
nodes_list: list[dict[str, Any]] = []
|
|
186
|
+
for node_id, node_spec in spec.nodes.items():
|
|
187
|
+
node_info: dict[str, Any] = {
|
|
188
|
+
"id": node_id,
|
|
189
|
+
"type": str(getattr(node_spec, "type", "")),
|
|
190
|
+
"tool_name": getattr(node_spec, "tool_name", None),
|
|
191
|
+
"tool_version": getattr(node_spec, "tool_version", None),
|
|
192
|
+
"expected_inputs": list(getattr(node_spec, "expected_input_keys", []) or []),
|
|
193
|
+
"expected_outputs": list(getattr(node_spec, "expected_output_keys", []) or []),
|
|
194
|
+
"output_keys": list(getattr(node_spec, "output_keys", []) or []),
|
|
195
|
+
}
|
|
196
|
+
nodes_list.append(node_info)
|
|
197
|
+
|
|
198
|
+
# ---- Edges from dependencies ----
|
|
199
|
+
edge_set: set[tuple[str, str]] = set()
|
|
200
|
+
for node_id, node_spec in spec.nodes.items():
|
|
201
|
+
for dep_id in getattr(node_spec, "dependencies", []):
|
|
202
|
+
edge_set.add((str(dep_id), str(node_id)))
|
|
203
|
+
|
|
204
|
+
edges_list: list[dict[str, Any]] = [
|
|
205
|
+
{"source": src, "target": dst} for (src, dst) in sorted(edge_set)
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
inputs = list(spec.io.required.keys()) + list(spec.io.optional.keys())
|
|
209
|
+
outputs = list(spec.io.outputs.keys())
|
|
210
|
+
desc = spec.meta.get("description") if hasattr(spec, "meta") else None
|
|
211
|
+
spec_tags = list(spec.meta.get("tags", [])) if hasattr(spec, "meta") else []
|
|
212
|
+
|
|
213
|
+
tags = meta_tags or spec_tags or ["graph"]
|
|
214
|
+
|
|
215
|
+
return GraphDetail(
|
|
216
|
+
graph_id=graph_id,
|
|
217
|
+
name=graph_id,
|
|
218
|
+
description=desc,
|
|
219
|
+
inputs=inputs,
|
|
220
|
+
outputs=outputs,
|
|
221
|
+
tags=tags,
|
|
222
|
+
kind="graph",
|
|
223
|
+
flow_id=flow_id,
|
|
224
|
+
entrypoint=entrypoint,
|
|
225
|
+
nodes=nodes_list,
|
|
226
|
+
edges=edges_list,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
except KeyError:
|
|
230
|
+
pass
|
|
231
|
+
|
|
232
|
+
# 2) Try GraphFunction
|
|
233
|
+
try:
|
|
234
|
+
gf = reg.get_graphfn(name=graph_id, version=None)
|
|
235
|
+
except KeyError as e:
|
|
236
|
+
raise HTTPException(status_code=404, detail="Graph not found") from e
|
|
237
|
+
|
|
238
|
+
meta = reg.get_meta(nspace=GRAPHFN_NS, name=graph_id, version=None) or {}
|
|
239
|
+
flow_id = meta.get("flow_id")
|
|
240
|
+
entrypoint = bool(meta.get("entrypoint", False))
|
|
241
|
+
meta_tags = list(meta.get("tags", []))
|
|
242
|
+
|
|
243
|
+
inputs = list(getattr(gf, "inputs", []) or [])
|
|
244
|
+
outputs = list(getattr(gf, "outputs", []) or [])
|
|
245
|
+
desc = getattr(gf, "description", None)
|
|
246
|
+
|
|
247
|
+
return GraphDetail(
|
|
248
|
+
graph_id=graph_id,
|
|
249
|
+
name=graph_id,
|
|
250
|
+
description=desc,
|
|
251
|
+
inputs=inputs,
|
|
252
|
+
outputs=outputs,
|
|
253
|
+
tags=meta_tags or ["graphfn"],
|
|
254
|
+
kind="graphfn",
|
|
255
|
+
flow_id=flow_id,
|
|
256
|
+
entrypoint=entrypoint,
|
|
257
|
+
nodes=[], # GraphFunction has no static node DAG
|
|
258
|
+
edges=[],
|
|
259
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from fastapi import APIRouter, Depends
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
|
|
4
|
+
from aethergraph.api.v1.deps import RequestIdentity, get_identity
|
|
5
|
+
|
|
6
|
+
router = APIRouter()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class IdentityResponse(BaseModel):
|
|
10
|
+
mode: str
|
|
11
|
+
user_id: str | None
|
|
12
|
+
org_id: str | None
|
|
13
|
+
roles: list[str]
|
|
14
|
+
client_id: str | None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@router.get("/whoami", response_model=IdentityResponse)
|
|
18
|
+
def whoami(identity: RequestIdentity = Depends(get_identity)): # noqa: B008
|
|
19
|
+
return IdentityResponse(
|
|
20
|
+
mode=identity.mode,
|
|
21
|
+
user_id=identity.user_id,
|
|
22
|
+
org_id=identity.org_id,
|
|
23
|
+
roles=identity.roles,
|
|
24
|
+
client_id=identity.client_id,
|
|
25
|
+
)
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
# memory-related inspection
|
|
2
|
+
|
|
3
|
+
from contextlib import suppress
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Annotated, Any
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Depends, Query
|
|
8
|
+
|
|
9
|
+
from aethergraph.api.v1.pagination import decode_cursor, encode_cursor
|
|
10
|
+
from aethergraph.contracts.services.memory import Event
|
|
11
|
+
from aethergraph.core.runtime.runtime_services import current_services
|
|
12
|
+
|
|
13
|
+
from .deps import RequestIdentity, get_identity
|
|
14
|
+
from .schemas import (
|
|
15
|
+
MemoryEvent,
|
|
16
|
+
MemoryEventListResponse,
|
|
17
|
+
MemorySearchHit,
|
|
18
|
+
MemorySearchRequest,
|
|
19
|
+
MemorySearchResponse,
|
|
20
|
+
MemorySummaryEntry,
|
|
21
|
+
MemorySummaryListResponse,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# NOTE: since hotlog is bounded in memory, it is fine to filter and rank in-memory for now.
|
|
25
|
+
# In future, if we need to process large volumes of memory data, we should look into changing the
|
|
26
|
+
# backend memory storage to support indexed queries (not changing the API contracts).
|
|
27
|
+
|
|
28
|
+
router = APIRouter(tags=["memory"])
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ------------ helpers / stubs ------------ #
|
|
32
|
+
def _parse_ts(ts: str) -> datetime:
|
|
33
|
+
"""Parse ISO8601 timestamp string to datetime."""
|
|
34
|
+
if ts.endswith("Z"):
|
|
35
|
+
ts = ts[:-1] + "+00:00"
|
|
36
|
+
return datetime.fromisoformat(ts)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _event_to_api_event(evt: Event) -> MemoryEvent:
|
|
40
|
+
created_at = _parse_ts(evt.ts)
|
|
41
|
+
|
|
42
|
+
data: dict[str, Any] | None = None
|
|
43
|
+
if evt.data is not None:
|
|
44
|
+
data = evt.data
|
|
45
|
+
elif evt.text:
|
|
46
|
+
data = {"text": evt.text}
|
|
47
|
+
|
|
48
|
+
# Fallback: if old events had no scope_id, use run_id so UI still works.
|
|
49
|
+
scope = evt.scope_id or evt.run_id
|
|
50
|
+
|
|
51
|
+
return MemoryEvent(
|
|
52
|
+
event_id=evt.event_id,
|
|
53
|
+
scope_id=scope,
|
|
54
|
+
kind=evt.kind,
|
|
55
|
+
tags=evt.tags or [],
|
|
56
|
+
created_at=created_at,
|
|
57
|
+
data=data or {},
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _doc_to_summary_entry(doc_id: str, doc: dict[str, Any]) -> MemorySummaryEntry:
|
|
62
|
+
ts_str = doc.get("ts") or doc.get("created_at") or ""
|
|
63
|
+
created_at = _parse_ts(ts_str) if ts_str else datetime.utcnow()
|
|
64
|
+
|
|
65
|
+
tw = doc.get("time_window") or {} # expected to have 'from' and 'to'
|
|
66
|
+
from_str = tw.get("from") or tw.get("start") or ""
|
|
67
|
+
to_str = tw.get("to") or tw.get("end") or ""
|
|
68
|
+
time_from = _parse_ts(from_str) if from_str else created_at
|
|
69
|
+
time_to = _parse_ts(to_str) if to_str else created_at
|
|
70
|
+
|
|
71
|
+
# prefer 'summary' field, fallback to text if present
|
|
72
|
+
text = doc.get("summary") or doc.get("text") or ""
|
|
73
|
+
|
|
74
|
+
# Strip out the core fields from metadata
|
|
75
|
+
meta_keys = {
|
|
76
|
+
"summary",
|
|
77
|
+
"text",
|
|
78
|
+
"scope_id",
|
|
79
|
+
"run_id",
|
|
80
|
+
"summary_tag",
|
|
81
|
+
"ts",
|
|
82
|
+
"time_window",
|
|
83
|
+
}
|
|
84
|
+
metadata = {k: v for k, v in doc.items() if k not in meta_keys}
|
|
85
|
+
|
|
86
|
+
return MemorySummaryEntry(
|
|
87
|
+
summary_id=doc_id,
|
|
88
|
+
scope_id=doc.get("scope_id") or doc.get("run_id") or "",
|
|
89
|
+
summary_tag=doc.get("summary_tag"),
|
|
90
|
+
created_at=created_at,
|
|
91
|
+
time_from=time_from,
|
|
92
|
+
time_to=time_to,
|
|
93
|
+
text=text,
|
|
94
|
+
metadata=metadata,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _string_score(haystack: str, needle: str) -> float:
|
|
99
|
+
"""
|
|
100
|
+
Very simple scoring: 0.0 if no match, 1.0 if case-insensitive substring match.
|
|
101
|
+
Placeholder until a real semantic index is wired in.
|
|
102
|
+
"""
|
|
103
|
+
if not needle:
|
|
104
|
+
return 0.0
|
|
105
|
+
h = haystack.lower()
|
|
106
|
+
n = needle.lower()
|
|
107
|
+
return 1.0 if n in h else 0.0
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# ------------ API endpoints ------------ #
|
|
111
|
+
@router.get("/memory/events", response_model=MemoryEventListResponse)
|
|
112
|
+
async def list_memory_events(
|
|
113
|
+
scope_id: str,
|
|
114
|
+
kinds: Annotated[
|
|
115
|
+
str | None, Query(description="Comma-separated list of kinds to filter")
|
|
116
|
+
] = None, # noqa: B008
|
|
117
|
+
tags: Annotated[str | None, Query(description="Comma-separated list of tags to filter")] = None, # noqa: B008
|
|
118
|
+
after: Annotated[datetime | None, Query()] = None, # noqa: B008
|
|
119
|
+
before: Annotated[datetime | None, Query()] = None, # noqa: B008
|
|
120
|
+
cursor: Annotated[str | None, Query()] = None, # noqa: B008
|
|
121
|
+
limit: Annotated[int, Query(ge=1, le=200)] = 50, # noqa: B008
|
|
122
|
+
identity: RequestIdentity = Depends(get_identity), # noqa: B008
|
|
123
|
+
) -> MemoryEventListResponse:
|
|
124
|
+
"""
|
|
125
|
+
List raw memory events for a scope.
|
|
126
|
+
|
|
127
|
+
Currently:
|
|
128
|
+
- Treats `scope_id` as the underlying `run_id` used by HotLog/Persistence.
|
|
129
|
+
- Reads from HotLog only (recent in-memory events).
|
|
130
|
+
- Applies basic filters by kind, tag, and time.
|
|
131
|
+
|
|
132
|
+
TODO:
|
|
133
|
+
- Integrate with a long-term event store (Persistence queries).
|
|
134
|
+
- Implement cursor-based pagination.
|
|
135
|
+
- Optionally map scope_id → multiple runs.
|
|
136
|
+
- Filter by identity.user_id / org_id when multi-tenant.
|
|
137
|
+
|
|
138
|
+
NOTE:
|
|
139
|
+
- Currently reads from HotLog only (recent in-memory events),
|
|
140
|
+
NOT the long-term persistence/event log.
|
|
141
|
+
- Fetches up to hot_limit+10 and applies filters + cursor (offset) in Python.
|
|
142
|
+
- Pagination is therefore limited to the hot buffer; older events are not visible.
|
|
143
|
+
In the future, we may want to:
|
|
144
|
+
- Integrate with EventLog-based persistence for full history,
|
|
145
|
+
- Move filtering + pagination closer to the store layer.
|
|
146
|
+
"""
|
|
147
|
+
container = current_services()
|
|
148
|
+
mem_factory = getattr(container, "memory_factory", None)
|
|
149
|
+
if mem_factory is None:
|
|
150
|
+
# No memory configured
|
|
151
|
+
return MemoryEventListResponse(events=[], next_cursor=None)
|
|
152
|
+
|
|
153
|
+
hotlog = mem_factory.hotlog
|
|
154
|
+
|
|
155
|
+
# Parse filters
|
|
156
|
+
kinds_list: list[str] | None = None
|
|
157
|
+
if kinds:
|
|
158
|
+
kinds_list = [k.strip() for k in kinds.split(",") if k.strip()]
|
|
159
|
+
tags_list: list[str] | None = None
|
|
160
|
+
if tags:
|
|
161
|
+
tags_list = [t.strip() for t in tags.split(",") if t.strip()]
|
|
162
|
+
|
|
163
|
+
# Fetch slightly more than limit to determine if there's a next page and
|
|
164
|
+
# we can filter in python
|
|
165
|
+
raw_events: list[Event] = await hotlog.recent(
|
|
166
|
+
scope_id,
|
|
167
|
+
kinds=kinds_list,
|
|
168
|
+
limit=mem_factory.hot_limit + 10,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
filtered: list[Event] = []
|
|
172
|
+
for evt in raw_events:
|
|
173
|
+
dt = _parse_ts(evt.ts)
|
|
174
|
+
if after and dt <= after:
|
|
175
|
+
continue
|
|
176
|
+
if before and dt >= before:
|
|
177
|
+
continue
|
|
178
|
+
if tags_list:
|
|
179
|
+
evt_tags = evt.tags or []
|
|
180
|
+
if not any(t in evt_tags for t in tags_list):
|
|
181
|
+
continue
|
|
182
|
+
filtered.append(evt)
|
|
183
|
+
|
|
184
|
+
# Apply offset and limit
|
|
185
|
+
offset = decode_cursor(cursor)
|
|
186
|
+
page = filtered[offset : offset + limit]
|
|
187
|
+
api_events = [_event_to_api_event(e) for e in page]
|
|
188
|
+
|
|
189
|
+
next_cursor = encode_cursor(offset + limit) if len(filtered) > offset + limit else None
|
|
190
|
+
return MemoryEventListResponse(events=api_events, next_cursor=next_cursor)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@router.get("/memory/summaries", response_model=MemorySummaryListResponse)
|
|
194
|
+
async def list_memory_summaries(
|
|
195
|
+
scope_id: Annotated[str, Query()],
|
|
196
|
+
summary_tag: Annotated[str | None, Query()] = None,
|
|
197
|
+
cursor: Annotated[str | None, Query()] = None,
|
|
198
|
+
limit: Annotated[int, Query(ge=1, le=200)] = 50,
|
|
199
|
+
identity: RequestIdentity = Depends(get_identity), # noqa: B008
|
|
200
|
+
) -> MemorySummaryListResponse:
|
|
201
|
+
"""
|
|
202
|
+
List long-term memory summaries for a scope.
|
|
203
|
+
|
|
204
|
+
Currently:
|
|
205
|
+
- Scans the DocStore (memory_factory.docs) and filters docs where:
|
|
206
|
+
doc["scope_id"] == scope_id
|
|
207
|
+
and (summary_tag is None or doc["summary_tag"] == summary_tag)
|
|
208
|
+
- Converts each summary doc into MemorySummaryEntry.
|
|
209
|
+
|
|
210
|
+
TODO:
|
|
211
|
+
- Avoid full scan for large DocStores (add indexed queries).
|
|
212
|
+
- Implement cursor-based pagination.
|
|
213
|
+
- Optionally filter by identity.user_id / org_id.
|
|
214
|
+
"""
|
|
215
|
+
container = current_services()
|
|
216
|
+
mem_factory = getattr(container, "memory_factory", None)
|
|
217
|
+
if mem_factory is None:
|
|
218
|
+
return MemorySummaryListResponse(summaries=[], next_cursor=None)
|
|
219
|
+
|
|
220
|
+
docs = mem_factory.docs
|
|
221
|
+
|
|
222
|
+
# DocStore.list() returns a list of doc_ids; we load and filter them
|
|
223
|
+
try:
|
|
224
|
+
doc_ids = await docs.list()
|
|
225
|
+
except TypeError:
|
|
226
|
+
# If the concrete DocStore doesn't support list(), return empty
|
|
227
|
+
return MemorySummaryListResponse(summaries=[], next_cursor=None)
|
|
228
|
+
|
|
229
|
+
entries: list[MemorySummaryEntry] = []
|
|
230
|
+
|
|
231
|
+
for doc_id in doc_ids:
|
|
232
|
+
doc = await docs.get(doc_id)
|
|
233
|
+
if not doc:
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
if doc.get("scope_id") != scope_id:
|
|
237
|
+
continue
|
|
238
|
+
|
|
239
|
+
if summary_tag is not None and doc.get("summary_tag") != summary_tag:
|
|
240
|
+
continue
|
|
241
|
+
|
|
242
|
+
entries.append(_doc_to_summary_entry(doc_id, doc))
|
|
243
|
+
|
|
244
|
+
# Sort by created_at descending
|
|
245
|
+
entries.sort(key=lambda e: e.created_at, reverse=True)
|
|
246
|
+
|
|
247
|
+
if len(entries) > limit:
|
|
248
|
+
entries = entries[:limit]
|
|
249
|
+
return MemorySummaryListResponse(summaries=entries, next_cursor=None)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@router.post("/memory/search", response_model=MemorySearchResponse)
|
|
253
|
+
async def search_memory(
|
|
254
|
+
req: MemorySearchRequest,
|
|
255
|
+
identity: RequestIdentity = Depends(get_identity), # noqa: B008
|
|
256
|
+
) -> MemorySearchResponse:
|
|
257
|
+
"""
|
|
258
|
+
Semantic/keyword memory search.
|
|
259
|
+
|
|
260
|
+
Current behavior:
|
|
261
|
+
- Uses a naive substring match over:
|
|
262
|
+
• recent HotLog events for the given scope_id (treated as run_id)
|
|
263
|
+
• all summary docs for that scope_id
|
|
264
|
+
- Returns MemorySearchHit with either `event` or `summary` populated.
|
|
265
|
+
|
|
266
|
+
TODO:
|
|
267
|
+
- Plug into a real semantic index / RAG backend (mem_factory.rag_facade).
|
|
268
|
+
- Support more advanced filters (kinds, tags, summary_tag) on the request.
|
|
269
|
+
"""
|
|
270
|
+
container = current_services()
|
|
271
|
+
mem_factory = getattr(container, "memory_factory", None)
|
|
272
|
+
if mem_factory is None:
|
|
273
|
+
return MemorySearchResponse(hits=[])
|
|
274
|
+
|
|
275
|
+
scope_id = req.scope_id or ""
|
|
276
|
+
query = req.query or ""
|
|
277
|
+
top_k = getattr(req, "top_k", 10) or 10
|
|
278
|
+
|
|
279
|
+
hotlog = mem_factory.hotlog
|
|
280
|
+
docs = mem_factory.docs
|
|
281
|
+
|
|
282
|
+
hits: list[MemorySearchHit] = []
|
|
283
|
+
|
|
284
|
+
# 1) Search recent HotLog events
|
|
285
|
+
if scope_id:
|
|
286
|
+
raw_events: list[Event] = await hotlog.recent(
|
|
287
|
+
scope_id,
|
|
288
|
+
kinds=None,
|
|
289
|
+
limit=mem_factory.hot_limit,
|
|
290
|
+
)
|
|
291
|
+
for evt in raw_events:
|
|
292
|
+
text_parts: list[str] = []
|
|
293
|
+
if evt.text:
|
|
294
|
+
text_parts.append(evt.text)
|
|
295
|
+
if evt.data:
|
|
296
|
+
with suppress(Exception):
|
|
297
|
+
text_parts.append(str(evt.data))
|
|
298
|
+
haystack = " ".join(text_parts)
|
|
299
|
+
score = _string_score(haystack, query)
|
|
300
|
+
if score <= 0.0:
|
|
301
|
+
continue
|
|
302
|
+
|
|
303
|
+
api_evt = _event_to_api_event(evt)
|
|
304
|
+
hits.append(
|
|
305
|
+
MemorySearchHit(
|
|
306
|
+
score=score,
|
|
307
|
+
event=api_evt,
|
|
308
|
+
summary=None,
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# 2) Search summary docs
|
|
313
|
+
try:
|
|
314
|
+
doc_ids = await docs.list()
|
|
315
|
+
except TypeError:
|
|
316
|
+
doc_ids = []
|
|
317
|
+
|
|
318
|
+
for doc_id in doc_ids:
|
|
319
|
+
doc = await docs.get(doc_id)
|
|
320
|
+
if not doc:
|
|
321
|
+
continue
|
|
322
|
+
|
|
323
|
+
if scope_id and doc.get("scope_id") != scope_id:
|
|
324
|
+
continue
|
|
325
|
+
|
|
326
|
+
text_parts: list[str] = []
|
|
327
|
+
if doc.get("summary"):
|
|
328
|
+
text_parts.append(str(doc.get("summary")))
|
|
329
|
+
|
|
330
|
+
if doc.get("key_facts"):
|
|
331
|
+
with suppress(Exception):
|
|
332
|
+
text_parts.append(" ".join(map(str, doc["key_facts"])))
|
|
333
|
+
|
|
334
|
+
haystack = " ".join(text_parts)
|
|
335
|
+
score = _string_score(haystack, query)
|
|
336
|
+
if score <= 0.0:
|
|
337
|
+
continue
|
|
338
|
+
|
|
339
|
+
summary_entry = _doc_to_summary_entry(doc_id, doc)
|
|
340
|
+
hits.append(
|
|
341
|
+
MemorySearchHit(
|
|
342
|
+
score=score,
|
|
343
|
+
event=None,
|
|
344
|
+
summary=summary_entry,
|
|
345
|
+
)
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
# Sort by score (desc) and truncate
|
|
349
|
+
hits.sort(key=lambda h: h.score, reverse=True)
|
|
350
|
+
if len(hits) > top_k:
|
|
351
|
+
hits = hits[:top_k]
|
|
352
|
+
|
|
353
|
+
return MemorySearchResponse(hits=hits)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# /health /config
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from fastapi import APIRouter, Depends
|
|
5
|
+
|
|
6
|
+
from .deps import RequestIdentity, get_identity
|
|
7
|
+
from .schemas import ConfigLLMProvider, ConfigResponse, HealthResponse
|
|
8
|
+
|
|
9
|
+
router = APIRouter(tags=["misc"])
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@router.get("/health", response_model=HealthResponse)
|
|
13
|
+
async def health_check() -> HealthResponse:
|
|
14
|
+
"""
|
|
15
|
+
Simple health check endpoint.
|
|
16
|
+
"""
|
|
17
|
+
# TODO: optionally include deeper checks (DB, Redis, etc.)
|
|
18
|
+
return HealthResponse(status="ok", version="0.1.0a1")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@router.get("/config", response_model=ConfigResponse)
|
|
22
|
+
async def config_info(
|
|
23
|
+
identity: RequestIdentity = Depends(get_identity), # noqa: B008
|
|
24
|
+
) -> ConfigResponse:
|
|
25
|
+
"""
|
|
26
|
+
Return sanitized config info that's safe for UI.
|
|
27
|
+
|
|
28
|
+
TODO:
|
|
29
|
+
- Read from AppSettings.
|
|
30
|
+
- Mask secrets; only expose high-level info.
|
|
31
|
+
"""
|
|
32
|
+
# Stub example
|
|
33
|
+
return ConfigResponse(
|
|
34
|
+
version="0.1.0a1",
|
|
35
|
+
storage_backends={
|
|
36
|
+
"memory": "fs_jsonl",
|
|
37
|
+
"artifacts": "fs",
|
|
38
|
+
},
|
|
39
|
+
llm_providers=[
|
|
40
|
+
ConfigLLMProvider(name="openai", model="gpt-4o-mini", enabled=True),
|
|
41
|
+
],
|
|
42
|
+
features={
|
|
43
|
+
"ws_channels": True,
|
|
44
|
+
"artifact_search": True,
|
|
45
|
+
"memory_search": True,
|
|
46
|
+
},
|
|
47
|
+
)
|