@agentikos/omega-os 0.1.0 → 0.2.0

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 (73) hide show
  1. package/README.md +25 -13
  2. package/bootstrap/lib/steps.sh +214 -9
  3. package/bootstrap/manifest.example.yaml +6 -1
  4. package/docs/COMPLETION-PLAN.md +48 -0
  5. package/omega/Agentik_Engine/README.md +25 -10
  6. package/omega/Agentik_Engine/omega_engine/__init__.py +66 -2
  7. package/omega/Agentik_Engine/omega_engine/account.py +505 -0
  8. package/omega/Agentik_Engine/omega_engine/autonomous.py +538 -0
  9. package/omega/Agentik_Engine/omega_engine/cli.py +467 -29
  10. package/omega/Agentik_Engine/omega_engine/daemons/__init__.py +14 -0
  11. package/omega/Agentik_Engine/omega_engine/daemons/autonomous.py +56 -0
  12. package/omega/Agentik_Engine/omega_engine/daemons/engine.py +187 -0
  13. package/omega/Agentik_Engine/omega_engine/daemons/telegram.py +231 -0
  14. package/omega/Agentik_Engine/omega_engine/educators/__init__.py +51 -0
  15. package/omega/Agentik_Engine/omega_engine/educators/artifact.py +65 -0
  16. package/omega/Agentik_Engine/omega_engine/educators/automation.py +76 -0
  17. package/omega/Agentik_Engine/omega_engine/educators/base.py +327 -0
  18. package/omega/Agentik_Engine/omega_engine/educators/claudecode.py +71 -0
  19. package/omega/Agentik_Engine/omega_engine/educators/connection.py +75 -0
  20. package/omega/Agentik_Engine/omega_engine/educators/coworker.py +68 -0
  21. package/omega/Agentik_Engine/omega_engine/educators/loop.py +82 -0
  22. package/omega/Agentik_Engine/omega_engine/educators/prompt.py +68 -0
  23. package/omega/Agentik_Engine/omega_engine/educators/skill.py +69 -0
  24. package/omega/Agentik_Engine/omega_engine/executor.py +46 -6
  25. package/omega/Agentik_Engine/omega_engine/mission.py +13 -1
  26. package/omega/Agentik_Engine/omega_engine/provider.py +247 -1
  27. package/omega/Agentik_Engine/omega_engine/rag/__init__.py +21 -0
  28. package/omega/Agentik_Engine/omega_engine/rag/agentic.py +83 -0
  29. package/omega/Agentik_Engine/omega_engine/rag/base.py +42 -0
  30. package/omega/Agentik_Engine/omega_engine/rag/corrective.py +119 -0
  31. package/omega/Agentik_Engine/omega_engine/rag/graph.py +169 -0
  32. package/omega/Agentik_Engine/omega_engine/rag/hybrid.py +205 -0
  33. package/omega/Agentik_Engine/omega_engine/rag/multimodal.py +136 -0
  34. package/omega/Agentik_Engine/omega_engine/rag/router.py +110 -0
  35. package/omega/Agentik_Engine/omega_engine/reducer.py +21 -3
  36. package/omega/Agentik_Engine/omega_engine/store.py +65 -5
  37. package/omega/Agentik_Engine/omega_engine/sync.py +304 -0
  38. package/omega/Agentik_Engine/omega_engine/tools.py +272 -0
  39. package/omega/Agentik_Engine/pyproject.toml +1 -1
  40. package/omega/Agentik_Engine/tests/test_account.py +333 -0
  41. package/omega/Agentik_Engine/tests/test_autonomous.py +361 -0
  42. package/omega/Agentik_Engine/tests/test_educators.py +233 -0
  43. package/omega/Agentik_Engine/tests/test_rag.py +287 -0
  44. package/omega/Agentik_Engine/tests/test_snapshot_partial.py +172 -0
  45. package/omega/Agentik_Engine/tests/test_tools_and_sync.py +312 -0
  46. package/omega/Agentik_SSOT/skills/rag-route.md +73 -0
  47. package/package.json +1 -1
  48. package/omega/Agentik_Engine/omega_engine/__pycache__/__init__.cpython-313.pyc +0 -0
  49. package/omega/Agentik_Engine/omega_engine/__pycache__/audit.cpython-313.pyc +0 -0
  50. package/omega/Agentik_Engine/omega_engine/__pycache__/audit_arsenal.cpython-313.pyc +0 -0
  51. package/omega/Agentik_Engine/omega_engine/__pycache__/barrier.cpython-313.pyc +0 -0
  52. package/omega/Agentik_Engine/omega_engine/__pycache__/bus.cpython-313.pyc +0 -0
  53. package/omega/Agentik_Engine/omega_engine/__pycache__/cli.cpython-313.pyc +0 -0
  54. package/omega/Agentik_Engine/omega_engine/__pycache__/events.cpython-313.pyc +0 -0
  55. package/omega/Agentik_Engine/omega_engine/__pycache__/executor.cpython-313.pyc +0 -0
  56. package/omega/Agentik_Engine/omega_engine/__pycache__/mission.cpython-313.pyc +0 -0
  57. package/omega/Agentik_Engine/omega_engine/__pycache__/progress.cpython-313.pyc +0 -0
  58. package/omega/Agentik_Engine/omega_engine/__pycache__/project.cpython-313.pyc +0 -0
  59. package/omega/Agentik_Engine/omega_engine/__pycache__/provider.cpython-313.pyc +0 -0
  60. package/omega/Agentik_Engine/omega_engine/__pycache__/reducer.cpython-313.pyc +0 -0
  61. package/omega/Agentik_Engine/omega_engine/__pycache__/report.cpython-313.pyc +0 -0
  62. package/omega/Agentik_Engine/omega_engine/__pycache__/router.cpython-313.pyc +0 -0
  63. package/omega/Agentik_Engine/omega_engine/__pycache__/store.cpython-313.pyc +0 -0
  64. package/omega/Agentik_Engine/omega_engine/__pycache__/supervisor.cpython-313.pyc +0 -0
  65. package/omega/Agentik_Engine/omega_engine/__pycache__/task.cpython-313.pyc +0 -0
  66. package/omega/Agentik_Engine/omega_engine/__pycache__/telegram.cpython-313.pyc +0 -0
  67. package/omega/Agentik_Engine/tests/__pycache__/test_audit_arsenal.cpython-313.pyc +0 -0
  68. package/omega/Agentik_Engine/tests/__pycache__/test_executor.cpython-313.pyc +0 -0
  69. package/omega/Agentik_Engine/tests/__pycache__/test_mission.cpython-313.pyc +0 -0
  70. package/omega/Agentik_Engine/tests/__pycache__/test_progress.cpython-313.pyc +0 -0
  71. package/omega/Agentik_Engine/tests/__pycache__/test_project.cpython-313.pyc +0 -0
  72. package/omega/Agentik_Engine/tests/__pycache__/test_reducer.cpython-313.pyc +0 -0
  73. package/omega/Agentik_Engine/tests/__pycache__/test_report.cpython-313.pyc +0 -0
