loopentx 0.1.0__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.
- loopentx/__init__.py +31 -0
- loopentx/backends/__init__.py +11 -0
- loopentx/backends/base.py +115 -0
- loopentx/backends/memory.py +146 -0
- loopentx/backends/redis_backend.py +196 -0
- loopentx/cli/__init__.py +0 -0
- loopentx/cli/main.py +398 -0
- loopentx/core/__init__.py +28 -0
- loopentx/core/config.py +67 -0
- loopentx/core/context.py +362 -0
- loopentx/core/events.py +35 -0
- loopentx/core/exceptions.py +67 -0
- loopentx/core/loop.py +240 -0
- loopentx/core/memory.py +110 -0
- loopentx/core/models.py +172 -0
- loopentx/core/orchestrator.py +166 -0
- loopentx/core/skill.py +159 -0
- loopentx/llm/__init__.py +3 -0
- loopentx/llm/caller.py +103 -0
- loopentx/trust/__init__.py +4 -0
- loopentx/trust/policy.py +185 -0
- loopentx/trust/scorer.py +141 -0
- loopentx-0.1.0.dist-info/METADATA +555 -0
- loopentx-0.1.0.dist-info/RECORD +28 -0
- loopentx-0.1.0.dist-info/WHEEL +5 -0
- loopentx-0.1.0.dist-info/entry_points.txt +2 -0
- loopentx-0.1.0.dist-info/licenses/LICENSE +21 -0
- loopentx-0.1.0.dist-info/top_level.txt +1 -0
loopentx/cli/main.py
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"""Loopentx CLI — deploy, inspect, trust, runs, escalations, memory, worker."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import importlib
|
|
7
|
+
import importlib.util
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
from rich.panel import Panel
|
|
16
|
+
from rich import box
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
def _load_config(config_path: str) -> None:
|
|
24
|
+
path = Path(config_path)
|
|
25
|
+
if not path.exists():
|
|
26
|
+
return
|
|
27
|
+
spec = importlib.util.spec_from_file_location("loopentx_config", path)
|
|
28
|
+
if spec and spec.loader:
|
|
29
|
+
mod = importlib.util.module_from_spec(spec)
|
|
30
|
+
spec.loader.exec_module(mod) # type: ignore[attr-defined]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _load_app(app: str) -> object:
|
|
34
|
+
try:
|
|
35
|
+
return importlib.import_module(app)
|
|
36
|
+
except Exception as e:
|
|
37
|
+
console.print(f"[red]Failed to load app module '{app}': {e}[/red]")
|
|
38
|
+
sys.exit(1)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ── Root ──────────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
@click.group()
|
|
44
|
+
@click.version_option()
|
|
45
|
+
def cli() -> None:
|
|
46
|
+
"""Loopentx — write the loop once. Step back."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ── Worker ────────────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
@cli.group()
|
|
52
|
+
def worker() -> None:
|
|
53
|
+
"""Manage the Loopentx worker."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@worker.command("start")
|
|
57
|
+
@click.option("--app", "-a", required=True, help="Module path (e.g. myapp.loops)")
|
|
58
|
+
@click.option("--config", "-c", default="loopentx_config.py")
|
|
59
|
+
def worker_start(app: str, config: str) -> None:
|
|
60
|
+
"""Start the worker and begin processing loops and events."""
|
|
61
|
+
console.print(Panel.fit("[bold green]Starting Loopentx Worker[/bold green]"))
|
|
62
|
+
_load_config(config)
|
|
63
|
+
module = _load_app(app)
|
|
64
|
+
|
|
65
|
+
from loopentx.core.orchestrator import Orchestrator
|
|
66
|
+
orch = Orchestrator()
|
|
67
|
+
|
|
68
|
+
for name in dir(module):
|
|
69
|
+
obj = getattr(module, name)
|
|
70
|
+
kind = getattr(obj, "_loopentx_kind", None)
|
|
71
|
+
if kind in ("skill", "loop"):
|
|
72
|
+
orch.register(obj)
|
|
73
|
+
|
|
74
|
+
console.print(f"[green]Registered {len(orch._skills)} skill(s), "
|
|
75
|
+
f"{len(orch._loops)} loop(s)[/green]")
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
asyncio.run(orch.start())
|
|
79
|
+
except KeyboardInterrupt:
|
|
80
|
+
console.print("\n[yellow]Worker stopped.[/yellow]")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ── Deploy ────────────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
@cli.group()
|
|
86
|
+
def deploy() -> None:
|
|
87
|
+
"""Deploy loops and skills."""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@deploy.command("skill")
|
|
91
|
+
@click.argument("skill_name")
|
|
92
|
+
@click.option("--app", "-a", required=True)
|
|
93
|
+
@click.option("--config", "-c", default="loopentx_config.py")
|
|
94
|
+
def deploy_skill(skill_name: str, app: str, config: str) -> None:
|
|
95
|
+
"""Register a skill with the orchestrator."""
|
|
96
|
+
async def _run() -> None:
|
|
97
|
+
_load_config(config)
|
|
98
|
+
module = _load_app(app)
|
|
99
|
+
fn = getattr(module, skill_name, None)
|
|
100
|
+
if fn is None or not hasattr(fn, "_loopentx_skill"):
|
|
101
|
+
console.print(f"[red]Skill '{skill_name}' not found in {app}[/red]")
|
|
102
|
+
sys.exit(1)
|
|
103
|
+
|
|
104
|
+
from loopentx.core.config import get_config
|
|
105
|
+
from loopentx.core.models import SkillRegistration
|
|
106
|
+
cfg = get_config()
|
|
107
|
+
pc = fn._loopentx_skill.policy_context
|
|
108
|
+
reg = pc.to_registration(skill_name) if pc else SkillRegistration(
|
|
109
|
+
name=skill_name, kind="skill", is_active=True)
|
|
110
|
+
await cfg.backend.save_skill_registration(reg)
|
|
111
|
+
|
|
112
|
+
console.print(f"[green]✓ Deployed:[/green] {skill_name}")
|
|
113
|
+
if reg.is_shadow:
|
|
114
|
+
console.print(f" [yellow]Shadow mode:[/yellow] {reg.shadow_cycles} cycles required")
|
|
115
|
+
elif reg.require_approval:
|
|
116
|
+
console.print(f" [yellow]Awaiting approval[/yellow]")
|
|
117
|
+
else:
|
|
118
|
+
console.print(f" [green]Status:[/green] Active")
|
|
119
|
+
asyncio.run(_run())
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@deploy.command("loop")
|
|
123
|
+
@click.argument("loop_name")
|
|
124
|
+
@click.option("--app", "-a", required=True)
|
|
125
|
+
@click.option("--config", "-c", default="loopentx_config.py")
|
|
126
|
+
def deploy_loop(loop_name: str, app: str, config: str) -> None:
|
|
127
|
+
"""Register a loop with the orchestrator."""
|
|
128
|
+
async def _run() -> None:
|
|
129
|
+
_load_config(config)
|
|
130
|
+
module = _load_app(app)
|
|
131
|
+
fn = getattr(module, loop_name, None)
|
|
132
|
+
if fn is None or not hasattr(fn, "_loopentx_loop"):
|
|
133
|
+
console.print(f"[red]Loop '{loop_name}' not found in {app}[/red]")
|
|
134
|
+
sys.exit(1)
|
|
135
|
+
from loopentx.core.config import get_config
|
|
136
|
+
from loopentx.core.models import SkillRegistration
|
|
137
|
+
ld = fn._loopentx_loop
|
|
138
|
+
cfg = get_config()
|
|
139
|
+
reg = SkillRegistration(
|
|
140
|
+
name=loop_name, kind="loop", is_active=True,
|
|
141
|
+
cron=ld.cron, description=ld.description,
|
|
142
|
+
)
|
|
143
|
+
await cfg.backend.save_skill_registration(reg)
|
|
144
|
+
console.print(f"[green]✓ Deployed loop:[/green] {loop_name}")
|
|
145
|
+
asyncio.run(_run())
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# ── Inspect ───────────────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
@cli.group()
|
|
151
|
+
def inspect() -> None:
|
|
152
|
+
"""Inspect loops, skills, and runs."""
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@inspect.command("skill")
|
|
156
|
+
@click.argument("skill_name")
|
|
157
|
+
@click.option("--config", "-c", default="loopentx_config.py")
|
|
158
|
+
def inspect_skill(skill_name: str, config: str) -> None:
|
|
159
|
+
"""Show policy, trust score, and recent runs for a skill."""
|
|
160
|
+
async def _run() -> None:
|
|
161
|
+
_load_config(config)
|
|
162
|
+
from loopentx.core.config import get_config
|
|
163
|
+
from loopentx.trust.scorer import TrustScorer
|
|
164
|
+
cfg = get_config()
|
|
165
|
+
reg = await cfg.backend.get_skill_registration(skill_name)
|
|
166
|
+
trust = await cfg.backend.get_trust_record(skill_name)
|
|
167
|
+
runs = await cfg.backend.get_runs(skill_name=skill_name, limit=10)
|
|
168
|
+
|
|
169
|
+
if not reg:
|
|
170
|
+
console.print(f"[red]Skill '{skill_name}' not registered.[/red]")
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
console.print(Panel(
|
|
174
|
+
f"[bold]Status:[/bold] {'🟢 Active' if reg.is_active else '🟡 Inactive'}\n"
|
|
175
|
+
f"[bold]Kind:[/bold] {reg.kind}\n"
|
|
176
|
+
f"[bold]Blast radius:[/bold] {reg.blast_radius.value}\n"
|
|
177
|
+
f"[bold]Can read:[/bold] {', '.join(reg.can_read) or 'none'}\n"
|
|
178
|
+
f"[bold]Can write:[/bold] {', '.join(reg.can_write) or 'none'}\n"
|
|
179
|
+
f"[bold]Shadow cycles remaining:[/bold] {reg.shadow_cycles_remaining}",
|
|
180
|
+
title=f"[bold cyan]{skill_name}[/bold cyan]",
|
|
181
|
+
))
|
|
182
|
+
|
|
183
|
+
if trust:
|
|
184
|
+
scorer = TrustScorer()
|
|
185
|
+
console.print(Panel(scorer.explain(trust), title="Trust"))
|
|
186
|
+
else:
|
|
187
|
+
console.print("[dim]No trust data yet.[/dim]")
|
|
188
|
+
|
|
189
|
+
if runs:
|
|
190
|
+
table = Table(box=box.SIMPLE, title="Recent runs")
|
|
191
|
+
table.add_column("Run ID"); table.add_column("Status")
|
|
192
|
+
table.add_column("Duration"); table.add_column("Shadow")
|
|
193
|
+
for run in runs:
|
|
194
|
+
c = {"completed": "green", "failed": "red"}.get(run.status.value, "white")
|
|
195
|
+
table.add_row(
|
|
196
|
+
run.id[:10] + "…", f"[{c}]{run.status.value}[/{c}]",
|
|
197
|
+
f"{run.duration_ms}ms" if run.duration_ms else "-",
|
|
198
|
+
"yes" if run.is_shadow else "no",
|
|
199
|
+
)
|
|
200
|
+
console.print(table)
|
|
201
|
+
asyncio.run(_run())
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# ── Runs ──────────────────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
@cli.group()
|
|
207
|
+
def runs() -> None:
|
|
208
|
+
"""Query run history."""
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@runs.command("list")
|
|
212
|
+
@click.option("--skill", "-s", default=None)
|
|
213
|
+
@click.option("--last", "-l", default=None, help="e.g. 7d, 24h")
|
|
214
|
+
@click.option("--limit", "-n", default=20)
|
|
215
|
+
@click.option("--config", "-c", default="loopentx_config.py")
|
|
216
|
+
def runs_list(skill: Optional[str], last: Optional[str], limit: int, config: str) -> None:
|
|
217
|
+
"""List recent runs."""
|
|
218
|
+
async def _run() -> None:
|
|
219
|
+
import time as _time
|
|
220
|
+
_load_config(config)
|
|
221
|
+
from loopentx.core.config import get_config
|
|
222
|
+
cfg = get_config()
|
|
223
|
+
since = None
|
|
224
|
+
if last:
|
|
225
|
+
u = last[-1]
|
|
226
|
+
v = int(last[:-1])
|
|
227
|
+
since = _time.time() - v * {"h": 3600, "d": 86400, "w": 604800}.get(u, 3600)
|
|
228
|
+
run_list = await cfg.backend.get_runs(skill_name=skill, since=since, limit=limit)
|
|
229
|
+
table = Table(box=box.SIMPLE)
|
|
230
|
+
table.add_column("Run ID"); table.add_column("Skill")
|
|
231
|
+
table.add_column("Status"); table.add_column("Trigger"); table.add_column("Iter")
|
|
232
|
+
for r in run_list:
|
|
233
|
+
c = {"completed": "green", "failed": "red"}.get(r.status.value, "white")
|
|
234
|
+
table.add_row(
|
|
235
|
+
r.id[:10] + "…", r.skill_name,
|
|
236
|
+
f"[{c}]{r.status.value}[/{c}]", r.trigger, str(r.iteration),
|
|
237
|
+
)
|
|
238
|
+
console.print(table)
|
|
239
|
+
console.print(f"[dim]{len(run_list)} run(s)[/dim]")
|
|
240
|
+
asyncio.run(_run())
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ── Trust ─────────────────────────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
@cli.group()
|
|
246
|
+
def trust() -> None:
|
|
247
|
+
"""Manage skill trust scores and approvals."""
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@trust.command("list")
|
|
251
|
+
@click.option("--config", "-c", default="loopentx_config.py")
|
|
252
|
+
def trust_list(config: str) -> None:
|
|
253
|
+
"""Show trust scores for all registered skills."""
|
|
254
|
+
async def _run() -> None:
|
|
255
|
+
_load_config(config)
|
|
256
|
+
from loopentx.core.config import get_config
|
|
257
|
+
cfg = get_config()
|
|
258
|
+
skills = await cfg.backend.list_skill_registrations()
|
|
259
|
+
table = Table(box=box.SIMPLE, title="Trust scores")
|
|
260
|
+
table.add_column("Name"); table.add_column("Kind")
|
|
261
|
+
table.add_column("Level"); table.add_column("Score")
|
|
262
|
+
table.add_column("Runs"); table.add_column("Status")
|
|
263
|
+
for reg in skills:
|
|
264
|
+
tr = await cfg.backend.get_trust_record(reg.name)
|
|
265
|
+
score = f"{tr.trust_score:.2f}" if tr else "-"
|
|
266
|
+
level = tr.trust_level.value if tr else "-"
|
|
267
|
+
runs = str(tr.total_runs) if tr else "0"
|
|
268
|
+
status = "🟢 active" if reg.is_active else "🟡 pending"
|
|
269
|
+
table.add_row(reg.name, reg.kind, level, score, runs, status)
|
|
270
|
+
console.print(table)
|
|
271
|
+
asyncio.run(_run())
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@trust.command("approve")
|
|
275
|
+
@click.argument("skill_name")
|
|
276
|
+
@click.option("--by", default="human")
|
|
277
|
+
@click.option("--config", "-c", default="loopentx_config.py")
|
|
278
|
+
def trust_approve(skill_name: str, by: str, config: str) -> None:
|
|
279
|
+
"""Approve a skill for live execution."""
|
|
280
|
+
async def _run() -> None:
|
|
281
|
+
_load_config(config)
|
|
282
|
+
from loopentx.core.config import get_config
|
|
283
|
+
from loopentx.trust.scorer import TrustScore
|
|
284
|
+
cfg = get_config()
|
|
285
|
+
await cfg.backend.approve_skill(skill_name, approved_by=by)
|
|
286
|
+
await TrustScore.approve(skill_name, approved_by=by)
|
|
287
|
+
console.print(f"[green]✓ Approved:[/green] {skill_name} (by {by})")
|
|
288
|
+
asyncio.run(_run())
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@trust.command("reject")
|
|
292
|
+
@click.argument("skill_name")
|
|
293
|
+
@click.option("--by", default="human")
|
|
294
|
+
@click.option("--config", "-c", default="loopentx_config.py")
|
|
295
|
+
def trust_reject(skill_name: str, by: str, config: str) -> None:
|
|
296
|
+
"""Reject and pause a skill."""
|
|
297
|
+
async def _run() -> None:
|
|
298
|
+
_load_config(config)
|
|
299
|
+
from loopentx.core.config import get_config
|
|
300
|
+
from loopentx.trust.scorer import TrustScore
|
|
301
|
+
cfg = get_config()
|
|
302
|
+
await cfg.backend.set_skill_active(skill_name, active=False)
|
|
303
|
+
await TrustScore.reject(skill_name, rejected_by=by)
|
|
304
|
+
console.print(f"[red]✗ Rejected:[/red] {skill_name} — skill paused")
|
|
305
|
+
asyncio.run(_run())
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# ── Memory ────────────────────────────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
@cli.group()
|
|
311
|
+
def memory() -> None:
|
|
312
|
+
"""Inspect and manage loop memory."""
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@memory.command("show")
|
|
316
|
+
@click.argument("loop_name")
|
|
317
|
+
@click.option("--config", "-c", default="loopentx_config.py")
|
|
318
|
+
def memory_show(loop_name: str, config: str) -> None:
|
|
319
|
+
"""Show current memory for a loop."""
|
|
320
|
+
async def _run() -> None:
|
|
321
|
+
_load_config(config)
|
|
322
|
+
from loopentx.core.config import get_config
|
|
323
|
+
cfg = get_config()
|
|
324
|
+
rec = await cfg.backend.get_loop_memory(loop_name)
|
|
325
|
+
if not rec:
|
|
326
|
+
console.print(f"[dim]No memory stored for '{loop_name}'.[/dim]")
|
|
327
|
+
return
|
|
328
|
+
console.print(Panel(
|
|
329
|
+
"\n".join(
|
|
330
|
+
f"[bold]{k}:[/bold] {v.value}" for k, v in rec.entries.items()
|
|
331
|
+
) or "(empty)",
|
|
332
|
+
title=f"Memory: {loop_name}",
|
|
333
|
+
))
|
|
334
|
+
for k, v in rec.lists.items():
|
|
335
|
+
console.print(f" [bold]{k}[/bold] ({len(v)} items): {v[-3:]}")
|
|
336
|
+
asyncio.run(_run())
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@memory.command("clear")
|
|
340
|
+
@click.argument("loop_name")
|
|
341
|
+
@click.option("--config", "-c", default="loopentx_config.py")
|
|
342
|
+
def memory_clear(loop_name: str, config: str) -> None:
|
|
343
|
+
"""Clear all memory for a loop."""
|
|
344
|
+
async def _run() -> None:
|
|
345
|
+
_load_config(config)
|
|
346
|
+
from loopentx.core.config import get_config
|
|
347
|
+
from loopentx.core.models import LoopMemoryRecord
|
|
348
|
+
cfg = get_config()
|
|
349
|
+
await cfg.backend.save_loop_memory(LoopMemoryRecord(loop_name=loop_name))
|
|
350
|
+
console.print(f"[yellow]Cleared memory for '{loop_name}'.[/yellow]")
|
|
351
|
+
asyncio.run(_run())
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
# ── Escalations ───────────────────────────────────────────────────────────────
|
|
355
|
+
|
|
356
|
+
@cli.group()
|
|
357
|
+
def escalations() -> None:
|
|
358
|
+
"""Manage loop escalations."""
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
@escalations.command("list")
|
|
362
|
+
@click.option("--config", "-c", default="loopentx_config.py")
|
|
363
|
+
def escalations_list(config: str) -> None:
|
|
364
|
+
"""List pending escalations awaiting human response."""
|
|
365
|
+
async def _run() -> None:
|
|
366
|
+
_load_config(config)
|
|
367
|
+
from loopentx.core.config import get_config
|
|
368
|
+
cfg = get_config()
|
|
369
|
+
pending = await cfg.backend.list_pending_escalations()
|
|
370
|
+
if not pending:
|
|
371
|
+
console.print("[dim]No pending escalations.[/dim]")
|
|
372
|
+
return
|
|
373
|
+
table = Table(box=box.SIMPLE, title="Pending escalations")
|
|
374
|
+
table.add_column("ID"); table.add_column("Skill")
|
|
375
|
+
table.add_column("Message"); table.add_column("Fallback")
|
|
376
|
+
for e in pending:
|
|
377
|
+
table.add_row(e.id[:10] + "…", e.skill_name, e.message[:60], e.fallback)
|
|
378
|
+
console.print(table)
|
|
379
|
+
asyncio.run(_run())
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@escalations.command("respond")
|
|
383
|
+
@click.argument("escalation_id")
|
|
384
|
+
@click.argument("response")
|
|
385
|
+
@click.option("--config", "-c", default="loopentx_config.py")
|
|
386
|
+
def escalations_respond(escalation_id: str, response: str, config: str) -> None:
|
|
387
|
+
"""Provide a human response to an escalation."""
|
|
388
|
+
async def _run() -> None:
|
|
389
|
+
_load_config(config)
|
|
390
|
+
from loopentx.core.orchestrator import Orchestrator
|
|
391
|
+
orch = Orchestrator()
|
|
392
|
+
await orch.respond_to_escalation(escalation_id, response)
|
|
393
|
+
console.print(f"[green]✓ Response sent:[/green] {response}")
|
|
394
|
+
asyncio.run(_run())
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
if __name__ == "__main__":
|
|
398
|
+
cli()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Loopentx core primitives."""
|
|
2
|
+
|
|
3
|
+
from loopentx.core.loop import loop
|
|
4
|
+
from loopentx.core.skill import skill
|
|
5
|
+
from loopentx.core.context import LoopContext
|
|
6
|
+
from loopentx.core.memory import LoopMemory
|
|
7
|
+
from loopentx.core.orchestrator import Orchestrator
|
|
8
|
+
from loopentx.core.config import configure, get_config
|
|
9
|
+
from loopentx.core.events import event, LoopentxEvent
|
|
10
|
+
from loopentx.core.models import (
|
|
11
|
+
RunRecord, StepRecord, SkillRegistration,
|
|
12
|
+
TrustRecord, LoopMemoryRecord,
|
|
13
|
+
)
|
|
14
|
+
from loopentx.core.exceptions import (
|
|
15
|
+
LoopentxError, StepError, SkillError,
|
|
16
|
+
PolicyViolationError, SkillNotApprovedError,
|
|
17
|
+
EscalationTimeoutError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"loop", "skill", "event", "configure", "get_config",
|
|
22
|
+
"LoopContext", "LoopMemory", "Orchestrator", "LoopentxEvent",
|
|
23
|
+
"RunRecord", "StepRecord", "SkillRegistration",
|
|
24
|
+
"TrustRecord", "LoopMemoryRecord",
|
|
25
|
+
"LoopentxError", "StepError", "SkillError",
|
|
26
|
+
"PolicyViolationError", "SkillNotApprovedError",
|
|
27
|
+
"EscalationTimeoutError",
|
|
28
|
+
]
|
loopentx/core/config.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Global configuration for Loopentx."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Optional, TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from loopentx.backends.base import BaseBackend
|
|
10
|
+
|
|
11
|
+
_config: Optional["LoopentxConfig"] = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class LoopentxConfig:
|
|
16
|
+
backend: "BaseBackend"
|
|
17
|
+
llm_provider: str = "openai"
|
|
18
|
+
llm_model: str = "gpt-4o"
|
|
19
|
+
llm_api_key: Optional[str] = None
|
|
20
|
+
llm_extra: dict[str, Any] = field(default_factory=dict)
|
|
21
|
+
default_shadow_cycles: int = 0
|
|
22
|
+
default_blast_radius: str = "low"
|
|
23
|
+
auto_approve_low_blast: bool = True
|
|
24
|
+
worker_concurrency: int = 10
|
|
25
|
+
worker_poll_interval: float = 1.0
|
|
26
|
+
log_level: str = "INFO"
|
|
27
|
+
store_step_outputs: bool = True
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def configure(
|
|
31
|
+
backend: "BaseBackend",
|
|
32
|
+
llm_provider: str = "openai",
|
|
33
|
+
llm_model: str = "gpt-4o",
|
|
34
|
+
llm_api_key: Optional[str] = None,
|
|
35
|
+
**kwargs: Any,
|
|
36
|
+
) -> LoopentxConfig:
|
|
37
|
+
"""Configure Loopentx. Call once at application startup.
|
|
38
|
+
|
|
39
|
+
Example:
|
|
40
|
+
from loopentx import configure
|
|
41
|
+
from loopentx.backends import RedisBackend
|
|
42
|
+
|
|
43
|
+
configure(
|
|
44
|
+
backend=RedisBackend("redis://localhost:6379"),
|
|
45
|
+
llm_provider="anthropic",
|
|
46
|
+
llm_model="claude-sonnet-4-6",
|
|
47
|
+
)
|
|
48
|
+
"""
|
|
49
|
+
global _config
|
|
50
|
+
_config = LoopentxConfig(
|
|
51
|
+
backend=backend,
|
|
52
|
+
llm_provider=llm_provider,
|
|
53
|
+
llm_model=llm_model,
|
|
54
|
+
llm_api_key=llm_api_key,
|
|
55
|
+
**kwargs,
|
|
56
|
+
)
|
|
57
|
+
return _config
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_config() -> LoopentxConfig:
|
|
61
|
+
"""Return the current global configuration."""
|
|
62
|
+
if _config is None:
|
|
63
|
+
raise RuntimeError(
|
|
64
|
+
"Loopentx is not configured. "
|
|
65
|
+
"Call loopentx.configure() before using the framework."
|
|
66
|
+
)
|
|
67
|
+
return _config
|