Skip to content

feat(chat): join-table attachments, PersistedFilesInfo, docker HF cache#326

Open
abdulrafey1 wants to merge 20 commits into
mainfrom
rafey/rag-intent-router-frontend
Open

feat(chat): join-table attachments, PersistedFilesInfo, docker HF cache#326
abdulrafey1 wants to merge 20 commits into
mainfrom
rafey/rag-intent-router-frontend

Conversation

@abdulrafey1
Copy link
Copy Markdown
Contributor

What

Completes the drive-file attachment model migration by removing the
denormalised active_drive_file_id / active_drive_file_ids columns,
replacing them with the authoritative ConversationAttachment join
table. Adds frontend components and hooks to make attach/detach
immediate, restores persisted attachments on conversation load, and
fixes several UX issues in the Resources tab. Also fixes a Docker
startup hang caused by the HuggingFace XET download protocol.

Changes

  • chore(docker): add named hf_cache volume and set HF_HUB_DISABLE_XET=1 on api, rag-cleanup, and rag-mcp services
  • feat(chat): drop active_drive_file_id / active_drive_file_ids columns; new GET /conversations/{id}/attachments endpoint; drive_file_ids field on ChatCompletionRequest; scope classifier receives file names; section SSE emitted before LLM stream; migration 87e049122b83
  • feat(frontend): PersistedFilesInfo component (read-only list of persisted files); immediate POST/DELETE /attachments on file select/deselect; remove drive_file inline content blocks from buildUserMessages; load persisted attachments from new endpoint on conversation load
  • fix(frontend): Queued chip colour changed from amber to grey; Failed tooltip always rendered with fallback message; RAG status polling every 5 s for folders with in-progress files, stops on terminal state

How to Test

  1. make clean && make up — confirm the api container starts without hanging on embedding model download; confirm the hf_cache volume is created.
  2. Open a new conversation, attach a Google Drive file via the file picker — confirm POST /conversations/{id}/attachments fires immediately.
  3. Deselect the file — confirm DELETE /conversations/{id}/attachments/{file_id} fires immediately.
  4. Send a message that references the attached document — confirm the completion request includes drive_file_ids and the response uses retrieved sections.
  5. Reload the conversation — confirm the attached file list is restored from GET /conversations/{id}/attachments.
  6. Navigate to the Resources tab with a folder that has files in Queued state — confirm the chip is grey, not amber.
  7. Leave a file in Failed state — confirm the tooltip renders even with no ragError message.
  8. Index a file that takes time — confirm the status chip updates to Ready without a page refresh (polling).
  9. Run make test — all tests pass including tests/chat/test_conversation_attachments.py and tests/chat/test_persistent_attachment.py.

Notes

Migration required: 87e049122b83_remove_active_drive_file_columns_from_.py drops active_drive_file_id and active_drive_file_ids from the conversation table. Run make migrations after deploying.

Breaking change: GET /conversations/{id} no longer returns active_drive_file_id or active_drive_file_ids fields. Any client reading those fields must switch to GET /conversations/{id}/attachments. The old DELETE /conversations/{id}/active-file endpoint is removed; use DELETE /conversations/{id}/attachments/{file_id}.

Docker: The hf_cache named volume is new — existing docker-compose up will create it automatically. No manual steps required.

Abdul Rafey and others added 16 commits May 13, 2026 16:08
…nagement

Introduces an LLM-based intent router that inspects the user's message and
attached-file metadata to decide whether RAG retrieval should run for the
current turn. Previously RAG fired only when drive_file blocks were inlined
in the request; now it fires based on persisted per-conversation attachments
and a routing decision, reducing unnecessary MCP roundtrips on chit-chat.

- New chat_conversation_attachments join table (Alembic migration with
  back-fill from active_drive_file_id/s columns)
- POST/DELETE /conversations/{uuid}/attachments endpoints
- RAGIntentRouter: fail-closed on LangChainException/ValidationError → 502
- SSE status events: scanning_attachments / skipping_rag
- 28 new tests (unit + attachment + integration), all passing
- Use dialect-aware SQL in back-fill migration (PostgreSQL vs SQLite)
- Replace bare except with specific SQLAlchemyError + warning log
- Fix deprecated datetime.utcnow → datetime.now(timezone.utc)
- Chain HTTPException from RAGIntentRouterError to preserve traceback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Accessing _create_llm() from outside the class broke encapsulation.
Add a stable public delegate so callers in routes.py are insulated
from internal provider implementation changes.
…ate_llm

