Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion reflexio/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
IS_TEST_ENV = os.environ.get("IS_TEST_ENV", "false").strip() == "true"

BACKEND_URL = "http://127.0.0.1:8000" if IS_TEST_ENV else "https://www.reflexio.ai/"
REVIEW_USER_PLAYBOOKS_TIMEOUT_SECONDS = 600

from reflexio.models.api_schema.domain.entities import (
UpgradeProfilesRequest,
Expand Down Expand Up @@ -1630,7 +1631,11 @@ def review_user_playbooks(
"""Re-review current user playbooks created within a bounded window.

The default ``report_only=True`` runs inline and returns one decision per
selected playbook.
selected playbook. Each playbook is reviewed from its full finalized
extraction-run window plus any cited consolidation evidence, independent
of the current extractor window size or source filter. Rows with missing
or invalid persisted provenance are skipped. Only cited interactions are
reviewer evidence; other run-window interactions provide chronology.

With ``report_only=False`` the server accepts the run and applies it in
the background, so the response carries only ``run_id`` — not
Expand All @@ -1650,6 +1655,11 @@ def review_user_playbooks(
"POST",
"/api/review_user_playbooks",
json=request.model_dump(mode="json"),
timeout=(
max(self.timeout, REVIEW_USER_PLAYBOOKS_TIMEOUT_SECONDS)
if report_only
else self.timeout
),
)
return ReviewUserPlaybooksResponse(**response)

Expand Down
7 changes: 4 additions & 3 deletions reflexio/models/api_schema/domain/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -1737,9 +1737,10 @@ class ReviewUserPlaybookEdit(BaseModel):
class ReviewUserPlaybookResult(BaseModel):
"""One persisted user playbook's re-review outcome.

``skip`` means the row could not be reviewed at all (its cited evidence is
no longer reconstructable), not that the reviewer chose to leave it alone —
that is ``accept``. A skipped row is never written to.
``skip`` means the row could not be reviewed at all because its finalized
generation-window or cited-evidence provenance is absent, missing, or
invalid, not that the reviewer chose to leave it alone — that is ``accept``.
A skipped row is never written to.
"""

user_playbook_id: int = Field(gt=0)
Expand Down
6 changes: 5 additions & 1 deletion reflexio/server/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
SYNC_REQUEST_TIMEOUT_SECONDS = (
600 # Longer timeout for synchronous processing (wait_for_response=true)
)
SYNC_REQUEST_PATHS = frozenset({"/api/review_user_playbooks"})
SUSPICIOUS_USER_AGENTS = ["bot", "crawler", "spider", "scraper", "curl", "wget"]
ALLOWED_EMPTY_UA_PATHS = ["/health", "/"] # Paths that allow empty user agents
DEFAULT_MAX_BODY_BYTES = 10 * 1024 * 1024
Expand Down Expand Up @@ -128,7 +129,10 @@ async def dispatch(

# Use longer timeout for synchronous processing requests
timeout = REQUEST_TIMEOUT_SECONDS
if request.query_params.get("wait_for_response", "").lower() == "true":
if (
request.url.path in SYNC_REQUEST_PATHS
or request.query_params.get("wait_for_response", "").lower() == "true"
):
timeout = SYNC_REQUEST_TIMEOUT_SECONDS

try:
Expand Down
15 changes: 13 additions & 2 deletions reflexio/server/services/playbook/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,19 @@ do not use this reviewer.
### Persisted Review (`review_service.py`)

`POST /api/review_user_playbooks` selects the newest current user playbooks in
an inclusive creation-time window, capped by `top_k`. A row whose cited evidence
can no longer be reconstructed yields a `skip` decision and the run continues.
an inclusive creation-time window, capped by `top_k`. A row whose original
generation window or cited evidence can no longer be reconstructed yields a
`skip` decision and the run continues.

Manual review reconstructs context from the full interaction window persisted on
the row's finalized playbook-extraction run, plus any extra cited interactions
retained through consolidation. It never substitutes the current extractor
window or silently falls back to the smaller cited-evidence subset. Automatic
post-generation review continues to use the generation call's configured window.
Only the playbook row's cited interaction IDs become candidate evidence units;
the rest of the generation window is ancillary chronology. Evidence spans are
rebuilt from those exact stored interactions instead of requiring every cited
span to remain duplicated in the row's bounded combined `source_span`.

Report mode runs inline and returns `accept`, `edit`, `reject`, and `skip`
decisions without writes. Apply mode runs in the **background** (one LLM call
Expand Down
47 changes: 16 additions & 31 deletions reflexio/server/services/playbook/components/reviewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
}


class PlaybookCandidateEvidenceError(ValueError):
"""A persisted candidate's cited evidence cannot be reconstructed."""


class CandidateRevision(BaseModel):
"""Complete replacement fields for one narrowly revised candidate."""

Expand Down Expand Up @@ -253,42 +257,21 @@ def _candidate_evidence_units(
units_by_candidate: dict[str, list[CandidateEvidenceUnit]] = {}
for candidate_index, candidate in enumerate(candidates, start=1):
candidate_id = f"C{candidate_index}"
combined_span = candidate.source_span or ""
units: list[CandidateEvidenceUnit] = []
for interaction_id in candidate.source_interaction_ids:
mapped_units = by_interaction_id.get(interaction_id)
if not mapped_units:
raise ValueError(
raise PlaybookCandidateEvidenceError(
f"Reviewer cannot map {candidate_id} evidence to a local turn"
)
matching_units = [
unit
for unit in mapped_units
if unit.source_span
and (
unit.source_span in combined_span
or combined_span in unit.source_span
)
]
if not matching_units:
span_fragments = [
fragment.strip()
for fragment in combined_span.split("\n\n")
if fragment.strip()
]
matching_units = [
unit
for unit in mapped_units
if any(
fragment == unit.source_span or fragment in unit.source_span
for fragment in span_fragments
)
]
if not matching_units:
raise ValueError(
f"Reviewer cannot resolve {candidate_id} validated source span"
)
for mapped_unit in matching_units:
# Exact persisted interaction IDs are the durable evidence
# identity. Rebuild their bounded visible spans from storage;
# do not require every span to survive in the playbook's
# combined ``source_span``, which may be truncated or may have
# been assembled from multiple rows during consolidation.
for mapped_unit in mapped_units:
if not mapped_unit.source_span:
continue
if any(
unit.turn_ref == mapped_unit.turn_ref
and unit.source_span == mapped_unit.source_span
Expand All @@ -304,7 +287,9 @@ def _candidate_evidence_units(
)
)
if not units:
raise ValueError(f"Reviewer received {candidate_id} without evidence")
raise PlaybookCandidateEvidenceError(
f"Reviewer received {candidate_id} without evidence"
)
units_by_candidate[candidate_id] = units
return units_by_candidate

Expand Down
Loading