@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.
- package/README.md +25 -13
- package/bootstrap/lib/steps.sh +214 -9
- package/bootstrap/manifest.example.yaml +6 -1
- package/docs/COMPLETION-PLAN.md +48 -0
- package/omega/Agentik_Engine/README.md +25 -10
- package/omega/Agentik_Engine/omega_engine/__init__.py +66 -2
- package/omega/Agentik_Engine/omega_engine/account.py +505 -0
- package/omega/Agentik_Engine/omega_engine/autonomous.py +538 -0
- package/omega/Agentik_Engine/omega_engine/cli.py +467 -29
- package/omega/Agentik_Engine/omega_engine/daemons/__init__.py +14 -0
- package/omega/Agentik_Engine/omega_engine/daemons/autonomous.py +56 -0
- package/omega/Agentik_Engine/omega_engine/daemons/engine.py +187 -0
- package/omega/Agentik_Engine/omega_engine/daemons/telegram.py +231 -0
- package/omega/Agentik_Engine/omega_engine/educators/__init__.py +51 -0
- package/omega/Agentik_Engine/omega_engine/educators/artifact.py +65 -0
- package/omega/Agentik_Engine/omega_engine/educators/automation.py +76 -0
- package/omega/Agentik_Engine/omega_engine/educators/base.py +327 -0
- package/omega/Agentik_Engine/omega_engine/educators/claudecode.py +71 -0
- package/omega/Agentik_Engine/omega_engine/educators/connection.py +75 -0
- package/omega/Agentik_Engine/omega_engine/educators/coworker.py +68 -0
- package/omega/Agentik_Engine/omega_engine/educators/loop.py +82 -0
- package/omega/Agentik_Engine/omega_engine/educators/prompt.py +68 -0
- package/omega/Agentik_Engine/omega_engine/educators/skill.py +69 -0
- package/omega/Agentik_Engine/omega_engine/executor.py +46 -6
- package/omega/Agentik_Engine/omega_engine/mission.py +13 -1
- package/omega/Agentik_Engine/omega_engine/provider.py +247 -1
- package/omega/Agentik_Engine/omega_engine/rag/__init__.py +21 -0
- package/omega/Agentik_Engine/omega_engine/rag/agentic.py +83 -0
- package/omega/Agentik_Engine/omega_engine/rag/base.py +42 -0
- package/omega/Agentik_Engine/omega_engine/rag/corrective.py +119 -0
- package/omega/Agentik_Engine/omega_engine/rag/graph.py +169 -0
- package/omega/Agentik_Engine/omega_engine/rag/hybrid.py +205 -0
- package/omega/Agentik_Engine/omega_engine/rag/multimodal.py +136 -0
- package/omega/Agentik_Engine/omega_engine/rag/router.py +110 -0
- package/omega/Agentik_Engine/omega_engine/reducer.py +21 -3
- package/omega/Agentik_Engine/omega_engine/store.py +65 -5
- package/omega/Agentik_Engine/omega_engine/sync.py +304 -0
- package/omega/Agentik_Engine/omega_engine/tools.py +272 -0
- package/omega/Agentik_Engine/pyproject.toml +1 -1
- package/omega/Agentik_Engine/tests/test_account.py +333 -0
- package/omega/Agentik_Engine/tests/test_autonomous.py +361 -0
- package/omega/Agentik_Engine/tests/test_educators.py +233 -0
- package/omega/Agentik_Engine/tests/test_rag.py +287 -0
- package/omega/Agentik_Engine/tests/test_snapshot_partial.py +172 -0
- package/omega/Agentik_Engine/tests/test_tools_and_sync.py +312 -0
- package/omega/Agentik_SSOT/skills/rag-route.md +73 -0
- package/package.json +1 -1
- package/omega/Agentik_Engine/omega_engine/__pycache__/__init__.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/audit.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/audit_arsenal.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/barrier.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/bus.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/cli.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/events.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/executor.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/mission.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/progress.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/project.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/provider.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/reducer.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/report.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/router.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/store.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/supervisor.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/task.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/telegram.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_audit_arsenal.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_executor.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_mission.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_progress.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_project.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_reducer.cpython-313.pyc +0 -0
- 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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
131
|
-
#
|
|
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"
|
|
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
|
-
|
|
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`
|
|
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: ...
|