- Use asyncio.gather in RAGIntentRouter.decide() so document structure
  calls for multiple attached files run concurrently instead of serially
- Add BaseChatProvider.create_llm() public delegate so callers are
  insulated from the private _create_llm() implementation detail

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A single FK violation (drive_file_id referencing a deleted file) aborted
the whole PostgreSQL transaction, causing all subsequent inserts and the
Alembic version update to fail. Wrapping each per-file insert in a nested
savepoint means only that row is rolled back on error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Matches the model's datetime.now(timezone.utc) default and avoids
silent timezone truncation on PostgreSQL (TIMESTAMPTZ vs TIMESTAMP).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The detach endpoint only checked conversation ownership, not file
ownership. A caller could detach a file they don't own if it was
somehow attached to their conversation. Added the same ownership
check as the attach endpoint.

Also fix f-string logger in service.py -> % formatting.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Router-driven RAG built synthetic messages with only drive_file blocks,
causing three failures: empty similarity-search query, user question
dropped from the non-streaming LLM prompt, and RAG context silently
discarded in the streaming assembly pass.

Fix by appending the query text block to synthetic messages and building
the streaming current from unresolved_messages so drive_file blocks are
visible to the in-stream replacement pass.
… to join table

SQLModel inferred sa.DateTime() (no timezone) for attached_at despite the
default producing timezone-aware datetimes — causing asyncpg to reject the
insert. Added explicit sa_column=Column(DateTime(timezone=True)) to match
the DB column type.

Also sync inline drive_file blocks from existing conversations into the
chat_conversation_attachments join table so the intent router can find them.
Frontend still sends drive_file blocks inline; without this sync the router
saw no attachments and skipped RAG on every turn.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Routes now call provider.create_llm() (public method). Mock setup for
_create_llm had no effect — replaced with create_llm.return_value.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mounts a named Docker volume at /home/nonroot/.cache/huggingface so
HuggingFace model weights survive container restarts without
re-downloading. Creates the directory with correct nonroot ownership
in Dockerfile.local.

Sets HF_HUB_DISABLE_XET=1 on the api, rag-cleanup, and rag-mcp
services. The XET chunked download protocol hangs indefinitely on
unauthenticated requests; the standard HTTP path is reliable and
unblocks embedding model initialisation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ent api

Drops the denormalised active_drive_file_id and active_drive_file_ids
columns from the Conversation model and removes all code that read or
wrote them. Attachment state is now authoritative in the
ConversationAttachment join table only.

New GET /conversations/{id}/attachments endpoint returns READY drive
files attached to a conversation, replacing the old active-file fields
baked into the conversation response. Replaces the deleted
DELETE /conversations/{id}/active-file endpoint.

Adds drive_file_ids to ChatCompletionRequest so the completion handler
can attach files selected before a conversation exists in the DB
(new-conversation flow). Removes the brittle inline drive_file
content-block scanning that extracted file IDs from message bodies.

The scope classifier now receives attached file names so it correctly
classifies document-related queries as in-scope. Section SSE events
are emitted before the LLM stream so the frontend renders retrieved
sections above the response bubble.

Migration 87e049122b83 drops the two columns.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eFileIds in send payload

Replaces the old Pill component (which displayed attached files as
removable pills) with PersistedFilesInfo, which shows a read-only
list of files that are already persisted to the conversation in the
backend. This reflects that drive file attachments are now managed
via the join-table API rather than inline content blocks.

Drive file selections now fire an immediate POST to
/conversations/{id}/attachments and removals fire an immediate DELETE,
so the backend is always in sync with what the user sees. For new
conversations (no id yet) the attach is deferred and sent in
onConversationCreated.

Removes the drive_file content block type from buildUserMessages in
useChatStream — file IDs are now passed explicitly as driveFileIds in
the send payload and forwarded to the backend as drive_file_ids on the
completion request, eliminating the need for the backend to parse file
references out of message content.

useConversation now loads persisted attachments from the new
GET /conversations/{id}/attachments endpoint on conversation load,
restoring the correct attachment list including file size.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…s polling

Changes the Queued status chip from warning (amber) to muted (grey) to
avoid the impression that something is wrong when files are waiting to
be indexed.

