173 lines
5.8 KiB
Python
173 lines
5.8 KiB
Python
import json
|
|
from pathlib import Path
|
|
|
|
import aiosqlite
|
|
from pydantic import BaseModel
|
|
|
|
from duck_core.tasks.store import utc_now
|
|
|
|
|
|
class ExperienceRecord(BaseModel):
|
|
id: int | None = None
|
|
task_id: str
|
|
skill_id: str | None = None
|
|
summary: str
|
|
result: str
|
|
what_worked: list[str] = []
|
|
what_failed: list[str] = []
|
|
reusable_lesson: str | None = None
|
|
suggested_skill_patch: str | None = None
|
|
confidence: float | None = None
|
|
created_at: str
|
|
|
|
|
|
class ExperienceRecorder:
|
|
def __init__(self, db_path: str):
|
|
self.db_path = Path(db_path)
|
|
|
|
async def init(self) -> None:
|
|
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
async with aiosqlite.connect(self.db_path) as db:
|
|
await db.execute(
|
|
"""
|
|
create table if not exists experience_records (
|
|
id integer primary key autoincrement,
|
|
task_id text not null,
|
|
skill_id text,
|
|
summary text not null,
|
|
result text not null,
|
|
what_worked_json text,
|
|
what_failed_json text,
|
|
reusable_lesson text,
|
|
suggested_skill_patch text,
|
|
confidence real,
|
|
created_at text not null
|
|
)
|
|
"""
|
|
)
|
|
await db.commit()
|
|
|
|
async def record(
|
|
self,
|
|
task_id: str,
|
|
summary: str,
|
|
result: str,
|
|
skill_id: str | None = None,
|
|
what_worked: list[str] | None = None,
|
|
what_failed: list[str] | None = None,
|
|
reusable_lesson: str | None = None,
|
|
suggested_skill_patch: str | None = None,
|
|
confidence: float | None = None,
|
|
) -> ExperienceRecord:
|
|
await self.init()
|
|
now = utc_now()
|
|
async with aiosqlite.connect(self.db_path) as db:
|
|
cursor = await db.execute(
|
|
"""
|
|
insert into experience_records(
|
|
task_id, skill_id, summary, result, what_worked_json,
|
|
what_failed_json, reusable_lesson, suggested_skill_patch,
|
|
confidence, created_at
|
|
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(
|
|
task_id,
|
|
skill_id,
|
|
summary,
|
|
result,
|
|
json.dumps(what_worked or []),
|
|
json.dumps(what_failed or []),
|
|
reusable_lesson,
|
|
suggested_skill_patch,
|
|
confidence,
|
|
now,
|
|
),
|
|
)
|
|
await db.commit()
|
|
row_id = cursor.lastrowid
|
|
if suggested_skill_patch and skill_id:
|
|
self.write_skill_update_proposal(task_id, skill_id, suggested_skill_patch)
|
|
return ExperienceRecord(
|
|
id=row_id,
|
|
task_id=task_id,
|
|
skill_id=skill_id,
|
|
summary=summary,
|
|
result=result,
|
|
what_worked=what_worked or [],
|
|
what_failed=what_failed or [],
|
|
reusable_lesson=reusable_lesson,
|
|
suggested_skill_patch=suggested_skill_patch,
|
|
confidence=confidence,
|
|
created_at=now,
|
|
)
|
|
|
|
async def list_records(self) -> list[ExperienceRecord]:
|
|
await self.init()
|
|
async with aiosqlite.connect(self.db_path) as db:
|
|
db.row_factory = aiosqlite.Row
|
|
cursor = await db.execute(
|
|
"select * from experience_records order by created_at desc"
|
|
)
|
|
rows = await cursor.fetchall()
|
|
return [self._row_to_record(row) for row in rows]
|
|
|
|
async def get_record(self, record_id: int) -> ExperienceRecord | None:
|
|
await self.init()
|
|
async with aiosqlite.connect(self.db_path) as db:
|
|
db.row_factory = aiosqlite.Row
|
|
cursor = await db.execute(
|
|
"select * from experience_records where id = ?", (record_id,)
|
|
)
|
|
row = await cursor.fetchone()
|
|
return self._row_to_record(row) if row else None
|
|
|
|
def write_skill_update_proposal(self, task_id: str, skill_id: str, patch: str) -> Path:
|
|
directory = Path("skills/_proposals")
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
path = directory / f"{utc_now().replace(':', '').replace('+', '_')}_{skill_id}.patch.md"
|
|
path.write_text(
|
|
"\n".join(
|
|
[
|
|
"# Skill update proposal",
|
|
"",
|
|
f"Skill: {skill_id}",
|
|
"",
|
|
"## Reason",
|
|
"",
|
|
"Reflection suggested a reusable skill improvement.",
|
|
"",
|
|
"## Proposed changes",
|
|
"",
|
|
patch,
|
|
"",
|
|
"## Evidence",
|
|
"",
|
|
f"Task id: {task_id}",
|
|
"",
|
|
"## Risk",
|
|
"",
|
|
"Low.",
|
|
"",
|
|
"## Requires human approval",
|
|
"",
|
|
"Yes.",
|
|
]
|
|
)
|
|
)
|
|
return path
|
|
|
|
def _row_to_record(self, row: aiosqlite.Row) -> ExperienceRecord:
|
|
return ExperienceRecord(
|
|
id=row["id"],
|
|
task_id=row["task_id"],
|
|
skill_id=row["skill_id"],
|
|
summary=row["summary"],
|
|
result=row["result"],
|
|
what_worked=json.loads(row["what_worked_json"] or "[]"),
|
|
what_failed=json.loads(row["what_failed_json"] or "[]"),
|
|
reusable_lesson=row["reusable_lesson"],
|
|
suggested_skill_patch=row["suggested_skill_patch"],
|
|
confidence=row["confidence"],
|
|
created_at=row["created_at"],
|
|
)
|