fix tests

This commit is contained in:
Dinh Long Nguyen 2025-10-09 04:28:08 +07:00
parent 340042682a
commit fc784620e0
10 changed files with 141 additions and 64 deletions

View File

@ -12,6 +12,8 @@ export type SettingComponentProps = {
extensionName?: string extensionName?: string
requireModelReload?: boolean requireModelReload?: boolean
configType?: ConfigType configType?: ConfigType
titleKey?: string
descriptionKey?: string
} }
export type ConfigType = 'runtime' | 'setting' export type ConfigType = 'runtime' | 'setting'

View File

@ -41,7 +41,7 @@ export function getRAGTools(retrievalLimit: number): MCPTool[] {
{ {
name: GET_CHUNKS, name: GET_CHUNKS,
description: description:
'Retrieve chunks from a file by their order range. For a single chunk, use start_order = end_order. Thread context is inferred automatically; you may optionally provide {"scope":"thread"}.', 'Retrieve chunks from a file by their order range. For a single chunk, use start_order = end_order. Thread context is inferred automatically; you may optionally provide {"scope":"thread"}. Use sparingly; intended for advanced usage. Prefer using retrieve instead for relevance-based fetching.',
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {

View File

@ -565,7 +565,7 @@ const ChatInput = ({
// If thread exists, ingest images immediately // If thread exists, ingest images immediately
if (currentThreadId) { if (currentThreadId) {
;(async () => { void (async () => {
for (const img of newFiles) { for (const img of newFiles) {
try { try {
// Mark as processing // Mark as processing

View File

@ -184,9 +184,9 @@ export const ThreadContent = memo(
} }
return null return null
}) })
.filter(Boolean) .filter((v) => v !== null)
// Keep embedded document metadata in the message for regenerate // Keep embedded document metadata in the message for regenerate
sendMessage(rawText, true, attachments) sendMessage(textContent, true, attachments)
} }
}, [deleteMessage, getMessages, item, sendMessage]) }, [deleteMessage, getMessages, item, sendMessage])

View File

@ -1,6 +1,6 @@
import { create } from 'zustand' import { create } from 'zustand'
import { ExtensionManager } from '@/lib/extension' import { ExtensionManager } from '@/lib/extension'
import { ExtensionTypeEnum, type RAGExtension } from '@janhq/core' import { ExtensionTypeEnum, type RAGExtension, type SettingComponentProps } from '@janhq/core'
export type AttachmentsSettings = { export type AttachmentsSettings = {
enabled: boolean enabled: boolean
@ -14,7 +14,7 @@ export type AttachmentsSettings = {
type AttachmentsStore = AttachmentsSettings & { type AttachmentsStore = AttachmentsSettings & {
// Dynamic controller definitions for rendering UI // Dynamic controller definitions for rendering UI
settingsDefs: any[] settingsDefs: SettingComponentProps[]
loadSettingsDefs: () => Promise<void> loadSettingsDefs: () => Promise<void>
setEnabled: (v: boolean) => void setEnabled: (v: boolean) => void
setMaxFileSizeMB: (v: number) => void setMaxFileSizeMB: (v: number) => void
@ -27,7 +27,7 @@ type AttachmentsStore = AttachmentsSettings & {
const getRagExtension = (): RAGExtension | undefined => { const getRagExtension = (): RAGExtension | undefined => {
try { try {
return ExtensionManager.getInstance().get<RAGExtension>(ExtensionTypeEnum.RAG) as any return ExtensionManager.getInstance().get<RAGExtension>(ExtensionTypeEnum.RAG)
} catch { } catch {
return undefined return undefined
} }
@ -43,94 +43,124 @@ export const useAttachments = create<AttachmentsStore>()((set) => ({
searchMode: 'auto', searchMode: 'auto',
settingsDefs: [], settingsDefs: [],
loadSettingsDefs: async () => { loadSettingsDefs: async () => {
const ext = getRagExtension() as any const ext = getRagExtension()
if (!ext?.getSettings) return if (!ext?.getSettings) return
try { try {
const defs = await ext.getSettings() const defs = await ext.getSettings()
if (Array.isArray(defs)) set({ settingsDefs: defs }) if (Array.isArray(defs)) set({ settingsDefs: defs })
} catch {} } catch (e) {
console.debug('Failed to load attachment settings defs:', e)
}
}, },
setEnabled: async (v) => { setEnabled: async (v) => {
const ext = getRagExtension() const ext = getRagExtension()
if (ext?.updateSettings) { if (ext?.updateSettings) {
await ext.updateSettings([{ key: 'enabled', controllerProps: { value: !!v } } as any]) await ext.updateSettings([
{ key: 'enabled', controllerProps: { value: !!v } } as Partial<SettingComponentProps>,
])
} }
set((s) => ({ set((s) => ({
enabled: v, enabled: v,
settingsDefs: s.settingsDefs.map((d) => settingsDefs: s.settingsDefs.map((d) =>
d.key === 'enabled' ? { ...d, controllerProps: { ...d.controllerProps, value: !!v } } : d d.key === 'enabled'
? ({ ...d, controllerProps: { ...d.controllerProps, value: !!v } } as SettingComponentProps)
: d
), ),
})) }))
}, },
setMaxFileSizeMB: async (val) => { setMaxFileSizeMB: async (val) => {
const ext = getRagExtension() const ext = getRagExtension()
if (ext?.updateSettings) { if (ext?.updateSettings) {
await ext.updateSettings([{ key: 'max_file_size_mb', controllerProps: { value: val } } as any]) await ext.updateSettings([
{ key: 'max_file_size_mb', controllerProps: { value: val } } as Partial<SettingComponentProps>,
])
} }
set((s) => ({ set((s) => ({
maxFileSizeMB: val, maxFileSizeMB: val,
settingsDefs: s.settingsDefs.map((d) => settingsDefs: s.settingsDefs.map((d) =>
d.key === 'max_file_size_mb' ? { ...d, controllerProps: { ...d.controllerProps, value: val } } : d d.key === 'max_file_size_mb'
? ({ ...d, controllerProps: { ...d.controllerProps, value: val } } as SettingComponentProps)
: d
), ),
})) }))
}, },
setRetrievalLimit: async (val) => { setRetrievalLimit: async (val) => {
const ext = getRagExtension() const ext = getRagExtension()
if (ext?.updateSettings) { if (ext?.updateSettings) {
await ext.updateSettings([{ key: 'retrieval_limit', controllerProps: { value: val } } as any]) await ext.updateSettings([
{ key: 'retrieval_limit', controllerProps: { value: val } } as Partial<SettingComponentProps>,
])
} }
set((s) => ({ set((s) => ({
retrievalLimit: val, retrievalLimit: val,
settingsDefs: s.settingsDefs.map((d) => settingsDefs: s.settingsDefs.map((d) =>
d.key === 'retrieval_limit' ? { ...d, controllerProps: { ...d.controllerProps, value: val } } : d d.key === 'retrieval_limit'
? ({ ...d, controllerProps: { ...d.controllerProps, value: val } } as SettingComponentProps)
: d
), ),
})) }))
}, },
setRetrievalThreshold: async (val) => { setRetrievalThreshold: async (val) => {
const ext = getRagExtension() const ext = getRagExtension()
if (ext?.updateSettings) { if (ext?.updateSettings) {
await ext.updateSettings([{ key: 'retrieval_threshold', controllerProps: { value: val } } as any]) await ext.updateSettings([
{ key: 'retrieval_threshold', controllerProps: { value: val } } as Partial<SettingComponentProps>,
])
} }
set((s) => ({ set((s) => ({
retrievalThreshold: val, retrievalThreshold: val,
settingsDefs: s.settingsDefs.map((d) => settingsDefs: s.settingsDefs.map((d) =>
d.key === 'retrieval_threshold' ? { ...d, controllerProps: { ...d.controllerProps, value: val } } : d d.key === 'retrieval_threshold'
? ({ ...d, controllerProps: { ...d.controllerProps, value: val } } as SettingComponentProps)
: d
), ),
})) }))
}, },
setChunkSizeTokens: async (val) => { setChunkSizeTokens: async (val) => {
const ext = getRagExtension() const ext = getRagExtension()
if (ext?.updateSettings) { if (ext?.updateSettings) {
await ext.updateSettings([{ key: 'chunk_size_tokens', controllerProps: { value: val } } as any]) await ext.updateSettings([
{ key: 'chunk_size_tokens', controllerProps: { value: val } } as Partial<SettingComponentProps>,
])
} }
set((s) => ({ set((s) => ({
chunkSizeTokens: val, chunkSizeTokens: val,
settingsDefs: s.settingsDefs.map((d) => settingsDefs: s.settingsDefs.map((d) =>
d.key === 'chunk_size_tokens' ? { ...d, controllerProps: { ...d.controllerProps, value: val } } : d d.key === 'chunk_size_tokens'
? ({ ...d, controllerProps: { ...d.controllerProps, value: val } } as SettingComponentProps)
: d
), ),
})) }))
}, },
setOverlapTokens: async (val) => { setOverlapTokens: async (val) => {
const ext = getRagExtension() const ext = getRagExtension()
if (ext?.updateSettings) { if (ext?.updateSettings) {
await ext.updateSettings([{ key: 'overlap_tokens', controllerProps: { value: val } } as any]) await ext.updateSettings([
{ key: 'overlap_tokens', controllerProps: { value: val } } as Partial<SettingComponentProps>,
])
} }
set((s) => ({ set((s) => ({
overlapTokens: val, overlapTokens: val,
settingsDefs: s.settingsDefs.map((d) => settingsDefs: s.settingsDefs.map((d) =>
d.key === 'overlap_tokens' ? { ...d, controllerProps: { ...d.controllerProps, value: val } } : d d.key === 'overlap_tokens'
? ({ ...d, controllerProps: { ...d.controllerProps, value: val } } as SettingComponentProps)
: d
), ),
})) }))
}, },
setSearchMode: async (v) => { setSearchMode: async (v) => {
const ext = getRagExtension() const ext = getRagExtension()
if (ext?.updateSettings) { if (ext?.updateSettings) {
await ext.updateSettings([{ key: 'search_mode', controllerProps: { value: v } } as any]) await ext.updateSettings([
{ key: 'search_mode', controllerProps: { value: v } } as Partial<SettingComponentProps>,
])
} }
set((s) => ({ set((s) => ({
searchMode: v, searchMode: v,
settingsDefs: s.settingsDefs.map((d) => settingsDefs: s.settingsDefs.map((d) =>
d.key === 'search_mode' ? { ...d, controllerProps: { ...d.controllerProps, value: v } } : d d.key === 'search_mode'
? ({ ...d, controllerProps: { ...d.controllerProps, value: v } } as SettingComponentProps)
: d
), ),
})) }))
}, },
@ -139,22 +169,26 @@ export const useAttachments = create<AttachmentsStore>()((set) => ({
// Initialize from extension settings once on import // Initialize from extension settings once on import
;(async () => { ;(async () => {
try { try {
const ext = getRagExtension() as any const ext = getRagExtension()
if (!ext?.getSettings) return if (!ext?.getSettings) return
const settings = await ext.getSettings() const settings = await ext.getSettings()
if (!Array.isArray(settings)) return if (!Array.isArray(settings)) return
const map = new Map<string, any>() const map = new Map<string, unknown>()
for (const s of settings) map.set(s.key, s?.controllerProps?.value) for (const s of settings) map.set(s.key, s?.controllerProps?.value)
// seed defs and values // seed defs and values
useAttachments.setState((prev) => ({ useAttachments.setState((prev) => ({
settingsDefs: settings, settingsDefs: settings,
enabled: map.get('enabled') ?? prev.enabled, enabled: (map.get('enabled') as boolean | undefined) ?? prev.enabled,
maxFileSizeMB: map.get('max_file_size_mb') ?? prev.maxFileSizeMB, maxFileSizeMB: (map.get('max_file_size_mb') as number | undefined) ?? prev.maxFileSizeMB,
retrievalLimit: map.get('retrieval_limit') ?? prev.retrievalLimit, retrievalLimit: (map.get('retrieval_limit') as number | undefined) ?? prev.retrievalLimit,
retrievalThreshold: map.get('retrieval_threshold') ?? prev.retrievalThreshold, retrievalThreshold:
chunkSizeTokens: map.get('chunk_size_tokens') ?? prev.chunkSizeTokens, (map.get('retrieval_threshold') as number | undefined) ?? prev.retrievalThreshold,
overlapTokens: map.get('overlap_tokens') ?? prev.overlapTokens, chunkSizeTokens: (map.get('chunk_size_tokens') as number | undefined) ?? prev.chunkSizeTokens,
searchMode: map.get('search_mode') ?? prev.searchMode, overlapTokens: (map.get('overlap_tokens') as number | undefined) ?? prev.overlapTokens,
searchMode:
(map.get('search_mode') as 'auto' | 'ann' | 'linear' | undefined) ?? prev.searchMode,
})) }))
} catch {} } catch (e) {
console.debug('Failed to initialize attachment settings from extension:', e)
}
})() })()

View File

@ -380,7 +380,7 @@ export const useChat = () => {
// Build the user content once; use it for both the outbound request // Build the user content once; use it for both the outbound request
// and persisting to the store so both are identical. // and persisting to the store so both are identical.
if (updateAttachmentProcessing) { if (updateAttachmentProcessing) {
updateAttachmentProcessing('__CLEAR_ALL__' as any, 'clear_all') updateAttachmentProcessing('__CLEAR_ALL__', 'clear_all')
} }
const userContent = newUserThreadContent( const userContent = newUserThreadContent(
activeThread.id, activeThread.id,

View File

@ -241,7 +241,10 @@ export const sendCompletion = async (
usableTools = [...tools, ...ragTools] usableTools = [...tools, ...ragTools]
} }
} }
} catch {} } catch (e) {
// Ignore RAG tool injection errors during completion setup
console.debug('Skipping RAG tools injection:', e)
}
const engine = ExtensionManager.getInstance().getEngine(provider.provider) const engine = ExtensionManager.getInstance().getEngine(provider.provider)

View File

@ -1,10 +1,11 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ChatCompletionMessageParam } from 'token.js' import { ChatCompletionMessageParam } from 'token.js'
import { ChatCompletionMessageToolCall } from 'openai/resources' import { ChatCompletionMessageToolCall } from 'openai/resources'
import { ThreadMessage } from '@janhq/core' import { ThreadMessage, ContentType } from '@janhq/core'
import { removeReasoningContent } from '@/utils/reasoning' import { removeReasoningContent } from '@/utils/reasoning'
// Attachments are now handled upstream in newUserThreadContent // Attachments are now handled upstream in newUserThreadContent
type ThreadContent = NonNullable<ThreadMessage['content']>[number]
/** /**
* @fileoverview Helper functions for creating chat completion request. * @fileoverview Helper functions for creating chat completion request.
* These functions are used to create chat completion request objects * These functions are used to create chat completion request objects
@ -22,7 +23,14 @@ export class CompletionMessagesBuilder {
this.messages.push( this.messages.push(
...messages ...messages
.filter((e) => !e.metadata?.error) .filter((e) => !e.metadata?.error)
.map<ChatCompletionMessageParam>((msg) => this.toCompletionParamFromThread(msg)) .map<ChatCompletionMessageParam>((msg) => {
const param = this.toCompletionParamFromThread(msg)
// In constructor context, normalize empty user text to a placeholder
if (param.role === 'user' && typeof param.content === 'string' && param.content === '') {
return { ...param, content: '.' }
}
return param
})
) )
} }
@ -45,19 +53,20 @@ export class CompletionMessagesBuilder {
// User messages: handle multimodal content // User messages: handle multimodal content
if (Array.isArray(msg.content) && msg.content.length > 1) { if (Array.isArray(msg.content) && msg.content.length > 1) {
const content = msg.content.map((part: any) => { const content = msg.content.map((part: ThreadContent) => {
if (part.type === 'text') { if (part.type === ContentType.Text) {
return { type: 'text', text: part.text?.value ?? '' } return { type: 'text' as const, text: part.text?.value ?? '' }
} }
if (part.type === 'image_url') { if (part.type === ContentType.Image) {
return { return {
type: 'image_url', type: 'image_url' as const,
image_url: { url: part.image_url?.url || '', detail: part.image_url?.detail || 'auto' }, image_url: { url: part.image_url?.url || '', detail: part.image_url?.detail || 'auto' },
} }
} }
return part // Fallback for unknown content types
return { type: 'text' as const, text: '' }
}) })
return { role: 'user', content } as any return { role: 'user', content } as ChatCompletionMessageParam
} }
// Single text part // Single text part
const text = msg?.content?.[0]?.text?.value ?? '.' const text = msg?.content?.[0]?.text?.value ?? '.'

View File

@ -1,9 +1,9 @@
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router'
import { route } from '@/constants/routes'
import SettingsMenu from '@/containers/SettingsMenu' import SettingsMenu from '@/containers/SettingsMenu'
import HeaderPage from '@/containers/HeaderPage' import HeaderPage from '@/containers/HeaderPage'
import { Card, CardItem } from '@/containers/Card' import { Card, CardItem } from '@/containers/Card'
import { useAttachments } from '@/hooks/useAttachments' import { useAttachments } from '@/hooks/useAttachments'
import type { SettingComponentProps } from '@janhq/core'
import { useTranslation } from '@/i18n/react-i18next-compat' import { useTranslation } from '@/i18n/react-i18next-compat'
import { PlatformGuard } from '@/lib/platform/PlatformGuard' import { PlatformGuard } from '@/lib/platform/PlatformGuard'
import { DynamicControllerSetting } from '@/containers/dynamicControllerSetting' import { DynamicControllerSetting } from '@/containers/dynamicControllerSetting'
@ -11,14 +11,13 @@ import { PlatformFeature } from '@/lib/platform/types'
import { useEffect, useState, useCallback, useRef } from 'react' import { useEffect, useState, useCallback, useRef } from 'react'
import { useShallow } from 'zustand/react/shallow' import { useShallow } from 'zustand/react/shallow'
// eslint-disable-next-line @typescript-eslint/no-explicit-any export const Route = createFileRoute('/settings/attachments')({
export const Route = createFileRoute(route.settings.attachments as any)({
component: AttachmentsSettings, component: AttachmentsSettings,
}) })
// Helper to extract constraints from settingsDefs // Helper to extract constraints from settingsDefs
function getConstraints(def: any) { function getConstraints(def: SettingComponentProps) {
const props = def?.controllerProps || {} const props = def.controllerProps as Partial<{ min: number; max: number; step: number }>
return { return {
min: props.min ?? -Infinity, min: props.min ?? -Infinity,
max: props.max ?? Infinity, max: props.max ?? Infinity,
@ -27,7 +26,7 @@ function getConstraints(def: any) {
} }
// Helper to validate and clamp numeric values // Helper to validate and clamp numeric values
function clampValue(val: any, def: any, currentValue: number): number { function clampValue(val: unknown, def: SettingComponentProps, currentValue: number): number {
const num = typeof val === 'number' ? val : Number(val) const num = typeof val === 'number' ? val : Number(val)
if (!Number.isFinite(num)) return currentValue if (!Number.isFinite(num)) return currentValue
const { min, max, step } = getConstraints(def) const { min, max, step } = getConstraints(def)
@ -40,7 +39,7 @@ function AttachmentsSettings() {
const { t } = useTranslation() const { t } = useTranslation()
const hookDefs = useAttachments((s) => s.settingsDefs) const hookDefs = useAttachments((s) => s.settingsDefs)
const loadDefs = useAttachments((s) => s.loadSettingsDefs) const loadDefs = useAttachments((s) => s.loadSettingsDefs)
const [defs, setDefs] = useState<any[]>([]) const [defs, setDefs] = useState<SettingComponentProps[]>([])
// Load schema from extension via the hook once // Load schema from extension via the hook once
useEffect(() => { useEffect(() => {
@ -73,32 +72,36 @@ function AttachmentsSettings() {
) )
// Local state for inputs to allow intermediate values while typing // Local state for inputs to allow intermediate values while typing
const [localValues, setLocalValues] = useState<Record<string, any>>({}) const [localValues, setLocalValues] = useState<Record<string, string | number | boolean | string[]>>({})
// Debounce timers // Debounce timers
const timersRef = useRef<Record<string, NodeJS.Timeout>>({}) const timersRef = useRef<Record<string, ReturnType<typeof setTimeout>>>({})
// Cleanup timers on unmount // Cleanup timers on unmount
useEffect(() => { useEffect(() => {
const timers = timersRef.current
return () => { return () => {
Object.values(timersRef.current).forEach(clearTimeout) Object.values(timers).forEach(clearTimeout)
} }
}, []) }, [])
// Debounced setter with validation // Debounced setter with validation
const debouncedSet = useCallback((key: string, val: any, def: any) => { const debouncedSet = useCallback((key: string, val: unknown, def: SettingComponentProps) => {
// Clear existing timer for this key // Clear existing timer for this key
if (timersRef.current[key]) { if (timersRef.current[key]) {
clearTimeout(timersRef.current[key]) clearTimeout(timersRef.current[key])
} }
// Set local value immediately for responsive UI // Set local value immediately for responsive UI
setLocalValues((prev) => ({ ...prev, [key]: val })) setLocalValues((prev) => ({
...prev,
[key]: val as string | number | boolean | string[]
}))
// For non-numeric inputs, apply immediately without debounce // For non-numeric inputs, apply immediately without debounce
if (key === 'enabled' || key === 'search_mode') { if (key === 'enabled' || key === 'search_mode') {
if (key === 'enabled') sel.setEnabled(!!val) if (key === 'enabled') sel.setEnabled(!!val)
else if (key === 'search_mode') sel.setSearchMode(val) else if (key === 'search_mode') sel.setSearchMode(val as 'auto' | 'ann' | 'linear')
return return
} }
@ -136,7 +139,10 @@ function AttachmentsSettings() {
} }
// Update local value to validated one // Update local value to validated one
setLocalValues((prev) => ({ ...prev, [key]: validated })) setLocalValues((prev) => ({
...prev,
[key]: validated as string | number | boolean | string[]
}))
}, 500) // 500ms debounce }, 500) // 500ms debounce
}, [sel]) }, [sel])
@ -174,8 +180,30 @@ function AttachmentsSettings() {
} }
})() })()
const currentValue = localValues[d.key] !== undefined ? localValues[d.key] : storeValue const currentValue =
const props = { ...(d.controllerProps || {}), value: currentValue } localValues[d.key] !== undefined ? localValues[d.key] : storeValue
// Convert to DynamicControllerSetting compatible props
const baseProps = d.controllerProps
const normalizedValue: string | number | boolean = (() => {
if (Array.isArray(currentValue)) {
return currentValue.join(',')
}
return currentValue as string | number | boolean
})()
const props = {
value: normalizedValue,
placeholder: 'placeholder' in baseProps ? baseProps.placeholder : undefined,
type: 'type' in baseProps ? baseProps.type : undefined,
options: 'options' in baseProps ? baseProps.options : undefined,
input_actions: 'inputActions' in baseProps ? baseProps.inputActions : undefined,
rows: undefined,
min: 'min' in baseProps ? baseProps.min : undefined,
max: 'max' in baseProps ? baseProps.max : undefined,
step: 'step' in baseProps ? baseProps.step : undefined,
recommended: 'recommended' in baseProps ? baseProps.recommended : undefined,
}
const title = d.titleKey ? t(d.titleKey) : d.title const title = d.titleKey ? t(d.titleKey) : d.title
const description = d.descriptionKey ? t(d.descriptionKey) : d.description const description = d.descriptionKey ? t(d.descriptionKey) : d.description
@ -188,7 +216,7 @@ function AttachmentsSettings() {
actions={ actions={
<DynamicControllerSetting <DynamicControllerSetting
controllerType={d.controllerType} controllerType={d.controllerType}
controllerProps={props as any} controllerProps={props}
onChange={(val) => debouncedSet(d.key, val, d)} onChange={(val) => debouncedSet(d.key, val, d)}
/> />
} }

View File

@ -16,14 +16,15 @@ export class DefaultRAGService implements RAGService {
return [] return []
} }
async callTool(args: { toolName: string; arguments: object; threadId?: string }): Promise<MCPToolCallResult> { async callTool(args: { toolName: string; arguments: Record<string, unknown>; threadId?: string }): Promise<MCPToolCallResult> {
const ext = ExtensionManager.getInstance().get<RAGExtension>(ExtensionTypeEnum.RAG) const ext = ExtensionManager.getInstance().get<RAGExtension>(ExtensionTypeEnum.RAG)
if (!ext?.callTool) { if (!ext?.callTool) {
return { error: 'RAG extension not available', content: [{ type: 'text', text: 'RAG extension not available' }] } return { error: 'RAG extension not available', content: [{ type: 'text', text: 'RAG extension not available' }] }
} }
try { try {
// Inject thread context when scope requires it // Inject thread context when scope requires it
const a: any = { ...(args.arguments as any) } type ToolCallArgs = Record<string, unknown> & { scope?: string; thread_id?: string }
const a: ToolCallArgs = { ...(args.arguments as Record<string, unknown>) }
if (!a.scope) a.scope = 'thread' if (!a.scope) a.scope = 'thread'
if (a.scope === 'thread' && !a.thread_id) { if (a.scope === 'thread' && !a.thread_id) {
a.thread_id = args.threadId a.thread_id = args.threadId