Makes the Failed tooltip always render even when ragError is null,
showing a generic fallback message so users know why a file has failed
indexing and what to do about it. Previously the Tooltip was only
rendered when ragError was non-null, leaving a bare unclickable chip.

Adds a polling loop that refreshes RAG statuses every 5 seconds for
any folder that has files in queued or processing state. Uses a single
getFolderRagStatus call per active folder and dispatches
UPDATE_RAG_STATUSES to patch the relevant resource rows in place.
Polling stops automatically once all files reach a terminal state
(ready or failed).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude Bot commented May 14, 2026

Claude finished @abdulrafey1's task in 5m 7s —— View job


PR Review: feat(chat): join-table attachments, PersistedFilesInfo, docker HF cache

  • Check for previously addressed review comments
  • Review security issues
  • Review backend changes (models, routes, service, schemas, migrations)
  • Review frontend changes (components, hooks, UI)
  • Review test coverage
  • Review Docker/infrastructure changes
  • Post inline comments on specific issues
  • Post final summary

Overview

This PR replaces the denormalised active_drive_file_id/active_drive_file_ids columns with a proper ConversationAttachment join table, adds a RAG intent router, and builds the frontend plumbing for immediate attach/detach and persisted file display. The Docker HF cache fix is a good operational improvement. Overall, this is a well-structured PR with solid test coverage and clean separation of concerns.

Previous Review Comments

No previous review comments to resolve.

Critical / Blocking

None.

High Priority

# File Issue
1 frontend/plugins/chat/hooks/useChatInput.ts:76-87 Deselected files not detached from backend. When a user deselects a file via the DriveFilePicker, it's removed from UI state but NOT deleted from the backend join table. The RAG system will continue using the deselected file for context. See inline comment for suggested fix.

Medium Priority

# File Issue
2 app/core_plugins/chat/schemas.py:94-97 No upper bound on drive_file_ids. A request with thousands of IDs triggers sequential DB queries. Add max_length=20 (or similar). See inline comment.
3 frontend/plugins/chat/hooks/useChatInput.ts:82-86 Fire-and-forget attachment persistence. The fetch calls for attach/detach only console.warn on failure. If the API call fails, the user sees the file as attached/detached in the UI but the backend disagrees. Consider showing a toast or reverting the UI state on failure.
4 frontend/plugins/chat/components/ChatInterfaceInner.tsx:45-51 Same fire-and-forget pattern for syncing drive files on new conversation creation.

Low Priority / Nits

# File Issue
5 app/core_plugins/chat/routes.py:603 Pre-existing: f-string in logger.error(f"Provider API error: {e}") — should use lazy formatting logger.error("Provider API error: %s", e).
6 app/core_plugins/chat/routes.py:605-607 Pre-existing: raise HTTPException(...) without from e in the _PROVIDER_API_ERRORS handler — exception chain is lost.
7 app/core_plugins/chat/intent_router.py:25 Nit: pass on RAGIntentRouterError body is unnecessary — can use ... or nothing.

What's Good

  • Clean migration strategy: Two-phase migration (create join table with back-fill, then drop old columns) is the right approach. Savepoints in the back-fill handle FK violations gracefully.
  • Idempotent attach: ChatService.attach_drive_file handles the check-then-insert race with proper IntegrityError recovery.
  • Thorough authorization: All attachment endpoints verify both conversation ownership and drive file ownership.
  • Test coverage: 450+ lines of new tests covering model, service, endpoints, and integration with the intent router. The integration tests verify SSE event emission for both retrieve/skip paths.
  • RAG intent router: Clean abstraction with structured output and proper error typing (RAGIntentRouterError). Fail-closed (502) on router failure is the right call.
  • Frontend: PersistedFilesInfo component is well-tested (139 lines of tests). RAG status polling with cleanup on terminal state is a good UX improvement.
  • Docker: hf_cache volume shared across services prevents redundant downloads; HF_HUB_DISABLE_XET=1 fixes the startup hang.

Summary

Solid PR overall. The one actionable bug is #1 (deselected files not detached from backend) — this causes a real data inconsistency where RAG uses files the user thought they removed. #2 is a hardening suggestion. The rest are pre-existing issues or minor nits.

@abdulrafey1 abdulrafey1 self-assigned this May 14, 2026
Comment thread frontend/plugins/chat/hooks/useChatInput.ts
Comment thread app/core_plugins/chat/schemas.py
- Revert UI state and surface uploadError when drive file attach/detach
  API calls fail, replacing silent fire-and-forget console.warn
