Skip to content

feat: run shadow comparisons at publish time#307

Merged
yyiilluu merged 1 commit into
mainfrom
codex/decouple-shadow-head-to-head
Jul 8, 2026
Merged

feat: run shadow comparisons at publish time#307
yyiilluu merged 1 commit into
mainfrom
codex/decouple-shadow-head-to-head

Conversation

@yyiilluu

@yyiilluu yyiilluu commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Decouples shadow head-to-head judging from sampled session-level group evaluation.
  • Runs publish-time, interaction-level shadow comparisons for persisted assistant turns with shadow_content.
  • Keeps session metrics such as success rate and correction count gated by the existing sampling and delayed scheduler.
  • Bumps the pinned shadow comparison prompt to v1.1.0 to include request-local transcript context and intentionally start a new dashboard epoch.

Changes

  • Added shadow_comparison.dispatcher and a bounded background worker for immediate best-effort per-request judging.
  • Replaced scattered scheduling with GenerationService._schedule_post_publish_evaluations(...), including the learning-stall path.
  • Removed shadow dispatch from run_group_evaluation(...) so regen/session evaluation no longer duplicates verdict writes.
  • Added storage capability checks before judge calls and support methods for storage backends.
  • Added prompt shadow_comparison/v1.1.0, deactivated v1.0.0, and updated tests/docs.

Test Plan

  • uv run pytest open_source/reflexio/tests/server/services/test_generation_service_scheduling.py -o 'addopts=' -v
  • uv run pytest open_source/reflexio/tests/server/services/shadow_comparison open_source/reflexio/tests/server/services/agent_success_evaluation/test_group_evaluation_shadow_integration.py -o 'addopts=' -v
  • uv run pytest open_source/reflexio/tests/models/test_config_f1_fields.py open_source/reflexio/tests/server/prompt/test_shadow_comparison_prompt_present.py open_source/reflexio/tests/server/services/test_prompt_model_mapping.py -o 'addopts=' -v
  • uv run ruff check ... and uv run pyright ... on staged Python files
  • Local real-LLM E2E on backend http://localhost:18091 with MiniMax and Supabase: verified zero-sampling shadow verdict creation, no-shadow no-op, and sampled group evaluation + shadow verdict rows.

Summary by CodeRabbit

  • New Features

    • Updated shadow comparison to use the newer evaluation prompt version.
    • Added publish-time shadow comparison for assistant turns that include shadow content, with background processing for smoother handling.
  • Bug Fixes

    • Session-level shadow comparison is no longer triggered during group evaluation, preventing duplicate or unexpected verdict writes.
    • New shadow comparison results are now stored reliably in supported backends.
  • Documentation

    • Clarified how shadow comparison is scheduled and recorded, and updated the related overview and component list.

Decouple interaction-level shadow head-to-head judging from sampled session-level group evaluation. Add a bounded background worker, request-transcript prompt v1.1.0, storage capability checks, and focused regression coverage.
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Shadow comparison verdict generation moves from session-level group evaluation to publish-time dispatch. A new v1.1.0 prompt, dispatcher module, and bounded background worker handle per-turn judging, replacing the removed dispatch logic in the group evaluation runner. Storage backends gain a capability probe, and GenerationService schedules the new flow.

Changes

Publish-time shadow comparison feature

