Add LegalBench-RAG benchmark harness + parity report (composite of #1239/#1353/#1354/#1376) - #1380
Conversation
…e-1279-y7Gsc # Conflicts: # CHANGELOG.md
The README badge pointed at `flag=frontend-unit` — the Vitest slice only — so months of Playwright component + E2E tests never moved the displayed number. The three frontend suites each upload to their own flag (`frontend-unit`, `frontend-component`, `frontend-e2e`) and were never merged into a single lcov, so Codecov had nothing unified to render for a badge. Each producing job now publishes its `lcov.info` as a GitHub Actions artifact in addition to its per-flag Codecov upload. `codecov-notify.yml`, which already waits cross-workflow by SHA before calling `send-notifications`, now also downloads the three artifacts by run id (tolerating path-filtered skips and upload failures), merges them with `npx lcov-result-merger`, and uploads the combined lcov under a new `frontend` flag. The per-suite flags still upload from their own jobs, so drill-in by suite continues to work. Secondary fix: Vitest's v8 coverage now runs with `all: true`. Without it, untested files were silently dropped from the lcov, inflating the `frontend-unit` ratio and misaligning the v8 file universe with the Istanbul-based component/E2E lcovs — which breaks the merge. - `.github/workflows/frontend.yml`: publish `frontend-unit-lcov` and `frontend-ct-lcov` artifacts. - `.github/workflows/frontend-e2e.yml`: publish `frontend-e2e-lcov` artifact. - `.github/workflows/codecov-notify.yml`: emit producing-run ids from the existing check step, cross-workflow-download the three artifacts, merge with `lcov-result-merger`, upload under `frontend` flag before `send-notifications`. - `.codecov.yml`: add `frontend` flag; leaves the `frontend-.*` `flag_regexes` untouched so the component keeps aggregating only the three per-suite flags (no double-count). - `frontend/vite.config.ts`: add `all: true` to the v8 coverage block. - `frontend/package.json` + `yarn.lock`: add `lcov-result-merger@^5.0.1` devDep plus a `coverage:merge` script for local reproduction. - `README.md`: point the Frontend coverage badge at `flag=frontend`. - `CHANGELOG.md`: document the fix under `[Unreleased] > Fixed`.
…S wait - ChatMessageCoverage.ct: add a test for sources.length > 1 so the plural branch of the pluralisation ternary is exercised (completes the singular/plural/view-sources triple). - SelectAnalyzerOrFieldsetModal.ct: the overlay-close test now polls for opacity=1 (framer-motion settle) instead of sleeping 500ms. Force-click is retained with an explicit reason comment — the overlay is a portaled full-viewport div whose centred ModalContainer registers as the pointer-event target.
Removes the stale 'tracked separately' comment and adds a regression test that mounts the same UserProfileRoute at both /profile and /users/:slug. Pre-fix, the fiber would transition from 0 → 1 hook across the redirect and React threw; now the hook is unconditional with skip:!slug, so the fiber's hook ordering is stable and the destination renders normally. Also covers the previously-uncovered slug branch of useQuery.
…eporting-83wMw Merge frontend coverage reports for accurate badge display
- Backend CI `changes` job: scope to pull_request events. dorny/paths-filter@v3 shells out to `git branch --show-current` on push events and fails without a checkout, so every push to main produced a red-X `changes` job silently masked by `continue-on-error: true` plus the fail-open `|| push` gate on downstream jobs. Downstream `if:` conditions rewritten with `always()` so the intentional skip on push no longer cascades. - Frontend CI: drop the leftover tippy-debug step in the lint job. - codecov-notify: sort `matching` by created_at desc before taking [0] instead of relying on undocumented API ordering. Closes #1319 follow-ups 1 and 2.
…e-1280-8RzXd Add comprehensive component tests for ModernDocumentItem and DocumentRelationshipModal
…1295-2uMj6 Fix Rules-of-Hooks violation in UserProfileRoute
…e-1279-y7Gsc # Conflicts: # CHANGELOG.md
…us-generation-SWmBV # Conflicts: # CHANGELOG.md
- Fix misleading post_init comment in runner.py (compute_aggregates is
called unconditionally).
- Add select_related('column') in _evaluate to eliminate per-cell lazy
fetch of Column.query.
- Replace magic numbers 64/61 in _make_column_name with
BENCHMARK_QUERY_PREVIEW_MAX_LEN / _TRIM_LEN constants.
- Use consistent single-char U+2026 ellipsis in both truncation paths
(query preview and column-name suffix).
- Document NotImplementedError in use_eager_extraction docstring.
Closes #1316 The four conditional branches that each returned `undefined` in `updateForAnnotationDeletion` collectively covered exactly `newSourceIds.length === 0 || newTargetIds.length === 0`, and the `sourceEmpty` / `targetEmpty` pre-filter variables were only used to derive those branches. Collapsed the four conditions into a single check and removed the now-unused pre-filter locals. No behavior change. Items #1 (preserve id/structural on surviving relation) and #3 (`string | string` typo in the constructor) from issue #1316 were already fixed in prior PRs (commits cef984a and 8704e51 respectively), so this PR addresses the remaining item #2. All 28 tests in `annotations.test.ts` continue to pass; `tsc --noEmit` clean.
Closes #1317 (follow-up to #1314). The four branching return-undefined conditions in RelationGroup.updateForAnnotationDeletion() are all equivalent to a single `newSourceIds.length === 0 || newTargetIds.length === 0` check: filter is monotonic, so an originally-empty side stays empty after filtering. Collapsed the four branches into one and removed the now-unused sourceEmpty / targetEmpty locals per the project's DRY guideline. Behavior is unchanged; the existing 28-test regression suite in annotations.test.ts passes unmodified, confirming the simplification is semantics-preserving (sole source, sole target, both sides, one-of-many source, one-of-many target, and not-in-relation cases all covered).
- resolve_full_datacell_list now bounds every code path (no-args, offset-only, limit+offset) at MAX_FULL_DATACELL_LIST_LIMIT so direct API callers cannot bypass the payload cap by omitting limit. - GraphQL limit-arg description documents the cap behaviour. - MAX_FULL_DATACELL_LIST_LIMIT and EXTRACT_GRID_EMBED_CELL_LIMIT carry matching 'update together; CI sync-check tracked in #1256' warnings. - resolve_datacell_count comment corrected to reflect the actual N+1 shape (COUNT(*) in addition to the main list query). - TODO added next to the ExtractQueryOptimizer inline import so the circular-dependency cleanup is tracked. - New regression test creates 501 cells and asserts the no-args response is capped while datacellCount still reports the true total. Closes #1256
Address review feedback on the test additions from PR #1297: - filterRelationshipLabels: add explicit labelType=undefined test so the strict-equality guard is pinned for both null and undefined non-matches. - ModernDocumentItem.ct.tsx: * Call out the build-hash suffix risk in the __reactProps$ comments so a future maintainer knows the key suffix rotates on every React build and hard-coding it will silently break. * openContextMenu falls back to scanning the document for an element with a React onContextMenu prop when the .checkbox anchor is absent, instead of throwing an unexplained error. * Document that relationship-popup toBeAttached() assertions only verify DOM presence (popup is visibility:hidden + hover reveal). - DocumentRelationshipModal.ct.tsx: * Replace removeButtons.nth(1) with an XPath-scoped walk from the target pill's visible title so layout re-ordering cannot silently test the wrong button. * Wait for the submit button to re-enable before polling onSuccessCalled === false so the negative assertion has a real timing window instead of passing immediately on the initial value. - ReactiveVarObserver: doc comment now describes the three-step pattern for extending the observer to additional reactive vars. Closes #1321
Clean up CI bypassers exposed by PR #1318 merge
…T loss Adds 36 new Playwright component tests across the four lowest-coverage components under frontend/src/components/corpuses/ toward the ≥60% target set by issue #1276, and fixes a latent bug in CorpusChat's WebSocket handler that dropped SYNC_CONTENT frames. CorpusChat.tsx: SYNC_CONTENT now appends the message to the visible chat list (mirroring ChatTray). Previously the handler only routed the content to chatSourceState for citation storage, so synchronous, non-streaming server replies rendered nothing. New regression test pins the behavior. Test additions (all pass): - CorpusChat.ct.tsx (+13): initialQuery auto-send, ASYNC_THOUGHT tool-call timeline, ASYNC_SOURCES, SYNC_CONTENT, ASYNC_RESUME, ask_document sub-tool approval name remapping, unknown-type default branch, back-to-list nav, server-message-with-sources, title-filter debounce. - CreateCorpusActionModal.ct.tsx (+8): analyzer validation, inline-agent validations (empty name / empty instructions), existing-agent selection validation, inline-agent create happy path, backend error toast, edit analyzer pre-population, legacy trigger-casing fallback. - CorpusAgentManagement.ct.tsx (+8): query loading, query error, multi-tool overflow badge, inactive-status badge, update-mutation happy path, create backend-error toast, tool deselection, edit-modal cancel. - CorpusDescriptionEditor.ct.tsx (+7): save ok:false error, save network error, reapply missing-snapshot, twice-click collapse, Cancel Version Edit reset, fetch-md URL failure, version-count pluralization. Closes #1276
Signed-off-by: JSIV <5049984+JSv4@users.noreply.github.com>
…s mock variables
- ChatMessageCoverage.ct.tsx: swap 2x waitForTimeout(100) for expect.poll
to avoid arbitrary sleeps on slow CI runners
- SelectAnalyzerOrFieldsetModalMocks.ts: buildFieldsetsMock now omits
searchText when empty so the request matches UnifiedFieldsetSelector's
"searchQuery ? { searchText } : {}" pattern; a stray { searchText: "" }
silently missed the initial query and left the fieldset tab empty
- Adds docScreenshot for ChatMessage markdown rendering
Backend CI run 24646911374 (commit 7a11ae2, push to main) aborted in the Build the Stack step with ssl.SSLError: record layer failure while pip wheel was 22 MB into a 60 MB opencv-python-headless download. Pip's default --retries 5 only covers connection setup; resuming a broken mid-stream download requires --resume-retries, added in pip 24.1 (env var PIP_RESUME_RETRIES). Added ENV PIP_RETRIES=10 PIP_TIMEOUT=60 PIP_RESUME_RETRIES=5 to both the build and run stages of compose/local/django/Dockerfile and compose/production/django/Dockerfile, covering every pip invocation (wheel build, --upgrade pip, spacy model installs). Verified the base image pytorch/pytorch:2.7.1-cuda12.6-cudnn9-runtime ships pip 25.1.1 and honours all three variables.
Removes GraphQL operations and associated TypeScript Input/Output types that have no call sites in frontend/src/ or frontend/tests/. Every deletion was verified with a word-boundary grep across both trees. - landing-queries.ts: GET_TRENDING_CORPUSES, GET_RECENT_DOCUMENTS, GET_COMMUNITY_STATS, GET_GLOBAL_LEADERBOARD (kept GetCommunityStatsOutput since GetDiscoveryDataOutput still uses it). - queries/folders.ts: GET_CORPUS_FOLDER (singular), MOVE_DOCUMENTS_TO_FOLDER (plural), plus Inputs/Outputs type pairs. - metadataOperations.ts: GET_METADATA_COMPLETION_STATUS and dead Input/Output types (inlined DocumentMetadataResult into GetDocumentsMetadataBatchOutput). - queries.ts: USER_BY_SLUG, CORPUS_BY_SLUGS, DOCUMENT_BY_SLUGS, GET_LABELSET_BY_ID_FOR_REDIRECT, REQUEST_PAGE_ANNOTATION_DATA, GET_EXPORT, GET_FIELDSET, GET_DOCUMENT_ANNOTATIONS_AND_RELATIONSHIPS, getAnnotationsByDocumentId, listAnnotations, GET_DOCUMENT_DETAILS, GET_UNREAD_NOTIFICATION_COUNT, GET_DOCUMENT_RELATIONSHIP_COUNT. - mutations.ts: UPDATE_LABELSET, CREATE_ANNOTATION_LABEL, SMART_LABEL_LIST, REMOVE_ANNOTATION_LABELS_FROM_LABELSET, DELETE_ANNOTATION_LABEL, DELETE_DOCUMENT, REQUEST_DELETE_DOC_TYPE_ANNOTATION, UPDATE_CORPUS_SETTINGS, UPDATE_BADGE, AWARD_BADGE, REVOKE_BADGE, DELETE_CONVERSATION, the entire notification-mutations block (MARK_NOTIFICATION_READ, MARK_NOTIFICATION_UNREAD, MARK_ALL_NOTIFICATIONS_READ, DELETE_NOTIFICATION), PERMANENTLY_DELETE_DOCUMENT, UPDATE_DOCUMENT_RELATIONSHIP, DELETE_DOCUMENT_RELATIONSHIPS. Per the issue scope, types/graphql-api.ts and types/graphql-slug-queries.ts were intentionally left untouched — test wrappers and mock fixtures import many type names from these files. tsc --noEmit passes clean and yarn build succeeds. Closes #1244
…1277) Targets the four highest-uncovered files in `frontend/src/components/knowledge_base/document/`: - RelationshipActionModal.tsx (16.2% → add 13 specs covering corpus-loaded flow, role picker, structural filter, label search/create/change, submit, cancel, ellipsis preview, singular/plural count). - UnifiedContentFeed.tsx (29.8% → add 8 specs covering selection toolbar, Select All/Clear, relationship modal open, readOnly + noCorpus hides toolbar, sort/search/structural/OC_-prefix filters, content-type routing). - ChatTray.tsx (25.3% → add 10 specs covering empty list, back button w/ refetch, ASYNC_ERROR → reconnect banner, context meter + compaction banner, SYNC_CONTENT, Shift+Enter, character-count thresholds). - DocumentKnowledgeBase.tsx (25.5% → add 2 specs covering the !documentId invalid-document modal and its Close-button path). Wrapper upgrades: - RelationshipActionModalTestWrapper: accepts `withCorpus`, `hasLabelset`, `relationLabels`, `onAddToExisting`, `onCreate`, `onClose`, `corpusId`. Seeds corpusStateAtom via an internal children-wrapping effect component (`CorpusSetupInner`) so Playwright CT's babel transform mounts it. - UnifiedContentFeedTestWrapper: adds `noCorpus`, `showStructural`, `searchText`, `textSearchMatches` props. - New RelationshipActionModalFixtures.ts holds `buildRelationLabel` in a plain .ts file to respect the CT split-import rule. All 1523 CT tests pass (`yarn test:ct --reporter=list`). https://claude.ai/code/session_012YjXHsfGDx3F5YfaHh5pTU
…1316-tMTai Simplify RelationGroup.updateForAnnotationDeletion pruning branches
Signed-off-by: JSIV <5049984+JSv4@users.noreply.github.com>
…1256-Demmi Cap fullDatacellList no-args path and address #1256 follow-ups
Signed-off-by: JSIV <5049984+JSv4@users.noreply.github.com>
# Conflicts: # CHANGELOG.md # opencontractserver/utils/permissioning.py
…mber, kwargs split - Wire AgentConfig.similarity_top_k into _make_similarity_search_tool's default_k so the configured retrieval depth wins when the LLM omits k (previously hard-coded k=8 partially defeated similarity_top_k plumbing). - Bump MAX_DOC_LENGTH_FOR_FUZZY 50K -> 200K. Per-query timeout and n-gram anchor pre-filter are the real safety valves; the cap only needs to guard pathological inputs the timeout might miss. - Replace the second 30000 magic-number site in OpenAIEmbedder.embed_texts_batch with the existing OPENAI_EMBEDDER_MAX_INPUT_CHARS constant. - Pop similarity_top_k (config-time) from kwargs before forwarding to agent.structured_response (run-time) in api.py to make the routing explicit.
Code Review - PR #1380This is a large composite PR (benchmark harness + paragraph chunker + reranker framework + corpus-isolation fix). The code is well-structured and the test plan is thorough. Below are specific findings, ordered by severity. Bugs
Security
Performance
Style / Convention
Test Coverage Gaps
Positive Highlights
|
…ts-gte-2.33.1 Update requests requirement from >=2.32.2 to >=2.33.1
- runner.py: set finished_at before BenchmarkReport snapshot
- metrics.py: token_recall("","") now returns 0.0 with warning to mirror
char_recall and avoid silently inflating aggregate F1
- legalbench_rag.py: precompute keys with isolated random.Random so the
sort no longer mutates global random state
- data_extract_tasks.py: document model_override trust assumption
- local.yml: comment API_KEY as local-only placeholder
- retrieval.py: elevate struct_set→doc cache to module level keyed by
(corpus_id, struct_set_id) so a full benchmark run amortises lookups
- text_alignment.py: hoist doc_text.lower() out of per-query loop
- run_benchmark.py: replace magic 194 with PAPER_MAX_TESTS_PER_BENCHMARK
- constants/benchmarks.py: derive TRIM_LEN from MAX_LEN
- cross_encoder_reranker.py: rename comprehension var to avoid shadowing
- text_chunkers.py: tighten _INVISIBLE_CHARS_RE to format chars only,
preserving en/em dash and thin space
Code Review — PR #1380 (LegalBench-RAG benchmark harness + parity report)This is a thorough composite PR. The engineering quality is generally high, the benchmark methodology is honest (the self-correction on the inflated "+24.9 pts" headline is particularly good), and most of the production code changes are well-reasoned. Below are the issues I found, grouped by severity. Blockers / Must-Fix1. Agent pipeline is broken and documented as such The committed benchmark report states: The PR description says this is tracked in #1381, but the PR is also still introducing the prompt-tightening that was supposed to fix it — and that fix apparently doesn't work on the current branch. Merging a PR that claims to deliver a working pipeline while shipping committed benchmark artifacts that prove the pipeline is broken creates a confusing paper trail. Options:
2. ~724K additions are large committed data artifacts The net change count is dominated by Significant Issues3. TODO placeholder left in committed documentation
This is committed as-is. Replace or remove before merge. 4. Inline import inside
from opencontractserver.pipeline.utils import (
invalidate_reranker_cache,
)This should be a module-level import. Inline imports inside mutation handlers are surprising and complicate tooling (import linters, refactors). The import is cheap so there's no lazy-loading justification here. 5.
DEFAULT_EMBEDDER = env("DEFAULT_EMBEDDER", default="...TestEmbedder")If a developer has 6. The reduction is a production behavior change: documents between 200–500 KB will now skip fuzzy grounding entirely. The comment correctly notes that the timeout and n-gram filter are the "real safety valves," but the cap reduction is a regression in recall for large legal documents (EPC agreements, ISDA schedules mentioned in the same comment routinely exceed 200 KB). This change needs an explicit statement in the PR of what the measured impact on production extraction recall is, or the cap should be kept at 500 KB and the timeout + n-gram filter relied on exclusively. Minor / Polish7. The function is well-implemented and the logic for the three failure modes is correct. But given its operational importance — operators will grep for 8.
9. The comment explains the math (256 × ~375 tokens ≈ 96K tokens/call, well under the 8M cap). But this is a behavior change that may hit RPM limits on Tier-1 accounts (3000 req/min ÷ 256 = ~12 effective calls/min for a corpus ingest). For teams running parallel corpus ingests this could cause unexpected 429s that the 8-retry budget won't cover. At minimum this should be documented in the CHANGELOG as a tunable config change rather than a silent class-level constant. 10.
11. Process-local reranker instance cache cross-worker coherence note
12.
Positives Worth Calling Out
Summary: The retrieval-only harness and the production code improvements (chunker, reranker framework, fuzzy hardening, GC signal, failure-mode classification) are solid. The main blockers are the broken agent pipeline shipped alongside agent-pipeline claims, the committed data artifacts, and the unresolved TODO. I'd recommend resolving #1 and #2 before merge (or explicitly re-scoping to retrieval-only), and addressing #3–#6 in the same pass. |
…, reranker - _link_retrieval_citations: 4 tests covering happy path, defensive filtering of non-int / negative ids, missing-id graceful fallback, and noop-on-empty - _classify_none_result: 7 tests pinning each classification (empty, empty_history with messages-but-no-response, agent_committed_none for text and output_tool parts, no_final_response for single tool call and thinking, tool_loop_no_output for repeated tool calls, and the text-after-loop precedence rule) - CrossEncoderReranker._rerank_impl: 3 tests covering happy-path scoring, scalar-response normalization, and length-mismatch -inf padding — uses a mocked CrossEncoder loader so CI doesn't download weights
Code Review — PR #1380This is a large, well-motivated composite PR. The scientific integrity in the benchmark results section is commendable — proactively retracting prior headline numbers and explaining the three root causes (annotation contamination, sampling drift, metric drift) is exactly the right behaviour. The overall architecture for the reranker framework, text chunker registry, and benchmark harness is solid. Below are findings from most-to-least critical. Blockers1. Broken agent pipeline shipped in committed docs
This is committed as a permanent artifact in 2. Unfixed
This will render as a broken Markdown link reference forever once merged. Significant Issues3.
The benchmark runner does not sidestep it — it hands full ORM-writing tasks to threads. Recommend either: (a) add a 4.
5.
Celery tasks can be triggered via Celery Beat, management commands, and in some deployments via admin interfaces that accept arbitrary kwargs. Recommend adding the allowlist now rather than after the fact — a Minor Issues6. DEFAULT_EMBEDDER = env("DEFAULT_EMBEDDER", default="...TestEmbedder")If 7. Inconsistent empty-input edge cases in
This inconsistency means a query where both prediction and gold are empty strings scores F1=1.0 but recall=0.0 — a contradiction. Either follow SQuAD convention consistently ( 8. Character-count truncation in text = text[:OPENAI_EMBEDDER_MAX_INPUT_CHARS]Truncating by character count rather than token count will still exceed 8192 tokens for dense non-ASCII text (e.g., Chinese/Japanese legal documents). The fix is pragmatic and solves the Latin-script case, but the limit should be noted in 9. These helpers return 10. If spaCy is not installed, 11. This is informal/potentially offensive section header language. Suggest replacing with "Per-subset character precision (both axes)" or similar. Positive Notes
SummaryThe infrastructure work (reranker framework, chunker registry, benchmark harness, fuzzy-match hardening) is high quality and ready to merge. The two blockers — known agent regression in committed docs (#1381) and the unfixed TODO link — should be resolved before merge. Items 3–5 are worth addressing before merge for correctness and security reasons. Items 6–11 can be filed as follow-up issues if timeline pressure is high. |
- Drop the broken agent-pipeline section (775/776 cells failed with no_final_response on this branch); the agent fix is being landed separately in PR #1399 / issue #1381. - Reframe scope and TL;DR around the retrieval probe. - Replace committed-artifacts language and the runs/MANIFEST.md pointer with reproduction-from-CLI instructions. Run artifacts are no longer committed because (a) ~22 MB across four configs bloats clone size and (b) gold.json contains verbatim contract excerpts whose redistribution licensing is unsettled. - Drop the unresolved 'TODO replace with link' placeholder. - Remove the Agent row from the Configurations table.
Resolves divergent-history merge against main. PR's prior main-merge commit (0883061) was tree-identical to main's f3ae088 but had a different SHA, so naive merge produced 74 spurious "both added" conflicts. Used the equivalent main commit as the virtual merge base to reduce that to one real content conflict in opencontractserver/llms/vector_stores/core_vector_stores.py: - Combined reranker imports (ours) with VectorStoreProtocol import (theirs) at module top. - Kept the rerank-aware return in the empty-query-text branch (ours) and added the __all__ re-export from theirs at module level.
|
|
||
| from opencontractserver.constants.annotations import * # noqa: F401, F403 | ||
| from opencontractserver.constants.auth import * # noqa: F401, F403 | ||
| from opencontractserver.constants.benchmarks import * # noqa: F401, F403 |
Code Review — PR #1380: LegalBench-RAG Benchmark Harness + Parity ReportScope note: This is a large composite PR (~10,650 additions) merging four feature branches. I've focused the review on the novel code added on top of those merges: the benchmark runner/metrics/retrieval, fuzzy-match hardening, None-failure classifier, chunker registry, reranker base, and OpenAI embedder fixes. OverviewThe PR delivers a well-structured benchmark infrastructure that integrates cleanly with the existing pipeline. The parity methodology is unusually rigorous — the PR proactively audits its own earlier claims, calls out the retrieval-budget caveat for Config C, and documents which metrics are paper-comparable vs. scope-extension. That intellectual honesty is a feature. IssuesMedium —
|
The _get_function_tools helper reached into agent._function_tools (0.2.x) and agent._function_toolset (1.x) — both private dataclass fields. The tool-dispatch and requires-approval lookup paths additionally probed speculative private attributes (_wrapped_function, callable_function, _core_tool, wrapped_tool) on Tool objects that do not exist on the public Tool dataclass. Refactor to walk the public Agent.toolsets property (documented to include the auto-built function toolset for tools registered directly on the agent), pick FunctionToolset instances, and read the public FunctionToolset.tools dict. Use the public Tool.function and Tool.requires_approval fields for callable extraction and fallback approval checks. Verified against pydantic-ai 1.87.0 source (our minimum pin). Update test mocks in test_pydantic_ai_agents.py and test_nested_approval_gates.py to set inst.toolsets via a MagicMock(spec=FunctionToolset) helper so isinstance() recognises the fake. The integration test test_check_tool_requires_approval_with_real_pydantic_ai_agent exercises the helper end-to-end against a real Agent instance.
Code Review — PR #1380 (LegalBench-RAG benchmark harness + parity report)This is a substantial composite PR (~10.7K additions) merging four branches plus new work. The overall quality is high — the PR description is unusually thorough and honest (including proactively retracting earlier incorrect benchmark claims). Below is an analysis by area. OverviewThe PR delivers:
Correctness Issues1. In conf.task_always_eager = True # Celery 4 attribute name
conf.task_eager_propagates = TrueBut the test suite correctly uses the Celery 5 form: @override_settings(CELERY_TASK_ALWAYS_EAGER=True, CELERY_TASK_EAGER_PROPAGATES=True)In Celery 5, 2. # constants/extraction.py
MAX_DOC_LENGTH_FOR_FUZZY = 200_000 # was 500_000Any document between 200K–500K characters will silently drop fuzzy alignment and fall back to exact + normalized matching. For many CUAD/MAUD commercial contracts this is the corpus — production deployments upgrading from the previous version will see citation grounding quality changes without any warning. This should be noted in 3. existing = set(
Annotation.objects.filter(id__in=valid_ids).values_list("id", flat=True)
)
existing_ids = [aid for aid in valid_ids if aid in existing]
if existing_ids:
datacell.sources.add(*existing_ids)The Security4. The docstring correctly flags this:
This is good documentation but should have a GitHub issue filed now, before the benchmark harness matures. The path from "benchmark CLI only" to "webhook-triggered" is short in active projects, and an unbounded model identifier can redirect extraction traffic to arbitrary endpoints. Recommend filing a tracking issue. 5. Path traversal protection in The Performance6. Reranker cache cross-worker propagation lag is 5 minutes
This means after a 7. Batched embedding with The microservice embedder changes (connection pooling, Code Quality8. keys = {id(test): _seed_value(test["snippets"][0]["file_path"]) for test in valid}
valid.sort(key=lambda test: keys[id(test)])Using 9. # config/settings/test.py
DEFAULT_EMBEDDER = env(
"DEFAULT_EMBEDDER",
default="opencontractserver.pipeline.embedders.test_embedder.TestEmbedder",
)This is necessary for benchmark runs but creates a hermeticity hole in CI: any 10. CHANGELOG for The changelog entry covers the reranker and chunker additions thoroughly, but the fuzzy-matcher hardening (reduced doc length cap, new query length cap, per-query timeout, n-gram anchor filter) isn't listed under Changed — only the Testing11. Test coverage is strong
12. If the DocumentationThe benchmark results doc ( Summary
The Celery 5 |
Patch coverage on PR #1380 was 85.02% (target 87.30%). Added: - run_benchmark management command: full happy-path test (real adapter, mocked runner) plus retrieval-only/corpus-wide flag passthrough and user-not-found error path. Brings ~50 lines of run_benchmark.py from 0% to covered. - CohereReranker fault-tolerance: request-exception, non-200, non-JSON body, missing 'results' key, malformed item skipping, and empty results all fall back to identity ordering. - CrossEncoderReranker (no prior tests): success path with score extraction, max_length forwarding, padding when the model returns fewer scores than passages, scalar→list normalization, and the per-key model cache fast path. Uses an injected stub model so CI runs without sentence-transformers/torch installed. - OpenAI embedder truncation: oversize single-text and oversize batch inputs are clipped to OPENAI_EMBEDDER_MAX_INPUT_CHARS before the wire call; empty/whitespace batch inputs become None slots.
Code Review — PR #1380: LegalBench-RAG Benchmark Harness + Parity ReportThis is a large composite PR (~11k additions) covering the benchmark harness, paragraph chunker, reranker framework, and corpus-isolation fix. Overall the engineering quality is high — the areas below are targeted observations rather than a broad rejection. OverviewThe PR introduces:
Strengths
Issues and Suggestions1.
|
| Area | Assessment |
|---|---|
| Security | ✅ Good — secrets encrypted, model_override documented, one runtime guard suggested |
| Performance | ✅ Good — oversample cap, process cache, graceful degradation |
| Async/sync | ✅ No violations found |
| Error handling | ✅ Excellent — safe/strict wrapper pattern |
| Tests | ✅ Comprehensive — 90 tests, edge cases covered |
| Code quality | ✅ Clean — minor issues noted above |
| Backward compat | ✅ Reranking opt-in, no behavior change by default |
The core concern is the model_override trust boundary — a lightweight runtime guard would make this safe to widen the call surface later without a separate audit. The remaining items are low-risk quality suggestions. Happy to approve once the model_override guard is addressed or explicitly deferred with a tracking issue.
- Add a runtime allowlist guard for ``model_override`` in ``doc_extract_query_task``. When the optional Django setting ``BENCHMARK_ALLOWED_MODEL_OVERRIDES`` is unset (default), no enforcement runs — preserves operator-only workflows while giving operators a no-code-change path to lock down this surface if the task is ever exposed to untrusted input. Rejected overrides mark the Datacell as failed with a clear stacktrace and re-raise so celery workers log the violation. - Update the merge-from-main fix in ``test_tool_approval_gate``: the earlier refactor of ``_get_function_tools`` to use the public ``agent.toolsets`` API broke the test mock that exposed the old private ``_function_tools`` attribute. Mock now exposes a real ``FunctionToolset`` via ``inst.toolsets`` so all four approval-flow tests pass on this branch. - Document the failure-mode convention on ``Datacell.stacktrace`` (the field name implies unstructured exception text but we also persist the structured ``failure_mode=`` lines that ``_classify_none_result`` produces — operators ``grep failure_mode=`` to triage). - Document reranker cache invalidation semantics in ``get_default_reranker_instance`` (DB-write busts the cache; in-memory test patches don't — set ``STRICT_RERANKER`` or call ``invalidate_reranker_cache`` explicitly). - Document TLS verification posture on ``MicroserviceReranker`` (relies on system trust store, no per-instance opt-out). - Document one-shot semantics on the 0037 migration so operators don't expect re-running ``migrate`` to re-seed an already-set value. - Add ``ModelOverrideAllowlistTests`` covering the unknown-model rejection path end-to-end.
Code Review: LegalBench-RAG Benchmark Harness + Parity ReportThis is a substantial composite PR (11,175 additions). I've focused my review on correctness, security, and architecture rather than the benchmark numbers themselves. OverviewThe PR lands five interlocking pieces:
What's done well
IssuesMedium1. The function checks Suggestion: add a canary assertion in tests that pydantic-ai's from pydantic_ai.messages import ModelResponse, ToolCallPart, TextPart
response_msgs = [m for m in messages if isinstance(m, ModelResponse)]2. In Suggestion: either document this as acceptable, or use explicit with ThreadPoolExecutor(max_workers=max_workers) as executor:
...
transient_exc = None
for future in as_completed(future_to_idx):
try:
...
except (requests.exceptions.Timeout, ...):
transient_exc = sys.exc_info()
break # don't raise inside; let executor drain cleanly
if transient_exc:
raise transient_exc[1]3.
This is marked as a known limitation ("Non-eager extraction is not yet supported"), but the docstring and the management command should warn explicitly that Minor4. Corpus agent system prompt says "most legal documents" (document-scoped wording) In 5.
DEFAULT_EMBEDDER = env("DEFAULT_EMBEDDER", default="...TestEmbedder")If 6. The if allowed is None:
logger.info("BENCHMARK_ALLOWED_MODEL_OVERRIDES unset; accepting model_override=%r (operator-only path)", model_override)7. The cache key for Test coverage gaps
Changelog / docsThe SummaryThis is a well-thought-out PR with solid test coverage and good documentation. The main concerns are: (1) |
Hardens the extraction-related plumbing introduced by Open-Source-Legal#1381 / Open-Source-Legal#1380 / Open-Source-Legal#1399 against three medium-impact failure modes and four documentation / observability gaps surfaced during code review. Medium: * embeddings_task: transient HTTP errors now drop queued sub-batches and shutdown the executor with cancel_futures=True before re-raising, so Celery autoretry isn't blocked waiting on in-flight peer round trips. * benchmarks/loader: force_celery_eager refuses to mutate the global Celery config unless MODE=TEST or OC_BENCHMARK_CLI=1; refuses when task_always_eager is already True. run_benchmark management command sets OC_BENCHMARK_CLI=1 automatically. * config/settings/test: DEFAULT_EMBEDDER env override is now gated behind an explicit BENCHMARK_MODE=1 to prevent stray CI env from silently pushing the test suite onto a real embedder. Minor: * CorpusAgent prompt now says "most legal corpora" instead of "most legal documents" — corpus-scoped wording. * doc_extract_query_task: log when model_override is accepted in the unrestricted (operator-only) path so operators can grep production logs. * get_default_reranker_instance: docstring warns that bulk_update / data-migration writes to PipelineSettings.default_reranker bypass the auto_now `modified` field used as part of the cache key. Tests: * PydanticAiSchemaCanaryTests pin ModelResponse.kind == "response", ToolCallPart.part_kind == "tool-call", TextPart.part_kind == "text" so a future pydantic-ai version bump surfaces immediately. * test_transient_error_does_not_block_on_in_flight_peers asserts the executor fast-fail path completes well inside the 10s peer block. * ForceCeleryEagerSafetyGuardsTestCase pins the three new safety refusals. Closes Open-Source-Legal#1410
Summary
Composite branch built on top of #1239 (benchmark harness), #1353 (paragraph chunker), #1354 (reranker framework), and #1376 (corpus-isolation fix). On top of those merges, this PR adds:
--retrieval-onlyand--corpus-widebenchmark modesDatacell.llm_call_logOpenAIEmbedderinput truncation (was 400ing on inputs >8192 tokens)similarity_top_kplumbed from CLI through to the agent'sdoc_extract_query_taskVECTOR_EMBEDDER_API_KEYwired throughlocal.ymldocs/benchmarks/legalbench_rag_results.mdScope of the LegalBench-RAG comparison
The paper measures single-shot retrieval only (no LLM, no agent, no grounding pass). Our headline claim against the paper is at that exact protocol — single-shot top-k vector search, character-level recall/precision against gold, on the corpus-wide retrieval surface that mirrors
legalbenchrag/methods/baseline.py:193.probe_char_recall/probe_char_precisioncitation_char_recall/citation_char_precisionanswer_token_f1extraction_success_ratePaper-comparison claims here always refer to probe
char_recallvs the paper's published tables, at matched k, using the paper's exact formulas. Agent-pipeline numbers are reported separately as scope-extension and not part of any paper-comparison claim.Headline result — retrieval-only, corpus-wide, k=32
Apples-to-apples retrieval comparison (no agent, no LLM, single-shot top-k):
¹ contractnli's 194-task slice covers only 20 docs of ~10 KB each; k=32 retrieves a sizeable fraction of any given document. The paper's fixed-500 chunker on the same slice still got 69.9%.
Agent pipeline (scope-extension, not LB-RAG-comparable)
Full OpenContracts pipeline (retrieval + iterative agent + structured extraction + citation grounding) on the same task slices.
Agent failure-mode fix
The "extraction returned None" outcome on
doc_extract_query_taskwas hiding three distinct failure modes under one error message:agent_committed_none— agent searched, decided absent, returned None. Legitimate.no_final_response— agent issued a tool call, the message log ends there with no tool-return / no synthesis. The pydantic-ai loop exited without producing a structured answer. Pipeline bug, not a data signal.tool_loop_no_output— agent stuck issuing the same tool call without ever synthesising an answer.The structured-extraction system prompt at all three sites in
pydantic_ai_agents.pysaid "If the information cannot be found using the tools, return null/None." Combined withoutput_type=strand gpt-4o-mini's eagerness to satisfy structured output schemas, this gave the agent a license to bail after a single search. Tightened to require 2-3 distinct queries before concluding absence. Cross-subset before / after at k=32:Quality wins on every subset on every quality axis. LLM call counts actually drop because the agent commits faster once it has data, instead of looping. New
_classify_none_resulthelper indata_extract_tasksrecords the failure mode in the cell's stacktrace so operators cangrep failure_mode=to separate legitimate "data not present" outcomes from pipeline bugs.Model sweep (privacy_qa, post-prompt fix not yet rerun for gpt-4o / sonnet)
What's NOT in this PR
answer_token_f1has no external reference point. Tracked in Benchmark answer quality (not just retrieval) against derivative LegalBench-RAG work #1382.Test plan
python manage.py test opencontractserver.tests.test_benchmarks— 36 tests passpython manage.py test opencontractserver.tests.test_text_alignment— 24 tests pass (5 new inTestFuzzyHardening)python manage.py test opencontractserver.tests.test_extraction_grounding— 24 tests passpython manage.py test opencontractserver.tests.test_corpus_isolation_vector_store— 6 tests pass (added by Scope structural annotations to the queried corpus in vector store #1376)docs/benchmarks/legalbench_rag_results.md