- Issue DELETE calls for files deselected in DriveFilePicker
- Surface new-conversation sync failures via the Alert banner
- Add max_length=20 guard on drive_file_ids request field
- Fix f-string logger and missing `from e` in provider error handler
- Remove redundant pass from RAGIntentRouterError

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude Bot commented May 15, 2026

Claude finished @abdulrafey1's task in 5m 38s —— View job


PR Review: feat(chat): join-table attachments, PersistedFilesInfo, docker HF cache

  • Check for previously addressed review comments
  • Review new/changed backend code
  • Review new/changed frontend code
  • Review test coverage
  • Review Docker/infrastructure changes
  • Post inline comments on specific issues
  • Post final summary

Previous Review Comments

Both comments from the previous review have been resolved in commit 9292da6:

# Issue Status
1 Deselected files not detached from backend (useChatInput.ts) ✅ Fixed — DELETE calls now issued for removed files (lines 93-106), with UI revert on failure
2 No upper bound on drive_file_ids (schemas.py) ✅ Fixed — max_length=20 added (line 96)

Additional nits from the first review also addressed in 9292da6:

  • ✅ f-string logger → lazy formatting (routes.py:603)
  • ✅ Missing from e on provider error handler (routes.py:607)
  • ✅ Fire-and-forget console.warn → UI revert + setUploadError on attach/detach failure
  • ✅ Fire-and-forget on new-conversation sync → setError alert banner
  • ✅ Redundant pass removed from RAGIntentRouterError

Overview

This PR replaces the denormalised active_drive_file_id/active_drive_file_ids columns with a proper ConversationAttachment join table, adds a RAG intent router (LLM-based per-turn retrieval decision), builds frontend plumbing for immediate attach/detach with persisted file display, and fixes Docker startup hangs via HuggingFace XET protocol disabling.

High Priority

# File Issue
1 frontend/plugins/chat/hooks/useChatInput.ts:82-91 fetch() does not reject on HTTP errors. All .catch() handlers on attach/detach only fire on network errors. HTTP 4xx/5xx responses resolve normally — the UI shows success while the backend rejected the operation. Same issue in ChatInterfaceInner.tsx:46-56 and useChatInput.ts:98-106, 115-122. See inline comments (useChatInput, ChatInterfaceInner). Fix this →
2 frontend/plugins/chat/hooks/useChatInput.ts:48 Stale closure in handleUploadAsText. setAttachments([...attachments, ...]) reads attachments from the closure. If a user uploads two files rapidly, the second upload's closure captures stale state, dropping the first. Should use setAttachments((prev) => [...prev, ...]). Fix this →
3 frontend/plugins/chat/hooks/useChatInput.ts:124-126 handleRemoveAttachment clears ALL attachments without backend detach. When called without a driveFileDbId (removing a text attachment), setAttachments([]) wipes drive file attachments from UI without issuing DELETE calls. Backend still considers them attached.

Medium Priority

# File Issue
4 app/core_plugins/chat/routes.py:608-613 Missing from e on the last exception handler in chat_completion. The provider handler above was fixed, but this block still drops the exception chain. Fix this →
5 app/core_plugins/chat/routes.py:340-354 Sequential DB queries for drive_file_ids. Each file ID triggers a separate SELECT + INSERT (up to 40 queries for 20 files). Batch the ownership check into WHERE id IN (...). See inline comment.
6 app/core_plugins/chat/routes.py:288 Typo: Double period: "...in your chat plugin settings.."
7 app/core_plugins/chat/intent_router.py:37-43 Unused session parameter in RAGIntentRouter.decide()session is accepted but never used. get_document_structure creates its own session internally, which also means it operates outside the caller's transaction boundary.
8 frontend/plugins/google-drive/GoogleDrive.tsx:712-716 RAG status polling silently swallows errors with bare catch {}. Per project rules, should at least console.warn. No backoff strategy — hammers server every 5s indefinitely on failure.

Low Priority / Nits

# File Issue
9 app/core_plugins/chat/routes.py:116-118 _strip_md function is defined but never called — dead code.
10 app/core_plugins/chat/schemas.py:45 Pre-existing: bare except Exception — should be except binascii.Error.
11 app/core_plugins/chat/models.py:86-114 ConversationAttachment inherits TimestampedModel (giving created_at/updated_at) and also defines attached_atcreated_at and attached_at are semantically identical.
12 frontend/plugins/chat/components/messages/ChatMessages.tsx:29 useEffect depends on messages.length only — doesn't trigger auto-scroll during streaming token updates.
13 tests/chat/test_conversation_attachments.py:259 Assertion status_code in (200, 201) is overly permissive — the endpoint always returns 201. Same for line 308 (403 or 404 — always returns 404).

