aethergraph 0.1.0a2__py3-none-any.whl → 0.1.0a4__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 (114) hide show
  1. aethergraph/__main__.py +3 -0
  2. aethergraph/api/v1/artifacts.py +23 -4
  3. aethergraph/api/v1/schemas.py +7 -0
  4. aethergraph/api/v1/session.py +123 -4
  5. aethergraph/config/config.py +2 -0
  6. aethergraph/config/search.py +49 -0
  7. aethergraph/contracts/services/channel.py +18 -1
  8. aethergraph/contracts/services/execution.py +58 -0
  9. aethergraph/contracts/services/llm.py +26 -0
  10. aethergraph/contracts/services/memory.py +10 -4
  11. aethergraph/contracts/services/planning.py +53 -0
  12. aethergraph/contracts/storage/event_log.py +8 -0
  13. aethergraph/contracts/storage/search_backend.py +47 -0
  14. aethergraph/contracts/storage/vector_index.py +73 -0
  15. aethergraph/core/graph/action_spec.py +76 -0
  16. aethergraph/core/graph/graph_fn.py +75 -2
  17. aethergraph/core/graph/graphify.py +74 -2
  18. aethergraph/core/runtime/graph_runner.py +2 -1
  19. aethergraph/core/runtime/node_context.py +66 -3
  20. aethergraph/core/runtime/node_services.py +8 -0
  21. aethergraph/core/runtime/run_manager.py +263 -271
  22. aethergraph/core/runtime/run_types.py +54 -1
  23. aethergraph/core/runtime/runtime_env.py +35 -14
  24. aethergraph/core/runtime/runtime_services.py +308 -18
  25. aethergraph/plugins/agents/default_chat_agent.py +266 -74
  26. aethergraph/plugins/agents/default_chat_agent_v2.py +487 -0
  27. aethergraph/plugins/channel/adapters/webui.py +69 -21
  28. aethergraph/plugins/channel/routes/webui_routes.py +8 -48
  29. aethergraph/runtime/__init__.py +12 -0
  30. aethergraph/server/app_factory.py +10 -1
  31. aethergraph/server/ui_static/assets/index-CFktGdbW.js +4913 -0
  32. aethergraph/server/ui_static/assets/index-DcfkFlTA.css +1 -0
  33. aethergraph/server/ui_static/index.html +2 -2
  34. aethergraph/services/artifacts/facade.py +157 -21
  35. aethergraph/services/artifacts/types.py +35 -0
  36. aethergraph/services/artifacts/utils.py +42 -0
  37. aethergraph/services/channel/channel_bus.py +3 -1
  38. aethergraph/services/channel/event_hub copy.py +55 -0
  39. aethergraph/services/channel/event_hub.py +81 -0
  40. aethergraph/services/channel/factory.py +3 -2
  41. aethergraph/services/channel/session.py +709 -74
  42. aethergraph/services/container/default_container.py +69 -7
  43. aethergraph/services/execution/__init__.py +0 -0
  44. aethergraph/services/execution/local_python.py +118 -0
  45. aethergraph/services/indices/__init__.py +0 -0
  46. aethergraph/services/indices/global_indices.py +21 -0
  47. aethergraph/services/indices/scoped_indices.py +292 -0
  48. aethergraph/services/llm/generic_client.py +342 -46
  49. aethergraph/services/llm/generic_embed_client.py +359 -0
  50. aethergraph/services/llm/types.py +3 -1
  51. aethergraph/services/memory/distillers/llm_long_term.py +60 -109
  52. aethergraph/services/memory/distillers/llm_long_term_v1.py +180 -0
  53. aethergraph/services/memory/distillers/llm_meta_summary.py +57 -266
  54. aethergraph/services/memory/distillers/llm_meta_summary_v1.py +342 -0
  55. aethergraph/services/memory/distillers/long_term.py +48 -131
  56. aethergraph/services/memory/distillers/long_term_v1.py +170 -0
  57. aethergraph/services/memory/facade/chat.py +18 -8
  58. aethergraph/services/memory/facade/core.py +159 -19
  59. aethergraph/services/memory/facade/distillation.py +86 -31
  60. aethergraph/services/memory/facade/retrieval.py +100 -1
  61. aethergraph/services/memory/factory.py +4 -1
  62. aethergraph/services/planning/__init__.py +0 -0
  63. aethergraph/services/planning/action_catalog.py +271 -0
  64. aethergraph/services/planning/bindings.py +56 -0
  65. aethergraph/services/planning/dependency_index.py +65 -0
  66. aethergraph/services/planning/flow_validator.py +263 -0
  67. aethergraph/services/planning/graph_io_adapter.py +150 -0
  68. aethergraph/services/planning/input_parser.py +312 -0
  69. aethergraph/services/planning/missing_inputs.py +28 -0
  70. aethergraph/services/planning/node_planner.py +613 -0
  71. aethergraph/services/planning/orchestrator.py +112 -0
  72. aethergraph/services/planning/plan_executor.py +506 -0
  73. aethergraph/services/planning/plan_types.py +321 -0
  74. aethergraph/services/planning/planner.py +617 -0
  75. aethergraph/services/planning/planner_service.py +369 -0
  76. aethergraph/services/planning/planning_context_builder.py +43 -0
  77. aethergraph/services/planning/quick_actions.py +29 -0
  78. aethergraph/services/planning/routers/__init__.py +0 -0
  79. aethergraph/services/planning/routers/simple_router.py +26 -0
  80. aethergraph/services/rag/facade.py +0 -3
  81. aethergraph/services/scope/scope.py +30 -30
  82. aethergraph/services/scope/scope_factory.py +15 -7
  83. aethergraph/services/skills/__init__.py +0 -0
  84. aethergraph/services/skills/skill_registry.py +465 -0
  85. aethergraph/services/skills/skills.py +220 -0
  86. aethergraph/services/skills/utils.py +194 -0
  87. aethergraph/storage/artifacts/artifact_index_jsonl.py +16 -10
  88. aethergraph/storage/artifacts/artifact_index_sqlite.py +12 -2
  89. aethergraph/storage/docstore/sqlite_doc_sync.py +1 -1
  90. aethergraph/storage/memory/event_persist.py +42 -2
  91. aethergraph/storage/memory/fs_persist.py +32 -2
  92. aethergraph/storage/search_backend/__init__.py +0 -0
  93. aethergraph/storage/search_backend/generic_vector_backend.py +230 -0
  94. aethergraph/storage/search_backend/null_backend.py +34 -0
  95. aethergraph/storage/search_backend/sqlite_lexical_backend.py +387 -0
  96. aethergraph/storage/search_backend/utils.py +31 -0
  97. aethergraph/storage/search_factory.py +75 -0
  98. aethergraph/storage/vector_index/faiss_index.py +72 -4
  99. aethergraph/storage/vector_index/sqlite_index.py +521 -52
  100. aethergraph/storage/vector_index/sqlite_index_vanila.py +311 -0
  101. aethergraph/storage/vector_index/utils.py +22 -0
  102. {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/METADATA +1 -1
  103. {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/RECORD +108 -64
  104. {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/WHEEL +1 -1
  105. aethergraph/plugins/agents/default_chat_agent copy.py +0 -90
  106. aethergraph/server/ui_static/assets/index-BR5GtXcZ.css +0 -1
  107. aethergraph/server/ui_static/assets/index-CQ0HZZ83.js +0 -400
  108. aethergraph/services/eventhub/event_hub.py +0 -76
  109. aethergraph/services/llm/generic_client copy.py +0 -691
  110. aethergraph/services/prompts/file_store.py +0 -41
  111. {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/entry_points.txt +0 -0
  112. {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/licenses/LICENSE +0 -0
  113. {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/licenses/NOTICE +0 -0
  114. {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/top_level.txt +0 -0
@@ -6,6 +6,7 @@ from typing import Any
6
6
 
7
7
  # ---- core services ----
8
8
  from aethergraph.config.config import AppSettings
9
+ from aethergraph.contracts.services.execution import ExecutionService
9
10
 
10
11
  # ---- optional services (not used by default) ----
11
12
  from aethergraph.contracts.services.llm import LLMClientProtocol
@@ -27,6 +28,9 @@ from aethergraph.services.auth.authn import DevTokenAuthn
27
28
  from aethergraph.services.auth.authz import AllowAllAuthz
28
29
  from aethergraph.services.channel.channel_bus import ChannelBus
29
30
 
31
+ # from aethergraph.services.eventhub.event_hub import EventHub
32
+ from aethergraph.services.channel.event_hub import EventHub
33
+
30
34
  # ---- channel services ----
31
35
  from aethergraph.services.channel.factory import build_bus, make_channel_adapters_from_env
32
36
  from aethergraph.services.channel.ingress import ChannelIngress
@@ -35,9 +39,14 @@ from aethergraph.services.continuations.stores.fs_store import (
35
39
  FSContinuationStore, # AsyncContinuationStore
36
40
  )
37
41
  from aethergraph.services.eventbus.inmem import InMemoryEventBus
42
+ from aethergraph.services.execution.local_python import LocalPythonExecutionService
43
+
44
+ # ---- Global Indices ----
45
+ from aethergraph.services.indices.global_indices import GlobalIndices
38
46
 
39
47
  # ---- kv services ----
40
48
  from aethergraph.services.llm.factory import build_llm_clients
49
+ from aethergraph.services.llm.generic_embed_client import GenericEmbeddingClient
41
50
  from aethergraph.services.llm.service import LLMService
42
51
  from aethergraph.services.logger.std import LoggingConfig, StdLoggerService
43
52
  from aethergraph.services.mcp.service import MCPService
@@ -45,11 +54,15 @@ from aethergraph.services.mcp.service import MCPService
45
54
  # ---- memory services ----
46
55
  from aethergraph.services.memory.factory import MemoryFactory
47
56
  from aethergraph.services.metering.eventlog_metering import EventLogMeteringService
48
- from aethergraph.services.prompts.file_store import FilePromptStore
57
+
58
+ # ---- Planning components ----
59
+ from aethergraph.services.planning.action_catalog import ActionCatalog
60
+ from aethergraph.services.planning.flow_validator import FlowValidator
61
+ from aethergraph.services.planning.planner_service import PlannerService
49
62
  from aethergraph.services.rag.chunker import TextSplitter
50
63
  from aethergraph.services.rag.facade import RAGFacade
51
64
 
52
- # ---- RAG components ----
65
+ # ---- Other components ----
53
66
  from aethergraph.services.rate_limit.inmem_rate_limit import SimpleRateLimiter
54
67
  from aethergraph.services.redactor.simple import RegexRedactor # Simple PII redactor
55
68
  from aethergraph.services.registry.unified_registry import UnifiedRegistry
@@ -58,10 +71,13 @@ from aethergraph.services.resume.router import ResumeRouter
58
71
  from aethergraph.services.schedulers.registry import SchedulerRegistry
59
72
  from aethergraph.services.scope.scope_factory import ScopeFactory
60
73
  from aethergraph.services.secrets.env import EnvSecrets
74
+ from aethergraph.services.skills.skill_registry import SkillRegistry
61
75
  from aethergraph.services.tracing.noop import NoopTracer
62
76
  from aethergraph.services.viz.viz_service import VizService
63
77
  from aethergraph.services.waits.wait_registry import WaitRegistry
64
78
  from aethergraph.services.wakeup.memory_queue import ThreadSafeWakeupQueue
79
+
80
+ # ---- storage builders ----
65
81
  from aethergraph.storage.factory import (
66
82
  build_artifact_index,
67
83
  build_artifact_store,
@@ -78,6 +94,7 @@ from aethergraph.storage.factory import (
78
94
  )
79
95
  from aethergraph.storage.kv.inmem_kv import InMemoryKV as EphemeralKV
80
96
  from aethergraph.storage.metering.meter_event import EventLogMeteringStore
97
+ from aethergraph.storage.search_factory import build_search_backend
81
98
 
82
99
  SERVICE_KEYS = [
83
100
  # core
@@ -129,6 +146,7 @@ class DefaultContainer:
129
146
 
130
147
  # channels and interactions
131
148
  channels: ChannelBus
149
+ eventhub: EventHub
132
150
 
133
151
  # continuations and resume
134
152
  cont_store: FSContinuationStore
@@ -144,6 +162,7 @@ class DefaultContainer:
144
162
  artifacts: AsyncArtifactStore
145
163
  artifact_index: AsyncArtifactIndex
146
164
  eventlog: EventLog
165
+ global_indices: GlobalIndices
147
166
 
148
167
  # memory
149
168
  memory_factory: MemoryFactory
@@ -161,9 +180,15 @@ class DefaultContainer:
161
180
  run_manager: RunManager | None = None # RunManager
162
181
  session_store: SessionStore | None = None # SessionStore
163
182
 
183
+ # planner
184
+ planner_service: PlannerService | None = None
185
+
186
+ # skills
187
+ skills_registry: SkillRegistry | None = None
188
+
164
189
  # optional services (not used by default)
190
+ execution: ExecutionService | None = None
165
191
  event_bus: InMemoryEventBus | None = None
166
- prompts: FilePromptStore | None = None
167
192
  authn: DevTokenAuthn | None = None
168
193
  authz: AllowAllAuthz | None = None
169
194
  redactor: RegexRedactor | None = None
@@ -255,7 +280,10 @@ def build_default_container(
255
280
  }
256
281
 
257
282
  # channels
258
- channel_adapters = make_channel_adapters_from_env(cfg, event_log=eventlog)
283
+ event_hub = (
284
+ EventHub()
285
+ ) # in-memory event hub for WebUI and other real-time events; not configurable yet
286
+ channel_adapters = make_channel_adapters_from_env(cfg, event_log=eventlog, event_hub=event_hub)
259
287
  channels = build_bus(
260
288
  channel_adapters,
261
289
  default="console:stdin",
@@ -278,6 +306,7 @@ def build_default_container(
278
306
  ) # get secrets from env vars -- for local development; in prod, use a proper secrets manager
279
307
  llm_clients = build_llm_clients(cfg.llm, secrets) # return {profile: GenericLLMClient}
280
308
  llm_service = LLMService(clients=llm_clients) if llm_clients else None
309
+ embed_client = GenericEmbeddingClient(provider="openai", model="text-embedding-3-small")
281
310
 
282
311
  # RAG facade
283
312
  vec_index = build_vector_index(cfg)
@@ -295,12 +324,12 @@ def build_default_container(
295
324
  # memory factory
296
325
  persistence = build_memory_persistence(cfg)
297
326
  hotlog = build_memory_hotlog(cfg)
298
- indices = build_memory_indices(cfg)
327
+ memory_indices = build_memory_indices(cfg)
299
328
  docs = build_doc_store(cfg)
300
329
  memory_factory = MemoryFactory(
301
330
  hotlog=hotlog,
302
331
  persistence=persistence,
303
- indices=indices,
332
+ indices=memory_indices,
304
333
  artifacts=artifacts,
305
334
  docs=docs,
306
335
  hot_limit=int(cfg.memory.hot_limit),
@@ -337,6 +366,35 @@ def build_default_container(
337
366
  authn = DevTokenAuthn()
338
367
  authz = AllowAllAuthz()
339
368
 
369
+ # global scoped indices
370
+ # from aethergraph.storage.search_backend.generic_vector_backend import SQLiteVectorSearchBackend
371
+
372
+ # search_backend = SQLiteVectorSearchBackend(
373
+ # index=vec_index,
374
+ # embedder=embed_client,
375
+ # )
376
+
377
+ search_backend = build_search_backend(cfg=cfg, embedder=embed_client)
378
+ global_indices = GlobalIndices(backend=search_backend) # to be set up later as needed
379
+
380
+ # Execution service
381
+ execution = (
382
+ LocalPythonExecutionService()
383
+ ) # simple local python executor -- NOT SANDBOXED; just for local functionality testing
384
+
385
+ # Planner service
386
+ catalog = ActionCatalog(registry=registry)
387
+ flow_validator = FlowValidator(catalog=catalog)
388
+ planner_service = PlannerService(
389
+ catalog=catalog,
390
+ llm=llm_service.get("default") if llm_service else None,
391
+ validator=flow_validator,
392
+ run_manager=run_manager,
393
+ )
394
+
395
+ # skills registry
396
+ skills_registry = SkillRegistry()
397
+
340
398
  container = DefaultContainer(
341
399
  root=str(root_p),
342
400
  scope_factory=scope_factory,
@@ -345,16 +403,21 @@ def build_default_container(
345
403
  logger=logger_factory,
346
404
  clock=clock,
347
405
  channels=channels,
406
+ eventhub=event_hub,
407
+ skills_registry=skills_registry,
348
408
  cont_store=cont_store,
349
409
  sched_registry=sched_registry,
350
410
  wait_registry=wait_registry,
351
411
  resume_bus=resume_bus,
352
412
  resume_router=resume_router,
353
413
  wakeup_queue=wakeup_queue,
414
+ execution=execution,
415
+ planner_service=planner_service,
354
416
  kv_hot=kv_hot,
355
417
  state_store=state_store,
356
418
  artifacts=artifacts,
357
419
  artifact_index=artifact_index,
420
+ global_indices=global_indices,
358
421
  viz_service=viz_service,
359
422
  eventlog=eventlog,
360
423
  memory_factory=memory_factory,
@@ -366,7 +429,6 @@ def build_default_container(
366
429
  session_store=session_store,
367
430
  secrets=secrets,
368
431
  event_bus=None,
369
- prompts=None,
370
432
  authn=authn,
371
433
  authz=authz,
372
434
  redactor=None,
File without changes
@@ -0,0 +1,118 @@
1
+ import asyncio
2
+ from pathlib import Path
3
+ import subprocess
4
+ import tempfile
5
+
6
+ from aethergraph.contracts.services.execution import (
7
+ CodeExecutionRequest,
8
+ CodeExecutionResult,
9
+ ExecutionService,
10
+ )
11
+
12
+
13
+ class LocalPythonExecutionService(ExecutionService):
14
+ def __init__(self, python_executable: str = "python"):
15
+ self.python_executable = python_executable
16
+
17
+ async def execute(self, request: CodeExecutionRequest) -> CodeExecutionResult:
18
+ if request.language != "python":
19
+ return CodeExecutionResult(
20
+ stdout="",
21
+ stderr="",
22
+ exit_code=1,
23
+ error=f"Unsupported language: {request.language}",
24
+ metadata={"reason": "unsupported_language"},
25
+ )
26
+
27
+ with tempfile.TemporaryDirectory() as tmpdir:
28
+ tmp_path = Path(tmpdir)
29
+ script_path = tmp_path / "script.py"
30
+ script_path.write_text(request.code, encoding="utf-8")
31
+
32
+ cmd: list[str] = [self.python_executable, str(script_path)]
33
+ if request.args:
34
+ cmd.extend(request.args)
35
+
36
+ cwd = request.workdir or tmpdir
37
+ env: dict[str, str] | None = None
38
+ if request.env is not None:
39
+ import os
40
+
41
+ env = os.environ.copy()
42
+ env.update(request.env)
43
+
44
+ # Try async subprocess first
45
+ try:
46
+ proc = await asyncio.create_subprocess_exec(
47
+ *cmd,
48
+ cwd=cwd,
49
+ env=env,
50
+ stdout=asyncio.subprocess.PIPE,
51
+ stderr=asyncio.subprocess.PIPE,
52
+ )
53
+
54
+ try:
55
+ stdout_bytes, stderr_bytes = await asyncio.wait_for(
56
+ proc.communicate(), timeout=request.timeout_s
57
+ )
58
+ stdout = stdout_bytes.decode("utf-8", errors="replace")
59
+ stderr = stderr_bytes.decode("utf-8", errors="replace")
60
+
61
+ return CodeExecutionResult(
62
+ stdout=stdout,
63
+ stderr=stderr,
64
+ exit_code=proc.returncode,
65
+ error=None,
66
+ metadata={
67
+ "timeout_s": request.timeout_s,
68
+ "mode": "async_subprocess",
69
+ },
70
+ )
71
+ except asyncio.TimeoutError:
72
+ proc.kill()
73
+ await proc.wait()
74
+ return CodeExecutionResult(
75
+ stdout="",
76
+ stderr="Execution timed out",
77
+ exit_code=-1,
78
+ error="timeout",
79
+ metadata={
80
+ "timeout_s": request.timeout_s,
81
+ "mode": "async_subprocess",
82
+ },
83
+ )
84
+
85
+ except NotImplementedError as exc:
86
+ # Fallback for event loops that don't support subprocesses (common on Windows)
87
+ def _run_sync():
88
+ completed = subprocess.run(
89
+ cmd,
90
+ cwd=cwd,
91
+ env=env,
92
+ capture_output=True,
93
+ text=True,
94
+ )
95
+ return completed
96
+
97
+ completed = await asyncio.to_thread(_run_sync)
98
+
99
+ return CodeExecutionResult(
100
+ stdout=completed.stdout,
101
+ stderr=completed.stderr,
102
+ exit_code=completed.returncode,
103
+ error=None,
104
+ metadata={
105
+ "timeout_s": request.timeout_s,
106
+ "mode": "thread_subprocess_fallback",
107
+ "exception_type": type(exc).__name__,
108
+ },
109
+ )
110
+
111
+ except Exception as exc:
112
+ return CodeExecutionResult(
113
+ stdout="",
114
+ stderr=str(exc),
115
+ exit_code=-1,
116
+ error="spawn_failed",
117
+ metadata={"exception_type": type(exc).__name__},
118
+ )
File without changes
@@ -0,0 +1,21 @@
1
+ from dataclasses import dataclass
2
+
3
+ from aethergraph.contracts.storage.search_backend import SearchBackend
4
+ from aethergraph.services.indices.scoped_indices import ScopedIndices
5
+ from aethergraph.services.scope.scope import Scope
6
+
7
+
8
+ @dataclass
9
+ class GlobalIndices:
10
+ backend: SearchBackend
11
+
12
+ def for_scope(
13
+ self,
14
+ scope: Scope,
15
+ scope_id: str | None = None,
16
+ ) -> ScopedIndices:
17
+ return ScopedIndices(
18
+ backend=self.backend,
19
+ scope=scope,
20
+ scope_id=scope_id,
21
+ )
@@ -0,0 +1,292 @@
1
+ # aethergraph/indices.py
2
+ from __future__ import annotations
3
+
4
+ from collections.abc import Mapping
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+ from aethergraph.contracts.storage.search_backend import ScoredItem, SearchBackend
9
+ from aethergraph.services.scope.scope import Scope
10
+
11
+
12
+ @dataclass
13
+ class ScopedIndices:
14
+ """
15
+ Scope-aware wrapper around the global SearchBackend.
16
+
17
+ - scope: Scope defining org/user/app/run/session/node
18
+ - scope_id: usually a memory_scope_id for memory-tied corpora,
19
+ but can be anything logical (or None).
20
+ """
21
+
22
+ backend: SearchBackend
23
+ scope: Scope
24
+ scope_id: str | None = None
25
+
26
+ # --- internals --------------------------------------------------------
27
+
28
+ def _base_metadata(self) -> dict[str, Any]:
29
+ """
30
+ Default metadata to attach on *writes*.
31
+
32
+ For memory-ish corpora, this matches RAG docs:
33
+ - user_id, org_id, client_id, app_id, session_id, run_id, graph_id, node_id
34
+ - scope_id (usually memory_scope_id)
35
+ """
36
+ return self.scope.rag_labels(scope_id=self.scope_id)
37
+
38
+ def _base_filters(self) -> dict[str, Any]:
39
+ """
40
+ Default filters for *reads*.
41
+
42
+ For memory-ish corpora, this matches RAG search:
43
+ - user_id, org_id, (and scope_id if provided)
44
+ """
45
+ return self.scope.rag_filter(scope_id=self.scope_id)
46
+
47
+ # --- public APIs ------------------------------------------------------
48
+
49
+ async def upsert(
50
+ self,
51
+ *,
52
+ corpus: str,
53
+ item_id: str,
54
+ text: str,
55
+ metadata: Mapping[str, Any] | None = None,
56
+ ) -> None:
57
+ """
58
+ Upsert (insert or update) a text item with associated metadata into the backend index.
59
+ This method merges base metadata with any provided metadata, strips out keys with None values,
60
+ and delegates the upsert operation to the backend. This ensures that only meaningful metadata
61
+ is stored and that None values are treated as wildcards by the backend.
62
+
63
+ Examples:
64
+ Basic usage to upsert a text item:
65
+ ```python
66
+ await service.upsert(
67
+ corpus="my_corpus",
68
+ item_id="item123",
69
+ text="Sample document text."
70
+ ```
71
+
72
+ Upserting with additional metadata:
73
+ ```python
74
+ await service.upsert(
75
+ corpus="my_corpus",
76
+ item_id="item123",
77
+ text="Sample document text.",
78
+ metadata={"author": "Alice", "category": "news"}
79
+
80
+ ```
81
+ Args:
82
+ corpus: The name of the corpus or collection to upsert the item into.
83
+ item_id: The unique identifier for the item within the corpus.
84
+ text: The text content to be indexed or updated.
85
+ metadata: Optional mapping of additional metadata to associate with the item.
86
+
87
+ Returns:
88
+ None
89
+
90
+ Notes:
91
+ Metadata keys with None values are omitted before upserting to the backend.
92
+ """
93
+
94
+ base = self._base_metadata()
95
+ merged: dict[str, Any] = {**base, **(metadata or {})}
96
+ # strip None so backends can treat them as wildcards
97
+ merged = {k: v for k, v in merged.items() if v is not None}
98
+
99
+ await self.backend.upsert(
100
+ corpus=corpus,
101
+ item_id=item_id,
102
+ text=text,
103
+ metadata=merged,
104
+ )
105
+
106
+ async def search(
107
+ self,
108
+ *,
109
+ corpus: str,
110
+ query: str,
111
+ top_k: int = 10,
112
+ filters: Mapping[str, Any] | None = None,
113
+ time_window: str | None = None,
114
+ created_at_min: float | None = None,
115
+ created_at_max: float | None = None,
116
+ ) -> list[ScoredItem]:
117
+ """
118
+ Perform a search operation on the specified corpus.
119
+ This method executes a search query against the backend, applying optional filters,
120
+ time constraints, and other parameters to refine the results.
121
+
122
+ Examples:
123
+ Basic usage to search a corpus:
124
+ ```python
125
+ results = await search(corpus="documents", query="machine learning")
126
+ ```
127
+
128
+ Searching with additional filters and time constraints:
129
+ ```python
130
+ results = await search(
131
+ corpus="articles",
132
+ query="AI advancements",
133
+ top_k=5,
134
+ filters={"author": "John Doe"},
135
+ time_window="7d"
136
+ ```
137
+
138
+ Args:
139
+ corpus: The name of the corpus to search within.
140
+ query: The search query string.
141
+ top_k: The maximum number of results to return (default: 10).
142
+ filters: Optional dictionary of additional filters to apply to the search.
143
+ time_window: Optional human-friendly duration (e.g., "7d", "24h", "30m")
144
+ interpreted as [now - window, now] in created_at_ts. Ignored if `created_at_min` is provided.
145
+ created_at_min: Optional minimum UNIX timestamp (float) for filtering results by creation time.
146
+ created_at_max: Optional maximum UNIX timestamp (float) for filtering results by creation time.
147
+
148
+ Returns:
149
+ A list of `ScoredItem` objects representing the search results.
150
+
151
+ Notes:
152
+ - If `time_window` is provided, it is used to calculate the time range unless `created_at_min` is explicitly set.
153
+ - Filters with `None` values are automatically excluded from the search.
154
+ """
155
+ base = self._base_filters()
156
+ merged: dict[str, Any] = {**base, **(filters or {})}
157
+ merged = {k: v for k, v in merged.items() if v is not None}
158
+
159
+ return await self.backend.search(
160
+ corpus=corpus,
161
+ query=query,
162
+ top_k=top_k,
163
+ filters=merged,
164
+ time_window=time_window,
165
+ created_at_min=created_at_min,
166
+ created_at_max=created_at_max,
167
+ )
168
+
169
+ # ergonomic helpers (optional but nice)
170
+
171
+ async def search_events(
172
+ self,
173
+ query: str,
174
+ *,
175
+ top_k: int = 20,
176
+ filters: Mapping[str, Any] | None = None,
177
+ time_window: str | None = None,
178
+ created_at_min: float | None = None,
179
+ created_at_max: float | None = None,
180
+ ) -> list[ScoredItem]:
181
+ """
182
+ Perform a search for events based on the given query and optional filters.
183
+
184
+ This method queries the "event" corpus using the specified parameters to retrieve
185
+ a list of scored items matching the search criteria.
186
+
187
+ Examples:
188
+ Basic usage to search for events:
189
+ ```python
190
+ results = await search_events("error logs")
191
+ ```
192
+
193
+ Searching with additional filters and a time window:
194
+ ```python
195
+ results = await search_events(
196
+ "user activity",
197
+ top_k=10,
198
+ filters={"status": "active"},
199
+ time_window="last_24_hours",
200
+ created_at_min=1672531200.0,
201
+ created_at_max=1672617600.0
202
+ ```
203
+
204
+ Args:
205
+ query: The search query string.
206
+ top_k: The maximum number of results to return (default: 20).
207
+ filters: Optional dictionary of filters to apply to the search.
208
+ time_window: Optional time window for the search (e.g., "last_24_hours").
209
+ created_at_min: Optional minimum creation timestamp for filtering results.
210
+ created_at_max: Optional maximum creation timestamp for filtering results.
211
+
212
+ Returns:
213
+ A list of `ScoredItem` objects representing the search results.
214
+
215
+ Notes:
216
+ - The `filters` parameter allows you to specify key-value pairs to narrow down the search results.
217
+ - The `time_window` parameter can be used to specify a predefined time range for the search.
218
+ - The `created_at_min` and `created_at_max` parameters allow for fine-grained control over the
219
+ creation time range of the events being searched.
220
+ """
221
+
222
+ items = await self.search(
223
+ corpus="event",
224
+ query=query,
225
+ top_k=top_k,
226
+ filters=filters,
227
+ time_window=time_window,
228
+ created_at_min=created_at_min,
229
+ created_at_max=created_at_max,
230
+ )
231
+
232
+ return items
233
+
234
+ async def search_artifacts(
235
+ self,
236
+ query: str,
237
+ *,
238
+ top_k: int = 20,
239
+ filters: Mapping[str, Any] | None = None,
240
+ time_window: str | None = None,
241
+ created_at_min: float | None = None,
242
+ created_at_max: float | None = None,
243
+ ) -> list[ScoredItem]:
244
+ """
245
+ Perform a search for artifacts based on the provided query and optional filters.
246
+
247
+ This method queries the "artifact" corpus using the specified parameters and returns
248
+ a list of scored items matching the search criteria.
249
+
250
+ Examples:
251
+ Basic usage to search for artifacts:
252
+ ```python
253
+ results = await search_artifacts("example query")
254
+ ```
255
+
256
+ Searching with additional filters and a time window:
257
+ ```python
258
+ results = await search_artifacts(
259
+ "example query",
260
+ top_k=10,
261
+ filters={"type": "document"},
262
+ time_window="last_7_days",
263
+ created_at_min=1672531200.0,
264
+ created_at_max=1672617600.0,
265
+ ```
266
+
267
+ Args:
268
+ query: The search query string.
269
+ top_k: The maximum number of results to return (default: 20).
270
+ filters: Optional dictionary of filters to apply to the search.
271
+ time_window: Optional time window for the search (e.g., "last_7_days").
272
+ created_at_min: Optional minimum creation timestamp for filtering results.
273
+ created_at_max: Optional maximum creation timestamp for filtering results.
274
+
275
+ Returns:
276
+ A list of `ScoredItem` objects representing the search results.
277
+
278
+ Notes:
279
+ - The `filters` parameter allows specifying additional constraints for the search.
280
+ - The `time_window` parameter can be used to limit results to a specific time range.
281
+ - The `created_at_min` and `created_at_max` parameters allow filtering by creation time.
282
+ """
283
+
284
+ return await self.search(
285
+ corpus="artifact",
286
+ query=query,
287
+ top_k=top_k,
288
+ filters=filters,
289
+ time_window=time_window,
290
+ created_at_min=created_at_min,
291
+ created_at_max=created_at_max,
292
+ )