Skip to content

feat(platform): translator + model gating + chat attachment retrieve#1586

Merged
larryro merged 3 commits into
mainfrom
wip/local-changes-2026-04-20
Apr 20, 2026
Merged

feat(platform): translator + model gating + chat attachment retrieve#1586
larryro merged 3 commits into
mainfrom
wip/local-changes-2026-04-20

Conversation

@larryro
Copy link
Copy Markdown
Collaborator

@larryro larryro commented Apr 20, 2026

Summary

Two commits on a WIP branch that together cover three independent improvements:

  • Translator agent — new agent config with clarify/draft/reflect-and-revise pass and de/fr i18n; drops qwen3-vl-32b-instruct from chat-agent supported models.
  • Model-access enforcement on implicit pathsunified_chat now applies the model_access governance policy to default/fallback model resolution (previously only the user-selected path was checked), so a blocked model can't leak through a governance default or an SDK retry. Adds getAccessibleModelsInternal query and a corresponding noModelsAvailable string (en/de/fr). Model selector gains tests and an empty-state. Minor upload-policy-editor layout refactor.
  • document_retrieve reads chat attachments — chat-uploaded files are auto-indexed into RAG but never create a documents-hub row, so the existing tool threw "Document not found" on them. Added a fileMetadata-based fallback (org-scoped) with distinct errors for still-indexing vs indexing-failed states. Tool description updated to advertise the chat-attachment path; 7 new unit tests covering the fallback branches.

Test plan

  • Translator in a fresh chat: translate short inline text, a document-length input, and an image attachment; confirm file output for long input.
  • Upload a multi-page PDF directly into a chat and ask the translator to translate it — confirm document_retrieve succeeds via the fileMetadata fallback (content comes back, not "Document not found").
  • Try the same translation immediately after upload, before indexing finishes — confirm the tool surfaces a "still being indexed" error that the agent handles gracefully.
  • Regression: retrieve a documents-hub file via an agent that uses document_retrieve; team ACL behavior unchanged.
  • Model-access: in an org with a model_access policy blocking a model, confirm that model does NOT appear via default fallback or SDK retry paths in unified_chat; the model selector shows the empty-state string when no models are accessible.
  • npx tsc --noEmit, npm run lint --workspace=@tale/platform, and the document_retrieve_tool vitest suite all green.

Summary by CodeRabbit

  • New Features

    • Added Translator agent configuration example with multi-pass translation workflow.
    • Auto-select single available model in chat; display warning when no models are available.
    • Support retrieving chat attachment files in document retrieval tool.
    • Enforce model access based on organization settings.
  • Localization

    • Added "No models available" translations in English, French, and German.
  • Bug Fixes

    • Improved error handling for document indexing states (pending, failed, completed).

larryro added 2 commits April 20, 2026 18:35
…icit paths

- Add Translator agent config with reflection pass and i18n (de/fr)
- Enforce model_access policy on implicit default/fallback model paths in
  unified_chat so blocked models can't be reached via governance default
  or SDK retry; add getAccessibleModelsInternal query
- Drop qwen3-vl-32b-instruct from chat-agent supportedModels
- Model selector: updates + tests, plus "noModelsAvailable" string (en/de/fr)
- Upload policy editor: layout/structure changes
…data

Chat-uploaded files are auto-indexed into the RAG service but do not
create a documents-hub row, so document_retrieve previously failed with
"Document not found" for files users drop into chat. Fall back to a
fileMetadata lookup (org-scoped) when the hub miss would otherwise
throw, then proceed with the existing RAG content fetch. Surface
distinct errors for still-indexing vs indexing-failed states so agents
can retry or stop appropriately. Tool description updated to advertise
the chat-attachment path and indexing-state semantics.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 20, 2026

📝 Walkthrough

Walkthrough