What's Good

  • Clean two-phase migration: Create join table with savepoint-protected back-fill, then drop old columns. Dialect-aware SQL (SQLite vs PostgreSQL). Reversible downgrade path.
  • Idempotent attach: ChatService.attach_drive_file handles check-then-insert race with IntegrityError recovery — correct upsert pattern.
  • Thorough authorization: All attachment endpoints verify both conversation ownership and drive file ownership.
  • RAG intent router: Clean abstraction with structured output, proper error typing (RAGIntentRouterError), and fail-closed (502) on failure. File metadata fetched in parallel via asyncio.gather(return_exceptions=True).
  • Test coverage: 1100+ lines of new tests covering model, service, endpoints, SSE emission, and router decision paths.
  • Scope classifier enhancement: attached_file_names parameter ensures document-related queries aren't flagged out-of-scope.
  • Docker: hf_cache volume shared across services prevents redundant downloads; HF_HUB_DISABLE_XET=1 fixes the startup hang.

Summary

The previous review's two items are fully resolved. The most impactful new finding is #1 — all fetch calls for attach/detach silently ignore HTTP error responses because fetch() doesn't reject on 4xx/5xx. This means the error handling added in 9292da6 (UI revert + error messages) only triggers on network failures, not server-side rejections. #2 (stale closure) and #3 (clear-all wiping drive files) are additional bugs in the same hook. Backend-side, #4 and #5 are straightforward fixes.

Comment thread app/core_plugins/chat/routes.py Outdated
Comment thread frontend/plugins/chat/hooks/useChatInput.ts
Comment thread frontend/plugins/chat/components/ChatInterfaceInner.tsx
10 steps was too low — a normal agentic RAG query (get_document_structure
+ search_section_by_keyword + final-answer tool) already costs 6+ steps.
Legitimate queries on large documents were hitting GraphRecursionError.
25 matches LangGraph's own default and still terminates true infinite loops.
@claude
Copy link
Copy Markdown

claude Bot commented May 15, 2026

Claude finished @abdulrafey1's task in 2m 56s —— View job


PR Review: feat(chat): join-table attachments, PersistedFilesInfo, docker HF cache

  • Check for previously addressed review comments
  • Review backend changes (models, routes, service, schemas, intent router)
  • Review frontend changes (components, hooks, UI)
  • Review test coverage
  • Review Docker/infrastructure changes
  • Post inline comments on specific issues
  • Post final summary

Previous Review Comments

Review 1 (2 items): Both resolved in 9292da6.

# Issue Status
1 Deselected files not detached from backend ✅ Fixed
2 No upper bound on drive_file_ids ✅ Fixed (max_length=20)

Review 2 (8 items): 3 unresolved threads remain open. The latest commit (1860d43) only touches app/rag/agent.py and does not address the outstanding frontend or backend issues.

# Issue Status
1 fetch() doesn't reject on HTTP errors (useChatInput + ChatInterfaceInner) ❌ Still open
2 Stale closure in handleUploadAsText ❌ Still open
3 handleRemoveAttachment clears ALL attachments without backend detach ❌ Still open
4 Missing from e on last exception handler ❌ Still open
5 Sequential DB queries for drive_file_ids ❌ Still open
6 Typo: double period ❌ Still open
7 Unused session parameter in RAGIntentRouter.decide() ❌ Still open
8 RAG status polling silently swallows errors ❌ Still open

Overview

This PR replaces denormalised active_drive_file_id/active_drive_file_ids columns with a ConversationAttachment join table, adds a RAG intent router, builds frontend plumbing for immediate attach/detach with persisted file display, and fixes Docker startup hangs. Overall solid architecture with thorough test coverage (1100+ lines of new tests).

High Priority