@@ -0,0 +1,69 @@
1
+ """`skill-educator` — generates new skill specs + maintains the catalog.
2
+
3
+ The signature: a skill spec is the contract between the engine and a unit of
4
+ capability (a `/command`). It declares: the trigger phrases, the inputs, the
5
+ steps, the artifacts it produces, the verification gate, and the failure modes.
6
+
7
+ Target SSOT path: ``Agentik_SSOT/skills/<name>.md``.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ from omega_engine.educators.base import EducatorProposal, run_education
14
+ from omega_engine.provider import AgentProvider
15
+
16
+ _INSTRUCTIONS = """\
17
+ Produce ONE skill spec the engine will load as a runnable capability. The spec
18
+ MUST contain, in order, these sections:
19
+
20
+ 1. Name + one-line purpose (the question the skill answers).
21
+ 2. Trigger phrases (case-insensitive, FR + EN if applicable) — the strings
22
+ the router matches against. Be exhaustive: a missing trigger means the
23
+ skill never fires.
24
+ 3. Inputs schema — every parameter, with type + whether it is required +
25
+ what the default is. No optional-and-undocumented fields.
26
+ 4. Steps — numbered, each step one verb in imperative voice with a verify
27
+ checkpoint. State the input and output of each step.
28
+ 5. Artifacts produced — the file/path the skill writes, the schema of any
29
+ structured output.
30
+ 6. Verification gate — which forensic audit(s) (from the Quality Arsenal)
31
+ run on the artifact, and the minimum score. If none applies, state that
32
+ and explain why.
33
+ 7. Failure modes — list at least 3 ways the skill can fail and what the
34
+ engine does in each (retry, escalate, abort).
35
+
36
+ The spec MUST NOT:
37
+ - paraphrase another existing skill — state the boundary if related.
38
+ - omit the audit gate (every skill is auditable, no exceptions).
39
+ - hand-wave the failure modes.
40
+
41
+ Format the artifact as Markdown.
42
+ """
43
+
44
+
45
+ class SkillEducator:
46
+ """Generates skill specs for the SSOT skill catalog."""
47
+
48
+ name = "skill"
49
+ domain = "skills"
50
+
51
+ def generate(
52
+ self,
53
+ intent: str,
54
+ context: dict[str, Any],
55
+ provider: AgentProvider,
56
+ ) -> EducatorProposal:
57
+ skill_name = str(context.get("skill_name", "new-skill"))
58
+ target_path = f"Agentik_SSOT/skills/{skill_name}.md"
59
+ return run_education(
60
+ educator_name=self.name,
61
+ domain=self.domain,
62
+ intent=intent,
63
+ target_kind="skill",
64
+ target_path=target_path,
65
+ domain_instructions=_INSTRUCTIONS,
66
+ context={**context, "skill_name": skill_name},
67
+ provider=provider,
68
+ artifact_id=skill_name,
69
+ )
@@ -49,11 +49,21 @@ class Executor:
49
49
  bus: EventBus,
50
50
  router: ModelRouter,
51
51
  audit: AuditGate,
52
+ partial_policy: str = "fail_up",
52
53
  ) -> None:
54
+ # partial_policy: how a dispatcher reacts to a PARTIAL scope (a child
55
+ # FAILED). One of: "fail_up" (default — the dispatcher fails too),
56
+ # "accept_partial" (the dispatcher completes with the verified children
57
+ # only), "retry_failed" (one retry pass on failed children, then
58
+ # fail_up if still PARTIAL). Declared per-topology in
59
+ # Agentik_Orchestration/topologies/<name>.yaml -> policy.on_partial.
60
+ if partial_policy not in ("fail_up", "accept_partial", "retry_failed"):
61
+ raise ValueError(f"unknown partial_policy: {partial_policy}")
53
62
  self._store = store
54
63
  self._bus = bus
55
64
  self._router = router
56
65
  self._audit = audit
66
+ self._partial_policy = partial_policy
57
67
  self._tasks: dict[str, Task] = {}
58
68
 
59
69
  # -- public ---------------------------------------------------------------
@@ -119,20 +129,50 @@ class Executor:
119
129
  children.append(child)
120
130
 
121
131
  status = scope_status([self._state(c.id) for c in children])
122
- if status is ScopeStatus.JOINABLE:
132
+
133
+ # Per-topology PARTIAL policy
134
+ if status is ScopeStatus.PARTIAL and self._partial_policy == "retry_failed":
135
+ # one retry pass for every FAILED child — children are re-spawned as
136
+ # fresh tasks, each with their own budget. If they all pass, the
137
+ # scope becomes JOINABLE; otherwise we fall through to fail_up.
138
+ failed_children = [c for c in children
139
+ if self._state(c.id) is TaskState.FAILED]
140
+ for failed in failed_children:
141
+ replacement = self._new_task(
142
+ kind=failed.kind, role=failed.role,
143
+ spec=failed.spec, parent_id=task.id, mission_id=mission_id,
144
+ )
145
+ self._run(replacement, mission_id)
146
+ children.append(replacement)
147
+ status = scope_status([self._state(c.id) for c in children])
148
+
149
+ accept_partial = (
150
+ status is ScopeStatus.PARTIAL
151
+ and self._partial_policy == "accept_partial"
152
+ )
153
+
154
+ if status is ScopeStatus.JOINABLE or accept_partial:
123
155
  # the barrier resolved — the dispatcher may now close
124
- self._emit(task.id, EventType.SCOPE_JOINABLE, {"children": len(children)})
156
+ self._emit(task.id, EventType.SCOPE_JOINABLE,
157
+ {"children": len(children),
158
+ "partial_accepted": accept_partial})
125
159
  self._emit(task.id, EventType.CLAIMED_DONE)
126
160
  self._emit(task.id, EventType.VERIFYING)
127
- self._emit(task.id, EventType.VERIFIED, {"reason": "all children verified"})
161
+ self._emit(task.id, EventType.VERIFIED,
162
+ {"reason": "partial accepted" if accept_partial
163
+ else "all children verified"})
128
164
  self._emit(task.id, EventType.COMPLETED)
129
165
  else:
130
- # PARTIAL a child failed. on_partial = fail_up (children already
131
- # retried within their own budget). Honest partial failure.
166
+ # PARTIAL or RUNNING under fail_up. RUNNING here means recursion
167
+ # left non-terminal children that is a real bug. PARTIAL under
168
+ # fail_up is honest failure.
132
169
  failed = [c.id for c in children
133
170
  if self._state(c.id) is TaskState.FAILED]
134
171
  self._emit(task.id, EventType.FAILED,
135
- {"reason": "partial scope", "failed_children": failed})
172
+ {"reason": "partial scope" if status is ScopeStatus.PARTIAL
173
+ else "scope unresolved",
174
+ "failed_children": failed,
175
+ "policy": self._partial_policy})
136
176
 
137
177
  def _run_worker(self, task: Task) -> None:
138
178
  provider = self._router.resolve(task.role)
@@ -109,7 +109,19 @@ def run_mission(
109
109
  gate: object = ArsenalGate(AuditRegistry.load(audits_dir), router)
110
110
  else:
111
111
  gate = AuditGate()
112
- executor = Executor(store, bus, router, gate)
112
+ # read the topology's PARTIAL policy if a topology config is present;
113
+ # the default (fail_up) is the safe behaviour.
114
+ partial_policy = "fail_up"
115
+ topo_path = home / "Agentik_Orchestration" / "topologies" / "aisb-oracle-worker.yaml"
116
+ if topo_path.exists():
117
+ try:
118
+ import yaml
119
+ topo = yaml.safe_load(topo_path.read_text()) or {}
120
+ partial_policy = (topo.get("policy") or {}).get("on_partial", "fail_up")
121
+ except Exception: # noqa: BLE001 — bad topology config falls back safely
122
+ partial_policy = "fail_up"
123
+
124
+ executor = Executor(store, bus, router, gate, partial_policy=partial_policy)
113
125
  result = executor.run_mission(intent)
114
126
 
115
127
  # the Oracle ALWAYS produces a report
@@ -1,12 +1,20 @@
1
1
  """The provider abstraction — one contract, any LLM.