This PR implements model-access governance, updates agent configurations, and enhances model selection UI behavior. Changes include: removing an older Qwen model from the chat-agent configuration and adding a new Translator agent; refactoring model-selector component to auto-pin single available models, display "No models available" warnings, and clearing stale model overrides; enhancing document retrieval with a fallback path for chat-uploaded files using file metadata; adding model-access enforcement in unified chat to filter agent models by organizational allowlists; introducing a new internal governance query for accessible models; and adding corresponding i18n translations across three languages.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures all three major changes in the PR: the new Translator agent, model-access gating, and chat attachment document retrieval support.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wip/local-changes-2026-04-20

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@services/platform/app/features/chat/components/model-selector.tsx`:
- Around line 179-195: The selector auto-pins a single model into
selectedModelOverrides inside the useEffect (effectiveAgent?.name,
filteredModels, setSelectedModelOverride, isImageGenAgent) but never clears that
synthetic auto-pick when filteredModels later expands, so the UI/backend remain
stuck; to fix, record that the override was auto-pinned (e.g., maintain a small
autoPinned map keyed by effectiveAgent.name when you call
setSelectedModelOverride during the single-model branch) and then in the same
useEffect clear that auto-pinned override (call
setSelectedModelOverride(effectiveAgent.name, null)) whenever
filteredModels.length > 1 and the current override exists in
selectedModelOverrides and is marked as auto-pinned; update the autoPinned map
when you explicitly clear or when the user manually changes the override so user
choices are preserved.

In
`@services/platform/app/features/settings/governance/components/upload-policy-editor.tsx`:
- Around line 127-156: The toggle is updated optimistically in
handleToggleEnabled by calling setEnabled(checked) before saveConfig, but
saveConfig swallows errors so a failed upsert (upsertMutation.mutateAsync)
leaves the UI inconsistent; change handleToggleEnabled to save first and only
setEnabled on success (or revert to previous value on failure) — call
saveConfig(buildConfig(checked)) and, if it throws, call
setEnabled(previousValue) (or setEnabled(!checked)) and surface the error (or
rethrow) so the toast/error path can run; reference functions:
handleToggleEnabled, saveConfig, setEnabled, upsertMutation.mutateAsync.

In `@services/platform/convex/agents/unified_chat.ts`:
- Around line 143-162: The test harness must stub the new governance query so
implicit-model requests don't fail; in the TTFT test update the stub for
internal.governance.internal_queries.getAccessibleModelsInternal (used by
unified_chat's runQuery) to return the model IDs corresponding to the agent's
configResult.supportedModels (use stripModelRefQualifier if necessary) so
accessiblePlain contains the supported model IDs and accessibleRefs is
non-empty; ensure the test's Convex/mocking setup intercepts runQuery for
getAccessibleModelsInternal and returns that list before invoking the unified
chat flow that may call
internal.threads.internal_mutations.clearGenerationStatus.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2bf7dd10-78e0-411b-b33a-a1b8fa74798e

📥 Commits

Reviewing files that changed from the base of the PR and between 10539d1 and b78b9b5.

📒 Files selected for processing (13)
  • examples/agents/chat-agent.json
  • examples/agents/translator.json
  • services/platform/app/features/chat/components/__tests__/model-selector.test.tsx
  • services/platform/app/features/chat/components/model-selector.tsx
  • services/platform/app/features/settings/governance/components/upload-policy-editor.tsx
  • services/platform/convex/agent_tools/documents/__tests__/document_retrieve_tool.test.ts
  • services/platform/convex/agent_tools/documents/document_retrieve_tool.ts
  • services/platform/convex/agent_tools/documents/helpers/retrieve_document.ts
  • services/platform/convex/agents/unified_chat.ts
  • services/platform/convex/governance/internal_queries.ts
  • services/platform/messages/de.json
  • services/platform/messages/en.json
  • services/platform/messages/fr.json

Comment on lines 179 to 195
useEffect(() => {
if (!effectiveAgent?.name) return;
const override = selectedModelOverrides[effectiveAgent.name];
if (override && !filteredModels.includes(override)) {
setSelectedModelOverride(effectiveAgent.name, null);
return;
}
if (!override && !isImageGenAgent && filteredModels.length === 1) {
setSelectedModelOverride(effectiveAgent.name, filteredModels[0]);
}
}, [
effectiveAgent?.name,
filteredModels,
selectedModelOverrides,
setSelectedModelOverride,
isImageGenAgent,
]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Auto-pinned single-model overrides stay stuck after the choice set widens.

Lines 186-187 persist the sole model into selectedModelOverrides, and chat-layout-context.tsx keeps that value for 24 hours. When governance/provider data later exposes multiple models again, nothing clears this synthetic override, so the selector stays pinned to the old model and the backend no longer uses Auto/fallback resolution until the user manually switches back.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/app/features/chat/components/model-selector.tsx` around
lines 179 - 195, The selector auto-pins a single model into
selectedModelOverrides inside the useEffect (effectiveAgent?.name,
filteredModels, setSelectedModelOverride, isImageGenAgent) but never clears that
synthetic auto-pick when filteredModels later expands, so the UI/backend remain
stuck; to fix, record that the override was auto-pinned (e.g., maintain a small
autoPinned map keyed by effectiveAgent.name when you call
setSelectedModelOverride during the single-model branch) and then in the same
useEffect clear that auto-pinned override (call
setSelectedModelOverride(effectiveAgent.name, null)) whenever
filteredModels.length > 1 and the current override exists in
selectedModelOverrides and is marked as auto-pinned; update the autoPinned map
when you explicitly clear or when the user manually changes the override so user
choices are preserved.