# File Issue
1 useChatInput.ts:82-91 fetch() does not reject on HTTP errors. All .catch() handlers only fire on network errors. HTTP 4xx/5xx resolve normally — the error handling added in 9292da6 (UI revert + error messages) never triggers on server-side rejections. Same issue in ChatInterfaceInner.tsx:46-56 and useChatInput.ts:98-106, 115-122. See inline comment.
2 useChatInput.ts:48 Stale closure in handleUploadAsText. setAttachments([...attachments, ...]) reads attachments from closure. Rapid uploads cause the second to overwrite the first. Use setAttachments((prev) => [...prev, ...]). (Line not in diff — see Review 2 thread.)
3 useChatInput.ts:124-126 handleRemoveAttachment clears ALL attachments without backend detach. When called without a driveFileDbId, setAttachments([]) wipes drive file attachments from UI without issuing DELETE calls. Backend still considers them attached. See inline comment.

Medium Priority

# File Issue
4 routes.py:608-613 Missing from e on the last exception handler in chat_completion. The provider handler above was fixed, but this block still drops the exception chain.
5 routes.py:340-354 Sequential DB queries for drive_file_ids. Each file ID triggers a separate SELECT + INSERT. Batch the ownership check into WHERE id IN (...). See inline comment.
6 routes.py:288 Typo: Double period: "...settings.."
7 intent_router.py:37-44 Unused session parameter in RAGIntentRouter.decide(). get_document_structure creates its own session, operating outside the caller's transaction. See inline comment.
8 schemas.py:45 Pre-existing: Bare except Exception — should be except binascii.Error per project rules.

Low Priority / Nits

# File Issue
9 routes.py:116-118 _strip_md is defined but never called — dead code.
10 models.py:86-114 ConversationAttachment inherits TimestampedModel (giving created_at/updated_at) and also defines attached_atcreated_at and attached_at are semantically identical.
11 GoogleDrive.tsx:712-716 RAG status polling has a bare catch {} — should at least console.warn. No backoff strategy. See inline comment.
12 test_conversation_attachments.py:285 assert response2.status_code in (200, 201) is overly permissive — the endpoint always returns 201 for idempotent attach. Similarly line 308 (403 or 404 — always 404).

What's Good

  • Clean two-phase migration: Create join table with savepoint-protected back-fill, then drop old columns. Dialect-aware SQL (SQLite vs PostgreSQL). Reversible downgrade path.
  • Idempotent attach: ChatService.attach_drive_file handles the check-then-insert race with IntegrityError recovery — correct upsert pattern.
  • Thorough authorization: All attachment endpoints verify both conversation ownership and drive file ownership.
  • RAG intent router: Clean abstraction with structured output, proper error typing, and fail-closed (502) on failure. File metadata fetched in parallel via asyncio.gather(return_exceptions=True).
  • Test coverage: 1100+ lines of new tests covering model, service, endpoints, SSE emission, and router decision paths.
  • Docker: hf_cache volume shared across services; HF_HUB_DISABLE_XET=1 fixes the startup hang.
  • Scope classifier enhancement: attached_file_names parameter ensures document-related queries aren't incorrectly flagged out-of-scope.

Summary

The two items from the first review are resolved. The 8 items from the second review remain unaddressed. The most impactful are #1 (fetch doesn't reject on HTTP errors — all error recovery for attach/detach is effectively dead code) and #3 (clearing text attachments silently wipes drive files from UI without backend sync). These should be fixed before merge to avoid data inconsistency between UI and backend state.

Comment thread frontend/plugins/chat/hooks/useChatInput.ts
Comment thread frontend/plugins/chat/hooks/useChatInput.ts
Comment thread app/core_plugins/chat/routes.py Outdated
Comment thread app/core_plugins/chat/intent_router.py
Comment thread frontend/plugins/google-drive/GoogleDrive.tsx Outdated
- Batch drive_file_ids ownership check into single IN query
- Fix stale closure and add res.ok guards on all fire-and-forget fetches
- Issue DELETEs for drive files when clearing all attachments
- Drop unused session param from RAGIntentRouter.decide()
- Drop redundant created_at/updated_at from ConversationAttachment
- Add exponential backoff to RAG status poller
- Remove dead _strip_md, fix typo, tighten test assertions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude Bot commented May 15, 2026

Claude finished @abdulrafey1's task in 2m 47s —— View job


PR Review: feat(chat): join-table attachments, PersistedFilesInfo, docker HF cache

  • Check for previously addressed review comments
  • Review new/changed backend code
  • Review new/changed frontend code
  • Review test coverage
  • Review Docker/infrastructure changes
  • Post inline comments on specific issues
  • Post final summary

Previous Review Comments

