open-reflection-protocol 0.3.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.
- open_reflection_protocol-0.3.0.dist-info/METADATA +262 -0
- open_reflection_protocol-0.3.0.dist-info/RECORD +29 -0
- open_reflection_protocol-0.3.0.dist-info/WHEEL +4 -0
- open_reflection_protocol-0.3.0.dist-info/entry_points.txt +2 -0
- orp/__init__.py +66 -0
- orp/adapters/__init__.py +6 -0
- orp/adapters/generic_json.py +24 -0
- orp/adapters/langgraph.py +24 -0
- orp/adapters/openai_agents.py +27 -0
- orp/adapters/otel.py +52 -0
- orp/capture.py +162 -0
- orp/cli.py +366 -0
- orp/compiler.py +124 -0
- orp/conflicts.py +62 -0
- orp/delivery.py +110 -0
- orp/effects.py +112 -0
- orp/evidence.py +92 -0
- orp/examples/failing_coding_agent.py +38 -0
- orp/experience.py +114 -0
- orp/export.py +60 -0
- orp/lessons.py +95 -0
- orp/mcp_server.py +171 -0
- orp/reflect.py +97 -0
- orp/replay.py +108 -0
- orp/rollback.py +82 -0
- orp/schema.py +303 -0
- orp/storage.py +459 -0
- orp/training.py +94 -0
- orp/viewer.py +104 -0
orp/storage.py
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
"""存储层 — SQLite + artifact directory"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sqlite3
|
|
5
|
+
import shutil
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
from orp.schema import (
|
|
11
|
+
ExperienceRecord, Lesson, EvalArtifact, CounterfactualReplay,
|
|
12
|
+
LessonDelivery, LessonEvaluation, LessonRollback, TrainingCandidate,
|
|
13
|
+
TimelineEvent, Outcome, ReflectionAnalysis, Feedback,
|
|
14
|
+
LessonStatus, TrustLevel, TrainingStatus, TrainingFormat, DeliveryStrategy,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
DEFAULT_ORP_DIR = Path.home() / ".orp"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _ensure_dir(path: Path) -> Path:
|
|
22
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
return path
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ORPStorage:
|
|
27
|
+
"""ORP 本地存储 — SQLite + artifact directory"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, base_dir: Optional[Path] = None):
|
|
30
|
+
self.base = Path(base_dir) if base_dir else DEFAULT_ORP_DIR
|
|
31
|
+
self.db_path = self.base / "orp.db"
|
|
32
|
+
self.artifact_dir = _ensure_dir(self.base / "artifacts")
|
|
33
|
+
self.eval_dir = _ensure_dir(self.base / "evals")
|
|
34
|
+
self.report_dir = _ensure_dir(self.base / "reports")
|
|
35
|
+
self._conn: Optional[sqlite3.Connection] = None
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def conn(self) -> sqlite3.Connection:
|
|
39
|
+
if self._conn is None:
|
|
40
|
+
self._conn = sqlite3.connect(str(self.db_path), timeout=5)
|
|
41
|
+
self._conn.row_factory = sqlite3.Row
|
|
42
|
+
self._conn.execute("PRAGMA journal_mode=WAL")
|
|
43
|
+
self._conn.execute("PRAGMA busy_timeout=5000")
|
|
44
|
+
self._init_schema()
|
|
45
|
+
return self._conn
|
|
46
|
+
|
|
47
|
+
def _init_schema(self) -> None:
|
|
48
|
+
"""初始化数据库 schema"""
|
|
49
|
+
self._conn.executescript("""
|
|
50
|
+
CREATE TABLE IF NOT EXISTS experiences (
|
|
51
|
+
experience_id TEXT PRIMARY KEY,
|
|
52
|
+
orp_version TEXT NOT NULL DEFAULT '0.3',
|
|
53
|
+
trace_ref TEXT,
|
|
54
|
+
agent_json TEXT NOT NULL,
|
|
55
|
+
task_json TEXT NOT NULL,
|
|
56
|
+
outcome_json TEXT NOT NULL,
|
|
57
|
+
reflection_json TEXT,
|
|
58
|
+
artifacts_json TEXT,
|
|
59
|
+
created_at TEXT NOT NULL
|
|
60
|
+
);
|
|
61
|
+
CREATE TABLE IF NOT EXISTS timeline_events (
|
|
62
|
+
event_id TEXT PRIMARY KEY,
|
|
63
|
+
experience_id TEXT NOT NULL REFERENCES experiences(experience_id),
|
|
64
|
+
kind TEXT NOT NULL,
|
|
65
|
+
source TEXT NOT NULL DEFAULT 'agent',
|
|
66
|
+
content TEXT NOT NULL,
|
|
67
|
+
evidence_refs_json TEXT,
|
|
68
|
+
parent_event TEXT,
|
|
69
|
+
timestamp TEXT NOT NULL
|
|
70
|
+
);
|
|
71
|
+
CREATE TABLE IF NOT EXISTS lessons (
|
|
72
|
+
lesson_id TEXT PRIMARY KEY,
|
|
73
|
+
trigger_json TEXT NOT NULL,
|
|
74
|
+
recommendation TEXT NOT NULL,
|
|
75
|
+
provenance_json TEXT,
|
|
76
|
+
scope_json TEXT,
|
|
77
|
+
relationships_json TEXT,
|
|
78
|
+
validation_json TEXT,
|
|
79
|
+
metrics_json TEXT,
|
|
80
|
+
status TEXT NOT NULL DEFAULT 'candidate',
|
|
81
|
+
expires_at TEXT,
|
|
82
|
+
created_at TEXT NOT NULL,
|
|
83
|
+
updated_at TEXT NOT NULL
|
|
84
|
+
);
|
|
85
|
+
CREATE TABLE IF NOT EXISTS evals (
|
|
86
|
+
eval_id TEXT PRIMARY KEY,
|
|
87
|
+
origin_experience TEXT NOT NULL,
|
|
88
|
+
runner TEXT NOT NULL DEFAULT 'pytest',
|
|
89
|
+
command TEXT NOT NULL,
|
|
90
|
+
expected_json TEXT,
|
|
91
|
+
generated_by TEXT DEFAULT 'agent',
|
|
92
|
+
review_json TEXT,
|
|
93
|
+
last_result_json TEXT,
|
|
94
|
+
created_at TEXT NOT NULL
|
|
95
|
+
);
|
|
96
|
+
CREATE TABLE IF NOT EXISTS replays (
|
|
97
|
+
replay_id TEXT PRIMARY KEY,
|
|
98
|
+
experience_id TEXT NOT NULL,
|
|
99
|
+
original_strategy TEXT NOT NULL,
|
|
100
|
+
alternative_strategy TEXT NOT NULL,
|
|
101
|
+
verification_mode TEXT DEFAULT 'sandbox_replay',
|
|
102
|
+
result_json TEXT,
|
|
103
|
+
created_at TEXT NOT NULL
|
|
104
|
+
);
|
|
105
|
+
CREATE TABLE IF NOT EXISTS deliveries (
|
|
106
|
+
delivery_id TEXT PRIMARY KEY,
|
|
107
|
+
lesson_id TEXT NOT NULL,
|
|
108
|
+
experience_id TEXT NOT NULL,
|
|
109
|
+
strategy TEXT NOT NULL,
|
|
110
|
+
delivered_at TEXT NOT NULL,
|
|
111
|
+
delivery_context TEXT,
|
|
112
|
+
acknowledged INTEGER DEFAULT 0,
|
|
113
|
+
applied INTEGER DEFAULT 0,
|
|
114
|
+
application_evidence_json TEXT
|
|
115
|
+
);
|
|
116
|
+
CREATE TABLE IF NOT EXISTS lesson_evals (
|
|
117
|
+
evaluation_id TEXT PRIMARY KEY,
|
|
118
|
+
lesson_id TEXT NOT NULL,
|
|
119
|
+
method TEXT NOT NULL,
|
|
120
|
+
population_json TEXT,
|
|
121
|
+
results_json TEXT,
|
|
122
|
+
decision TEXT DEFAULT 'keep_active',
|
|
123
|
+
evidence_refs_json TEXT,
|
|
124
|
+
created_at TEXT NOT NULL
|
|
125
|
+
);
|
|
126
|
+
CREATE TABLE IF NOT EXISTS rollbacks (
|
|
127
|
+
rollback_id TEXT PRIMARY KEY,
|
|
128
|
+
lesson_id TEXT NOT NULL,
|
|
129
|
+
reason TEXT NOT NULL,
|
|
130
|
+
previous_status TEXT NOT NULL,
|
|
131
|
+
new_status TEXT NOT NULL,
|
|
132
|
+
affected_deliveries_json TEXT,
|
|
133
|
+
replacement_lesson_id TEXT,
|
|
134
|
+
evidence_refs_json TEXT,
|
|
135
|
+
created_at TEXT NOT NULL
|
|
136
|
+
);
|
|
137
|
+
CREATE TABLE IF NOT EXISTS training_candidates (
|
|
138
|
+
candidate_id TEXT PRIMARY KEY,
|
|
139
|
+
source_experience_ids_json TEXT,
|
|
140
|
+
format TEXT NOT NULL,
|
|
141
|
+
validation_json TEXT,
|
|
142
|
+
status TEXT NOT NULL DEFAULT 'candidate',
|
|
143
|
+
artifact_ref TEXT,
|
|
144
|
+
created_at TEXT NOT NULL
|
|
145
|
+
);
|
|
146
|
+
CREATE INDEX IF NOT EXISTS idx_timeline_exp ON timeline_events(experience_id);
|
|
147
|
+
CREATE INDEX IF NOT EXISTS idx_lessons_status ON lessons(status);
|
|
148
|
+
CREATE INDEX IF NOT EXISTS idx_deliveries_lesson ON deliveries(lesson_id);
|
|
149
|
+
CREATE INDEX IF NOT EXISTS idx_lesson_evals_lesson ON lesson_evals(lesson_id);
|
|
150
|
+
""")
|
|
151
|
+
|
|
152
|
+
# ─── Experience ───
|
|
153
|
+
|
|
154
|
+
def save_experience(self, exp: ExperienceRecord) -> None:
|
|
155
|
+
self.conn.execute("""
|
|
156
|
+
INSERT OR REPLACE INTO experiences
|
|
157
|
+
(experience_id, orp_version, trace_ref, agent_json, task_json,
|
|
158
|
+
outcome_json, reflection_json, artifacts_json, created_at)
|
|
159
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
160
|
+
""", (
|
|
161
|
+
exp.experience_id, exp.orp_version, exp.trace_ref,
|
|
162
|
+
json.dumps(exp.agent), json.dumps(exp.task),
|
|
163
|
+
exp.outcome.model_dump_json(),
|
|
164
|
+
json.dumps(exp.reflection.model_dump()) if exp.reflection else None,
|
|
165
|
+
json.dumps(exp.artifacts), exp.created_at.isoformat(),
|
|
166
|
+
))
|
|
167
|
+
self.conn.commit()
|
|
168
|
+
for evt in exp.timeline:
|
|
169
|
+
self.conn.execute("""
|
|
170
|
+
INSERT OR REPLACE INTO timeline_events
|
|
171
|
+
(event_id, experience_id, kind, source, content,
|
|
172
|
+
evidence_refs_json, parent_event, timestamp)
|
|
173
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
174
|
+
""", (
|
|
175
|
+
evt.id, exp.experience_id, evt.kind.value, evt.source,
|
|
176
|
+
evt.content, json.dumps(evt.evidence_refs),
|
|
177
|
+
evt.parent_event, evt.timestamp.isoformat(),
|
|
178
|
+
))
|
|
179
|
+
# Save feedback
|
|
180
|
+
for fb in exp.feedback:
|
|
181
|
+
self._save_feedback(exp.experience_id, fb)
|
|
182
|
+
|
|
183
|
+
def get_experience(self, experience_id: str) -> Optional[ExperienceRecord]:
|
|
184
|
+
row = self.conn.execute(
|
|
185
|
+
"SELECT * FROM experiences WHERE experience_id = ?",
|
|
186
|
+
(experience_id,)
|
|
187
|
+
).fetchone()
|
|
188
|
+
if not row:
|
|
189
|
+
return None
|
|
190
|
+
return self._row_to_experience(row)
|
|
191
|
+
|
|
192
|
+
def list_experiences(self, limit: int = 20, offset: int = 0) -> list[ExperienceRecord]:
|
|
193
|
+
rows = self.conn.execute(
|
|
194
|
+
"SELECT * FROM experiences ORDER BY created_at DESC LIMIT ? OFFSET ?",
|
|
195
|
+
(limit, offset)
|
|
196
|
+
).fetchall()
|
|
197
|
+
return [self._row_to_experience(r) for r in rows]
|
|
198
|
+
|
|
199
|
+
def _row_to_experience(self, row: sqlite3.Row) -> ExperienceRecord:
|
|
200
|
+
events = self.conn.execute(
|
|
201
|
+
"SELECT * FROM timeline_events WHERE experience_id = ? ORDER BY timestamp",
|
|
202
|
+
(row["experience_id"],)
|
|
203
|
+
).fetchall()
|
|
204
|
+
return ExperienceRecord(
|
|
205
|
+
orp_version=row["orp_version"],
|
|
206
|
+
experience_id=row["experience_id"],
|
|
207
|
+
trace_ref=row["trace_ref"],
|
|
208
|
+
agent=json.loads(row["agent_json"]),
|
|
209
|
+
task=json.loads(row["task_json"]),
|
|
210
|
+
timeline=[TimelineEvent(**{
|
|
211
|
+
"id": e["event_id"],
|
|
212
|
+
"kind": e["kind"],
|
|
213
|
+
"source": e["source"],
|
|
214
|
+
"content": e["content"],
|
|
215
|
+
"evidence_refs": json.loads(e["evidence_refs_json"]) if e["evidence_refs_json"] else [],
|
|
216
|
+
"parent_event": e["parent_event"],
|
|
217
|
+
"timestamp": e["timestamp"],
|
|
218
|
+
}) for e in events],
|
|
219
|
+
outcome=Outcome(**json.loads(row["outcome_json"])),
|
|
220
|
+
reflection=ReflectionAnalysis(**json.loads(row["reflection_json"])) if row["reflection_json"] else None,
|
|
221
|
+
artifacts=json.loads(row["artifacts_json"]) if row["artifacts_json"] else {},
|
|
222
|
+
created_at=datetime.fromisoformat(row["created_at"]),
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# ─── Lesson ───
|
|
226
|
+
|
|
227
|
+
def save_lesson(self, lesson: Lesson) -> None:
|
|
228
|
+
self.conn.execute("""
|
|
229
|
+
INSERT OR REPLACE INTO lessons
|
|
230
|
+
(lesson_id, trigger_json, recommendation, provenance_json,
|
|
231
|
+
scope_json, relationships_json, validation_json, metrics_json,
|
|
232
|
+
status, expires_at, created_at, updated_at)
|
|
233
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
234
|
+
""", (
|
|
235
|
+
lesson.lesson_id, json.dumps(lesson.trigger), lesson.recommendation,
|
|
236
|
+
json.dumps(lesson.provenance), json.dumps(lesson.scope),
|
|
237
|
+
json.dumps(lesson.relationships), json.dumps(lesson.validation),
|
|
238
|
+
json.dumps(lesson.metrics), lesson.status.value,
|
|
239
|
+
lesson.expires_at.isoformat() if lesson.expires_at else None,
|
|
240
|
+
lesson.created_at.isoformat(), lesson.updated_at.isoformat(),
|
|
241
|
+
))
|
|
242
|
+
self.conn.commit()
|
|
243
|
+
|
|
244
|
+
def get_lesson(self, lesson_id: str) -> Optional[Lesson]:
|
|
245
|
+
row = self.conn.execute(
|
|
246
|
+
"SELECT * FROM lessons WHERE lesson_id = ?", (lesson_id,)
|
|
247
|
+
).fetchone()
|
|
248
|
+
if not row:
|
|
249
|
+
return None
|
|
250
|
+
return self._row_to_lesson(row)
|
|
251
|
+
|
|
252
|
+
def list_lessons(self, status: Optional[LessonStatus] = None, limit: int = 50) -> list[Lesson]:
|
|
253
|
+
if status:
|
|
254
|
+
rows = self.conn.execute(
|
|
255
|
+
"SELECT * FROM lessons WHERE status = ? ORDER BY updated_at DESC LIMIT ?",
|
|
256
|
+
(status.value, limit)
|
|
257
|
+
).fetchall()
|
|
258
|
+
else:
|
|
259
|
+
rows = self.conn.execute(
|
|
260
|
+
"SELECT * FROM lessons ORDER BY updated_at DESC LIMIT ?", (limit,)
|
|
261
|
+
).fetchall()
|
|
262
|
+
return [self._row_to_lesson(r) for r in rows]
|
|
263
|
+
|
|
264
|
+
def _row_to_lesson(self, row: sqlite3.Row) -> Lesson:
|
|
265
|
+
return Lesson(
|
|
266
|
+
lesson_id=row["lesson_id"],
|
|
267
|
+
trigger=json.loads(row["trigger_json"]),
|
|
268
|
+
recommendation=row["recommendation"],
|
|
269
|
+
provenance=json.loads(row["provenance_json"]) if row["provenance_json"] else {},
|
|
270
|
+
scope=json.loads(row["scope_json"]) if row["scope_json"] else {},
|
|
271
|
+
relationships=json.loads(row["relationships_json"]) if row["relationships_json"] else {},
|
|
272
|
+
validation=json.loads(row["validation_json"]) if row["validation_json"] else {},
|
|
273
|
+
metrics=json.loads(row["metrics_json"]) if row["metrics_json"] else {},
|
|
274
|
+
status=LessonStatus(row["status"]),
|
|
275
|
+
expires_at=datetime.fromisoformat(row["expires_at"]) if row["expires_at"] else None,
|
|
276
|
+
created_at=datetime.fromisoformat(row["created_at"]),
|
|
277
|
+
updated_at=datetime.fromisoformat(row["updated_at"]),
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
def update_lesson_status(self, lesson_id: str, status: LessonStatus) -> None:
|
|
281
|
+
self.conn.execute(
|
|
282
|
+
"UPDATE lessons SET status = ?, updated_at = ? WHERE lesson_id = ?",
|
|
283
|
+
(status.value, datetime.utcnow().isoformat(), lesson_id)
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# ─── Eval ───
|
|
287
|
+
|
|
288
|
+
def save_eval(self, eval_: EvalArtifact) -> None:
|
|
289
|
+
self.conn.execute("""
|
|
290
|
+
INSERT OR REPLACE INTO evals
|
|
291
|
+
(eval_id, origin_experience, runner, command, expected_json,
|
|
292
|
+
generated_by, review_json, last_result_json, created_at)
|
|
293
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
294
|
+
""", (
|
|
295
|
+
eval_.eval_id, eval_.origin_experience, eval_.runner, eval_.command,
|
|
296
|
+
json.dumps(eval_.expected), eval_.generated_by,
|
|
297
|
+
json.dumps(eval_.review) if eval_.review else None,
|
|
298
|
+
json.dumps(eval_.last_result) if eval_.last_result else None,
|
|
299
|
+
eval_.created_at.isoformat(),
|
|
300
|
+
))
|
|
301
|
+
|
|
302
|
+
# ─── Replay ───
|
|
303
|
+
|
|
304
|
+
def save_replay(self, replay: CounterfactualReplay) -> None:
|
|
305
|
+
self.conn.execute("""
|
|
306
|
+
INSERT OR REPLACE INTO replays
|
|
307
|
+
(replay_id, experience_id, original_strategy, alternative_strategy,
|
|
308
|
+
verification_mode, result_json, created_at)
|
|
309
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
310
|
+
""", (
|
|
311
|
+
replay.replay_id, replay.experience_id,
|
|
312
|
+
replay.original_strategy, replay.alternative_strategy,
|
|
313
|
+
replay.verification_mode, json.dumps(replay.result),
|
|
314
|
+
replay.created_at.isoformat(),
|
|
315
|
+
))
|
|
316
|
+
|
|
317
|
+
# ─── Delivery ───
|
|
318
|
+
|
|
319
|
+
def save_delivery(self, delivery: LessonDelivery) -> None:
|
|
320
|
+
self.conn.execute("""
|
|
321
|
+
INSERT OR REPLACE INTO deliveries
|
|
322
|
+
(delivery_id, lesson_id, experience_id, strategy, delivered_at,
|
|
323
|
+
delivery_context, acknowledged, applied, application_evidence_json)
|
|
324
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
325
|
+
""", (
|
|
326
|
+
delivery.delivery_id, delivery.lesson_id, delivery.experience_id,
|
|
327
|
+
delivery.strategy.value, delivery.delivered_at.isoformat(),
|
|
328
|
+
delivery.delivery_context,
|
|
329
|
+
1 if delivery.acknowledged else 0,
|
|
330
|
+
1 if delivery.applied else 0,
|
|
331
|
+
json.dumps(delivery.application_evidence_refs),
|
|
332
|
+
))
|
|
333
|
+
|
|
334
|
+
def get_deliveries_for_lesson(self, lesson_id: str) -> list[LessonDelivery]:
|
|
335
|
+
rows = self.conn.execute(
|
|
336
|
+
"SELECT * FROM deliveries WHERE lesson_id = ? ORDER BY delivered_at DESC",
|
|
337
|
+
(lesson_id,)
|
|
338
|
+
).fetchall()
|
|
339
|
+
return [LessonDelivery(
|
|
340
|
+
delivery_id=r["delivery_id"], lesson_id=r["lesson_id"],
|
|
341
|
+
experience_id=r["experience_id"],
|
|
342
|
+
strategy=DeliveryStrategy(r["strategy"]),
|
|
343
|
+
delivered_at=datetime.fromisoformat(r["delivered_at"]),
|
|
344
|
+
delivery_context=r["delivery_context"],
|
|
345
|
+
acknowledged=bool(r["acknowledged"]),
|
|
346
|
+
applied=bool(r["applied"]),
|
|
347
|
+
application_evidence_refs=json.loads(r["application_evidence_json"]) if r["application_evidence_json"] else [],
|
|
348
|
+
) for r in rows]
|
|
349
|
+
|
|
350
|
+
# ─── Lesson Evaluation ───
|
|
351
|
+
|
|
352
|
+
def save_lesson_evaluation(self, le: LessonEvaluation) -> None:
|
|
353
|
+
self.conn.execute("""
|
|
354
|
+
INSERT OR REPLACE INTO lesson_evals
|
|
355
|
+
(evaluation_id, lesson_id, method, population_json, results_json,
|
|
356
|
+
decision, evidence_refs_json, created_at)
|
|
357
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
358
|
+
""", (
|
|
359
|
+
le.evaluation_id, le.lesson_id, le.method.value,
|
|
360
|
+
json.dumps(le.population), json.dumps(le.results),
|
|
361
|
+
le.decision, json.dumps(le.evidence_refs),
|
|
362
|
+
le.created_at.isoformat(),
|
|
363
|
+
))
|
|
364
|
+
self.conn.commit()
|
|
365
|
+
|
|
366
|
+
# ─── Rollback ───
|
|
367
|
+
|
|
368
|
+
def save_rollback(self, rollback: LessonRollback) -> None:
|
|
369
|
+
self.conn.execute("""
|
|
370
|
+
INSERT OR REPLACE INTO rollbacks
|
|
371
|
+
(rollback_id, lesson_id, reason, previous_status, new_status,
|
|
372
|
+
affected_deliveries_json, replacement_lesson_id, evidence_refs_json, created_at)
|
|
373
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
374
|
+
""", (
|
|
375
|
+
rollback.rollback_id, rollback.lesson_id, rollback.reason,
|
|
376
|
+
rollback.previous_status.value, rollback.new_status.value,
|
|
377
|
+
json.dumps(rollback.affected_deliveries),
|
|
378
|
+
rollback.replacement_lesson_id,
|
|
379
|
+
json.dumps(rollback.evidence_refs),
|
|
380
|
+
rollback.created_at.isoformat(),
|
|
381
|
+
))
|
|
382
|
+
|
|
383
|
+
# ─── Training Candidate ───
|
|
384
|
+
|
|
385
|
+
def save_training_candidate(self, tc: TrainingCandidate) -> None:
|
|
386
|
+
self.conn.execute("""
|
|
387
|
+
INSERT OR REPLACE INTO training_candidates
|
|
388
|
+
(candidate_id, source_experience_ids_json, format, validation_json,
|
|
389
|
+
status, artifact_ref, created_at)
|
|
390
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
391
|
+
""", (
|
|
392
|
+
tc.candidate_id, json.dumps(tc.source_experience_ids),
|
|
393
|
+
tc.format.value, json.dumps(tc.validation),
|
|
394
|
+
tc.status.value, tc.artifact_ref, tc.created_at.isoformat(),
|
|
395
|
+
))
|
|
396
|
+
|
|
397
|
+
def list_training_candidates(self, status: Optional[TrainingStatus] = None) -> list[TrainingCandidate]:
|
|
398
|
+
if status:
|
|
399
|
+
rows = self.conn.execute(
|
|
400
|
+
"SELECT * FROM training_candidates WHERE status = ? ORDER BY created_at DESC",
|
|
401
|
+
(status.value,)
|
|
402
|
+
).fetchall()
|
|
403
|
+
else:
|
|
404
|
+
rows = self.conn.execute(
|
|
405
|
+
"SELECT * FROM training_candidates ORDER BY created_at DESC"
|
|
406
|
+
).fetchall()
|
|
407
|
+
result = []
|
|
408
|
+
for r in rows:
|
|
409
|
+
tc = TrainingCandidate(
|
|
410
|
+
candidate_id=r["candidate_id"],
|
|
411
|
+
source_experience_ids=json.loads(r["source_experience_ids_json"]),
|
|
412
|
+
format=TrainingFormat(r["format"]),
|
|
413
|
+
validation=json.loads(r["validation_json"]) if r["validation_json"] else {},
|
|
414
|
+
status=TrainingStatus(r["status"]),
|
|
415
|
+
artifact_ref=r["artifact_ref"],
|
|
416
|
+
created_at=datetime.fromisoformat(r["created_at"]),
|
|
417
|
+
)
|
|
418
|
+
result.append(tc)
|
|
419
|
+
return result
|
|
420
|
+
|
|
421
|
+
# ─── Feedback (internal) ───
|
|
422
|
+
|
|
423
|
+
def _save_feedback(self, experience_id: str, fb: Feedback) -> None:
|
|
424
|
+
self.conn.execute("""
|
|
425
|
+
INSERT INTO feedback (experience_id, target_ref, source_type, source_id,
|
|
426
|
+
verdict, explanation, evidence_refs_json)
|
|
427
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
428
|
+
""", (
|
|
429
|
+
experience_id, fb.target_ref, fb.source_type.value, fb.source_id,
|
|
430
|
+
fb.verdict, fb.explanation, json.dumps(fb.evidence_refs),
|
|
431
|
+
))
|
|
432
|
+
|
|
433
|
+
# ─── Utility ───
|
|
434
|
+
|
|
435
|
+
def save_artifact(self, name: str, content: str) -> str:
|
|
436
|
+
path = self.artifact_dir / name
|
|
437
|
+
path.write_text(content)
|
|
438
|
+
return str(path)
|
|
439
|
+
|
|
440
|
+
def close(self) -> None:
|
|
441
|
+
if self._conn:
|
|
442
|
+
try:
|
|
443
|
+
self._conn.commit()
|
|
444
|
+
except Exception:
|
|
445
|
+
pass
|
|
446
|
+
try:
|
|
447
|
+
self._conn.close()
|
|
448
|
+
except Exception:
|
|
449
|
+
pass
|
|
450
|
+
self._conn = None
|
|
451
|
+
|
|
452
|
+
def __enter__(self):
|
|
453
|
+
return self
|
|
454
|
+
|
|
455
|
+
def __exit__(self, *args):
|
|
456
|
+
self.close()
|
|
457
|
+
|
|
458
|
+
def __del__(self):
|
|
459
|
+
self.close()
|
orp/training.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Training Candidate Pipeline — 经验转训练数据"""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from orp.schema import (
|
|
6
|
+
ExperienceRecord, TrainingCandidate, TrainingFormat, TrainingStatus,
|
|
7
|
+
)
|
|
8
|
+
from orp.storage import ORPStorage
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TrainingPipeline:
|
|
12
|
+
"""训练候选管道 — 生成待审批的训练数据"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, storage: Optional[ORPStorage] = None):
|
|
15
|
+
self._storage = storage or ORPStorage()
|
|
16
|
+
|
|
17
|
+
def create_candidate(self, source_exp: ExperienceRecord,
|
|
18
|
+
format: TrainingFormat) -> Optional[TrainingCandidate]:
|
|
19
|
+
"""从已验证的 Experience 创建训练候选"""
|
|
20
|
+
if source_exp.outcome.status != "success":
|
|
21
|
+
return None # 只从成功 trace 创建 SFT 候选
|
|
22
|
+
|
|
23
|
+
tc = TrainingCandidate(
|
|
24
|
+
source_experience_ids=[source_exp.experience_id],
|
|
25
|
+
format=format,
|
|
26
|
+
validation={
|
|
27
|
+
"outcome_verified": source_exp.outcome.status == "success",
|
|
28
|
+
"human_reviewed": False,
|
|
29
|
+
"privacy_reviewed": False,
|
|
30
|
+
"license_reviewed": False,
|
|
31
|
+
},
|
|
32
|
+
status=TrainingStatus.CANDIDATE,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# 生成 artifact
|
|
36
|
+
artifact = self._render_artifact(source_exp, format)
|
|
37
|
+
if artifact:
|
|
38
|
+
artifact_path = f"training_{tc.candidate_id}.jsonl"
|
|
39
|
+
self._storage.save_artifact(artifact_path, artifact)
|
|
40
|
+
tc.artifact_ref = f"artifact:{artifact_path}"
|
|
41
|
+
|
|
42
|
+
self._storage.save_training_candidate(tc)
|
|
43
|
+
return tc
|
|
44
|
+
|
|
45
|
+
def approve(self, candidate_id: str,
|
|
46
|
+
human_reviewed: bool = True,
|
|
47
|
+
privacy_reviewed: bool = True,
|
|
48
|
+
license_reviewed: bool = True) -> bool:
|
|
49
|
+
"""审批训练候选(四重审批)"""
|
|
50
|
+
for tc in self._storage.list_training_candidates():
|
|
51
|
+
if tc.candidate_id == candidate_id:
|
|
52
|
+
tc.validation["human_reviewed"] = human_reviewed
|
|
53
|
+
tc.validation["privacy_reviewed"] = privacy_reviewed
|
|
54
|
+
tc.validation["license_reviewed"] = license_reviewed
|
|
55
|
+
all_ok = all(tc.validation.values())
|
|
56
|
+
tc.status = TrainingStatus.APPROVED if all_ok else TrainingStatus.CANDIDATE
|
|
57
|
+
self._storage.save_training_candidate(tc)
|
|
58
|
+
return all_ok
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
def export_approved(self) -> list[dict[str, Any]]:
|
|
62
|
+
"""导出所有已审批的训练候选"""
|
|
63
|
+
approved = [
|
|
64
|
+
tc for tc in self._storage.list_training_candidates()
|
|
65
|
+
if tc.status == TrainingStatus.APPROVED
|
|
66
|
+
]
|
|
67
|
+
results = []
|
|
68
|
+
for tc in approved:
|
|
69
|
+
results.append({
|
|
70
|
+
"candidate_id": tc.candidate_id,
|
|
71
|
+
"format": tc.format.value,
|
|
72
|
+
"artifact_ref": tc.artifact_ref,
|
|
73
|
+
})
|
|
74
|
+
return results
|
|
75
|
+
|
|
76
|
+
def _render_artifact(self, exp: ExperienceRecord, fmt: TrainingFormat) -> Optional[str]:
|
|
77
|
+
"""渲染训练数据 artifact"""
|
|
78
|
+
if fmt == TrainingFormat.SFT_EXAMPLE:
|
|
79
|
+
return self._render_sft(exp)
|
|
80
|
+
elif fmt == TrainingFormat.PREFERENCE_PAIR:
|
|
81
|
+
return self._render_preference(exp)
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
def _render_sft(self, exp: ExperienceRecord) -> str:
|
|
85
|
+
"""渲染为 SFT 示例"""
|
|
86
|
+
timeline_text = "\n".join(
|
|
87
|
+
f"[{e.kind.value}] {e.content[:200]}"
|
|
88
|
+
for e in exp.timeline
|
|
89
|
+
)
|
|
90
|
+
return f'{{"messages": [{{"role": "user", "content": "{exp.task.get("goal", "")}"}}, {{"role": "assistant", "content": "{timeline_text}"}}]}}\n'
|
|
91
|
+
|
|
92
|
+
def _render_preference(self, exp: ExperienceRecord) -> str:
|
|
93
|
+
"""渲染为偏好对(DPO 格式)"""
|
|
94
|
+
return f'{{"prompt": "{exp.task.get("goal", "")}", "chosen": "success", "rejected": "failed"}}\n'
|
orp/viewer.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""HTML Report Viewer"""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from orp.storage import ORPStorage
|
|
7
|
+
from orp.schema import LessonStatus
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _esc(s):
|
|
11
|
+
return (s.replace("&", "&").replace("<", "<")
|
|
12
|
+
.replace(">", ">").replace(chr(34), """))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HTMLReporter:
|
|
16
|
+
def __init__(self, storage=None):
|
|
17
|
+
self._storage = storage or ORPStorage()
|
|
18
|
+
|
|
19
|
+
def _row(self, e):
|
|
20
|
+
k = e.kind.value
|
|
21
|
+
c = _esc(e.content[:200])
|
|
22
|
+
s = _esc(e.source)
|
|
23
|
+
r = ', '.join(_esc(x) for x in e.evidence_refs[:3])
|
|
24
|
+
out = '<tr>'
|
|
25
|
+
out += '<td class="kind-' + k + '">' + k + '</td>'
|
|
26
|
+
out += '<td>' + s + '</td>'
|
|
27
|
+
out += '<td>' + c + '</td>'
|
|
28
|
+
out += '<td>' + r + '</td>'
|
|
29
|
+
out += '</tr>'
|
|
30
|
+
return out
|
|
31
|
+
|
|
32
|
+
def _exp_html(self, exp):
|
|
33
|
+
rows = ''.join(self._row(e) for e in exp.timeline)
|
|
34
|
+
goal = _esc(exp.task.get('goal', '?'))
|
|
35
|
+
eid = _esc(exp.experience_id[:16]) + '...'
|
|
36
|
+
st = _esc(exp.outcome.status)
|
|
37
|
+
ag = _esc(exp.agent.get('id', '?'))
|
|
38
|
+
out = '<div class="card">'
|
|
39
|
+
out += '<h2>' + goal + '</h2>'
|
|
40
|
+
out += '<div class="meta">'
|
|
41
|
+
out += '<span class="tag">ID: ' + eid + '</span>'
|
|
42
|
+
out += '<span class="tag">Status: ' + st + '</span>'
|
|
43
|
+
out += '<span class="tag">Agent: ' + ag + '</span>'
|
|
44
|
+
out += '</div><h3>Timeline</h3><table>'
|
|
45
|
+
out += '<tr><th>Kind</th><th>Source</th><th>Content</th><th>Evidence</th></tr>'
|
|
46
|
+
out += rows + '</table></div>'
|
|
47
|
+
return out
|
|
48
|
+
|
|
49
|
+
def _lesson_row(self, l):
|
|
50
|
+
lid = _esc(l.lesson_id[:12]) + '...'
|
|
51
|
+
rec = _esc(l.recommendation[:100])
|
|
52
|
+
sc = 'status-' + l.status.value
|
|
53
|
+
sv = l.status.value
|
|
54
|
+
vl = l.validation.get('level', 'asserted')
|
|
55
|
+
out = '<tr>'
|
|
56
|
+
out += '<td>' + lid + '</td>'
|
|
57
|
+
out += '<td>' + rec + '</td>'
|
|
58
|
+
out += '<td><span class="' + sc + '">' + sv + '</span></td>'
|
|
59
|
+
out += '<td>' + vl + '</td></tr>'
|
|
60
|
+
return out
|
|
61
|
+
|
|
62
|
+
def render_report(self, title="ORP Report"):
|
|
63
|
+
exps = self._storage.list_experiences(limit=20)
|
|
64
|
+
lessons = self._storage.list_lessons(limit=20)
|
|
65
|
+
exp_html = ''.join(self._exp_html(e) for e in exps)
|
|
66
|
+
lesson_html = ''.join(self._lesson_row(l) for l in lessons)
|
|
67
|
+
st = _esc(title)
|
|
68
|
+
ec = str(len(exps))
|
|
69
|
+
lc = str(len(lessons))
|
|
70
|
+
ac = str(sum(1 for l in lessons if l.status == LessonStatus.ACTIVE))
|
|
71
|
+
html = '<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">'
|
|
72
|
+
html += '<title>' + st + '</title><style>'
|
|
73
|
+
html += 'body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;max-width:960px;margin:0 auto;padding:20px;background:#f5f5f5;}'
|
|
74
|
+
html += 'h1{color:#333;}.card{background:#fff;border-radius:8px;padding:20px;margin:16px 0;box-shadow:0 1px 3px rgba(0,0,0,0.1);}'
|
|
75
|
+
html += '.meta{margin:8px 0;}.tag{display:inline-block;background:#e0e7ff;padding:2px 8px;border-radius:4px;font-size:12px;margin:2px;}'
|
|
76
|
+
html += 'table{width:100%;border-collapse:collapse;margin:8px 0;}'
|
|
77
|
+
html += 'th,td{text-align:left;padding:6px 8px;border-bottom:1px solid #eee;font-size:13px;}'
|
|
78
|
+
html += 'th{background:#f9fafb;font-weight:600;}'
|
|
79
|
+
html += '.kind-observation{color:#2563eb;}.kind-claim{color:#d97706;}.kind-action{color:#059669;}'
|
|
80
|
+
html += '.kind-outcome{color:#7c3aed;}.kind-feedback{color:#0891b2;}'
|
|
81
|
+
html += '.status-active{color:#059669;font-weight:600;}.status-candidate{color:#d97706;}'
|
|
82
|
+
html += '.status-under_review{color:#dc2626;}.status-deprecated{color:#6b7280;}'
|
|
83
|
+
html += '.stats{display:flex;gap:16px;flex-wrap:wrap;}'
|
|
84
|
+
html += '.stat-box{background:#fff;border-radius:8px;padding:16px;flex:1;min-width:120px;text-align:center;box-shadow:0 1px 3px rgba(0,0,0,0.1);}'
|
|
85
|
+
html += '.stat-value{font-size:28px;font-weight:700;color:#2563eb;}'
|
|
86
|
+
html += '.stat-label{font-size:12px;color:#6b7280;}'
|
|
87
|
+
html += '</style></head><body>'
|
|
88
|
+
html += '<h1>' + st + '</h1>'
|
|
89
|
+
html += '<div class="stats">'
|
|
90
|
+
html += '<div class="stat-box"><div class="stat-value">' + ec + '</div><div class="stat-label">Experiences</div></div>'
|
|
91
|
+
html += '<div class="stat-box"><div class="stat-value">' + lc + '</div><div class="stat-label">Lessons</div></div>'
|
|
92
|
+
html += '<div class="stat-box"><div class="stat-value">' + ac + '</div><div class="stat-label">Active</div></div>'
|
|
93
|
+
html += '</div><h2>Lessons</h2>'
|
|
94
|
+
html += '<div class="card"><table><tr><th>ID</th><th>Recommendation</th><th>Status</th><th>Validation</th></tr>'
|
|
95
|
+
html += lesson_html + '</table></div>'
|
|
96
|
+
html += '<h2>Recent Experiences</h2>'
|
|
97
|
+
html += exp_html
|
|
98
|
+
html += '<p style="color:#999;font-size:12px;text-align:center;margin-top:32px;">Generated by ORP v0.3</p>'
|
|
99
|
+
html += '</body></html>'
|
|
100
|
+
return html
|
|
101
|
+
|
|
102
|
+
def write_report(self, path="orp_report.html"):
|
|
103
|
+
Path(path).write_text(self.render_report(), encoding='utf-8')
|
|
104
|
+
return path
|