Comment on lines +127 to +156
const saveConfig = useCallback(
async (config: UploadPolicyConfig) => {
try {
await upsertMutation.mutateAsync({
organizationId,
policyType: 'upload_policy',
config,
});
toast({ title: t('uploadPolicy.saved'), variant: 'success' });
} catch {
toast({
title: t('uploadPolicy.saveFailed'),
variant: 'destructive',
});
}
},
[organizationId, upsertMutation, toast, t],
);

const handleSave = useCallback(async () => {
try {
await upsertMutation.mutateAsync({
organizationId,
policyType: 'upload_policy',
config: buildConfig(),
});
toast({ title: t('uploadPolicy.saved'), variant: 'success' });
} catch {
toast({
title: t('uploadPolicy.saveFailed'),
variant: 'destructive',
});
}
}, [organizationId, buildConfig, upsertMutation, toast, t]);
await saveConfig(buildConfig(enabled));
}, [saveConfig, buildConfig, enabled]);

const handleToggleEnabled = useCallback((checked: boolean) => {
setEnabled(checked);
}, []);
const handleToggleEnabled = useCallback(
(checked: boolean) => {
setEnabled(checked);
void saveConfig(buildConfig(checked));
},
[saveConfig, buildConfig],
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Revert the toggle when the auto-save fails.

Line 152 updates enabled optimistically, but saveConfig swallows the mutation error and only shows a toast. If the write fails, the switch stays in the new position even though the persisted policy never changed.

Proposed fix
   const saveConfig = useCallback(
     async (config: UploadPolicyConfig) => {
       try {
         await upsertMutation.mutateAsync({
           organizationId,
           policyType: 'upload_policy',
           config,
         });
         toast({ title: t('uploadPolicy.saved'), variant: 'success' });
+        return true;
       } catch {
         toast({
           title: t('uploadPolicy.saveFailed'),
           variant: 'destructive',
         });
+        return false;
       }
     },
     [organizationId, upsertMutation, toast, t],
   );
@@
   const handleToggleEnabled = useCallback(
-    (checked: boolean) => {
+    async (checked: boolean) => {
+      const previousEnabled = enabled;
       setEnabled(checked);
-      void saveConfig(buildConfig(checked));
+      const saved = await saveConfig(buildConfig(checked));
+      if (!saved) {
+        setEnabled(previousEnabled);
+      }
     },
-    [saveConfig, buildConfig],
+    [enabled, saveConfig, buildConfig],
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/platform/app/features/settings/governance/components/upload-policy-editor.tsx`
around lines 127 - 156, The toggle is updated optimistically in
handleToggleEnabled by calling setEnabled(checked) before saveConfig, but
saveConfig swallows errors so a failed upsert (upsertMutation.mutateAsync)
leaves the UI inconsistent; change handleToggleEnabled to save first and only
setEnabled on success (or revert to previous value on failure) — call
saveConfig(buildConfig(checked)) and, if it throws, call
setEnabled(previousValue) (or setEnabled(!checked)) and surface the error (or
rethrow) so the toast/error path can run; reference functions:
handleToggleEnabled, saveConfig, setEnabled, upsertMutation.mutateAsync.

Comment on lines +143 to +162
const accessiblePlain = await ctx.runQuery(
internal.governance.internal_queries.getAccessibleModelsInternal,
{
organizationId: args.organizationId,
userId: authUserId,
modelIds: configResult.supportedModels.map(stripModelRefQualifier),
},
);
const accessibleSet = new Set(accessiblePlain);
const accessibleRefs = configResult.supportedModels.filter((ref) =>
accessibleSet.has(stripModelRefQualifier(ref)),
);
if (accessibleRefs.length === 0) {
await ctx.runMutation(
internal.threads.internal_mutations.clearGenerationStatus,
{ threadId: args.threadId, streamId: preAllocatedStreamId },
);
throw new Error(
"No model in this agent is permitted by your organization's model access policy.",
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep the TTFT test harness in sync with this new query.

This branch makes getAccessibleModelsInternal mandatory for every implicit-model request, but convex/agents/__tests__/unified_chat_ttft.test.ts still doesn't stub it. CI is already failing here with No model in this agent is permitted... before the TTFT assertions run, so the new governance dependency needs the matching test fixture update before merge.

🧰 Tools
🪛 GitHub Check: Unit

[failure] 160-160: [server] convex/agents/tests/unified_chat_ttft.test.ts > chatWithAgent — TTFT parallelization > eliminates resolveAgentConfig action hop by reading files directly
Error: No model in this agent is permitted by your organization's model access policy.
❯ handler convex/agents/unified_chat.ts:160:15
❯ convex/agents/tests/unified_chat_ttft.test.ts:272:5


[failure] 160-160: [server] convex/agents/tests/unified_chat_ttft.test.ts > chatWithAgent — TTFT parallelization > still scrubs PII before startChat when PII is enabled
Error: No model in this agent is permitted by your organization's model access policy.
❯ handler convex/agents/unified_chat.ts:160:15
❯ convex/agents/tests/unified_chat_ttft.test.ts:250:5


[failure] 160-160: [server] convex/agents/tests/unified_chat_ttft.test.ts > chatWithAgent — TTFT parallelization > calls PII query and agent config resolution concurrently
Error: No model in this agent is permitted by your organization's model access policy.
❯ handler convex/agents/unified_chat.ts:160:15
❯ convex/agents/tests/unified_chat_ttft.test.ts:204:5

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/convex/agents/unified_chat.ts` around lines 143 - 162, The
test harness must stub the new governance query so implicit-model requests don't
fail; in the TTFT test update the stub for
internal.governance.internal_queries.getAccessibleModelsInternal (used by
unified_chat's runQuery) to return the model IDs corresponding to the agent's
configResult.supportedModels (use stripModelRefQualifier if necessary) so
accessiblePlain contains the supported model IDs and accessibleRefs is
non-empty; ensure the test's Convex/mocking setup intercepts runQuery for
getAccessibleModelsInternal and returns that list before invoking the unified
chat flow that may call
internal.threads.internal_mutations.clearGenerationStatus.

… tests

The model-access enforcement added in 443aecd calls a new internal
query on implicit-model paths; the TTFT test's runQuery mock returned
null for unrecognized names, producing an empty accessible set and
throwing. Echo back the requested modelIds so every supported model is
treated as accessible.
@larryro larryro merged commit cb02129 into main Apr 20, 2026
15 checks passed
@larryro larryro deleted the wip/local-changes-2026-04-20 branch April 20, 2026 12:17
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