Review 1 (2 items): Both resolved in 9292da6.

# Issue Status
1 Deselected files not detached from backend ✅ Resolved
2 No upper bound on drive_file_ids ✅ Resolved (max_length=20)

Review 2 (8 items): All resolved in 2a4660e.

# Issue Status
1 fetch() doesn't reject on HTTP errors ✅ Resolved — .then(res => { if (!res.ok) throw ... }) added to all fetch calls
2 Stale closure in handleUploadAsText ✅ Resolved — functional updater setAttachments((prev) => ...) used
3 handleRemoveAttachment clears ALL without backend detach ✅ Resolved — DELETE calls issued for each drive file before clearing
4 Missing from e on last exception handler ✅ Resolved
5 Sequential DB queries for drive_file_ids ✅ Resolved — batched IN(...) query
6 Typo: double period ✅ Resolved
7 Unused session parameter in RAGIntentRouter.decide() ✅ Resolved — parameter removed
8 RAG status polling silently swallows errors ✅ Resolved — console.warn + exponential backoff added

Review 3 (8 items): All were duplicates of Review 2 — all resolved.

All 10 previously open review threads have been resolved.


Overview

This PR replaces denormalised active_drive_file_id/active_drive_file_ids columns with a proper ConversationAttachment join table, adds a RAG intent router (LLM-based per-turn retrieval decision), builds frontend plumbing for immediate attach/detach with persisted file display, and fixes Docker startup hangs via HuggingFace XET protocol disabling.

Critical / Blocking

None.

Medium Priority

# File Issue
1 schemas.py:47 Pre-existing: Bare except Exception should be except binascii.Error. See inline comment.

Low Priority / Nits

# File Issue
2 useChatInput.ts:74 handleDriveFileSelected reads attachments from closure instead of using functional updater form — inconsistent with the rest of the file. Narrow race window but worth aligning. See inline comment.
3 routes.py:335-349 Silently skipping unowned file IDs in the batch query is correct for security, but a logger.warning would help diagnose issues. See inline comment.
4 ChatMessages.tsx:29 Pre-existing: useEffect depends on messages.length only — no auto-scroll during streaming token updates.

What's Good

  • Clean two-phase migration: Create join table with savepoint-protected back-fill (0dff7c4153d407b4), drop old columns (87e049122b83), then drop redundant timestamps (c49841c8bfbb). Dialect-aware SQL (SQLite vs PostgreSQL). Reversible downgrade path.
  • Idempotent attach: ChatService.attach_drive_file handles the check-then-insert race with IntegrityError recovery — correct upsert pattern.
  • Thorough authorization: All attachment endpoints verify both conversation ownership and drive file ownership. Batched ownership check in chat_completion is efficient.
  • RAG intent router: Clean abstraction with structured output, proper error typing (RAGIntentRouterError), and fail-closed (502) on failure. File metadata fetched in parallel via asyncio.gather(return_exceptions=True).
  • Test coverage: 1100+ lines of new tests across 3 test files — model, service, endpoints, SSE emission, and router decision paths. Integration tests verify the full router wiring including 502 on failure.
  • Frontend error handling: All attach/detach fetch calls now correctly check res.ok, revert UI state on failure, and show error messages. The handleRemoveAttachment properly issues DELETE calls for drive files before clearing.
  • Docker: hf_cache named volume shared across api, rag-cleanup, and rag-mcp prevents redundant model downloads. HF_HUB_DISABLE_XET=1 fixes the startup hang. Non-root user with proper cache directory permissions.
  • Scope classifier enhancement: attached_file_names parameter ensures document-related queries aren't incorrectly flagged out-of-scope.
  • PersistedFilesInfo component: Well-designed popover with per-file detach buttons. 139 lines of dedicated tests.

Summary

All 10 previously open review threads from Reviews 1–3 are now resolved. The latest commit (2a4660e) comprehensively addresses every issue raised. The remaining findings are minor — one pre-existing except Exception that should be narrowed, and two low-priority consistency suggestions. This PR is in good shape for merge.

Comment thread app/core_plugins/chat/schemas.py
Comment thread frontend/plugins/chat/hooks/useChatInput.ts Outdated
Comment thread app/core_plugins/chat/routes.py
… file ids

