mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
[F057] playbook: index/unindex as a post-commit step so the RAG corpus never leads the status transaction
This commit is contained in:
@@ -53,14 +53,17 @@ async def approve_playbook(
|
||||
"""Approve a draft playbook → approved (and indexed into the KB)."""
|
||||
_require_curator(agent)
|
||||
try:
|
||||
playbook = await get_playbook_service(db).approve(
|
||||
playbook_id, approver_id=agent.agent_id
|
||||
)
|
||||
svc = get_playbook_service(db)
|
||||
playbook = await svc.approve(playbook_id, approver_id=agent.agent_id)
|
||||
except NotFoundError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Playbook not found"
|
||||
) from exc
|
||||
# Commit the status change BEFORE indexing: the RAG index write runs through
|
||||
# its own auto-committing connection, so indexing before commit would durably
|
||||
# land an approved playbook in the corpus even if this commit rolled back.
|
||||
await db.commit()
|
||||
await svc.index_approved(playbook)
|
||||
return Playbook.model_validate(playbook)
|
||||
|
||||
|
||||
@@ -74,12 +77,15 @@ async def reject_playbook(
|
||||
"""Reject a playbook → archived, with the Auditor's reason."""
|
||||
_require_curator(agent)
|
||||
try:
|
||||
playbook = await get_playbook_service(db).reject(
|
||||
svc = get_playbook_service(db)
|
||||
playbook = await svc.reject(
|
||||
playbook_id, approver_id=agent.agent_id, reason=body.reason
|
||||
)
|
||||
except NotFoundError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Playbook not found"
|
||||
) from exc
|
||||
# Commit the status change BEFORE de-indexing (see approve_playbook).
|
||||
await db.commit()
|
||||
await svc.unindex_playbook(playbook)
|
||||
return Playbook.model_validate(playbook)
|
||||
|
||||
@@ -793,6 +793,18 @@ class ContentActions:
|
||||
status = "playbook_archived"
|
||||
except NotFoundError:
|
||||
return Envelope.not_found(message=f"playbook {playbook_id} not found")
|
||||
# Commit the status change BEFORE touching the RAG index: the index write
|
||||
# runs through its own auto-committing connection, so indexing before the
|
||||
# status commit would durably land (or drop) a playbook in the corpus even
|
||||
# if this transaction rolled back — a divergence agents surface in
|
||||
# briefings. ``get_db`` commits the session again after the route returns
|
||||
# (a no-op on the now-clean transaction); this explicit commit is what
|
||||
# gates the index.
|
||||
await self.task.session.commit()
|
||||
if action == "approve":
|
||||
await svc.index_approved(playbook)
|
||||
else:
|
||||
await svc.unindex_playbook(playbook)
|
||||
return Envelope.ok(
|
||||
status=status,
|
||||
task_id=None,
|
||||
|
||||
@@ -68,21 +68,30 @@ class PlaybookService(BaseService):
|
||||
return playbook
|
||||
|
||||
async def approve(self, playbook_id: UUID, approver_id: UUID) -> PlaybookTable:
|
||||
"""Auditor approves a draft: draft -> approved, stamped."""
|
||||
"""Auditor approves a draft: draft -> approved, stamped.
|
||||
|
||||
Flushes the status change ONLY — the RAG index write (``index_approved``)
|
||||
is a SEPARATE step the caller runs AFTER committing the status. The vector
|
||||
store writes through its own auto-committing pool connection, so indexing
|
||||
inline (before the status commit) would durably land an approved playbook
|
||||
in the corpus even if the status transaction rolled back — a divergence
|
||||
agents then surfaced in briefings.
|
||||
"""
|
||||
playbook = await self._get_or_raise(playbook_id)
|
||||
playbook.status = PlaybookStatus.APPROVED.value
|
||||
playbook.approved_by = approver_id
|
||||
playbook.approved_at = datetime.now(UTC)
|
||||
await self.session.flush()
|
||||
await self._index_approved(playbook)
|
||||
self.log.info("Playbook approved", playbook_id=str(playbook_id))
|
||||
return playbook
|
||||
|
||||
async def _index_approved(self, playbook: PlaybookTable) -> None:
|
||||
async def index_approved(self, playbook: PlaybookTable) -> None:
|
||||
"""Embed an approved playbook into the PLAYBOOKS RAG index (best-effort).
|
||||
|
||||
Gated on ``org_memory_enabled`` so the feature is fully inert when off;
|
||||
a failure (e.g. the embedder is down) never blocks the approval.
|
||||
Post-commit step: the caller commits the ``draft -> approved`` status
|
||||
change FIRST, then runs this so the index never leads the status
|
||||
transaction. Gated on ``org_memory_enabled`` so the feature is fully inert
|
||||
when off; a failure (e.g. the embedder is down) never blocks the approval.
|
||||
"""
|
||||
if not settings.org_memory_enabled:
|
||||
return
|
||||
@@ -114,17 +123,20 @@ class PlaybookService(BaseService):
|
||||
async def reject(
|
||||
self, playbook_id: UUID, approver_id: UUID, reason: str
|
||||
) -> PlaybookTable:
|
||||
"""Auditor rejects a playbook: -> archived (reason recorded in the log)."""
|
||||
"""Auditor rejects a playbook: -> archived (reason recorded in the log).
|
||||
|
||||
Flushes the status change ONLY — ``unindex_playbook`` is a separate
|
||||
post-commit step (see ``approve`` for the ordering rationale).
|
||||
"""
|
||||
playbook = await self._get_or_raise(playbook_id)
|
||||
playbook.status = PlaybookStatus.ARCHIVED.value
|
||||
playbook.approved_by = approver_id
|
||||
playbook.approved_at = datetime.now(UTC)
|
||||
await self.session.flush()
|
||||
await self._unindex_playbook(playbook)
|
||||
self.log.info("Playbook rejected", playbook_id=str(playbook_id), reason=reason)
|
||||
return playbook
|
||||
|
||||
async def _unindex_playbook(self, playbook: PlaybookTable) -> None:
|
||||
async def unindex_playbook(self, playbook: PlaybookTable) -> None:
|
||||
"""De-index a playbook from the PLAYBOOKS RAG index (best-effort).
|
||||
|
||||
The mirror of :meth:`_index_approved`: a rejected/archived playbook that
|
||||
|
||||
Reference in New Issue
Block a user