Compare commits
4 Commits
d91072ed6b
...
07f8a60b69
| Author | SHA1 | Date | |
|---|---|---|---|
| 07f8a60b69 | |||
| a482aaaad6 | |||
| cd6819b905 | |||
| 43f73a2ec7 |
@@ -446,27 +446,21 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="AI_MODEL_TAGS_OLLAMA">{t('admin.ai.model')}</Label>
|
<Label>{t('admin.ai.model')}</Label>
|
||||||
<select
|
<input type="hidden" name="AI_MODEL_TAGS_OLLAMA" value={selectedTagsModel} />
|
||||||
id="AI_MODEL_TAGS_OLLAMA"
|
<Combobox
|
||||||
name="AI_MODEL_TAGS_OLLAMA"
|
options={ollamaTagsModels.length > 0
|
||||||
|
? ollamaTagsModels.map((m) => ({ value: m, label: m }))
|
||||||
|
: selectedTagsModel
|
||||||
|
? [{ value: selectedTagsModel, label: `${selectedTagsModel} (${t('admin.ai.saved')})` }]
|
||||||
|
: []
|
||||||
|
}
|
||||||
value={selectedTagsModel}
|
value={selectedTagsModel}
|
||||||
onChange={(e) => setSelectedTagsModel(e.target.value)}
|
onChange={setSelectedTagsModel}
|
||||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
placeholder={selectedTagsModel || t('admin.ai.clickToLoadModels')}
|
||||||
>
|
searchPlaceholder={t('admin.ai.searchModel')}
|
||||||
{ollamaTagsModels.length > 0 ? (
|
emptyMessage={t('admin.ai.noModels')}
|
||||||
<>
|
/>
|
||||||
{!ollamaTagsModels.includes(selectedTagsModel) && selectedTagsModel && (
|
|
||||||
<option value={selectedTagsModel}>{selectedTagsModel} ({t('admin.ai.configured')})</option>
|
|
||||||
)}
|
|
||||||
{ollamaTagsModels.map((model) => (
|
|
||||||
<option key={model} value={model}>{model}</option>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<option value={selectedTagsModel || 'granite4:latest'}>{selectedTagsModel || 'granite4:latest'} {t('admin.ai.saved')}</option>
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{isLoadingTagsModels ? t('admin.ai.fetchingModels') : t('admin.ai.selectOllamaModel')}
|
{isLoadingTagsModels ? t('admin.ai.fetchingModels') : t('admin.ai.selectOllamaModel')}
|
||||||
</p>
|
</p>
|
||||||
@@ -620,27 +614,21 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="AI_MODEL_EMBEDDING_OLLAMA">{t('admin.ai.model')}</Label>
|
<Label>{t('admin.ai.model')}</Label>
|
||||||
<select
|
<input type="hidden" name="AI_MODEL_EMBEDDING_OLLAMA" value={selectedEmbeddingModel} />
|
||||||
id="AI_MODEL_EMBEDDING_OLLAMA"
|
<Combobox
|
||||||
name="AI_MODEL_EMBEDDING_OLLAMA"
|
options={ollamaEmbeddingsModels.length > 0
|
||||||
|
? ollamaEmbeddingsModels.map((m) => ({ value: m, label: m }))
|
||||||
|
: selectedEmbeddingModel
|
||||||
|
? [{ value: selectedEmbeddingModel, label: `${selectedEmbeddingModel} (${t('admin.ai.saved')})` }]
|
||||||
|
: []
|
||||||
|
}
|
||||||
value={selectedEmbeddingModel}
|
value={selectedEmbeddingModel}
|
||||||
onChange={(e) => setSelectedEmbeddingModel(e.target.value)}
|
onChange={setSelectedEmbeddingModel}
|
||||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
placeholder={selectedEmbeddingModel || t('admin.ai.clickToLoadModels')}
|
||||||
>
|
searchPlaceholder={t('admin.ai.searchModel')}
|
||||||
{ollamaEmbeddingsModels.length > 0 ? (
|
emptyMessage={t('admin.ai.noModels')}
|
||||||
<>
|
/>
|
||||||
{!ollamaEmbeddingsModels.includes(selectedEmbeddingModel) && selectedEmbeddingModel && (
|
|
||||||
<option value={selectedEmbeddingModel}>{selectedEmbeddingModel} ({t('admin.ai.configured')})</option>
|
|
||||||
)}
|
|
||||||
{ollamaEmbeddingsModels.map((model) => (
|
|
||||||
<option key={model} value={model}>{model}</option>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<option value={selectedEmbeddingModel || 'embeddinggemma:latest'}>{selectedEmbeddingModel || 'embeddinggemma:latest'} {t('admin.ai.saved')}</option>
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{isLoadingEmbeddingsModels ? t('admin.ai.fetchingModels') : t('admin.ai.selectEmbeddingModel')}
|
{isLoadingEmbeddingsModels ? t('admin.ai.fetchingModels') : t('admin.ai.selectEmbeddingModel')}
|
||||||
</p>
|
</p>
|
||||||
@@ -790,27 +778,21 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="AI_MODEL_CHAT_OLLAMA">{t('admin.ai.model')}</Label>
|
<Label>{t('admin.ai.model')}</Label>
|
||||||
<select
|
<input type="hidden" name="AI_MODEL_CHAT_OLLAMA" value={selectedChatModel} />
|
||||||
id="AI_MODEL_CHAT_OLLAMA"
|
<Combobox
|
||||||
name="AI_MODEL_CHAT_OLLAMA"
|
options={ollamaChatModels.length > 0
|
||||||
|
? ollamaChatModels.map((m) => ({ value: m, label: m }))
|
||||||
|
: selectedChatModel
|
||||||
|
? [{ value: selectedChatModel, label: `${selectedChatModel} (${t('admin.ai.saved')})` }]
|
||||||
|
: []
|
||||||
|
}
|
||||||
value={selectedChatModel}
|
value={selectedChatModel}
|
||||||
onChange={(e) => setSelectedChatModel(e.target.value)}
|
onChange={setSelectedChatModel}
|
||||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
placeholder={selectedChatModel || t('admin.ai.clickToLoadModels')}
|
||||||
>
|
searchPlaceholder={t('admin.ai.searchModel')}
|
||||||
{ollamaChatModels.length > 0 ? (
|
emptyMessage={t('admin.ai.noModels')}
|
||||||
<>
|
/>
|
||||||
{!ollamaChatModels.includes(selectedChatModel) && selectedChatModel && (
|
|
||||||
<option value={selectedChatModel}>{selectedChatModel} ({t('admin.ai.configured')})</option>
|
|
||||||
)}
|
|
||||||
{ollamaChatModels.map((model) => (
|
|
||||||
<option key={model} value={model}>{model}</option>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<option value={selectedChatModel || 'granite4:latest'}>{selectedChatModel || 'granite4:latest'} {t('admin.ai.saved')}</option>
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{isLoadingChatModels ? t('admin.ai.fetchingModels') : t('admin.ai.selectOllamaModel')}
|
{isLoadingChatModels ? t('admin.ai.fetchingModels') : t('admin.ai.selectOllamaModel')}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -99,40 +99,46 @@ export async function POST(req: Request) {
|
|||||||
// This ensures the AI always has access to the notebook content,
|
// This ensures the AI always has access to the notebook content,
|
||||||
// even for vague queries like "what's in this notebook?"
|
// even for vague queries like "what's in this notebook?"
|
||||||
let notebookContext = ''
|
let notebookContext = ''
|
||||||
if (notebookId) {
|
let searchNotes = ''
|
||||||
const notebookNotes = await prisma.note.findMany({
|
|
||||||
where: {
|
// When scope is "this note" (noteContext present), skip RAG retrieval entirely
|
||||||
notebookId,
|
// The note content is already injected as copilotContext below
|
||||||
userId,
|
if (!noteContext) {
|
||||||
trashedAt: null,
|
if (notebookId) {
|
||||||
},
|
const notebookNotes = await prisma.note.findMany({
|
||||||
orderBy: { updatedAt: 'desc' },
|
where: {
|
||||||
take: 20,
|
notebookId,
|
||||||
select: { id: true, title: true, content: true, updatedAt: true },
|
userId,
|
||||||
})
|
trashedAt: null,
|
||||||
if (notebookNotes.length > 0) {
|
},
|
||||||
notebookContext = notebookNotes
|
orderBy: { updatedAt: 'desc' },
|
||||||
.map(n => `NOTE [${n.title || untitledText}] (updated ${n.updatedAt.toLocaleDateString()}):\n${(n.content || '').substring(0, 1500)}`)
|
take: 20,
|
||||||
.join('\n\n---\n\n')
|
select: { id: true, title: true, content: true, updatedAt: true },
|
||||||
|
})
|
||||||
|
if (notebookNotes.length > 0) {
|
||||||
|
notebookContext = notebookNotes
|
||||||
|
.map(n => `NOTE [${n.title || untitledText}] (updated ${n.updatedAt.toLocaleDateString()}):\n${(n.content || '').substring(0, 1500)}`)
|
||||||
|
.join('\n\n---\n\n')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Also run semantic search for the specific query
|
// Also run semantic search for the specific query
|
||||||
let searchResults: any[] = []
|
let searchResults: any[] = []
|
||||||
try {
|
try {
|
||||||
searchResults = await semanticSearchService.search(currentMessage, {
|
searchResults = await semanticSearchService.search(currentMessage, {
|
||||||
notebookId,
|
notebookId,
|
||||||
limit: notebookId ? 10 : 5,
|
limit: notebookId ? 10 : 5,
|
||||||
threshold: notebookId ? 0.3 : 0.5,
|
threshold: notebookId ? 0.3 : 0.5,
|
||||||
defaultTitle: untitledText,
|
defaultTitle: untitledText,
|
||||||
})
|
})
|
||||||
} catch {
|
} catch {
|
||||||
// Search failure should not block chat
|
// Search failure should not block chat
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchNotes = searchResults
|
searchNotes = searchResults
|
||||||
.map((r) => `NOTE [${r.title || untitledText}]: ${r.content}`)
|
.map((r) => `NOTE [${r.title || untitledText}]: ${r.content}`)
|
||||||
.join('\n\n---\n\n')
|
.join('\n\n---\n\n')
|
||||||
|
}
|
||||||
|
|
||||||
// Combine: full notebook context + semantic search results (deduplicated)
|
// Combine: full notebook context + semantic search results (deduplicated)
|
||||||
const contextNotes = [notebookContext, searchNotes].filter(Boolean).join('\n\n---\n\n')
|
const contextNotes = [notebookContext, searchNotes].filter(Boolean).join('\n\n---\n\n')
|
||||||
@@ -354,7 +360,7 @@ ${noteContext.content || '(empty)'}
|
|||||||
${imageContextParts.length > 0 ? `\nImages: ${imageContextParts.length} image(s) attached. When the user asks about images, describe what you see in them.` : ''}
|
${imageContextParts.length > 0 ? `\nImages: ${imageContextParts.length} image(s) attached. When the user asks about images, describe what you see in them.` : ''}
|
||||||
|
|
||||||
The user wants you to write in a **${noteContext.tone || 'professional'}** tone.
|
The user wants you to write in a **${noteContext.tone || 'professional'}** tone.
|
||||||
Keep your suggestions tailored to this note and tone. You can suggest rewrites, answer questions about the note, or draft new sections.`
|
IMPORTANT: Focus ONLY on this note. Do NOT reference other notes or external information unless the user explicitly asks. Your job is to help with this specific note — suggest rewrites, answer questions about it, or draft new sections.`
|
||||||
}
|
}
|
||||||
|
|
||||||
const systemPrompt = `${prompts.system}
|
const systemPrompt = `${prompts.system}
|
||||||
@@ -413,7 +419,11 @@ Never switch to another language. Even if the user writes in a different languag
|
|||||||
webSearch: !!webSearch,
|
webSearch: !!webSearch,
|
||||||
config,
|
config,
|
||||||
}
|
}
|
||||||
const chatTools = toolRegistry.buildToolsForChat(chatToolContext)
|
// When scoped to "this note", only provide web tools — no note_search/note_read
|
||||||
|
// to prevent the AI from pulling information from other notes
|
||||||
|
const chatTools = noteContext
|
||||||
|
? toolRegistry.buildToolsForChat({ ...chatToolContext, webOnly: true })
|
||||||
|
: toolRegistry.buildToolsForChat(chatToolContext)
|
||||||
|
|
||||||
// 8. Save user message to DB before streaming
|
// 8. Save user message to DB before streaming
|
||||||
if (isNewMessage && lastIncoming) {
|
if (isNewMessage && lastIncoming) {
|
||||||
|
|||||||
@@ -613,31 +613,27 @@ export function NotesTabsView({
|
|||||||
setItems((prev) => {
|
setItems((prev) => {
|
||||||
const prevIds = prev.map((n) => n.id).join(',')
|
const prevIds = prev.map((n) => n.id).join(',')
|
||||||
const incomingIds = notes.map((n) => n.id).join(',')
|
const incomingIds = notes.map((n) => n.id).join(',')
|
||||||
|
const merge = (fresh: Note, local: Note) => {
|
||||||
|
const labelsChanged = JSON.stringify(fresh.labels?.sort()) !== JSON.stringify(local.labels?.sort())
|
||||||
|
return {
|
||||||
|
...fresh,
|
||||||
|
title: local.title || fresh.title,
|
||||||
|
content: local.content,
|
||||||
|
checkItems: local.checkItems,
|
||||||
|
labels: labelsChanged ? fresh.labels : local.labels
|
||||||
|
}
|
||||||
|
}
|
||||||
if (prevIds === incomingIds) {
|
if (prevIds === incomingIds) {
|
||||||
return prev.map((p) => {
|
return prev.map((p) => {
|
||||||
const fresh = notes.find((n) => n.id === p.id)
|
const fresh = notes.find((n) => n.id === p.id)
|
||||||
if (!fresh) return p
|
if (!fresh) return p
|
||||||
const labelsChanged = JSON.stringify(fresh.labels?.sort()) !== JSON.stringify(p.labels?.sort())
|
return merge(fresh, p)
|
||||||
return {
|
|
||||||
...fresh,
|
|
||||||
title: p.title,
|
|
||||||
content: p.content,
|
|
||||||
checkItems: p.checkItems,
|
|
||||||
labels: labelsChanged ? fresh.labels : p.labels
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return notes.map((fresh) => {
|
return notes.map((fresh) => {
|
||||||
const local = prev.find((p) => p.id === fresh.id)
|
const local = prev.find((p) => p.id === fresh.id)
|
||||||
if (!local) return fresh
|
if (!local) return fresh
|
||||||
const labelsChanged = JSON.stringify(fresh.labels?.sort()) !== JSON.stringify(local.labels?.sort())
|
return merge(fresh, local)
|
||||||
return {
|
|
||||||
...fresh,
|
|
||||||
title: local.title,
|
|
||||||
content: local.content,
|
|
||||||
checkItems: local.checkItems,
|
|
||||||
labels: labelsChanged ? fresh.labels : local.labels
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}, [notes])
|
}, [notes])
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { AIProvider } from './types';
|
|||||||
|
|
||||||
type ProviderType = 'ollama' | 'openai' | 'custom' | 'deepseek' | 'openrouter';
|
type ProviderType = 'ollama' | 'openai' | 'custom' | 'deepseek' | 'openrouter';
|
||||||
|
|
||||||
function createOllamaProvider(config: Record<string, string>, modelName: string, embeddingModelName: string): OllamaProvider {
|
function createOllamaProvider(config: Record<string, string>, modelName: string, embeddingModelName: string, baseUrlOverride?: string): OllamaProvider {
|
||||||
let baseUrl = config?.OLLAMA_BASE_URL || process.env.OLLAMA_BASE_URL
|
let baseUrl = baseUrlOverride || config?.OLLAMA_BASE_URL || process.env.OLLAMA_BASE_URL
|
||||||
|
|
||||||
// Only use localhost as fallback for local development (not in Docker)
|
// Only use localhost as fallback for local development (not in Docker)
|
||||||
if (!baseUrl && process.env.NODE_ENV !== 'production') {
|
if (!baseUrl && process.env.NODE_ENV !== 'production') {
|
||||||
@@ -62,10 +62,10 @@ function createOpenRouterProvider(config: Record<string, string>, modelName: str
|
|||||||
return new CustomOpenAIProvider(apiKey, 'https://openrouter.ai/api/v1', modelName, embeddingModelName);
|
return new CustomOpenAIProvider(apiKey, 'https://openrouter.ai/api/v1', modelName, embeddingModelName);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProviderInstance(providerType: ProviderType, config: Record<string, string>, modelName: string, embeddingModelName: string): AIProvider {
|
function getProviderInstance(providerType: ProviderType, config: Record<string, string>, modelName: string, embeddingModelName: string, ollamaBaseUrl?: string): AIProvider {
|
||||||
switch (providerType) {
|
switch (providerType) {
|
||||||
case 'ollama':
|
case 'ollama':
|
||||||
return createOllamaProvider(config, modelName, embeddingModelName);
|
return createOllamaProvider(config, modelName, embeddingModelName, ollamaBaseUrl);
|
||||||
case 'openai':
|
case 'openai':
|
||||||
return createOpenAIProvider(config, modelName, embeddingModelName);
|
return createOpenAIProvider(config, modelName, embeddingModelName);
|
||||||
case 'custom':
|
case 'custom':
|
||||||
@@ -75,7 +75,7 @@ function getProviderInstance(providerType: ProviderType, config: Record<string,
|
|||||||
case 'openrouter':
|
case 'openrouter':
|
||||||
return createOpenRouterProvider(config, modelName, embeddingModelName);
|
return createOpenRouterProvider(config, modelName, embeddingModelName);
|
||||||
default:
|
default:
|
||||||
return createOllamaProvider(config, modelName, embeddingModelName);
|
return createOllamaProvider(config, modelName, embeddingModelName, ollamaBaseUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,8 +102,9 @@ export function getTagsProvider(config?: Record<string, string>): AIProvider {
|
|||||||
const provider = providerType.toLowerCase() as ProviderType;
|
const provider = providerType.toLowerCase() as ProviderType;
|
||||||
const modelName = config?.AI_MODEL_TAGS || process.env.AI_MODEL_TAGS || 'granite4:latest';
|
const modelName = config?.AI_MODEL_TAGS || process.env.AI_MODEL_TAGS || 'granite4:latest';
|
||||||
const embeddingModelName = config?.AI_MODEL_EMBEDDING || process.env.AI_MODEL_EMBEDDING || 'embeddinggemma:latest';
|
const embeddingModelName = config?.AI_MODEL_EMBEDDING || process.env.AI_MODEL_EMBEDDING || 'embeddinggemma:latest';
|
||||||
|
const ollamaBaseUrl = config?.OLLAMA_BASE_URL_TAGS || config?.OLLAMA_BASE_URL;
|
||||||
|
|
||||||
return getProviderInstance(provider, config || {}, modelName, embeddingModelName);
|
return getProviderInstance(provider, config || {}, modelName, embeddingModelName, ollamaBaseUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEmbeddingsProvider(config?: Record<string, string>): AIProvider {
|
export function getEmbeddingsProvider(config?: Record<string, string>): AIProvider {
|
||||||
@@ -129,8 +130,9 @@ export function getEmbeddingsProvider(config?: Record<string, string>): AIProvid
|
|||||||
const provider = providerType.toLowerCase() as ProviderType;
|
const provider = providerType.toLowerCase() as ProviderType;
|
||||||
const modelName = config?.AI_MODEL_TAGS || process.env.AI_MODEL_TAGS || 'granite4:latest';
|
const modelName = config?.AI_MODEL_TAGS || process.env.AI_MODEL_TAGS || 'granite4:latest';
|
||||||
const embeddingModelName = config?.AI_MODEL_EMBEDDING || process.env.AI_MODEL_EMBEDDING || 'embeddinggemma:latest';
|
const embeddingModelName = config?.AI_MODEL_EMBEDDING || process.env.AI_MODEL_EMBEDDING || 'embeddinggemma:latest';
|
||||||
|
const ollamaBaseUrl = config?.OLLAMA_BASE_URL_EMBEDDING || config?.OLLAMA_BASE_URL;
|
||||||
|
|
||||||
return getProviderInstance(provider, config || {}, modelName, embeddingModelName);
|
return getProviderInstance(provider, config || {}, modelName, embeddingModelName, ollamaBaseUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAIProvider(config?: Record<string, string>): AIProvider {
|
export function getAIProvider(config?: Record<string, string>): AIProvider {
|
||||||
@@ -167,6 +169,7 @@ export function getChatProvider(config?: Record<string, string>): AIProvider {
|
|||||||
'granite4:latest'
|
'granite4:latest'
|
||||||
);
|
);
|
||||||
const embeddingModelName = config?.AI_MODEL_EMBEDDING || process.env.AI_MODEL_EMBEDDING || 'embeddinggemma:latest';
|
const embeddingModelName = config?.AI_MODEL_EMBEDDING || process.env.AI_MODEL_EMBEDDING || 'embeddinggemma:latest';
|
||||||
|
const ollamaBaseUrl = config?.OLLAMA_BASE_URL_CHAT || config?.OLLAMA_BASE_URL_TAGS || config?.OLLAMA_BASE_URL_EMBEDDING || config?.OLLAMA_BASE_URL;
|
||||||
|
|
||||||
return getProviderInstance(provider, config || {}, modelName, embeddingModelName);
|
return getProviderInstance(provider, config || {}, modelName, embeddingModelName, ollamaBaseUrl);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,9 +49,10 @@ class ToolRegistry {
|
|||||||
/**
|
/**
|
||||||
* Build tools for the chat endpoint.
|
* Build tools for the chat endpoint.
|
||||||
* Includes internal tools (note_search, note_read) and web tools when configured.
|
* Includes internal tools (note_search, note_read) and web tools when configured.
|
||||||
|
* When webOnly is true, only web tools are included (no note access).
|
||||||
*/
|
*/
|
||||||
buildToolsForChat(ctx: ToolContext): Record<string, any> {
|
buildToolsForChat(ctx: ToolContext & { webOnly?: boolean }): Record<string, any> {
|
||||||
const toolNames: string[] = ['note_search', 'note_read']
|
const toolNames: string[] = ctx.webOnly ? [] : ['note_search', 'note_read']
|
||||||
|
|
||||||
// Add web tools only when user toggled web search AND config is present
|
// Add web tools only when user toggled web search AND config is present
|
||||||
if (ctx.webSearch) {
|
if (ctx.webSearch) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user