Skip to content

fix(kb): add_document chunks, partial-success ingest, stable upload rejection codes (#17830) - #18405

Open
mrveiss wants to merge 10 commits into
mainfrom
issue-17530-kb-intake
Open

mrveiss wants to merge 10 commits into
mainfrom
issue-17530-kb-intake

Conversation

@mrveiss

@mrveiss mrveiss commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Closes #17830
Closes #18407
Refs #17986
Refs #17577
Refs #17530
Refs #17527

Thinking Path

What Changed

  • autobot_shared/doc_chunking.py: chunk_large_content(max_tokens=800), chunk_plain_text.

  • knowledge/document_ingest.py (new): chunked, budgeted, resumable ingest. Chunk 0 keeps fact_id == doc_id; later chunks are <doc_id>:chunk:<n>; chunk metadata carries document_id/chunk_index/chunk_count. Per-document Redis record (kb:ingest:<doc_id>) holds status, chunks stored and a content digest; a retry with the same id and content resumes after the last stored chunk, changed content restarts. Single-chunk documents behave exactly as before.

  • knowledge/documents.py: add_document delegates to it. Result status is success | partial | timeout | error, plus chunks_stored/chunks_total.

  • knowledge/upload_rejections.py (new): UploadRejectionCode enum, UploadRejection, handler (registered in app_factory.py), no_text_rejection, extract_within_deadline (moved out of api/knowledge.py, which shrinks 3414 -> 3380; both ceilings lowered).

  • Env vars AUTOBOT_KB_CHUNK_MAX_TOKENS, AUTOBOT_KB_INGEST_RECORD_TTL_SECONDS registered + ENV_VARS.md regenerated.

  • No response schema changed, so api.ts is not regenerated (detail_code is emitted by the exception handler, not a response model).

  • Review round: (1) doc_chunking._hard_cap splits a single token larger than the budget by characters; (2) document_ingest._drop_orphans deletes the document's own chunks past the new count when shorter content is re-added under the same id (and the record when it shrinks to one chunk); (3) bug(kb): the upload, /facts and /url routes bypass add_document — a PDF uploaded in the UI is still stored as one vector #18407: _store_fact_in_kb (upload, /facts, /url, add_text, audio) now stores through add_document; knowledge/chunk_provenance.py gives each chunk only the page_spans of the pages it covers, re-based to the chunk (a chunk that cannot be located gets none rather than a wrong one). The knowledge upload: rejection reasons reach the user in English only — the API composes prose where it should emit a code #17577 codes are unchanged. page_spans drift fixed at the root: knowledge/upload_pages.py clean_upload_text cleans a paginated PDF page by page and re-renders text and spans together, so spans index the text actually stored (previously computed before redaction/sanitizing).

Verification

  • Review-round tests: knowledge/document_ingest_test.py now 11 (oversized token, orphans, shrink-to-one, per-chunk pages), api/kb_upload_chunking_18407_test.py 3 (long upload / PDF-style pages / facts+url helper: more than one fact sharing the returned document id; contrast: with the helper back on store_fact, all 3 fail). Existing redaction and security(kb): backfill facts with no owner or visibility as SYSTEM before chat grounding is scoped #16693 tests updated to assert on add_document.

  • KB selection after the change: 2208 passed, 263 skipped, 1 failed (api/file_upload_comprehensive_test.py::test_knowledge_base_integration, asserts a cwd-relative data/file_manager_root path; touches nothing changed here; not verified against main). Ratchets and reach_scope_claim_17844 pass.

  • New tests: knowledge/document_ingest_test.py (7), api/knowledge_upload_rejection_codes_test.py (5, AST contract: no bare HTTPException on the upload path, every raise names an enum member, every member is used).

  • Contrast: with documents.py and api/knowledge.py restored to origin/main, 9 of the 12 new tests fail; with the change, 38 pass across them plus config/timeout_configuration_test.py and api/knowledge_upload_no_text_layer_test.py.

  • Broader KB selection (knowledge, api/knowledge*_test.py, config, -k "knowledge or document or upload or chunk or fact", e2e ignored): 1996 passed, 65 skipped.

  • symbol_fork_ratchet_17312, python_file_size_ratchet, ceiling_record_parity_17872, guard_reach_meta and reach_scope_claim_17844: 93 passed.

  • Final-tree runs: KB selection 2225 passed, 267 skipped, 1 failed (file_upload_comprehensive_test::test_knowledge_base_integration, reproduced on a detached origin/main checkout, so pre-existing; filed bug(tests): file_upload_comprehensive_test::test_knowledge_base_integration fails on main: it asserts a cwd-relative data/file_manager_root exists #18419); symbol_fork_ratchet_17312, python_file_size_ratchet, ceiling_record_parity_17872, guard_reach_meta: 83 passed; reach_scope_claim_17844 alone: 10 passed.

  • Contrast: bug(kb): the upload, /facts and /url routes bypass add_document — a PDF uploaded in the UI is still stored as one vector #18407 tests now count facts through a fake KB whose every path ends in store_fact: on the old helper all 3 fail with "stored as a single fact"; with spans taken before cleaning the redaction test fails with "span names the wrong page".

Folded in

From branch issue-18129-doc-indexers (handed over), cherry-picked with -x:

  • 72cba7a332 one documentation indexer: both standalone scripts become shims over DocIndexerService. Conflicts: _reach_policy.py and reach_scope_claim_17844_test.py (kept this branch's entries and added one-doc-indexer beside them; kept this branch's reach_declarations_test import in place of the hand list); autobot-infrastructure/shared/tools/index_documentation.py (took the shim; the tech-debt(backend): six regex concepts are forked across 2–8 files each, and one fork is a no-op #18093 sanitize_token edit it conflicted with is inside the code the shim removes, and the canonical service already uses sanitize_token).
  • c426fc07a9 (taxonomy formatting): content-identical after the pre-commit formatter ran on the first pick, so skipped as empty.
  • Added ddc0ae0: the indexer must use this PR's chunking. Its fallback already used the shared chunk_large_content, but a header-less or long-intro document was kept as ONE over-budget chunk (it relied on the embedder rejecting it and _split_and_embed halving it). The intro is now cut with chunk_plain_text; the test failed before (1 chunk vs 3). Not routed through add_document: the indexer writes its own ChromaDB docs collection with doc-level metadata, a different store from KB facts.
  • Added 551d91b (review fix: the dropped Redis path fed the docs browser). docs/browse, docs/categories and docs/stats scanned Redis doc_hash:*, whose only writer was the retired script, so a fresh install's browser was empty. They now read the autobot_docs collection (services/knowledge/doc_listing.py, grouped by file_path; category is the taxonomy id, content_hash a stable per-path id); an unopenable index is a 503. One store, no Redis mirror; response shapes unchanged. A re-index now deletes the file's prior chunks first, so a renamed section leaves no orphan. New tests: doc_listing_18129_test.py (browse, categories, stats, 503, orphan, and a contrast where the old Redis reader returns empty with no doc_hash keys) and repo_tests/doc_index_cli_behaviour_18129_test.py (--dry-run, --output, --search, --stats, scoping).

#18129 acceptance criteria (verified against this branch): one implementation, both infra scripts are 21-line runpy shims, tool path resolves DocIndexerService (one_doc_indexer_18129_test); tags from one helper (sanitize_token, single list in doc_indexer._extract_tags); no infra script re-derives front matter; guard test_only_the_canonical_service_chunks_markdown. The flag-by-flag comparison is now recorded on the issue (#18129 (comment)), so every criterion is met.

Closes #18129

Verification (folded work)

  • doc_indexer tests + doc_chunking_test + one_doc_indexer_18129 + doc_index_cli_flags_18129: 117 passed, plus 140 in the wider knowledge selection before the last commit.
  • symbol_fork_ratchet_17312, python_file_size_ratchet, ceiling_record_parity_17872, guard_reach_meta: 83 passed; reach_scope_claim_17844 alone: 10 passed.

Model Used

Claude Sonnet 5.5

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Long knowledge uploads are split into manageable chunks while retaining a shared document identity. Interrupted uploads can resume, and PDF page references remain associated with the relevant content.
    • Upload errors now provide structured codes and context, including for extraction timeouts and files with no usable text.
    • Documentation indexing supports tier, category and file selection, dry runs, search, statistics and JSON output. Documentation browsing and statistics use indexed documents.
  • Bug Fixes
    • Re-indexing replaces outdated chunks, and sensitive content continues to be redacted before storage.

@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

📝 Walkthrough

Knowledge ingestion now chunks long documents and preserves page metadata per chunk. Upload paths return structured rejection details. Documentation indexing uses a unified CLI and service, and documentation browsing reads from the indexed collection.

Changes: Knowledge ingestion and documentation indexing

Layer / File(s) Summary
Shared text chunking
autobot_shared/doc_chunking.py, autobot_shared/doc_chunking_test.py
Chunking accepts a configurable token limit and adds plain-text splitting for oversized content.
Document ingestion
autobot-backend/knowledge/document_ingest.py, autobot-backend/knowledge/documents.py, autobot-backend/knowledge/chunk_provenance.py, autobot-backend/knowledge/document_ingest_test.py, autobot_shared/env_registry_*.py, docs/developer/ENV_VARS.md
add_document delegates to chunked ingestion. Chunk records support resumption and carry page metadata rebased to chunk offsets. Tests cover ingestion results, retries, replacement, and provenance.
Upload validation and storage
autobot-backend/api/knowledge.py, autobot-backend/api/knowledge_office_upload.py, autobot-backend/knowledge/upload_*.py, autobot-backend/app_factory.py, autobot-backend/api/*upload*test.py, autobot-backend/api/kb_*test.py, autobot-backend/tests/test_page_provenance_13894.py, repo_tests/connector_redaction_guard_test.py
Upload paths use structured rejection codes and route content through add_document. Cleaning and redaction produce page metadata used during storage.
Indexer selection and replacement
autobot-backend/services/knowledge/doc_indexer.py, autobot-backend/services/knowledge/doc_taxonomy.py, autobot-backend/services/knowledge/test_doc_indexer_scope_18129.py, autobot-backend/knowledge/schemas/documents.py
The indexer supports tier and category selection, changed-file planning, and replacement of prior file chunks. Long introductions use shared text chunking.
Unified indexing CLI and shims
tools/index_documentation.py, autobot-infrastructure/shared/{tools,scripts/utilities}/index_documentation.py, repo_tests/doc_index_cli_*test.py, repo_tests/one_doc_indexer_18129_test.py, repo_tests/infra_backend_imports_allowlist_18288.py, repo_tests/_reach_policy.py, repo_tests/reach_scope_claim_17844_test.py, autobot-backend/utils/slug_equivalence_18093_test.py, autobot-infrastructure/shared/tools/index_documentation_category_default_test.py
The repository CLI handles indexing, search, statistics, and dry runs. The two infrastructure scripts delegate to it. CLI and source-guard tests cover the unified entry point.
Indexed-document browsing
autobot-backend/services/knowledge/doc_listing.py, autobot-backend/services/knowledge/doc_listing_18129_test.py, autobot-backend/api/knowledge.py
Browsing, category counts, and statistics use grouped metadata from the indexing collection. An unavailable collection produces HTTP 503.

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: 🟡 Moderate · up to 5719d

Uploads that are only partly stored, or not stored at all, can still be reported as successful. A failed documentation re-index can remove a file from search until its content changes. A known test failure also remains. Resolve these issues before merging.

Pre-merge checks · Passed 4 · Failed 1

Failed Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 45.29% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 170 functions across 34 files. (1 skipped… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks (4 passed)
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly summarises the main knowledge-base changes: add_document chunking, partial-success ingestion, and stable upload rejection codes.
Linked Issues check Passed The PR addresses all active direct issue requirements. For [#17830], add_document uses shared chunking, assigns shared document provenance and deterministic chunk IDs, and adds multi-fact tests. For…
Out of Scope Changes check Passed The reviewed changes remain connected to the direct issue objectives. Ingest budgets, environment variables, resumable records, and upload rejection handling support the chunked upload requirements. T…
Full details: Docstring Coverage

Explanation

Docstring coverage is 45.29% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 170 functions across 34 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@github-actions

Copy link
Copy Markdown
Contributor

Notice: 29 open PRs — past the runaway threshold (25)

This is not a request to defer this PR. The actual limit is the open-PR cap (AUTOBOT_OPEN_PR_CAP, default 40), enforced at pre-push against NEW branches only -- pushing more commits to this PR's own branch is never blocked by it.

This notice only means the count is high enough to be worth a glance for a runaway — something opening PRs in a loop, or a merge pipeline that has stalled so nothing is draining.

Currently open:

If the queue is draining normally, ignore this. Otherwise:

  1. Check whether CI is dispatching at all — see the ci-dispatch-watchdog status on these PRs
  2. Merge the ones whose CI has finished and review has passed: gh pr merge <number> --squash --delete-branch
  3. Look for a loop opening near-identical PRs

Warn-only runaway detector — .github/workflows/pr-queue-gate.yml. It never blocks a merge.

@github-actions

Copy link
Copy Markdown
Contributor

✅ SSOT Configuration Compliance: Passing

🎉 No new hardcoded values of either class — ssot and other both block.

Known backlog in pipeline-scripts/hardcoded_values_baseline.txt is suppressed and tracked in #14371.

@mrveiss

mrveiss commented Oct 12, 2026

Copy link
Copy Markdown
Owner Author

Review (Sonnet) at 6144390: blocked on two MEDIUMs. Every AC of #17830 is met as written:

  • add_document → ingest_document → chunk_plain_text → chunk_large_content;
  • shared document_id/chunk_index/chunk_count;
  • a multi-chunk test;
  • docstrings.

The chunker reuses the canonical one and is not a fork.

Severity Location Finding Fix
Medium autobot_shared/doc_chunking.py:208 sentence.split() keeps one token longer than the budget (CJK with no spaces, base64, minified text) as a single over-budget chunk: silently one vector. Hard-slice by characters when a word exceeds the budget, with a test.
Medium document_ingest.py:125-129 Resume trusts the record alone. Deleted chunk facts are skipped. When the same doc_id is re-added with shorter content, the chunks above the new total are left as orphans. On a digest change, delete chunk ids >= the new total. Verify existence on resume (at least on a stored == total replay).
Medium (follow-up) knowledge/upload_rejections.py The 9 stable codes have no frontend mapping (no upload.* keys in any of the 11 locales). Acceptable while #17577 is Refs; add the i18n keys in all 11 locales in its follow-up.
Low document_ingest.py:37 hset and expire are two non-atomic calls per chunk. The TTL env spec has no range. Pipeline the two calls; add range.
Low documents.py:73 The default doc_id is a fresh uuid, so a retry without an id never resumes. Document it.

For the closing decision: the ACs of #17830 are met literally, but the issue's own failure scenario (a manually uploaded PDF stored as one vector) persists until #18407 lands, because upload, /facts and /url still bypass add_document. Consider Refs #17830 here and Closes on #18407, unless the owner reads the ACs as the whole contract.

@mrveiss

mrveiss commented Oct 12, 2026

Copy link
Copy Markdown
Owner Author

Delta review (Sonnet) at 8d54a90: approve.

  • Over-budget token: doc_chunking._hard_cap slices by max_tokens*4 chars, tested at document_ingest_test.py:183 with no characters lost.
  • Orphans: _drop_orphans deletes chunks [new_total, old_total) on a digest change (:163, :174).
  • One ingest path: upload, /facts and /url all go through _store_fact_in_kb → add_document, tested in kb_upload_chunking_18407_test.py, so the bug(kb): add_document never chunks — a whole transcript or PDF becomes one embedding, against its own docstring #17830 scenario (a PDF stored as one vector) is fixed and both Closes lines are honest.
  • Page spans: built from the cleaned, redacted text per page, tested at :71.
  • Size: knowledge.py 3357, with the ceiling lowered from 3414.

LOWs:

Queued for the next merge train.

…scripts become shims over DocIndexerService (#18129)

(cherry picked from commit 72cba7a332090d477ec707f461a714d7c91fa437)
@mrveiss

mrveiss commented Oct 12, 2026

Copy link
Copy Markdown
Owner Author

Delta review (Sonnet, plus a merger check) at ddc0ae0: blocked on one High introduced by the #18129 fold.

Severity Location Finding Fix
High autobot-backend/api/knowledge.py:2586 _get_indexed_docs_from_redis → docs/browse (:2690), docs/categories (:2746), docs/stats (:2811) The docs browser goes empty. These endpoints scan Redis doc_hash:*. On main, the only writer of doc_hash:* is the old script this fold retires (autobot-infrastructure/shared/scripts/utilities/index_documentation.py:231,291). At this head, nothing writes them, so a fresh install's browse, categories and stats are empty and existing keys go stale. useDocumentationSearch.ts calls browse and categories. In this PR, point _get_indexed_docs_from_redis at the autobot_docs Chroma collection (grouped by file_path), which is now the one store the indexer writes. Alternatively, have doc_indexer write the doc_hash metadata, but a second store for the same concept is the weaker choice. Add a test that the browse endpoint lists documents indexed by doc_indexer.
Low doc_indexer.py:868 Chunk ids include the section name, so a section rename leaves orphan chunks. (Upsert ids are deterministic otherwise, so re-runs don't duplicate.) Delete a file's old chunks (where file_path=…) before re-indexing it.
Low repo_tests/doc_index_cli_flags_18129_test.py --dry-run, --output, --search and --stats have parse tests only, with no behavioural test. Add tests with a fake service.

Verified fine:

…e one indexer writes, and a re-index drops the file's prior chunks (#18129)
@mrveiss

mrveiss commented Oct 12, 2026

Copy link
Copy Markdown
Owner Author

Delta check at 551d91b: approve. The docs-browser High is fixed.

  • One store: _get_indexed_docs() (api/knowledge.py:2577) reads the autobot_docs Chroma collection via services/knowledge/doc_listing.py, grouped by file_path. All three endpoints use it: browse (:2671), categories (:2718) and stats (:2774). No code reads doc_hash:* any more; the two remaining mentions are prose.
  • Response shapes unchanged: category is the detect_category id (matching --category), and content_hash is a stable path md5, which useDocumentationSearch.ts keys on. No api.ts change is needed.
  • Errors: an unreachable index gives 503, not an empty list. A contrast test shows a doc_hash:* reader returning [] where the collection reader returns 2.
  • Orphan chunks: doc_indexer.py:929 deletes where={"file_path": rel_path} before re-upserting, scoped to that one file`s own derived chunks, so a renamed section leaves no orphans (the test fails without it).
  • CLI: 6 behavioural tests (--dry-run, --output, --search, --stats, --file/--tier/--category).
  • Size: knowledge.py 3357→3320.

Nit: the knowledge/schemas/documents.py:14 docstring still describes doc_hash:* entries.

Closes #17830, #18407 and #18129 are honest. Back in the next merge train.

@mrveiss

mrveiss commented Oct 12, 2026

Copy link
Copy Markdown
Owner Author

Delta review @ 567ab9c (main merge): approve.

  • Hand-resolved files: two, both resolved to this branch's side.
  • index_documentation.py: the 21-line shim replaces main's one-line change (active_embedding_model() instead of a literal model name). The canonical DocIndexerService path reads the org's effective embedding model, which falls back to the same SSOT value, so the shim loses nothing.
  • index_documentation_category_default_test.py: this branch deletes it along with the standalone indexer it tested, so main's import move in it is moot.

mrveiss added a commit that referenced this pull request Oct 13, 2026
…yed (#18472 moved lines), index_documentation shims leave the backend-import allowlist (#18405), migration gate and python filter cover what their tests read (#13421, #18467), plugin test restores sys.modules (#18371), excluded-tree debt 45
@mrveiss

mrveiss commented Oct 13, 2026

Copy link
Copy Markdown
Owner Author

Ejected from merge train #9 (#18507 went red; it is rebuilt as 9c without this PR). These were reproduced by CI on the merged tree:

  1. repo_tests/connector_redaction_guard_test.py::test_the_audio_api_bypass_also_reaches_store_fact: _store_fact_in_kb (api/knowledge.py:688) now calls kb.add_document(...), not kb.store_fact(...). The guard (:207) must learn the new chokepoint, and the redaction must still be shown to happen on that path.
  2. slug_equivalence_18093_test.py::...[index_documentation_tag]: index_documentation.py became a shim and no longer calls sanitize_token, so its row in the recorded call-site table (:156) is stale.
  3. pre-commit-no-print-console_test.py whole-repo scan: it reports 285 against a pinned 297. The 12 dropped violations are probably the removed indexer code; re-derive the number once you have confirmed that.
  4. Merged-tree fixes: these were applied in 9b and will be needed again after the rebase:

Back with the owner; it rejoins a train once fixed (delta review).

…t, env_registry_knowledge, redaction guard follows add_document (#17530)
@mrveiss

mrveiss commented Oct 13, 2026

Copy link
Copy Markdown
Owner Author

Delta review @ 5719de5: one blocker left; everything else checks out.

  • Blocker: env_registry_knowledge.py is imported only from env_registry_backend_services.py:587. repo_tests/env_registry_is_extensible_test.py::test_every_split_module_is_imported_by_the_parent reads env_registry.py's AST (PARENT), so a sibling's import does not count. This exact pattern failed for refactor(rag): de-fork graph retrieval — one neighbour contract, one graph blend, shared config (#18139, #18141) #18421's rag module on train-9b's tree. Fix: merge main after train-9c lands. It moves the four tls registrations out of env_registry.py (1214 → 1176 lines) and imports rag/tls there, which leaves room. Then add from autobot_shared import env_registry_knowledge # noqa: E402,F401 to env_registry.py and drop the one in backend_services.
  • Checked: the redaction-guard walk (_store_fact_in_kb → add_document → ingest_document → _add_document_internal → store_fact → sanitize_fact_content) with both contrast cases; the index_documentation_tag row deleted (doc_indexer_tag covers the pattern); content_digest, env_int, and the allowlist rows all in.
  • No-print pin: it stays 297 on your branch and gets re-derived on whichever train tree carries this.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository: mrveiss/AutoBot-AI/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: eb9d96d4-e4c0-40bf-ab3d-fb324bdf5639
📥 Commits

Reviewing files that changed from the base of the PR and between e83eb77 and 5719de5.

⛔ Files ignored due to path filters (3)
  • pipeline-scripts/hardcoded_values_baseline.txt is excluded by !**/*_baseline.txt, !pipeline-scripts/hardcoded_values_baseline.txt
  • repo_tests/python_file_size_ratchet_baseline.py is excluded by !repo_tests/python_file_size_ratchet_baseline.py
  • scripts/python_file_size_known_large.py is excluded by !scripts/python_file_size_known_large.py
📒 Files selected for processing (38)
  • autobot-backend/api/kb_ingestion_sites_system_16693_test.py
  • autobot-backend/api/kb_upload_chunking_18407_test.py
  • autobot-backend/api/knowledge.py
  • autobot-backend/api/knowledge_office_upload.py
  • autobot-backend/api/knowledge_upload_no_text_layer_test.py
  • autobot-backend/api/knowledge_upload_redaction_test.py
  • autobot-backend/api/knowledge_upload_rejection_codes_test.py
  • autobot-backend/app_factory.py
  • autobot-backend/knowledge/chunk_provenance.py
  • autobot-backend/knowledge/document_ingest.py
  • autobot-backend/knowledge/document_ingest_test.py
  • autobot-backend/knowledge/documents.py
  • autobot-backend/knowledge/schemas/documents.py
  • autobot-backend/knowledge/upload_pages.py
  • autobot-backend/knowledge/upload_rejections.py
  • autobot-backend/services/knowledge/doc_indexer.py
  • autobot-backend/services/knowledge/doc_listing.py
  • autobot-backend/services/knowledge/doc_listing_18129_test.py
  • autobot-backend/services/knowledge/doc_taxonomy.py
  • autobot-backend/services/knowledge/test_doc_indexer_scope_18129.py
  • autobot-backend/tests/test_page_provenance_13894.py
  • autobot-backend/utils/slug_equivalence_18093_test.py
  • autobot-infrastructure/shared/scripts/utilities/index_documentation.py
  • autobot-infrastructure/shared/tools/index_documentation.py
  • autobot-infrastructure/shared/tools/index_documentation_category_default_test.py
  • autobot_shared/doc_chunking.py
  • autobot_shared/doc_chunking_test.py
  • autobot_shared/env_registry_backend_services.py
  • autobot_shared/env_registry_knowledge.py
  • docs/developer/ENV_VARS.md
  • repo_tests/_reach_policy.py
  • repo_tests/connector_redaction_guard_test.py
  • repo_tests/doc_index_cli_behaviour_18129_test.py
  • repo_tests/doc_index_cli_flags_18129_test.py
  • repo_tests/infra_backend_imports_allowlist_18288.py
  • repo_tests/one_doc_indexer_18129_test.py
  • repo_tests/reach_scope_claim_17844_test.py
  • tools/index_documentation.py
💤 Files with no reviewable changes (3)
  • autobot-backend/utils/slug_equivalence_18093_test.py
  • repo_tests/infra_backend_imports_allowlist_18288.py
  • autobot-infrastructure/shared/tools/index_documentation_category_default_test.py

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

Comment on lines +585 to +587

# The knowledge-component registrations live in their own module (file-size ceiling).
from autobot_shared import env_registry_knowledge # noqa: E402,F401

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Import env_registry_knowledge from env_registry.py instead.

The repository documentation requires new component modules to be imported from autobot_shared/env_registry.py. This import is in a sibling registry module. The PR description reports that the extensibility test fails for this reason. Move the import to env_registry.py and remove it here.

As per path instructions (ENV_VARS.md): "if creating a component module, import it from autobot_shared/env_registry.py".

Source: Path instructions

Comment on lines +693 to +694
# The one ingest path (#18407): long input is chunked, with per-chunk page_spans.
result = await kb.add_document(content, metadata)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not report a partial or failed ingest as success.

add_document can now return partial, timeout or error. _store_fact_in_kb ignores status and returns only fact_id. On a partial result, upload_file_to_knowledge returns success=True with the full word_count, and the audit record says "success". Some chunks are missing, and the response does not tell the user. On a timeout or error result, the upload returns success=True with document_id=None. Check result["status"] in _store_fact_in_kb. Raise an error for timeout/error. Expose partial together with chunks_stored/chunks_total so the client can retry with the same id.

Comment on lines +37 to +38
KB_CHUNK_MAX_TOKENS = env_int("AUTOBOT_KB_CHUNK_MAX_TOKENS", 800)
KB_INGEST_RECORD_TTL_SECONDS = env_int("AUTOBOT_KB_INGEST_RECORD_TTL_SECONDS", 30 * 24 * 3600)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Read AUTOBOT_KB_CHUNK_MAX_TOKENS with a clamped reader.

env_int falls back only on a malformed value. It does not enforce the registered range=(50, 100000). If an operator sets the value to 0 or a negative number, _split_oversized uses a 1-word window and _hard_cap uses a 1-character limit. A long document then becomes thousands of chunks, and the code stores and embeds each one. Use the clamped integer reader from autobot_shared/env_utils.py with the registered bounds. Also give the TTL a lower bound. A non-positive expire deletes the resume record at once.

As per path instructions: "an EnvVarSpec range= is documentation that enforces nothing" and new numeric env reads must "Use the clamped readers in autobot_shared/env_utils.py".

Sources: Path instructions, Learnings

Comment on lines +481 to +486
def _select_files(root_dir: Path, tier: int | None = None, category: str | None = None) -> List[Tuple[str, int]]:
"""Discover files for ``tier`` (None = all), keeping only those in ``category`` when given (#18129)."""
files = _discover_files(root_dir, tier)
if category:
files = [(p, t) for p, t in files if detect_category(Path(p)) == category]
return files

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Match categories against the repository-relative path.

_discover_files returns absolute paths (root_dir / rel). detect_category then searches the full absolute path for directory patterns such as /developer/, /testing/, /api/ and /features/. Suppose the checkout sits under a directory with one of those names, for example ~/developer/AutoBot or /tmp/testing/repo. Every file then gets that category.

The result:

  • --category developer selects the whole tree.
  • --category api selects no files.

The documentation browser (doc_listing.group_indexed_documents) calls detect_category on the stored relative file_path. The CLI filter and the browser categories therefore disagree. Pass the path relative to root_dir.

🐛 Proposed fix
     files = _discover_files(root_dir, tier)
     if category:
-        files = [(p, t) for p, t in files if detect_category(Path(p)) == category]
+        files = [(p, t) for p, t in files if detect_category(Path(os.path.relpath(p, root_dir))) == category]
     return files

Comment on lines +928 to +929
# Ids hash the section name: drop the file's prior chunks so a renamed section leaves no orphan.
await asyncio.to_thread(self._collection.delete, where={"file_path": rel_path})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Delete the old chunks only after the new chunks are stored.

Line 929 deletes every row for rel_path before any new chunk is embedded. If embedding fails for every chunk, the file has no rows left in autobot_docs. A transient Ollama outage or a dimension error can cause this. Before this change, the old chunks stayed searchable after such a failure.

The hash cache prevents a retry:

  • index_all saves new_hashes for every selected file, including files that failed.
  • index_file updates the cache in _check_hash_and_update_cache before indexing.

The next incremental run therefore skips the file until its content changes. That documentation stays missing from search and from the documentation browser.

Do the cleanup after the upserts instead. One way is to give each run a token in the chunk metadata. After a run that stored at least one chunk, delete only the rows for rel_path that carry a different token. This still removes chunks left behind by a renamed section. A complete failure leaves the previous index in place.

🛡️ Proposed fix
--- "a/autobot-backend/services/knowledge/doc_indexer.py"
+++ "b/autobot-backend/services/knowledge/doc_indexer.py"
@@ -925,14 +925,21 @@
 
         file_tags = _extract_tags(body, file_str)
         file_tags = list(dict.fromkeys(fm_tags + fm_aliases + file_tags))[:20]
-        # Ids hash the section name: drop the file's prior chunks so a renamed section leaves no orphan.
-        await asyncio.to_thread(self._collection.delete, where={"file_path": rel_path})
+        run_token = uuid.uuid4().hex
         indexed = 0
         for i, chunk in enumerate(chunks):
-            ok = await asyncio.to_thread(self._index_chunk, chunk, i, len(chunks), rel_path, file_tags, tier)
+            ok = await asyncio.to_thread(
+                self._index_chunk, chunk, i, len(chunks), rel_path, file_tags, tier, run_token
+            )
             if ok:
                 indexed += 1
+        if indexed:
+            # Ids hash the section name: drop rows from earlier runs so a renamed section leaves no orphan.
+            await asyncio.to_thread(
+                self._collection.delete,
+                where={"$and": [{"file_path": rel_path}, {"index_run": {"$ne": run_token}}]},
+            )
         return indexed, len(chunks)
 
     async def index_file(self, file_path: Path, tier: int = 3, force: bool = False) -> IndexResult:
         """Index a single markdown file at the given priority tier; force skips the hash check."""

Add a run_token parameter to _index_chunk and write "index_run": run_token into its metadata. The split sub-chunks inherit that key through {**metadata, ...}. Update the _FakeCollection.delete test double so that it supports the $and/$ne filter.

Comment on lines +184 to +185
if args.verbose:
logging.getLogger().setLevel(logging.DEBUG)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make --verbose lower the level of the loggers that get_logger creates.

main sets the root logger to DEBUG. run_index then calls get_logger(__name__). LoggingManager.get_logger (autobot_shared/logging_manager.py:197-271) calls logger.setLevel(...) on each named logger, using the configured logging.log_level (default INFO). A logger's own level overrides the root level. As a result, debug records from this CLI and from services.knowledge.doc_indexer are still dropped, and --verbose has no effect.

Lower the level after the loggers exist, or route the setting through the configured log level.

🐛 Proposed fix
--- "a/tools/index_documentation.py"
+++ "b/tools/index_documentation.py"
@@ -180,10 +180,11 @@
 
 def main(argv: list[str] | None = None) -> int:
     """Entry point. `--incremental` is the default when no mode is given."""
     args = parse_args(argv)
     if args.verbose:
-        logging.getLogger().setLevel(logging.DEBUG)
+        os.environ["AUTOBOT_LOG_LEVEL"] = "DEBUG"  # read by LoggingManager when it configures each logger
+        logging.getLogger().setLevel(logging.DEBUG)
     return asyncio.run(run_index(force=args.force, rebuild=args.rebuild, args=args))
 
 
 if __name__ == "__main__":

Check that AUTOBOT_LOG_LEVEL maps to logging.log_level before the first get_logger call. If it does not, call setLevel(logging.DEBUG) on every logger in logging.root.manager.loggerDict after the service import.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

1 participant