Use functional updater in handleDriveFileSelected to avoid stale closure
over attachments during concurrent state updates. Add lastContent to the
auto-scroll useEffect dependency array so the view scrolls to the bottom
on every streaming token, not only when a new message is appended. Log a
warning in the batched ownership check when drive file IDs are skipped due
to missing ownership or soft-deletion, improving debuggability.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude Bot commented May 15, 2026

Claude finished @abdulrafey1's task in 3m 4s —— View job


PR Review: feat(chat): join-table attachments, PersistedFilesInfo, docker HF cache

  • Check for previously addressed review comments
  • Review backend changes (models, routes, service, schemas, intent router)
  • Review frontend changes (components, hooks, UI)
  • Review test coverage
  • Review Docker/infrastructure changes
  • Post inline comments on specific issues
  • Post final summary

Previous Review Comments

All 13 review threads from Reviews 1–4 are now resolved.

Review Items Status
Review 1 2 items (deselect bug, max_length) ✅ All resolved in 9292da6
Review 2 8 items (fetch error handling, stale closure, clear-all, from e, batched queries, typo, unused param, polling errors) ✅ All resolved in 2a4660e
Review 3 Duplicates of Review 2 ✅ All resolved
Review 4 3 items (binascii.Error, closure nit, skipped-IDs logging) ✅ All resolved in fded5b4

Overview

This PR replaces the denormalised active_drive_file_id/active_drive_file_ids columns with a proper ConversationAttachment join table, adds a RAG intent router (LLM-based per-turn retrieval decision), builds frontend plumbing for immediate attach/detach with persisted file display, and fixes Docker startup hangs via HuggingFace XET protocol disabling.

Critical / Blocking

None.

High Priority

None.

Low Priority / Nits

# File Issue
1 useChatInput.ts:73-80 existingIds and removedAttachments read attachments from closure after setAttachments((prev) => ...). Correct in this case (want old state for diff), but looks like a bug at first glance — worth a brief comment. See inline comment.
2 ChatInterfaceInner.tsx:63 inputAttachments in onNewConversation dependency array causes callback recreation on every attach/detach, cascading to useChatStream. Not a bug but could be optimized with a ref if performance matters. See inline comment.

What's Good

  • Clean three-phase migration: Create join table with savepoint-protected back-fill (0dff7c4153d407b4), drop old columns (87e049122b83), then drop redundant timestamps (c49841c8bfbb). Dialect-aware SQL (SQLite vs PostgreSQL). Reversible downgrade path.
  • Idempotent attach: ChatService.attach_drive_file handles the check-then-insert race with IntegrityError recovery — correct upsert pattern.
  • Thorough authorization: All attachment endpoints verify both conversation ownership and drive file ownership. Batched ownership check in chat_completion with logging for skipped IDs.
  • RAG intent router: Clean abstraction with structured output, proper error typing (RAGIntentRouterError), and fail-closed (502) on failure. File metadata fetched in parallel via asyncio.gather(return_exceptions=True). System prompt is clear and well-scoped.
  • Frontend error handling: All attach/detach fetch calls correctly check res.ok, revert UI state on failure, and show error messages. handleRemoveAttachment properly issues DELETE calls for each drive file before clearing state. Functional updater forms used consistently.
  • Test coverage: 1100+ lines of new tests across 3 test files — model, service, endpoints, SSE emission, and router decision paths. PersistedFilesInfo component has 139 lines of dedicated tests.
  • Scroll fix: ChatMessages now auto-scrolls during streaming by depending on lastContent in addition to messages.length.
  • Docker: hf_cache named volume shared across api, rag-cleanup, and rag-mcp prevents redundant model downloads. HF_HUB_DISABLE_XET=1 fixes the startup hang. Non-root user with proper cache directory permissions.
  • Exception handling: All except blocks catch specific exceptions per project rules. The pre-existing except Exception in schemas.py was correctly narrowed to except binascii.Error in this PR.
  • Scope classifier enhancement: attached_file_names parameter ensures document-related queries aren't incorrectly flagged out-of-scope.
  • RAG agent recursion: Limit raised to 25 to match LangGraph's default, with proper GraphRecursionError handling.

Summary

All 13 previously open review threads from Reviews 1–4 are resolved. The latest commits (2a4660e and fded5b4) comprehensively addressed every issue raised across the review cycle. The remaining findings are two minor nits that do not block merge. This PR is in good shape for merge.

Comment thread frontend/plugins/chat/hooks/useChatInput.ts
Comment thread frontend/plugins/chat/components/ChatInterfaceInner.tsx
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