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.
Files changed (267) hide show
  1. aethergraph/__init__.py +4 -10
  2. aethergraph/__main__.py +293 -0
  3. aethergraph/api/v1/__init__.py +0 -0
  4. aethergraph/api/v1/agents.py +46 -0
  5. aethergraph/api/v1/apps.py +70 -0
  6. aethergraph/api/v1/artifacts.py +415 -0
  7. aethergraph/api/v1/channels.py +89 -0
  8. aethergraph/api/v1/deps.py +168 -0
  9. aethergraph/api/v1/graphs.py +259 -0
  10. aethergraph/api/v1/identity.py +25 -0
  11. aethergraph/api/v1/memory.py +353 -0
  12. aethergraph/api/v1/misc.py +47 -0
  13. aethergraph/api/v1/pagination.py +29 -0
  14. aethergraph/api/v1/runs.py +568 -0
  15. aethergraph/api/v1/schemas.py +535 -0
  16. aethergraph/api/v1/session.py +323 -0
  17. aethergraph/api/v1/stats.py +201 -0
  18. aethergraph/api/v1/viz.py +152 -0
  19. aethergraph/config/config.py +22 -0
  20. aethergraph/config/loader.py +3 -2
  21. aethergraph/config/storage.py +209 -0
  22. aethergraph/contracts/__init__.py +0 -0
  23. aethergraph/contracts/services/__init__.py +0 -0
  24. aethergraph/contracts/services/artifacts.py +27 -14
  25. aethergraph/contracts/services/memory.py +45 -17
  26. aethergraph/contracts/services/metering.py +129 -0
  27. aethergraph/contracts/services/runs.py +50 -0
  28. aethergraph/contracts/services/sessions.py +87 -0
  29. aethergraph/contracts/services/state_stores.py +3 -0
  30. aethergraph/contracts/services/viz.py +44 -0
  31. aethergraph/contracts/storage/artifact_index.py +88 -0
  32. aethergraph/contracts/storage/artifact_store.py +99 -0
  33. aethergraph/contracts/storage/async_kv.py +34 -0
  34. aethergraph/contracts/storage/blob_store.py +50 -0
  35. aethergraph/contracts/storage/doc_store.py +35 -0
  36. aethergraph/contracts/storage/event_log.py +31 -0
  37. aethergraph/contracts/storage/vector_index.py +48 -0
  38. aethergraph/core/__init__.py +0 -0
  39. aethergraph/core/execution/forward_scheduler.py +13 -2
  40. aethergraph/core/execution/global_scheduler.py +21 -15
  41. aethergraph/core/execution/step_forward.py +10 -1
  42. aethergraph/core/graph/__init__.py +0 -0
  43. aethergraph/core/graph/graph_builder.py +8 -4
  44. aethergraph/core/graph/graph_fn.py +156 -15
  45. aethergraph/core/graph/graph_spec.py +8 -0
  46. aethergraph/core/graph/graphify.py +146 -27
  47. aethergraph/core/graph/node_spec.py +0 -2
  48. aethergraph/core/graph/node_state.py +3 -0
  49. aethergraph/core/graph/task_graph.py +39 -1
  50. aethergraph/core/runtime/__init__.py +0 -0
  51. aethergraph/core/runtime/ad_hoc_context.py +64 -4
  52. aethergraph/core/runtime/base_service.py +28 -4
  53. aethergraph/core/runtime/execution_context.py +13 -15
  54. aethergraph/core/runtime/graph_runner.py +222 -37
  55. aethergraph/core/runtime/node_context.py +510 -6
  56. aethergraph/core/runtime/node_services.py +12 -5
  57. aethergraph/core/runtime/recovery.py +15 -1
  58. aethergraph/core/runtime/run_manager.py +783 -0
  59. aethergraph/core/runtime/run_manager_local.py +204 -0
  60. aethergraph/core/runtime/run_registration.py +2 -2
  61. aethergraph/core/runtime/run_types.py +89 -0
  62. aethergraph/core/runtime/runtime_env.py +136 -7
  63. aethergraph/core/runtime/runtime_metering.py +71 -0
  64. aethergraph/core/runtime/runtime_registry.py +36 -13
  65. aethergraph/core/runtime/runtime_services.py +194 -6
  66. aethergraph/core/tools/builtins/toolset.py +1 -1
  67. aethergraph/core/tools/toolkit.py +5 -0
  68. aethergraph/plugins/agents/default_chat_agent copy.py +90 -0
  69. aethergraph/plugins/agents/default_chat_agent.py +171 -0
  70. aethergraph/plugins/agents/shared.py +81 -0
  71. aethergraph/plugins/channel/adapters/webui.py +112 -112
  72. aethergraph/plugins/channel/routes/webui_routes.py +367 -102
  73. aethergraph/plugins/channel/utils/slack_utils.py +115 -59
  74. aethergraph/plugins/channel/utils/telegram_utils.py +88 -47
  75. aethergraph/plugins/channel/websockets/weibui_ws.py +172 -0
  76. aethergraph/runtime/__init__.py +15 -0
  77. aethergraph/server/app_factory.py +190 -34
  78. aethergraph/server/clients/channel_client.py +202 -0
  79. aethergraph/server/http/channel_http_routes.py +116 -0
  80. aethergraph/server/http/channel_ws_routers.py +45 -0
  81. aethergraph/server/loading.py +117 -0
  82. aethergraph/server/server.py +131 -0
  83. aethergraph/server/server_state.py +240 -0
  84. aethergraph/server/start.py +227 -66
  85. aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  86. aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  87. aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  88. aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  89. aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  90. aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  91. aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  92. aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  93. aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  94. aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  95. aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  96. aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  97. aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  98. aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  99. aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  100. aethergraph/server/ui_static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  101. aethergraph/server/ui_static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  102. aethergraph/server/ui_static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  103. aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  104. aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  105. aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  106. aethergraph/server/ui_static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  107. aethergraph/server/ui_static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  108. aethergraph/server/ui_static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  109. aethergraph/server/ui_static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  110. aethergraph/server/ui_static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  111. aethergraph/server/ui_static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  112. aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  113. aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  114. aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  115. aethergraph/server/ui_static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  116. aethergraph/server/ui_static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  117. aethergraph/server/ui_static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  118. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  119. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  120. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  121. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  122. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  123. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  124. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  125. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  126. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  127. aethergraph/server/ui_static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  128. aethergraph/server/ui_static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  129. aethergraph/server/ui_static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  130. aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  131. aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  132. aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  133. aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  134. aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  135. aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  136. aethergraph/server/ui_static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  137. aethergraph/server/ui_static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  138. aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  139. aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  140. aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  141. aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  142. aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  143. aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  144. aethergraph/server/ui_static/assets/index-BR5GtXcZ.css +1 -0
  145. aethergraph/server/ui_static/assets/index-CQ0HZZ83.js +400 -0
  146. aethergraph/server/ui_static/index.html +15 -0
  147. aethergraph/server/ui_static/logo.png +0 -0
  148. aethergraph/services/artifacts/__init__.py +0 -0
  149. aethergraph/services/artifacts/facade.py +1239 -132
  150. aethergraph/services/auth/{dev.py → authn.py} +0 -8
  151. aethergraph/services/auth/authz.py +100 -0
  152. aethergraph/services/channel/__init__.py +0 -0
  153. aethergraph/services/channel/channel_bus.py +19 -1
  154. aethergraph/services/channel/factory.py +13 -1
  155. aethergraph/services/channel/ingress.py +311 -0
  156. aethergraph/services/channel/queue_adapter.py +75 -0
  157. aethergraph/services/channel/session.py +502 -19
  158. aethergraph/services/container/default_container.py +122 -43
  159. aethergraph/services/continuations/continuation.py +6 -0
  160. aethergraph/services/continuations/stores/fs_store.py +19 -0
  161. aethergraph/services/eventhub/event_hub.py +76 -0
  162. aethergraph/services/kv/__init__.py +0 -0
  163. aethergraph/services/kv/ephemeral.py +244 -0
  164. aethergraph/services/llm/__init__.py +0 -0
  165. aethergraph/services/llm/generic_client copy.py +691 -0
  166. aethergraph/services/llm/generic_client.py +1288 -187
  167. aethergraph/services/llm/providers.py +3 -1
  168. aethergraph/services/llm/types.py +47 -0
  169. aethergraph/services/llm/utils.py +284 -0
  170. aethergraph/services/logger/std.py +3 -0
  171. aethergraph/services/mcp/__init__.py +9 -0
  172. aethergraph/services/mcp/http_client.py +38 -0
  173. aethergraph/services/mcp/service.py +225 -1
  174. aethergraph/services/mcp/stdio_client.py +41 -6
  175. aethergraph/services/mcp/ws_client.py +44 -2
  176. aethergraph/services/memory/__init__.py +0 -0
  177. aethergraph/services/memory/distillers/llm_long_term.py +234 -0
  178. aethergraph/services/memory/distillers/llm_meta_summary.py +398 -0
  179. aethergraph/services/memory/distillers/long_term.py +225 -0
  180. aethergraph/services/memory/facade/__init__.py +3 -0
  181. aethergraph/services/memory/facade/chat.py +440 -0
  182. aethergraph/services/memory/facade/core.py +447 -0
  183. aethergraph/services/memory/facade/distillation.py +424 -0
  184. aethergraph/services/memory/facade/rag.py +410 -0
  185. aethergraph/services/memory/facade/results.py +315 -0
  186. aethergraph/services/memory/facade/retrieval.py +139 -0
  187. aethergraph/services/memory/facade/types.py +77 -0
  188. aethergraph/services/memory/facade/utils.py +43 -0
  189. aethergraph/services/memory/facade_dep.py +1539 -0
  190. aethergraph/services/memory/factory.py +9 -3
  191. aethergraph/services/memory/utils.py +10 -0
  192. aethergraph/services/metering/eventlog_metering.py +470 -0
  193. aethergraph/services/metering/noop.py +25 -4
  194. aethergraph/services/rag/__init__.py +0 -0
  195. aethergraph/services/rag/facade.py +279 -23
  196. aethergraph/services/rag/index_factory.py +2 -2
  197. aethergraph/services/rag/node_rag.py +317 -0
  198. aethergraph/services/rate_limit/inmem_rate_limit.py +24 -0
  199. aethergraph/services/registry/__init__.py +0 -0
  200. aethergraph/services/registry/agent_app_meta.py +419 -0
  201. aethergraph/services/registry/registry_key.py +1 -1
  202. aethergraph/services/registry/unified_registry.py +74 -6
  203. aethergraph/services/scope/scope.py +159 -0
  204. aethergraph/services/scope/scope_factory.py +164 -0
  205. aethergraph/services/state_stores/serialize.py +5 -0
  206. aethergraph/services/state_stores/utils.py +2 -1
  207. aethergraph/services/viz/__init__.py +0 -0
  208. aethergraph/services/viz/facade.py +413 -0
  209. aethergraph/services/viz/viz_service.py +69 -0
  210. aethergraph/storage/artifacts/artifact_index_jsonl.py +180 -0
  211. aethergraph/storage/artifacts/artifact_index_sqlite.py +426 -0
  212. aethergraph/storage/artifacts/cas_store.py +422 -0
  213. aethergraph/storage/artifacts/fs_cas.py +18 -0
  214. aethergraph/storage/artifacts/s3_cas.py +14 -0
  215. aethergraph/storage/artifacts/utils.py +124 -0
  216. aethergraph/storage/blob/fs_blob.py +86 -0
  217. aethergraph/storage/blob/s3_blob.py +115 -0
  218. aethergraph/storage/continuation_store/fs_cont.py +283 -0
  219. aethergraph/storage/continuation_store/inmem_cont.py +146 -0
  220. aethergraph/storage/continuation_store/kvdoc_cont.py +261 -0
  221. aethergraph/storage/docstore/fs_doc.py +63 -0
  222. aethergraph/storage/docstore/sqlite_doc.py +31 -0
  223. aethergraph/storage/docstore/sqlite_doc_sync.py +90 -0
  224. aethergraph/storage/eventlog/fs_event.py +136 -0
  225. aethergraph/storage/eventlog/sqlite_event.py +47 -0
  226. aethergraph/storage/eventlog/sqlite_event_sync.py +178 -0
  227. aethergraph/storage/factory.py +432 -0
  228. aethergraph/storage/fs_utils.py +28 -0
  229. aethergraph/storage/graph_state_store/state_store.py +64 -0
  230. aethergraph/storage/kv/inmem_kv.py +103 -0
  231. aethergraph/storage/kv/layered_kv.py +52 -0
  232. aethergraph/storage/kv/sqlite_kv.py +39 -0
  233. aethergraph/storage/kv/sqlite_kv_sync.py +98 -0
  234. aethergraph/storage/memory/event_persist.py +68 -0
  235. aethergraph/storage/memory/fs_persist.py +118 -0
  236. aethergraph/{services/memory/hotlog_kv.py → storage/memory/hotlog.py} +8 -2
  237. aethergraph/{services → storage}/memory/indices.py +31 -7
  238. aethergraph/storage/metering/meter_event.py +55 -0
  239. aethergraph/storage/runs/doc_store.py +280 -0
  240. aethergraph/storage/runs/inmen_store.py +82 -0
  241. aethergraph/storage/runs/sqlite_run_store.py +403 -0
  242. aethergraph/storage/sessions/doc_store.py +183 -0
  243. aethergraph/storage/sessions/inmem_store.py +110 -0
  244. aethergraph/storage/sessions/sqlite_session_store.py +399 -0
  245. aethergraph/storage/vector_index/chroma_index.py +138 -0
  246. aethergraph/storage/vector_index/faiss_index.py +179 -0
  247. aethergraph/storage/vector_index/sqlite_index.py +187 -0
  248. {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/METADATA +138 -31
  249. aethergraph-0.1.0a2.dist-info/RECORD +356 -0
  250. aethergraph-0.1.0a2.dist-info/entry_points.txt +3 -0
  251. aethergraph/services/artifacts/factory.py +0 -35
  252. aethergraph/services/artifacts/fs_store.py +0 -656
  253. aethergraph/services/artifacts/jsonl_index.py +0 -123
  254. aethergraph/services/artifacts/sqlite_index.py +0 -209
  255. aethergraph/services/memory/distillers/episode.py +0 -116
  256. aethergraph/services/memory/distillers/rolling.py +0 -74
  257. aethergraph/services/memory/facade.py +0 -633
  258. aethergraph/services/memory/persist_fs.py +0 -40
  259. aethergraph/services/rag/index/base.py +0 -27
  260. aethergraph/services/rag/index/faiss_index.py +0 -121
  261. aethergraph/services/rag/index/sqlite_index.py +0 -134
  262. aethergraph-0.1.0a1.dist-info/RECORD +0 -182
  263. aethergraph-0.1.0a1.dist-info/entry_points.txt +0 -2
  264. {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/WHEEL +0 -0
  265. {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/licenses/LICENSE +0 -0
  266. {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/licenses/NOTICE +0 -0
  267. {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,317 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ from aethergraph.services.rag.facade import RAGFacade, SearchHit
7
+ from aethergraph.services.scope.scope import Scope
8
+
9
+
10
+ @dataclass
11
+ class NodeRAG:
12
+ """
13
+ Node-scoped RAG helper.
14
+
15
+ - Wraps a global RAGFacade.
16
+ - Injects Scope into the common node-facing calls.
17
+ - Delegates everything else via __getattr__.
18
+ """
19
+
20
+ rag: RAGFacade
21
+ scope: Scope
22
+ default_scope_id: str | None = None
23
+
24
+ # -------- internals --------
25
+
26
+ def _scope_id(self, scope_id: str | None) -> str | None:
27
+ if scope_id is not None:
28
+ return scope_id
29
+ if self.default_scope_id is not None:
30
+ return self.default_scope_id
31
+ return self.scope.memory_scope_id()
32
+
33
+ # -------- scope-aware helpers --------
34
+
35
+ async def bind_corpus(
36
+ self,
37
+ *,
38
+ corpus_id: str | None = None,
39
+ key: str | None = None,
40
+ create_if_missing: bool = True,
41
+ labels: dict[str, Any] | None = None,
42
+ scope_id: str | None = None,
43
+ ) -> str:
44
+ """
45
+ Bind or create a RAG corpus for the current node scope.
46
+
47
+ This method ensures a corpus exists for the given scope and key, creating it if necessary.
48
+ It automatically injects scope labels and metadata, and returns the resolved corpus ID.
49
+
50
+ Examples:
51
+ Bind a default corpus for the current node:
52
+ ```python
53
+ corpus_id = await context.rag().bind_corpus()
54
+ ```
55
+
56
+ Bind or create a corpus with a custom key and extra labels:
57
+ ```python
58
+ corpus_id = await context.rag().bind_corpus(
59
+ key="my-data",
60
+ labels={"source": "user-upload"}
61
+ )
62
+ ```
63
+
64
+ Args:
65
+ corpus_id: Optional explicit corpus identifier. If not provided, one is generated from the scope and key.
66
+ key: Optional string to distinguish corpora within the same scope (e.g., "default", "my-data").
67
+ create_if_missing: If True (default), create the corpus if it does not exist.
68
+ labels: Optional dictionary of additional metadata to attach to the corpus.
69
+ scope_id: Optional override for the scope identifier. Defaults to the current node's scope.
70
+
71
+ Returns:
72
+ str: The resolved corpus ID, guaranteed to exist if `create_if_missing` is True.
73
+
74
+ Notes:
75
+ - The corpus ID is derived from the scope and key if not explicitly provided.
76
+ - Scope labels are automatically merged into the corpus metadata.
77
+ """
78
+ sid = self._scope_id(scope_id)
79
+ scope_labels = self.scope.rag_scope_labels(scope_id=sid)
80
+
81
+ if corpus_id:
82
+ cid = corpus_id
83
+ else:
84
+ # e.g. mem:<scope>:<key>
85
+ cid = self.scope.rag_corpus_id(scope_id=sid, key=key or "default")
86
+
87
+ meta = {"scope": scope_labels, **(labels or {})}
88
+
89
+ if create_if_missing:
90
+ await self.rag.add_corpus(
91
+ corpus_id=cid,
92
+ meta=meta,
93
+ scope_labels=scope_labels,
94
+ )
95
+ return cid
96
+
97
+ async def upsert_docs(
98
+ self,
99
+ corpus_id: str,
100
+ docs: list[dict[str, Any]],
101
+ *,
102
+ scope_id: str | None = None,
103
+ ) -> dict[str, Any]:
104
+ """
105
+ Ingest and index a list of documents into the specified corpus for the current node scope.
106
+
107
+ This method ensures the corpus exists for the given scope, merges scope labels into each document,
108
+ and handles both file-based and inline text documents. Documents are chunked, embedded, and indexed
109
+ for retrieval.
110
+
111
+ Examples:
112
+ Ingest a list of inline documents:
113
+ ```python
114
+ await context.rag().upsert_docs(
115
+ corpus_id="my-corpus",
116
+ docs=[
117
+ {"text": "Document content...", "title": "Doc Title"},
118
+ {"text": "Another doc", "labels": {"source": "user-upload"}}
119
+ ]
120
+ )
121
+ ```
122
+
123
+ Ingest a PDF file with custom labels:
124
+ ```python
125
+ await context.rag().upsert_docs(
126
+ corpus_id="my-corpus",
127
+ docs=[{"path": "/path/to/file.pdf", "labels": {"type": "pdf"}}]
128
+ )
129
+ ```
130
+
131
+ Args:
132
+ corpus_id: The target corpus identifier.
133
+ docs: A list of document specifications. Each document can be:
134
+ - File-based: {"path": "/path/to/doc.pdf", "labels": {...}}
135
+ - Inline text: {"text": "Document content...", "title": "Doc Title", "labels": {...}}
136
+ scope_id: Optional override for the scope identifier. Defaults to the current node's scope.
137
+
138
+ Returns:
139
+ dict[str, Any]: Summary of the ingestion, including number of documents and chunks added.
140
+
141
+ Notes:
142
+ - Scope labels are merged into each document's labels.
143
+ - File-based documents are read and chunked automatically.
144
+ - Inline text documents are chunked based on configured chunk size.
145
+ """
146
+ sid = self._scope_id(scope_id)
147
+ return await self.rag.upsert_docs(
148
+ corpus_id=corpus_id,
149
+ docs=docs,
150
+ scope=self.scope,
151
+ scope_id=sid,
152
+ )
153
+
154
+ async def search(
155
+ self,
156
+ corpus_id: str,
157
+ query: str,
158
+ *,
159
+ k: int = 8,
160
+ filters: dict[str, Any] | None = None,
161
+ scope_id: str | None = None,
162
+ mode: str = "hybrid",
163
+ ) -> list[SearchHit]:
164
+ """
165
+ Search the specified RAG corpus for relevant chunks matching a query.
166
+
167
+ This method performs a dense or hybrid (dense + lexical) search over the corpus,
168
+ automatically injecting node scope filters. It returns the top-k most relevant
169
+ results as `SearchHit` objects, including chunk text, metadata, and scores.
170
+
171
+ Examples:
172
+ Basic usage to search a corpus:
173
+ ```python
174
+ hits = await context.rag().search(
175
+ corpus_id="my-corpus",
176
+ query="What is the capital of France?"
177
+ )
178
+ ```
179
+
180
+ Search with custom filters and top-3 results:
181
+ ```python
182
+ hits = await context.rag().search(
183
+ corpus_id="my-corpus",
184
+ query="project roadmap",
185
+ k=3,
186
+ filters={"type": "meeting-notes"}
187
+ )
188
+ ```
189
+
190
+ Args:
191
+ corpus_id: The target corpus identifier to search within.
192
+ query: The search query string.
193
+ k: The number of top results to return (default: 8).
194
+ filters: Optional dictionary of metadata filters to apply (merged with scope filters).
195
+ scope_id: Optional override for the scope identifier. Defaults to the current node's scope.
196
+ mode: Search mode, either `"dense"` or `"hybrid"` (default: "hybrid").
197
+
198
+ Returns:
199
+ list[SearchHit]: A list of matching `SearchHit` objects, each containing chunk text,
200
+ metadata, score, and identifiers.
201
+
202
+ Notes:
203
+ - Scope filters are automatically merged with any provided filters.
204
+ - Hybrid mode fuses dense and lexical search for improved relevance.
205
+ - Results are sorted by descending relevance score.
206
+ """
207
+ sid = self._scope_id(scope_id)
208
+ scoped_filters = self.scope.rag_filter(scope_id=sid)
209
+ if filters:
210
+ scoped_filters.update(filters)
211
+ return await self.rag.search(
212
+ corpus_id=corpus_id,
213
+ query=query,
214
+ k=k,
215
+ filters=scoped_filters,
216
+ mode=mode,
217
+ )
218
+
219
+ async def answer(
220
+ self,
221
+ corpus_id: str,
222
+ question: str,
223
+ *,
224
+ llm: str | None = None,
225
+ style: str = "concise",
226
+ with_citations: bool = True,
227
+ k: int = 6,
228
+ scope_id: str | None = None,
229
+ ) -> dict[str, Any]:
230
+ """
231
+ Answer a question using retrieved context from a specified corpus.
232
+
233
+ This method retrieves relevant context chunks from the target corpus, constructs a prompt for the language model, and generates an answer. Citations to the retrieved chunks are included if requested. The function is accessed via `context.rag().answer(...)`.
234
+
235
+ Examples:
236
+ Basic usage to answer a question:
237
+ ```python
238
+ result = await context.rag().answer(
239
+ corpus_id="my-corpus",
240
+ question="What is the capital of France?"
241
+ print(result["answer"])
242
+ ```
243
+
244
+ Requesting a detailed answer with citations:
245
+ ```python
246
+ result = await context.rag().answer(
247
+ corpus_id="my-corpus",
248
+ question="Explain the process of photosynthesis.",
249
+ style="detailed",
250
+ with_citations=True,
251
+ k=8
252
+ )
253
+ print("Answer:", result["answer"])
254
+ for cite in result["citations"]:
255
+ print(f"Citation: {cite['text']} (Score: {cite['score']})")
256
+ ```
257
+
258
+ Args:
259
+ corpus_id: Identifier of the target corpus to search for context.
260
+ question: The question to be answered.
261
+ llm: Optional language model client to use for answer generation. If None, the default LLM is used.
262
+ style: The style of the answer, either "concise" (default) or "detailed".
263
+ with_citations: Whether to include citations to the retrieved context chunks in the answer (default: True).
264
+ k: Number of context chunks to retrieve for answering (default: 6).
265
+ scope_id: Optional identifier to restrict retrieval to a specific scope.
266
+
267
+ Returns:
268
+ dict[str, Any]: A dictionary containing the generated answer, citations, usage statistics, and optionally resolved citation metadata.
269
+
270
+ Notes:
271
+ - the generated dictionary includes:
272
+
273
+ - `answer`: The generated answer text.
274
+ - `citations`: List of retrieved context chunks used as citations.
275
+ - `usage`: LLM usage statistics (tokens, time, etc.).
276
+ - `resolved_citations`: Optional metadata for citations if available.
277
+
278
+ - Example response:
279
+ ```python
280
+ {
281
+ "answer": "The capital of France is Paris.",
282
+ "citations": [
283
+ {"text": "Paris is the capital city of France...", "score": 0.95, ...},
284
+ ...
285
+ ],
286
+ "usage": {"prompt_tokens": 150, "completion_tokens": 50, "total_tokens": 200, ...},
287
+ "resolved_citations": [
288
+ {"doc_id": "doc123", "title": "Geography of France", ...},
289
+ ...
290
+ ]
291
+ }
292
+ ```
293
+ """
294
+ sid = self._scope_id(scope_id)
295
+ return await self.rag.answer(
296
+ corpus_id=corpus_id,
297
+ question=question,
298
+ llm=llm,
299
+ style=style,
300
+ with_citations=with_citations,
301
+ k=k,
302
+ scope=self.scope,
303
+ scope_id=sid,
304
+ )
305
+
306
+ # -------- delegation: everything else --------
307
+
308
+ def __getattr__(self, name: str) -> Any:
309
+ """
310
+ Fallback: expose the underlying RAGFacade API for advanced users.
311
+
312
+ Node code can still call low-level stuff if needed:
313
+ ctx.rag.stats(...)
314
+ ctx.rag.list_corpora()
315
+ etc.
316
+ """
317
+ return getattr(self.rag, name)
@@ -0,0 +1,24 @@
1
+ from collections import defaultdict, deque
2
+ import time
3
+
4
+
5
+ class SimpleRateLimiter:
6
+ def __init__(self, max_events: int, window_seconds: int):
7
+ self.max_events = max_events
8
+ self.window = window_seconds
9
+ self._events: dict[str, deque[float]] = defaultdict(deque)
10
+
11
+ def allow(self, key: str) -> bool:
12
+ now = time.time()
13
+ dq = self._events[key]
14
+ cutoff = now - self.window
15
+
16
+ # Drop old events
17
+ while dq and dq[0] < cutoff:
18
+ dq.popleft()
19
+
20
+ if len(dq) >= self.max_events:
21
+ return False # Rate limit exceeded
22
+
23
+ dq.append(now)
24
+ return True
File without changes