Layer / File(s) Summary
Prompt v1.1.0 and config default
reflexio/models/config_schema.py, reflexio/server/prompt/prompt_bank/shadow_comparison/*.prompt.md, tests/models/test_config_f1_fields.py, tests/server/prompt/test_shadow_comparison_prompt_present.py, tests/server/services/test_prompt_model_mapping.py
New v1.1.0 prompt with conversation_context variable is added and activated, v1.0.0 is deactivated, default config version and tests are bumped to v1.1.0.
Judge conversation_context support
reflexio/server/services/shadow_comparison/judge.py
judge_turn accepts an optional conversation_context used in prompt rendering, falling back to user_message.
Storage capability probe
reflexio/server/services/storage/storage_base/_shadow_verdicts.py, reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py
supports_shadow_comparison_verdicts() is added, defaulting to False with SQLite returning True.
Dispatcher for per-turn judging
reflexio/server/services/shadow_comparison/dispatcher.py, tests/server/services/shadow_comparison/test_dispatcher.py
New dispatch_shadow_comparison_judge builds transcript context per eligible assistant turn, invokes the judge, and persists verdicts with per-interaction error handling.
Background worker
reflexio/server/services/shadow_comparison/worker.py, tests/server/services/shadow_comparison/test_dispatcher.py
A bounded queue-backed worker enqueues ShadowComparisonJobs and dispatches them via lazily-resolved per-org context, dropping jobs with metrics on overflow.
GenerationService scheduling
reflexio/server/services/generation_service.py, tests/server/services/test_generation_service_scheduling.py
_schedule_post_publish_evaluations conditionally enqueues shadow jobs and always schedules group evaluation; all run() paths call this helper.
Runner cleanup and docs
reflexio/server/services/agent_success_evaluation/runner.py, .../README.md, tests/server/services/agent_success_evaluation/test_group_evaluation_shadow_integration.py, reflexio/server/README.md
Per-interaction shadow dispatch is removed from group evaluation; READMEs and tests reflect the new publish-time flow.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant GenerationService
    participant ShadowComparisonWorker
    participant ShadowComparisonDispatcher
    participant ShadowComparisonJudge
    participant Storage

    Client->>GenerationService: run() publish request
    GenerationService->>GenerationService: store interactions
    GenerationService->>GenerationService: _schedule_post_publish_evaluations(interactions)
    alt interaction has shadow_content
        GenerationService->>ShadowComparisonWorker: enqueue(ShadowComparisonJob)
    end
    GenerationService->>GenerationService: schedule group evaluation (no shadow dispatch)

    ShadowComparisonWorker->>ShadowComparisonWorker: resolve get_reflexio(org_id)
    ShadowComparisonWorker->>ShadowComparisonDispatcher: dispatch_shadow_comparison_judge(storage, llm_client, interactions)
    ShadowComparisonDispatcher->>Storage: supports_shadow_comparison_verdicts()
    Storage-->>ShadowComparisonDispatcher: True/False
    alt supported and eligible turns found
        ShadowComparisonDispatcher->>ShadowComparisonJudge: judge_turn(conversation_context)
        ShadowComparisonJudge-->>ShadowComparisonDispatcher: verdict or None
        ShadowComparisonDispatcher->>Storage: save_shadow_comparison_verdict(verdict)
    end
Loading

Possibly related PRs

  • ReflexioAI/reflexio#39: Both PRs modify reflexio/server/services/generation_service.py's publish run() control flow at overlapping integration points.
  • ReflexioAI/reflexio#104: Extends the same shadow-comparison pipeline, evolving ShadowComparisonJudge and prompt versioning introduced there.
  • ReflexioAI/reflexio#168: Both touch the shadow-comparison verdict storage layer, adding related capability/query methods.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: moving shadow comparisons to publish time.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/decouple-shadow-head-to-head

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@yyiilluu yyiilluu merged commit 48629f0 into main Jul 8, 2026
1 check was pending

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/server/services/shadow_comparison/test_dispatcher.py (1)

69-273: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add tests for save_shadow_comparison_verdict failure paths.

The dispatcher handles two distinct save-failure branches that are not covered by any test:

  1. NotImplementedError from save_shadow_comparison_verdict (dispatcher.py:86-92) — stops the batch and returns early.
  2. Generic Exception from save_shadow_comparison_verdict (dispatcher.py:93-98) — logs and continues to the next interaction.

These branches affect batch-level behavior (stop vs. continue) and are easy to test with a MagicMock whose save_shadow_comparison_verdict side_effect raises.

🧪 Suggested test additions
+def test_dispatcher_stops_on_not_implemented_error(monkeypatch) -> None:
+    """NotImplementedError from save should stop the batch early."""
+    storage = MagicMock()
+    storage.supports_shadow_comparison_verdicts.return_value = True
+    storage.save_shadow_comparison_verdict.side_effect = NotImplementedError
+
+    monkeypatch.setattr(
+        "reflexio.server.services.shadow_comparison.judge."
+        "ShadowComparisonJudge.judge_turn",
+        lambda self, *, interaction, **_: _verdict(interaction),
+    )
+
+    dispatch_shadow_comparison_judge(
+        storage=storage,
+        interactions=[
+            _interaction(
+                interaction_id=1,
+                role="assistant",
+                content="a",
+                shadow_content="s",
+            ),
+            _interaction(
+                interaction_id=2,
+                role="assistant",
+                content="b",
+                shadow_content="t",
+            ),
+        ],
+        session_id="s1",
+        agent_version="v1",
+        request_context=_request_context(),  # type: ignore[arg-type]
+        llm_client=MagicMock(),
+    )
+
+    # First save raises NotImplementedError → batch stops, second turn never judged.
+    storage.save_shadow_comparison_verdict.assert_called_once()
+
+
+def test_dispatcher_continues_after_save_failure(monkeypatch) -> None:
+    """Generic save exception should not abort the batch."""
+    storage = MagicMock()
+    storage.supports_shadow_comparison_verdicts.return_value = True
+    saved: list = []
+
+    def _save(verdict):
+        if len(saved) == 0:
+            raise RuntimeError("db write failed")
+        saved.append(verdict)
+
+    storage.save_shadow_comparison_verdict.side_effect = _save
+
+    monkeypatch.setattr(
+        "reflexio.server.services.shadow_comparison.judge."
+        "ShadowComparisonJudge.judge_turn",
+        lambda self, *, interaction, **_: _verdict(interaction),
+    )
+
+    dispatch_shadow_comparison_judge(
+        storage=storage,
+        interactions=[
+            _interaction(
+                interaction_id=1,
+                role="assistant",
+                content="a",
+                shadow_content="s",
+            ),
+            _interaction(
+                interaction_id=2,
+                role="assistant",
+                content="b",
+                shadow_content="t",
+            ),
+        ],
+        session_id="s1",
+        agent_version="v1",
+        request_context=_request_context(),  # type: ignore[arg-type]
+        llm_client=MagicMock(),
+    )
+
+    # First save fails, second succeeds — both turns judged.
+    assert storage.save_shadow_comparison_verdict.call_count == 2
+    assert len(saved) == 1
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/server/services/shadow_comparison/test_dispatcher.py` around lines 69 -
273, Add coverage for the two save failure paths in
dispatch_shadow_comparison_judge: one test where
storage.save_shadow_comparison_verdict raises NotImplementedError and the
dispatcher returns early without processing later interactions, and another
where it raises a generic Exception and the dispatcher logs then continues to
the next interaction. Use the existing dispatch_shadow_comparison_judge,
_interaction, and MagicMock-based storage setup in test_dispatcher.py, and
assert the batch behavior changes (stop vs continue) via save call counts and
judged interactions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/server/services/shadow_comparison/test_dispatcher.py`:
- Around line 69-273: Add coverage for the two save failure paths in
dispatch_shadow_comparison_judge: one test where
storage.save_shadow_comparison_verdict raises NotImplementedError and the
dispatcher returns early without processing later interactions, and another
where it raises a generic Exception and the dispatcher logs then continues to
the next interaction. Use the existing dispatch_shadow_comparison_judge,
_interaction, and MagicMock-based storage setup in test_dispatcher.py, and
assert the batch behavior changes (stop vs continue) via save call counts and
judged interactions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a8423d8f-2d32-41cd-8c64-3247e5c2ab73

📥 Commits

Reviewing files that changed from the base of the PR and between 3bf034b and 3bdf01e.

📒 Files selected for processing (18)
  • reflexio/models/config_schema.py
  • reflexio/server/README.md
  • reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md
  • reflexio/server/prompt/prompt_bank/shadow_comparison/v1.1.0.prompt.md
  • reflexio/server/services/agent_success_evaluation/README.md
  • reflexio/server/services/agent_success_evaluation/runner.py
  • reflexio/server/services/generation_service.py
  • reflexio/server/services/shadow_comparison/dispatcher.py
  • reflexio/server/services/shadow_comparison/judge.py
  • reflexio/server/services/shadow_comparison/worker.py
  • reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py
  • reflexio/server/services/storage/storage_base/_shadow_verdicts.py
  • tests/models/test_config_f1_fields.py
  • tests/server/prompt/test_shadow_comparison_prompt_present.py
  • tests/server/services/agent_success_evaluation/test_group_evaluation_shadow_integration.py
  • tests/server/services/shadow_comparison/test_dispatcher.py
  • tests/server/services/test_generation_service_scheduling.py
  • tests/server/services/test_prompt_model_mapping.py
💤 Files with no reviewable changes (1)
  • reflexio/server/services/agent_success_evaluation/runner.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant