feat(knowledge): multi-level document folders with folder-scoped Q&A, made mergeable - #44
Draft
lyingbug wants to merge 16 commits into
Draft
feat(knowledge): multi-level document folders with folder-scoped Q&A, made mergeable#44lyingbug wants to merge 16 commits into
lyingbug wants to merge 16 commits into
Conversation
Signed-off-by: hahaha <a310608318@gmail.com>
…lders # Conflicts: # frontend/src/api/chat/streame.ts # frontend/src/api/knowledge-base/index.ts # frontend/src/components/manual-knowledge-editor.vue # frontend/src/i18n/locales/ko-KR.ts # frontend/src/i18n/locales/ru-RU.ts # frontend/src/i18n/locales/zh-CN.ts # frontend/src/views/knowledge/KnowledgeBase.vue # frontend/src/views/knowledge/components/DocumentBatchBar.vue # frontend/src/views/knowledge/components/UploadConfirmDialog.vue # internal/agent/tools/grep_chunks.go # internal/agent/tools/scope_authorization.go # internal/application/service/datasource_service.go # internal/application/service/knowledge.go # internal/handler/session/qa.go # internal/router/router.go # internal/types/qa_request.go
… RAG wait row The wait row only appeared once a completed knowledge_search step existed, so attachment-only turns (attachment_parsing / image_analysis with no KB hit) still showed nothing between the last step and the first answer token. Model waiting is now a three-state kind: 'model' after retrieval finished, the neutral 'preparing' row otherwise. A dropped SSE connection never sets is_completed (the stream layer only raises a toast), so the row used to promise an answer forever. It now stops claiming progress after RAG_WAIT_STALL_DELAY_MS and drops the shimmer. Also share RAG_RETRIEVAL_TOOL_NAMES instead of repeating the knowledge_search / search_knowledge alias check, name the reveal delay, and move the reveal/stall timers into a controller with an injectable scheduler so the timing is covered by tests instead of by source-matching alone. Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
role="status" lived on the wait row itself, so the live region was inserted together with its own text and screen readers had nothing to announce. Move the announcement to an sr-only region that stays mounted for the whole turn, which also covers the pre-pipeline wait row that had no announcement at all. Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
The folder migration sat on version 000071, which main already uses for 000071_platform_api_keys. golang-migrate refuses to open a source directory that holds two files with the same version, so the duplicate made RunMigrationsWithOptions fail and took the server down at startup. Move the PostgreSQL migration to 000079 and the Lite one to 000002, which is the next free version in the separate sqlite sequence. Also add IF NOT EXISTS guards, plus a pg_constraint check for the foreign key that has no such form, so the migration replays cleanly after a dirty-state recovery like the surrounding migrations do. Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
Folder creation and moves accepted an unbounded hierarchy, so a client could build a chain thousands of levels deep with ordinary API calls. Both the recursive CTE behind folder-filtered document listing and the ancestor walk behind move validation grow with that depth, which makes the cost of every later request a function of how deep a tenant chose to nest, and the tree renders unusably in the sidebar. Reject folders past MaxFolderDepth (16). A move is checked against the height of the subtree being moved, not just the target, because the whole subtree travels with the folder. SubtreeHeight carries its own depth counter in the recursion and stops at the supplied bound, so rows that are already deeper than the cap or form a parent cycle surface as a bounded result instead of an unbounded scan. Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
…ad path ListByIDs only verifies that a requested folder scope still exists, and it runs once per folder-scoped question. Holding SELECT ... FOR UPDATE on those rows made concurrent questions about the same folder serialize against each other and blocked renames and moves behind them for the duration of the lookup. The structural writes that do need a row lock take it through GetByIDForUpdate, which is unchanged. Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
…olve to A folder scope is enforced by handing the retrieval engines an explicit document-ID filter built from every document in the selected subtree. That list had no ceiling, so asking a question against a top-level folder of a knowledge base holding the hundred thousand documents this feature targets would build a filter far past what the backends accept: Elasticsearch and OpenSearch cap terms clauses at 65536 entries and PostgreSQL caps bind parameters at 65535. Depending on the backend that either errors out or truncates the filter, and a truncated filter answers from documents the user did not select. Stop the scan one row past MaxFolderScopeKnowledgeIDs and reject the request with a 400 that tells the user to pick a more specific folder or ask across the whole knowledge base, which needs no ID filter at all. Failing loudly keeps the scope honest; pushing folder_id down into the chunk indexes would lift the limit and is the better long-term fix. Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
CI checks gofmt on every Go file a pull request touches outside cli/, and this file's field alignment was left stale by the folder_id addition. Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
The sqlite migration round-trip pinned version 71, which was the number the folder migration used before it moved into the sqlite sequence at 02. Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
UploadConfirmDialog.test.ts asserts on the source text of KnowledgeBase.vue, and executeUploadBatch and executeUrlImport now take the upload target (knowledge base plus folder) as an argument. The assertions kept the pre-folder two-argument shape, so the suite failed even though the behaviour it guards — threading confirmed tags through instead of reading the list filter — is intact. Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
…column limit Keeps golangci-lint run --new-from-rev=origin/main clean for the diff. Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
This is Tencent#2234 by @Alorun (multi-level folders for document knowledge bases plus folder-scoped Q&A), merged with current
mainand with the blockers that kept it out of the tree fixed. Credit for the feature itself belongs to that PR; the commits after the merge commit are the additions.Why Tencent#2234 as the base
Of the 15 community PRs targeting Tencent#1311 I compared, Tencent#2234 is the one whose architecture holds up: the folder subtree filter for document listing is pushed into SQL as a recursive CTE instead of materializing an ID list, tenant isolation is enforced by composite foreign keys in PostgreSQL (with equivalent triggers on the Lite/SQLite path) rather than only in the application layer,
FolderScopehas a presence-aware JSON contract so a malformed restricted scope fails closed instead of silently widening to the whole knowledge base, and it is the only one that patches the shared agent-tool authorization helpers (searchTargetScope,searchTargetIsWholeKB) so a folder-scoped target cannot be treated as whole-KB access. It also has the most complete frontend (drag-and-drop moves, breadcrumb, resizable sidebar, folder mention scope) and all four locales translated with a key-completeness guard.What this branch adds on top
The server could not start. The folder migration sat on version
000071, whichmainalready uses for000071_platform_api_keys. golang-migrate refuses to open a source directory holding two files with the same version, soRunMigrationsWithOptionsfailed on boot. This was already true on the PR's own branch, not just after a rebase. Moved to000079for PostgreSQL and000002for the separate sqlite sequence (it had jumped to000071there too, which would have made every future Lite migration between000002and000070unreachable).The migration is now replayable. Added
IF NOT EXISTSguards, plus apg_constraintlookup for the foreign key that has no such form, matching how the surrounding migrations support dirty-state recovery. Replaying the original against an already-migrated database fails withrelation "folders" already exists; this version skips every statement and exits 0.Folder depth was unbounded. Any client could build a chain thousands of levels deep with ordinary API calls, and both the recursive CTE behind folder-filtered listing and the ancestor walk behind move validation grow with that depth, so one tenant's nesting choice became every later request's cost. Folders past
MaxFolderDepth(16) are now rejected. A move is validated against the height of the subtree being moved, not just the target, because the subtree travels with the folder.SubtreeHeightcarries its own depth counter in the recursion and stops at the supplied bound, so rows that are already deeper than the cap — or that form a parent cycle — produce a bounded result instead of an unbounded scan.Every folder-scoped question took write locks.
ListByIDsonly checks that a requested scope still exists, but it tookSELECT ... FOR UPDATEon those rows, so concurrent questions about the same folder serialized against each other and blocked renames and moves behind them. It is now lock-free; the structural writes that need a lock still go throughGetByIDForUpdate.A folder scope could resolve to an unbounded document filter. Folder scoping works by handing the retrieval engines an explicit document-ID filter built from the whole subtree, with no ceiling. At the hundred-thousand-document scale this issue describes, asking a question against a top-level folder builds a filter far past what the backends accept — Elasticsearch and OpenSearch cap
termsclauses at 65536 entries, PostgreSQL caps bind parameters at 65535 — which either errors out or, worse, truncates the filter and answers from documents the user did not select. The scan now stops one row pastMaxFolderScopeKnowledgeIDsand the request is rejected with a 400 pointing the user at a more specific folder or at asking across the whole knowledge base, which needs no ID filter at all. Failing loudly keeps the scope honest. Pushingfolder_iddown into the chunk indexes is the better long-term fix and would lift the limit entirely; that belongs in its own change since it touches all ten retriever implementations.Two test suites were red.
folder_migration_test.gopinned the old Lite migration version, andUploadConfirmDialog.test.tsasserts on the source text ofKnowledgeBase.vue, whoseexecuteUploadBatchandexecuteUrlImportnow take the upload target as an argument. Both assertions were updated; the behaviour they guard is unchanged.Known follow-ups, not addressed here
folders, while comparable sub-resources use aknowledge_prefix (knowledge_tags,knowledge_tag_relations). Renaming is cheap now and expensive later.WikiBrowser.vue.all=truereturns the whole tree for a knowledge base.Type of Change
Related Issue
Fixes Tencent#1311
Testing
All commands run on this branch. PostgreSQL 16 was installed locally for the migration checks;
app.skip_embedding=trueskips the one migration that needs pgvector.Startup blocker — golang-migrate source directory, opened with the same driver the server uses:
Full versioned chain up, folder migration down, then replayed, on real PostgreSQL 16:
Idempotency, replaying the up migration against an already-migrated database:
New tests, plus the existing folder and search-target tests:
CI-equivalent suite:
Checklist
git diff --check origin/main...HEADpassesgolangci-lint run --new-from-revreports 0 issues for this branch's diff; the pre-existinglll/revivefindings inherited from Feat/multi level folders Tencent/WeKnora#2234 are listed as follow-ups)Screenshots / Recordings
No UI change on top of Tencent#2234. The additions are the migration versioning, the depth cap, the read-path lock removal, and the folder-scope size bound, all of which are covered by the migration runs and tests above.