2
2
 
3
3
  The executor talks only to `AgentProvider`. `MockProvider` makes the whole
4
- orchestration testable with no network. `ClaudeProvider` is the real adapter.
4
+ orchestration testable with no network. `ClaudeProvider` wraps Anthropic;
5
+ `GLMProvider`, `OpenAIProvider`, and `DeepSeekProvider` wrap the OpenAI
6
+ chat-completions wire format used by Zhipu/BigModel, OpenAI, and DeepSeek.
7
+
8
+ The real adapters use the stdlib only (`urllib.request`, `json`). They never
9
+ fail at construction — a missing API key only raises when `.run()` is called.
5
10
  """
6
11
  from __future__ import annotations
7
12
 
8
13
  import json
14
+ import os
9
15
  import re
16
+ import urllib.error
17
+ import urllib.request
10
18
  from dataclasses import dataclass, field
11
19
  from typing import Any, Protocol, runtime_checkable
12
20
 
@@ -47,6 +55,8 @@ class MockProvider:
47
55
 
48
56
  def __init__(self, plan_size: int = 2) -> None:
49
57
  self._plan_size = plan_size
58
+ self._grader_calls = 0
59
+ self._agent_calls = 0
50
60
 
51
61
  def run(self, req: AgentRequest) -> AgentResult:
52
62
  role = req.role
@@ -75,6 +85,67 @@ class MockProvider:
75
85
  "summary": "mock forensic audit — no falsifiable claims found",
76
86
  "findings": [], "fix_plan": [],
77
87
  }})
88
+ if role == "educator" or role.startswith("educator-"):
89
+ # a structured generation result — what the Educators expect.
90
+ # The mock returns a plausible artifact + self-critique score so the
91
+ # promotion pipeline has something real to gate on.
92
+ intent = req.context.get("intent", "generate")
93
+ kind = req.context.get("kind", "artifact")
94
+ educator = role.split("-", 1)[1] if "-" in role else "educator"
95
+ return AgentResult(
96
+ text=f"educator {educator} produced {kind} for: {intent}",
97
+ claimed_done=True,
98
+ artifacts={"proposal": {
99
+ "kind": kind,
100
+ "content": (
101
+ f"# {kind} for {intent}\n\n"
102
+ f"Generated by mock {educator} educator.\n"
103
+ ),
104
+ "score": 92,
105
+ "summary": f"mock {educator} artifact for '{intent}'",
106
+ "fix_plan": [],
107
+ }})
108
+
109
+ # --- RAG roles ---
110
+ if role == "rag-route":
111
+ # Default to hybrid (matches the architecture's "hybrid is the
112
+ # default" guidance). The router still applies its own heuristic
113
+ # for query-specific overrides.
114
+ return AgentResult(
115
+ text="route: hybrid", claimed_done=True,
116
+ artifacts={"strategy": "hybrid"},
117
+ )
118
+ if role == "rag-agent":
119
+ self._agent_calls += 1
120
+ # Terminate after one refinement hop — bounded behaviour, no loops.
121
+ original = req.context.get("original_query") or req.context.get(
122
+ "current_query", "")
123
+ done = self._agent_calls >= 2
124
+ return AgentResult(
125
+ text="agent step", claimed_done=True,
126
+ artifacts={
127
+ "next_query": f"deeper: {original}",
128
+ "done": done,
129
+ },
130
+ )
131
+ if role == "rag-grader":
132
+ self._grader_calls += 1
133
+ docs = req.context.get("documents", []) or []
134
+ # First call: every doc scores 40 (forces a Corrective retry).
135
+ # Second+ call: every doc scores 90 (lands above the threshold).
136
+ score = 40 if self._grader_calls == 1 else 90
137
+ return AgentResult(
138
+ text=f"graded {len(docs)} docs at {score}",
139
+ claimed_done=True,
140
+ artifacts={"scores": [score for _ in docs] or [score]},
141
+ )
142
+ if role == "rag-caption":
143
+ path = req.context.get("path", "")
144
+ return AgentResult(
145
+ text="captioned", claimed_done=True,
146
+ artifacts={"caption": f"mock caption for {path}"},
147
+ )
148
+
78
149
  return AgentResult(text="ok", claimed_done=True)
79
150
 
80
151
 
@@ -127,6 +198,181 @@ class ClaudeProvider:
127
198
  )
128
199
 
129
200
 
201
+ # ---------------------------------------------------------------------------
202
+ # OpenAI-compatible providers: GLM (BigModel/Zhipu), OpenAI, DeepSeek.
203
+ #
204
+ # All three speak the same chat-completions wire format. We share one tiny
205
+ # stdlib HTTP transport (`_post_json`) and one response parser, then expose
206
+ # three small adapter classes — each with its own id, default model, env key,
207
+ # and base URL.
208
+ # ---------------------------------------------------------------------------
209
+
210
+ _DEFAULT_TIMEOUT = 120.0
211
+
212
+
213
+ def _post_json(url: str, headers: dict[str, str], body: dict[str, Any],
214
+ timeout: float = _DEFAULT_TIMEOUT) -> dict[str, Any]:
215
+ """POST JSON, return parsed JSON. Raises RuntimeError on protocol errors.
216
+
217
+ Stdlib-only (`urllib.request`) so the providers stay dependency-free.
218
+ """
219
+ data = json.dumps(body).encode("utf-8")
220
+ headers = {"Content-Type": "application/json", **headers}
221
+ req = urllib.request.Request(url, data=data, headers=headers, method="POST")
222
+ try:
223
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
224
+ raw = resp.read().decode("utf-8")
225
+ except urllib.error.HTTPError as exc:
226
+ detail = exc.read().decode("utf-8", errors="replace") if exc.fp else ""
227
+ raise RuntimeError(
228
+ f"HTTP {exc.code} from {url}: {detail[:500]}"
229
+ ) from exc
230
+ except urllib.error.URLError as exc:
231
+ raise RuntimeError(f"network error talking to {url}: {exc.reason}") from exc
232
+ try:
233
+ return json.loads(raw)
234
+ except json.JSONDecodeError as exc:
235
+ raise RuntimeError(
236
+ f"non-JSON response from {url}: {raw[:500]}"
237
+ ) from exc
238
+
239
+
240
+ def _parse_openai_chat_response(payload: dict[str, Any]) -> tuple[str, dict[str, int]]:
241
+ """Pull text + usage from an OpenAI-format chat completion response.
242
+
243
+ Returns ("", {}) if the structure is unexpected, rather than crashing.
244
+ """
245
+ text = ""
246
+ choices = payload.get("choices") or []
247
+ if choices and isinstance(choices, list):
248
+ first = choices[0] or {}
249
+ msg = first.get("message") or {}
250
+ content = msg.get("content")
251
+ if isinstance(content, str):
252
+ text = content
253
+ elif isinstance(content, list):
254
+ # GLM/anthropic-style content blocks: list of {"type": "text", "text": "..."}
255
+ parts: list[str] = []
256
+ for block in content:
257
+ if isinstance(block, dict):
258
+ val = block.get("text") or block.get("content") or ""
259
+ if isinstance(val, str):
260
+ parts.append(val)
261
+ text = "".join(parts)
262
+ usage_raw = payload.get("usage") or {}
263
+ usage = {
264
+ "input_tokens": int(usage_raw.get("prompt_tokens", 0) or 0),
265
+ "output_tokens": int(usage_raw.get("completion_tokens", 0) or 0),
266
+ }
267
+ return text, usage
268
+
269
+
270
+ class _OpenAICompatProvider:
271
+ """Shared transport for OpenAI-format chat completion APIs.
272
+
273
+ Subclasses set `id`, `_default_model`, `_env_var`, `_default_base_url`.
274
+ Construction never fails on missing key — `.run()` raises if it's still
275
+ missing when called.
276
+ """
277
+
278
+ id: str = "openai-compat"
279
+ _default_model: str = ""
280
+ _env_var: str = ""
281
+ _default_base_url: str = ""
282
+
283
+ def __init__(
284
+ self,
285
+ model: str | None = None,
286
+ api_key: str | None = None,
287
+ base_url: str | None = None,
288
+ ) -> None:
289
+ self._model = model or self._default_model
290
+ self._api_key = api_key
291
+ self._base_url = base_url or self._default_base_url
292
+
293
+ def _resolve_key(self) -> str:
294
+ key = self._api_key or os.environ.get(self._env_var)
295
+ if not key:
296
+ raise RuntimeError(
297
+ f"{type(self).__name__} requires an API key. "
298
+ f"Pass api_key=... or set {self._env_var} in the environment."
299
+ )
300
+ return key
301
+
302
+ def _build_messages(self, req: AgentRequest) -> list[dict[str, str]]:
303
+ prompt = req.prompt
304
+ if req.role in ("oracle", "manager", "aisb"):
305
+ prompt = prompt + (
306
+ "\n\nRespond with a JSON array of subtasks, each "
307
+ '{"role":"worker","spec":{"task":"..."}}. JSON only.'
308
+ )
309
+ return [{"role": "user", "content": prompt}]
310
+
311
+ def _extra_body(self) -> dict[str, Any]:
312
+ """Adapter-specific knobs (overridable). Default: no extras."""
313
+ return {}
314
+
315
+ def run(self, req: AgentRequest) -> AgentResult:
316
+ api_key = self._resolve_key()
317
+ body: dict[str, Any] = {
318
+ "model": self._model,
319
+ "messages": self._build_messages(req),
320
+ "max_tokens": 4096,
321
+ }
322
+ body.update(self._extra_body())
323
+ payload = _post_json(
324
+ self._base_url,
325
+ {"Authorization": f"Bearer {api_key}"},
326
+ body,
327
+ )
328
+ text, usage = _parse_openai_chat_response(payload)
329
+ return AgentResult(
330
+ text=text,
331
+ claimed_done=True,
332
+ plan=_extract_plan(text) if req.role in ("oracle", "manager", "aisb") else [],
333
+ usage=usage,
334
+ )
335
+
336
+
337
+ class GLMProvider(_OpenAICompatProvider):
338
+ """Zhipu / BigModel GLM via the OpenAI-compatible chat-completions API.
339
+
340
+ Endpoint: https://open.bigmodel.cn/api/paas/v4/chat/completions
341
+ Env: `GLM_API_KEY`.
342
+ """
343
+
344
+ id = "glm"
345
+ _default_model = "glm-4.6"
346
+ _env_var = "GLM_API_KEY"
347
+ _default_base_url = "https://open.bigmodel.cn/api/paas/v4/chat/completions"
348
+
349
+
350
+ class OpenAIProvider(_OpenAICompatProvider):
351
+ """OpenAI Chat Completions.
352
+
353
+ Endpoint: https://api.openai.com/v1/chat/completions
354
+ Env: `OPENAI_API_KEY`.
355
+ """
356
+
357
+ id = "openai"
358
+ _default_model = "gpt-4o"
359
+ _env_var = "OPENAI_API_KEY"
360
+ _default_base_url = "https://api.openai.com/v1/chat/completions"
361
+
362
+
363
+ class DeepSeekProvider(_OpenAICompatProvider):
364
+ """DeepSeek (OpenAI-compatible chat completions).
365
+
366
+ Endpoint: https://api.deepseek.com/v1/chat/completions
367
+ Env: `DEEPSEEK_API_KEY`.
368
+ """
369
+
370
+ id = "deepseek"
371
+ _default_model = "deepseek-chat"
372
+ _env_var = "DEEPSEEK_API_KEY"
373
+ _default_base_url = "https://api.deepseek.com/v1/chat/completions"
374
+
375
+
130
376
  def _extract_plan(text: str) -> list[dict[str, Any]]:
131
377
  """Pull a JSON subtask array out of an LLM response. Empty list if none."""
132
378
  match = re.search(r"\[.*\]", text, re.DOTALL)
@@ -0,0 +1,21 @@
1
+ """Omega OS multi-RAG subsystem — five strategies behind one Protocol.
2
+
3
+ `base.Retriever` is the contract every strategy honours. The Corrective RAG
4
+ envelope (corrective.CorrectiveRetriever) wraps the chosen strategy; the
5
+ router picks which strategy runs.
6
+
7
+ See docs/ARCHITECTURE.md §7 for the full design.
8
+ """
9
+ from omega_engine.rag.agentic import AgenticRetriever
10
+ from omega_engine.rag.base import Document, RetrievalResult, Retriever
11
+ from omega_engine.rag.corrective import CorrectiveRetriever
12
+ from omega_engine.rag.graph import GraphRetriever
13
+ from omega_engine.rag.hybrid import HybridRetriever
14
+ from omega_engine.rag.multimodal import MultimodalRetriever
15
+ from omega_engine.rag.router import RAGRouter
16
+
17
+ __all__ = [
18
+ "Document", "RetrievalResult", "Retriever",
19
+ "HybridRetriever", "GraphRetriever", "AgenticRetriever",
20
+ "CorrectiveRetriever", "MultimodalRetriever", "RAGRouter",
21
+ ]
@@ -0,0 +1,83 @@
1
+ """Agentic retriever — multi-hop retrieval steered by a provider.
2
+
3
+ After every hop, the provider (role "rag-agent") looks at the current
4
+ context and decides: keep going (return a refined `next_query`) or stop
5
+ (`done: true`). The retriever loops up to `max_hops` and returns the
6
+ accumulated, de-duplicated documents.
7
+
8
+ This is the strategy for "decompose then dig" queries — questions where the
9
+ first retrieval surfaces a clue, and the second clue is buried inside what
10
+ the first one found.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from omega_engine.provider import AgentProvider, AgentRequest
15
+ from omega_engine.rag.base import Document, RetrievalResult, Retriever
16
+
17
+
18
+ class AgenticRetriever:
19
+ """Multi-hop on top of any underlying retriever."""
20
+
21
+ strategy = "agentic"
22
+
23
+ def __init__(
24
+ self,
25
+ inner: Retriever,
26
+ provider: AgentProvider,
27
+ *,
28
+ max_hops: int = 3,
29
+ k_per_hop: int = 4,
30
+ ) -> None:
31
+ self.inner = inner
32
+ self.provider = provider
33
+ self.max_hops = max_hops
34
+ self.k_per_hop = k_per_hop
35
+
36
+ def retrieve(self, query: str, k: int = 5) -> RetrievalResult:
37
+ seen_ids: set[str] = set()
38
+ accumulated: list[Document] = []
39
+ current = query
40
+ last_score = 0.0
41
+
42
+ for hop in range(self.max_hops):
43
+ partial = self.inner.retrieve(current, k=self.k_per_hop)
44
+ last_score = partial.score
45
+ for d in partial.documents:
46
+ if d.id in seen_ids:
47
+ continue
48
+ seen_ids.add(d.id)
49
+ meta = dict(d.metadata)
50
+ meta["hop"] = hop
51
+ accumulated.append(Document(id=d.id, text=d.text, metadata=meta))
52
+
53
+ # Ask the provider whether to stop or refine.
54
+ ctx_snippets = [d.text[:200] for d in partial.documents[:3]]
55
+ decision = self.provider.run(AgentRequest(
56
+ role="rag-agent",
57
+ prompt=(
58
+ f"Original query: {query}\n"
59
+ f"Current hop: {hop}\n"
60
+ f"Just-retrieved snippets:\n- " + "\n- ".join(ctx_snippets)
61
+ ),
62
+ context={
63
+ "original_query": query,
64
+ "current_query": current,
65
+ "hop": hop,
66
+ "snippets": ctx_snippets,
67
+ },
68
+ ))
69
+ artifacts = decision.artifacts or {}
70
+ if artifacts.get("done"):
71
+ break
72
+ next_q = artifacts.get("next_query")
73
+ if not next_q or next_q == current:
74
+ break
75
+ current = str(next_q)
76
+
77
+ # Final ranking: keep accumulation order (earlier hops first), trim to k.
78
+ return RetrievalResult(
79
+ query=query,
80
+ documents=accumulated[:k],
81
+ score=last_score,
82
+ strategy=self.strategy,
83
+ )
@@ -0,0 +1,42 @@
1
+ """Shared contracts for every retriever — Document, RetrievalResult, Retriever.
2
+
3
+ The whole RAG subsystem talks to these three types. A retriever is anything
4
+ with `retrieve(query, k) -> RetrievalResult`. Strategies plug in behind the
5
+ same interface, the router picks between them, and the Corrective envelope
6
+ wraps any of them.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Any, Protocol, runtime_checkable
12
+
13
+
14
+ @dataclass
15
+ class Document:
16
+ """A single retrievable unit — text + provenance.
17
+
18
+ `metadata` is free-form: source path, modality, chunk index, score,
19
+ captions for images, anything a strategy wants to surface upward.
20
+ """
21
+
22
+ id: str
23
+ text: str
24
+ metadata: dict[str, Any] = field(default_factory=dict)
25
+
26
+
27
+ @dataclass
28
+ class RetrievalResult:
29
+ """What every retriever returns. The router fans these up; the corrective
30
+ envelope grades them; the caller reads `documents`."""
31
+
32
+ query: str
33
+ documents: list[Document]
34
+ score: float = 0.0 # aggregate confidence in [0, 1]
35
+ strategy: str = "" # which retriever produced this result
36
+
37
+
38
+ @runtime_checkable
39
+ class Retriever(Protocol):
40
+ """The whole RAG subsystem speaks this Protocol."""
41
+
42
+ def retrieve(self, query: str, k: int = 5) -> RetrievalResult: ...