mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-06 05:55:28 +08:00
feat(assistant): add configurable max tool calls setting (#13398)
### What this PR does **Before this PR:** The MCP tool call limit was hard-coded to 20 in `parameterBuilder.ts`: ```typescript stopWhen: stepCountIs(20) ``` Users could not customize this limit, which was reported in #13370 as a limitation for workflows requiring more tool calls. **After this PR:** - Added configurable `maxToolCalls` setting (range: 1-100) in Assistant Settings → Model Settings - Added `enableMaxToolCalls` toggle to control the feature - Default behavior unchanged: 20 tool calls limit (matching AI SDK default) - Users can increase the limit if their workflow requires more tool calls - All i18n translations added for 11 languages Fixes #13370 ### Why we need it and why it was done in this way **Tradeoffs:** - Default value kept at 20 to prevent infinite loops and maintain backward compatibility - Range limited to 1-100 to prevent excessive token usage and API costs **Alternatives considered:** - Setting default to "unlimited" - rejected because AI SDK designed 20 as default for safety - Making it a global setting - rejected because different assistants may need different limits ### Breaking changes None. Default behavior remains unchanged (20 tool calls). Existing users will continue to have the same experience. ### Special notes for your reviewer - The `enableMaxToolCalls` field defaults to `true` to maintain the original 20-call behavior - When disabled, it still uses 20 (AI SDK's default), not unlimited - `getAssistantSettings()` properly returns the new fields for consistent access ### Checklist - [x] PR: The PR description is expressive enough and will help future contributors - [x] Code: [Write code that humans can understand](https://en.wikiquote.org/wiki/Martin_Fowler#code-for-humans) and [Keep it simple](https://en.wikipedia.org/wiki/KISS_principle) - [x] Refactor: You have [left the code cleaner than you found it (Boy Scout Rule)](https://learning.oreilly.com/library/view/97-things-every/9780596809515/ch08.html) - [x] Upgrade: Impact of this change on upgrade flows was considered and addressed if required - [x] Documentation: A [user-guide update](https://docs.cherry-ai.com) was considered and is present (link) or not required. Check this only when the PR introduces or changes a user-facing feature or behavior. - [x] Self-review: I have reviewed my own code (e.g., via [`/gh-pr-review`](/.claude/skills/gh-pr-review/SKILL.md), `gh pr diff`, or GitHub UI) before requesting review from others ### Release note ```release-note Added configurable maximum tool calls setting for assistants. Users can now customize the tool call limit (1-100) in Assistant Settings → Model Settings. Default remains at 20 calls to prevent infinite loops, matching AI SDK's design. ``` --------- Co-authored-by: icarus <eurfelux@gmail.com>
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* Tests for parameterBuilder maxToolCalls functionality
|
||||
* These tests verify the maxToolCalls calculation and validation logic in isolation
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
// Mirror the constants from parameterBuilder.ts
|
||||
const MIN_TOOL_CALLS = 1
|
||||
const MAX_TOOL_CALLS = 100
|
||||
const DEFAULT_MAX_TOOL_CALLS = 20
|
||||
const DEFAULT_ENABLE_MAX_TOOL_CALLS = true
|
||||
|
||||
/**
|
||||
* Validates and clamps maxToolCalls to valid range
|
||||
* Mirrors the logic in parameterBuilder.ts
|
||||
*/
|
||||
function validateMaxToolCalls(value: number | undefined): number {
|
||||
if (value === undefined || value < MIN_TOOL_CALLS || value > MAX_TOOL_CALLS) {
|
||||
return DEFAULT_MAX_TOOL_CALLS
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the effective max tool calls based on assistant settings
|
||||
* Mirrors the logic in parameterBuilder.ts
|
||||
*/
|
||||
function calculateEffectiveMaxToolCalls(settings?: { maxToolCalls?: number; enableMaxToolCalls?: boolean }): {
|
||||
stopWhen: number | null
|
||||
maxToolCalls: number
|
||||
} {
|
||||
const enableMaxToolCalls = settings?.enableMaxToolCalls ?? DEFAULT_ENABLE_MAX_TOOL_CALLS
|
||||
|
||||
if (!enableMaxToolCalls) {
|
||||
// When disabled, don't pass stopWhen (return null to indicate no stopWhen)
|
||||
return { stopWhen: null, maxToolCalls: DEFAULT_MAX_TOOL_CALLS }
|
||||
}
|
||||
|
||||
// When enabled, validate and use user-defined value
|
||||
const maxToolCalls = validateMaxToolCalls(settings?.maxToolCalls)
|
||||
return { stopWhen: maxToolCalls, maxToolCalls }
|
||||
}
|
||||
|
||||
describe('validateMaxToolCalls', () => {
|
||||
it('returns valid values as-is', () => {
|
||||
expect(validateMaxToolCalls(1)).toBe(1)
|
||||
expect(validateMaxToolCalls(50)).toBe(50)
|
||||
expect(validateMaxToolCalls(100)).toBe(100)
|
||||
})
|
||||
|
||||
it('clamps values above 100 to default', () => {
|
||||
const result = validateMaxToolCalls(999)
|
||||
expect(result).toBe(DEFAULT_MAX_TOOL_CALLS)
|
||||
})
|
||||
|
||||
it('clamps zero to default', () => {
|
||||
const result = validateMaxToolCalls(0)
|
||||
expect(result).toBe(DEFAULT_MAX_TOOL_CALLS)
|
||||
})
|
||||
|
||||
it('clamps negative values to default', () => {
|
||||
const result = validateMaxToolCalls(-5)
|
||||
expect(result).toBe(DEFAULT_MAX_TOOL_CALLS)
|
||||
})
|
||||
|
||||
it('returns default when value is undefined', () => {
|
||||
const result = validateMaxToolCalls(undefined)
|
||||
expect(result).toBe(DEFAULT_MAX_TOOL_CALLS)
|
||||
})
|
||||
})
|
||||
|
||||
describe('maxToolCalls calculation logic', () => {
|
||||
describe('default behavior', () => {
|
||||
it('uses default value 20 when settings are undefined', () => {
|
||||
const result = calculateEffectiveMaxToolCalls(undefined)
|
||||
expect(result.maxToolCalls).toBe(20)
|
||||
expect(result.stopWhen).toBe(20)
|
||||
})
|
||||
|
||||
it('uses default value 20 when settings is empty object', () => {
|
||||
const result = calculateEffectiveMaxToolCalls({})
|
||||
expect(result.maxToolCalls).toBe(20)
|
||||
expect(result.stopWhen).toBe(20)
|
||||
})
|
||||
|
||||
it('uses default value 20 when maxToolCalls is undefined', () => {
|
||||
const result = calculateEffectiveMaxToolCalls({
|
||||
enableMaxToolCalls: true
|
||||
// maxToolCalls is undefined
|
||||
})
|
||||
expect(result.maxToolCalls).toBe(20)
|
||||
expect(result.stopWhen).toBe(20)
|
||||
})
|
||||
|
||||
it('uses custom value when maxToolCalls is set and enabled', () => {
|
||||
const result = calculateEffectiveMaxToolCalls({
|
||||
enableMaxToolCalls: true,
|
||||
maxToolCalls: 50
|
||||
})
|
||||
expect(result.maxToolCalls).toBe(50)
|
||||
expect(result.stopWhen).toBe(50)
|
||||
})
|
||||
})
|
||||
|
||||
describe('custom values when enabled', () => {
|
||||
it('uses custom value when enableMaxToolCalls is true and maxToolCalls is set', () => {
|
||||
const result = calculateEffectiveMaxToolCalls({
|
||||
enableMaxToolCalls: true,
|
||||
maxToolCalls: 50
|
||||
})
|
||||
expect(result.stopWhen).toBe(50)
|
||||
})
|
||||
|
||||
it('uses custom value at minimum boundary (1)', () => {
|
||||
const result = calculateEffectiveMaxToolCalls({
|
||||
enableMaxToolCalls: true,
|
||||
maxToolCalls: 1
|
||||
})
|
||||
expect(result.stopWhen).toBe(1)
|
||||
})
|
||||
|
||||
it('uses custom value at maximum boundary (100)', () => {
|
||||
const result = calculateEffectiveMaxToolCalls({
|
||||
enableMaxToolCalls: true,
|
||||
maxToolCalls: 100
|
||||
})
|
||||
expect(result.stopWhen).toBe(100)
|
||||
})
|
||||
|
||||
it('clamps values above 100 to default when enabled', () => {
|
||||
const result = calculateEffectiveMaxToolCalls({
|
||||
enableMaxToolCalls: true,
|
||||
maxToolCalls: 999
|
||||
})
|
||||
expect(result.stopWhen).toBe(DEFAULT_MAX_TOOL_CALLS)
|
||||
})
|
||||
|
||||
it('clamps zero to default when enabled', () => {
|
||||
const result = calculateEffectiveMaxToolCalls({
|
||||
enableMaxToolCalls: true,
|
||||
maxToolCalls: 0
|
||||
})
|
||||
expect(result.stopWhen).toBe(DEFAULT_MAX_TOOL_CALLS)
|
||||
})
|
||||
|
||||
it('clamps negative values to default when enabled', () => {
|
||||
const result = calculateEffectiveMaxToolCalls({
|
||||
enableMaxToolCalls: true,
|
||||
maxToolCalls: -5
|
||||
})
|
||||
expect(result.stopWhen).toBe(DEFAULT_MAX_TOOL_CALLS)
|
||||
})
|
||||
})
|
||||
|
||||
describe('disabled behavior', () => {
|
||||
it('does not pass stopWhen when enableMaxToolCalls is false', () => {
|
||||
const result = calculateEffectiveMaxToolCalls({
|
||||
enableMaxToolCalls: false,
|
||||
maxToolCalls: 50
|
||||
})
|
||||
// When disabled, stopWhen should be null (indicating no stopWhen passed)
|
||||
expect(result.stopWhen).toBeNull()
|
||||
})
|
||||
|
||||
it('does not pass stopWhen when both enableMaxToolCalls is false and maxToolCalls is undefined', () => {
|
||||
const result = calculateEffectiveMaxToolCalls({
|
||||
enableMaxToolCalls: false
|
||||
})
|
||||
expect(result.stopWhen).toBeNull()
|
||||
})
|
||||
|
||||
it('falls back to default when disabled with invalid maxToolCalls', () => {
|
||||
const result = calculateEffectiveMaxToolCalls({
|
||||
enableMaxToolCalls: false,
|
||||
maxToolCalls: 999
|
||||
})
|
||||
// When disabled, maxToolCalls should still be default (for reference)
|
||||
expect(result.maxToolCalls).toBe(DEFAULT_MAX_TOOL_CALLS)
|
||||
expect(result.stopWhen).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('backward compatibility', () => {
|
||||
it('maintains backward compatibility - existing assistants without new fields use default', () => {
|
||||
// Simulate an old assistant without the new fields
|
||||
const oldSettings = {
|
||||
// Old assistants don't have enableMaxToolCalls or maxToolCalls
|
||||
temperature: 0.7,
|
||||
contextCount: 10
|
||||
}
|
||||
const result = calculateEffectiveMaxToolCalls(
|
||||
oldSettings as { maxToolCalls?: number; enableMaxToolCalls?: boolean }
|
||||
)
|
||||
// Should default to enabled with 20 for backward compatibility
|
||||
expect(result.maxToolCalls).toBe(20)
|
||||
expect(result.stopWhen).toBe(20)
|
||||
})
|
||||
})
|
||||
|
||||
describe('security - invalid values from imported/migrated settings', () => {
|
||||
it('validates extremely large values from imported settings', () => {
|
||||
const result = calculateEffectiveMaxToolCalls({
|
||||
enableMaxToolCalls: true,
|
||||
maxToolCalls: 999999
|
||||
})
|
||||
expect(result.stopWhen).toBe(DEFAULT_MAX_TOOL_CALLS)
|
||||
})
|
||||
|
||||
it('validates negative values from imported settings', () => {
|
||||
const result = calculateEffectiveMaxToolCalls({
|
||||
enableMaxToolCalls: true,
|
||||
maxToolCalls: -100
|
||||
})
|
||||
expect(result.stopWhen).toBe(DEFAULT_MAX_TOOL_CALLS)
|
||||
})
|
||||
|
||||
it('validates zero from imported settings', () => {
|
||||
const result = calculateEffectiveMaxToolCalls({
|
||||
enableMaxToolCalls: true,
|
||||
maxToolCalls: 0
|
||||
})
|
||||
expect(result.stopWhen).toBe(DEFAULT_MAX_TOOL_CALLS)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -13,6 +13,7 @@ import type { AnthropicSearchConfig, WebSearchPluginConfig } from '@cherrystudio
|
||||
import { isBaseProvider } from '@cherrystudio/ai-core/core/providers/schemas'
|
||||
import type { BaseProviderId } from '@cherrystudio/ai-core/provider'
|
||||
import { loggerService } from '@logger'
|
||||
import { MAX_TOOL_CALLS, MIN_TOOL_CALLS } from '@renderer/config/constant'
|
||||
import {
|
||||
isAnthropicModel,
|
||||
isFixedReasoningModel,
|
||||
@@ -27,7 +28,7 @@ import {
|
||||
isWebSearchModel
|
||||
} from '@renderer/config/models'
|
||||
import { getHubModeSystemPrompt } from '@renderer/config/prompts-code-mode'
|
||||
import { getDefaultModel } from '@renderer/services/AssistantService'
|
||||
import { DEFAULT_ASSISTANT_SETTINGS, getDefaultModel } from '@renderer/services/AssistantService'
|
||||
import store from '@renderer/store'
|
||||
import type { CherryWebSearchConfig } from '@renderer/store/websearch'
|
||||
import type { Model } from '@renderer/types'
|
||||
@@ -49,6 +50,19 @@ import { getMaxTokens, getTemperature, getTopP } from './modelParameters'
|
||||
|
||||
const logger = loggerService.withContext('parameterBuilder')
|
||||
|
||||
/**
|
||||
* Validates and clamps maxToolCalls to valid range
|
||||
* Falls back to DEFAULT_ASSISTANT_SETTINGS.maxToolCalls if invalid
|
||||
* @param value - The maxToolCalls value from settings
|
||||
* @returns Validated maxToolCalls value
|
||||
*/
|
||||
function validateMaxToolCalls(value: number | undefined): number {
|
||||
if (value === undefined || value < MIN_TOOL_CALLS || value > MAX_TOOL_CALLS) {
|
||||
return DEFAULT_ASSISTANT_SETTINGS.maxToolCalls
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
type ProviderDefinedTool = Extract<Tool<any, any>, { type: 'provider' }>
|
||||
|
||||
function mapVertexAIGatewayModelToProviderId(model: Model): BaseProviderId | undefined {
|
||||
@@ -229,6 +243,12 @@ export async function buildStreamTextParams(
|
||||
// Note: standardParams (topK, frequencyPenalty, presencePenalty, stopSequences, seed)
|
||||
// are extracted from custom parameters and passed directly to streamText()
|
||||
// instead of being placed in providerOptions
|
||||
|
||||
// Get max tool calls from assistant settings
|
||||
// When enabled, validate and use user-defined value (1-100)
|
||||
// When disabled, don't pass stopWhen - let AI SDK use its own default
|
||||
const enableMaxToolCalls = assistant.settings?.enableMaxToolCalls ?? DEFAULT_ASSISTANT_SETTINGS.enableMaxToolCalls
|
||||
|
||||
const params: StreamTextParams = {
|
||||
messages: sdkMessages,
|
||||
maxOutputTokens: getMaxTokens(assistant, model),
|
||||
@@ -239,10 +259,16 @@ export async function buildStreamTextParams(
|
||||
abortSignal: finalSignal,
|
||||
headers,
|
||||
providerOptions,
|
||||
stopWhen: stepCountIs(20),
|
||||
maxRetries: 0
|
||||
}
|
||||
|
||||
// Only add stopWhen when explicitly enabled and validated
|
||||
if (enableMaxToolCalls) {
|
||||
const maxToolCalls = validateMaxToolCalls(assistant.settings?.maxToolCalls)
|
||||
params.stopWhen = stepCountIs(maxToolCalls)
|
||||
}
|
||||
// When disabled, don't pass stopWhen - let AI SDK use its own default
|
||||
|
||||
if (tools) {
|
||||
params.tools = tools
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@ export const DEFAULT_KNOWLEDGE_THRESHOLD = 0.0
|
||||
export const DEFAULT_WEBSEARCH_RAG_DOCUMENT_COUNT = 1
|
||||
export const DEFAULT_STREAM_OPTIONS_INCLUDE_USAGE = true
|
||||
|
||||
// Max tool calls validation constants
|
||||
export const MIN_TOOL_CALLS = 1
|
||||
export const MAX_TOOL_CALLS = 100
|
||||
|
||||
export const platform = window.electron?.process?.platform
|
||||
export const isMac = platform === 'darwin'
|
||||
export const isWin = platform === 'win32' || platform === 'win64'
|
||||
|
||||
@@ -621,6 +621,10 @@
|
||||
"tip": "The assistant will use the large model's intent recognition capability to determine whether to use the knowledge base for answering. This feature will depend on the model's capabilities"
|
||||
}
|
||||
},
|
||||
"max_tool_calls": {
|
||||
"label": "Max Tool Calls",
|
||||
"tip": "Maximum number of tool calls allowed in a single conversation. Higher values allow more complex multi-step operations but may increase token usage and response time."
|
||||
},
|
||||
"mcp": {
|
||||
"description": "Default enabled MCP servers",
|
||||
"enableFirst": "Enable this server in MCP settings first",
|
||||
|
||||
@@ -621,6 +621,10 @@
|
||||
"tip": "助手将调用大模型的意图识别能力,判断是否需要调用知识库进行回答,该功能将依赖模型的能力"
|
||||
}
|
||||
},
|
||||
"max_tool_calls": {
|
||||
"label": "最大工具调用次数",
|
||||
"tip": "单次对话中允许的最大工具调用次数。较高的值支持更复杂的多步操作,但可能增加 Token 消耗和响应时间。"
|
||||
},
|
||||
"mcp": {
|
||||
"description": "默认启用的 MCP 服务器",
|
||||
"enableFirst": "请先在 MCP 设置中启用此服务器",
|
||||
|
||||
@@ -621,6 +621,10 @@
|
||||
"tip": "助手會使用大型語言模型的意圖識別能力,判斷是否需要查詢知識庫;此功能仰賴模型能力"
|
||||
}
|
||||
},
|
||||
"max_tool_calls": {
|
||||
"label": "最大工具呼叫次數",
|
||||
"tip": "單次對話中允許的最大工具呼叫次數。較高的值支援更複雜的多步操作,但可能增加 Token 消耗和回應時間。"
|
||||
},
|
||||
"mcp": {
|
||||
"description": "預設啟用的 MCP 伺服器",
|
||||
"enableFirst": "請先在 MCP 設定中啟用此伺服器",
|
||||
|
||||
@@ -621,6 +621,10 @@
|
||||
"tip": "Der Assistent verwendet die Absichtserkennungsfähigkeit des großen Modells, um zu bestimmen, ob die Wissensdatenbank für die Antwort aufgerufen werden muss. Diese Funktion hängt von den Fähigkeiten des Modells ab"
|
||||
}
|
||||
},
|
||||
"max_tool_calls": {
|
||||
"label": "Maximale Werkzeugaufrufe",
|
||||
"tip": "Maximale Anzahl von Tool-Aufrufen, die in einer einzelnen Konversation erlaubt sind. Höhere Werte ermöglichen komplexere, mehrstufige Operationen, können aber den Token-Verbrauch und die Antwortzeit erhöhen."
|
||||
},
|
||||
"mcp": {
|
||||
"description": "Standardmäßig aktivierte MCP-Server",
|
||||
"enableFirst": "Bitte aktivieren Sie diesen Server zuerst in den MCP-Einstellungen",
|
||||
|
||||
@@ -621,6 +621,10 @@
|
||||
"tip": "Ο πράκτορας θα καλέσει τη δυνατότητα αναγνώρισης πρόθεσης του μεγάλου μοντέλου για να αποφασίσει αν χρειάζεται να κληθεί η βάση γνώσης για να απαντηθεί, και αυτή η λειτουργία θα εξαρτηθεί από τις δυνατότητες του μοντέλου"
|
||||
}
|
||||
},
|
||||
"max_tool_calls": {
|
||||
"label": "Μέγιστος Αριθμός Κλήσεων Εργαλείων",
|
||||
"tip": "Μέγιστος αριθμός κλήσεων εργαλείων που επιτρέπονται σε μία συνομιλία. Υψηλότερες τιμές επιτρέπουν πιο σύνθετες πολυσταδιακές λειτουργίες, αλλά ενδέχεται να αυξήσουν τη χρήση token και τον χρόνο απόκρισης."
|
||||
},
|
||||
"mcp": {
|
||||
"description": "Διακομιστής MCP που είναι ενεργοποιημένος εξ ορισμού",
|
||||
"enableFirst": "Πρώτα ενεργοποιήστε αυτόν τον διακομιστή στις ρυθμίσεις MCP",
|
||||
|
||||
@@ -621,6 +621,10 @@
|
||||
"tip": "El agente utilizará la capacidad del modelo grande para el reconocimiento de intenciones y decidirá si necesita invocar la base de conocimientos para responder. Esta función dependerá de las capacidades del modelo"
|
||||
}
|
||||
},
|
||||
"max_tool_calls": {
|
||||
"label": "Llamadas Máximas de Herramientas",
|
||||
"tip": "Número máximo de llamadas a herramientas permitidas en una sola conversación. Valores más altos permiten operaciones de múltiples pasos más complejas, pero pueden incrementar el uso de tokens y el tiempo de respuesta."
|
||||
},
|
||||
"mcp": {
|
||||
"description": "Servidor MCP habilitado por defecto",
|
||||
"enableFirst": "Habilite este servidor en la configuración de MCP primero",
|
||||
|
||||
@@ -621,6 +621,10 @@
|
||||
"tip": "L'agent utilisera la capacité du grand modèle à reconnaître les intentions afin de déterminer si la base de connaissances doit être utilisée pour répondre. Cette fonctionnalité dépend des capacités du modèle"
|
||||
}
|
||||
},
|
||||
"max_tool_calls": {
|
||||
"label": "Maximiser les appels d'outils",
|
||||
"tip": "Nombre maximum d'appels d'outils autorisés dans une seule conversation. Des valeurs plus élevées permettent des opérations multi-étapes plus complexes, mais peuvent augmenter l'utilisation de tokens et le temps de réponse."
|
||||
},
|
||||
"mcp": {
|
||||
"description": "Serveur MCP activé par défaut",
|
||||
"enableFirst": "Veuillez d'abord activer ce serveur dans les paramètres MCP",
|
||||
|
||||
@@ -621,6 +621,10 @@
|
||||
"tip": "アシスタントは大規模言語モデルの意図認識能力を使用して、ナレッジベースを参照する必要があるかどうかを判断します。この機能はモデルの能力に依存します"
|
||||
}
|
||||
},
|
||||
"max_tool_calls": {
|
||||
"label": "最大ツール呼び出し数",
|
||||
"tip": "1回の会話で許可される最大のツール呼び出し回数。高い値に設定すると、より複雑な多段階操作が可能になりますが、トークン使用量と応答時間が増加する可能性があります。"
|
||||
},
|
||||
"mcp": {
|
||||
"description": "デフォルトで有効な MCP サーバー",
|
||||
"enableFirst": "まず MCP 設定でこのサーバーを有効にしてください",
|
||||
|
||||
@@ -621,6 +621,10 @@
|
||||
"tip": "O agente usará a capacidade de reconhecimento de intenção do grande modelo para decidir se deve chamar a base de conhecimento para responder. Esta função depende da capacidade do modelo"
|
||||
}
|
||||
},
|
||||
"max_tool_calls": {
|
||||
"label": "Máximo de Chamadas de Ferramentas",
|
||||
"tip": "Número máximo de chamadas de ferramentas permitidas em uma única conversa. Valores mais altos permitem operações mais complexas de múltiplas etapas, mas podem aumentar o uso de tokens e o tempo de resposta."
|
||||
},
|
||||
"mcp": {
|
||||
"description": "Servidor MCP ativado por padrão",
|
||||
"enableFirst": "Por favor, ative este servidor nas configurações do MCP primeiro",
|
||||
|
||||
@@ -621,6 +621,10 @@
|
||||
"tip": "Asistentul va folosi capacitatea de recunoaștere a intenției modelului mare pentru a determina dacă să folosească baza de cunoștințe pentru a răspunde. Această funcție depinde de capacitățile modelului"
|
||||
}
|
||||
},
|
||||
"max_tool_calls": {
|
||||
"label": "Apeluri Maxime de Instrumente",
|
||||
"tip": "Numărul maxim de apeluri de instrumente permise într-o singură conversație. Valori mai mari permit operații mai complexe, în mai multe etape, dar pot crește consumul de tokeni și timpul de răspuns."
|
||||
},
|
||||
"mcp": {
|
||||
"description": "Servere MCP activate implicit",
|
||||
"enableFirst": "Activează mai întâi acest server în setările MCP",
|
||||
|
||||
@@ -621,6 +621,10 @@
|
||||
"tip": "Ассистент будет использовать возможности большой модели для распознавания намерений, чтобы определить, нужно ли обращаться к базе знаний для ответа. Эта функция будет зависеть от возможностей модели"
|
||||
}
|
||||
},
|
||||
"max_tool_calls": {
|
||||
"label": "Максимальное количество вызовов инструментов",
|
||||
"tip": "Максимальное количество вызовов инструментов, разрешённых в одном диалоге. Более высокие значения позволяют выполнять более сложные многошаговые операции, но могут увеличить использование токенов и время ответа."
|
||||
},
|
||||
"mcp": {
|
||||
"description": "Серверы MCP, включенные по умолчанию",
|
||||
"enableFirst": "Сначала включите этот сервер в настройках MCP",
|
||||
|
||||
@@ -6,7 +6,13 @@ import { DeleteIcon, ResetIcon } from '@renderer/components/Icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { SelectModelPopup } from '@renderer/components/Popups/SelectModelPopup'
|
||||
import Selector from '@renderer/components/Selector'
|
||||
import { DEFAULT_CONTEXTCOUNT, DEFAULT_TEMPERATURE, MAX_CONTEXT_COUNT } from '@renderer/config/constant'
|
||||
import {
|
||||
DEFAULT_CONTEXTCOUNT,
|
||||
DEFAULT_TEMPERATURE,
|
||||
MAX_CONTEXT_COUNT,
|
||||
MAX_TOOL_CALLS,
|
||||
MIN_TOOL_CALLS
|
||||
} from '@renderer/config/constant'
|
||||
import { isEmbeddingModel, isRerankModel } from '@renderer/config/models'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { SettingRow } from '@renderer/pages/settings'
|
||||
@@ -17,7 +23,7 @@ import { Button, Col, Divider, Input, InputNumber, Row, Select, Slider, Switch,
|
||||
import { isNull } from 'lodash'
|
||||
import { PlusIcon } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@@ -30,19 +36,40 @@ interface Props {
|
||||
const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateAssistantSettings }) => {
|
||||
const [temperature, setTemperature] = useState(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE)
|
||||
const [contextCount, setContextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONTEXTCOUNT)
|
||||
const [enableMaxTokens, setEnableMaxTokens] = useState(assistant?.settings?.enableMaxTokens ?? false)
|
||||
const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0)
|
||||
const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput)
|
||||
const [toolUseMode, setToolUseMode] = useState<AssistantSettings['toolUseMode']>(
|
||||
assistant?.settings?.toolUseMode ?? 'function'
|
||||
const enableMaxTokens = useMemo(
|
||||
() => assistant?.settings?.enableMaxTokens ?? DEFAULT_ASSISTANT_SETTINGS.enableMaxTokens,
|
||||
[assistant?.settings?.enableMaxTokens]
|
||||
)
|
||||
const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0)
|
||||
const streamOutput = useMemo(
|
||||
() => assistant?.settings?.streamOutput ?? DEFAULT_ASSISTANT_SETTINGS.streamOutput,
|
||||
[assistant?.settings?.streamOutput]
|
||||
)
|
||||
const toolUseMode = useMemo(
|
||||
() => assistant?.settings?.toolUseMode ?? DEFAULT_ASSISTANT_SETTINGS.toolUseMode,
|
||||
[assistant?.settings?.toolUseMode]
|
||||
)
|
||||
const [maxToolCalls, setMaxToolCalls] = useState(assistant?.settings?.maxToolCalls ?? 20)
|
||||
const enableMaxToolCalls = useMemo(
|
||||
() => assistant?.settings?.enableMaxToolCalls ?? DEFAULT_ASSISTANT_SETTINGS.enableMaxToolCalls,
|
||||
[assistant?.settings?.enableMaxToolCalls]
|
||||
)
|
||||
const defaultModel = useMemo(
|
||||
() => assistant?.defaultModel ?? DEFAULT_ASSISTANT_SETTINGS.defaultModel,
|
||||
[assistant?.defaultModel]
|
||||
)
|
||||
const [defaultModel, setDefaultModel] = useState(assistant?.defaultModel)
|
||||
const [topP, setTopP] = useState(assistant?.settings?.topP ?? 1)
|
||||
const [enableTopP, setEnableTopP] = useState(assistant?.settings?.enableTopP ?? false)
|
||||
const enableTopP = useMemo(
|
||||
() => assistant?.settings?.enableTopP ?? DEFAULT_ASSISTANT_SETTINGS.enableTopP,
|
||||
[assistant?.settings?.enableTopP]
|
||||
)
|
||||
const [customParameters, setCustomParameters] = useState<AssistantSettingCustomParameters[]>(
|
||||
assistant?.settings?.customParameters ?? []
|
||||
)
|
||||
const [enableTemperature, setEnableTemperature] = useState(assistant?.settings?.enableTemperature ?? false)
|
||||
const enableTemperature = useMemo(
|
||||
() => assistant?.settings?.enableTemperature ?? DEFAULT_ASSISTANT_SETTINGS.enableTemperature,
|
||||
[assistant?.settings?.enableTemperature]
|
||||
)
|
||||
|
||||
const customParametersRef = useRef(customParameters)
|
||||
|
||||
@@ -184,15 +211,11 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
|
||||
const onReset = () => {
|
||||
setTemperature(DEFAULT_ASSISTANT_SETTINGS.temperature)
|
||||
setEnableTemperature(DEFAULT_ASSISTANT_SETTINGS.enableTemperature ?? false)
|
||||
setContextCount(DEFAULT_ASSISTANT_SETTINGS.contextCount)
|
||||
setEnableMaxTokens(DEFAULT_ASSISTANT_SETTINGS.enableMaxTokens ?? false)
|
||||
setMaxTokens(DEFAULT_ASSISTANT_SETTINGS.maxTokens ?? 0)
|
||||
setStreamOutput(DEFAULT_ASSISTANT_SETTINGS.streamOutput)
|
||||
setMaxTokens(DEFAULT_ASSISTANT_SETTINGS.maxTokens)
|
||||
setTopP(DEFAULT_ASSISTANT_SETTINGS.topP)
|
||||
setEnableTopP(DEFAULT_ASSISTANT_SETTINGS.enableTopP ?? false)
|
||||
setCustomParameters(DEFAULT_ASSISTANT_SETTINGS.customParameters ?? [])
|
||||
setToolUseMode(DEFAULT_ASSISTANT_SETTINGS.toolUseMode)
|
||||
setCustomParameters(DEFAULT_ASSISTANT_SETTINGS.customParameters)
|
||||
setMaxToolCalls(DEFAULT_ASSISTANT_SETTINGS.maxToolCalls)
|
||||
updateAssistantSettings(DEFAULT_ASSISTANT_SETTINGS)
|
||||
}
|
||||
const modelFilter = (model: Model) => !isEmbeddingModel(model) && !isRerankModel(model)
|
||||
@@ -201,13 +224,12 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
const currentModel = defaultModel ? assistant?.model : undefined
|
||||
const selectedModel = await SelectModelPopup.show({ model: currentModel, filter: modelFilter })
|
||||
if (selectedModel) {
|
||||
setDefaultModel(selectedModel)
|
||||
updateAssistant({
|
||||
...assistant,
|
||||
model: selectedModel,
|
||||
defaultModel: selectedModel
|
||||
})
|
||||
// TODO: 需要根据配置来设置默认值
|
||||
// TODO: 移除根据模型自动修改参数的逻辑
|
||||
if (selectedModel.name.includes('kimi-k2')) {
|
||||
setTemperature(0.6)
|
||||
setTimeoutTimer('onSelectModel_1', () => updateAssistantSettings({ temperature: 0.6 }), 500)
|
||||
@@ -244,7 +266,6 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
variant="filled"
|
||||
icon={<DeleteIcon size={14} className="lucide-custom" />}
|
||||
onClick={() => {
|
||||
setDefaultModel(undefined)
|
||||
updateAssistant({ ...assistant, defaultModel: undefined })
|
||||
}}
|
||||
danger
|
||||
@@ -266,7 +287,6 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
<Switch
|
||||
checked={enableTemperature}
|
||||
onChange={(enabled) => {
|
||||
setEnableTemperature(enabled)
|
||||
updateAssistantSettings({ enableTemperature: enabled })
|
||||
}}
|
||||
/>
|
||||
@@ -314,7 +334,6 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
<Switch
|
||||
checked={enableTopP}
|
||||
onChange={(enabled) => {
|
||||
setEnableTopP(enabled)
|
||||
updateAssistantSettings({ enableTopP: enabled })
|
||||
}}
|
||||
/>
|
||||
@@ -422,8 +441,6 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
})
|
||||
if (!confirmed) return
|
||||
}
|
||||
|
||||
setEnableMaxTokens(enabled)
|
||||
updateAssistantSettings({ enableMaxTokens: enabled })
|
||||
}}
|
||||
/>
|
||||
@@ -455,7 +472,6 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
<Switch
|
||||
checked={streamOutput}
|
||||
onChange={(checked) => {
|
||||
setStreamOutput(checked)
|
||||
updateAssistantSettings({ streamOutput: checked })
|
||||
}}
|
||||
/>
|
||||
@@ -470,13 +486,46 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
{ label: t('assistants.settings.tool_use_mode.function'), value: 'function' }
|
||||
]}
|
||||
onChange={(value) => {
|
||||
setToolUseMode(value)
|
||||
updateAssistantSettings({ toolUseMode: value })
|
||||
}}
|
||||
size={14}
|
||||
/>
|
||||
</SettingRow>
|
||||
<Divider style={{ margin: '10px 0' }} />
|
||||
<SettingRow style={{ minHeight: 30 }}>
|
||||
<HStack alignItems="center">
|
||||
<Label>{t('assistants.settings.max_tool_calls.label')}</Label>
|
||||
<Tooltip title={t('assistants.settings.max_tool_calls.tip')}>
|
||||
<QuestionIcon />
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
<Switch
|
||||
checked={enableMaxToolCalls}
|
||||
onChange={(enabled) => {
|
||||
updateAssistantSettings({ enableMaxToolCalls: enabled })
|
||||
}}
|
||||
/>
|
||||
</SettingRow>
|
||||
{enableMaxToolCalls && (
|
||||
<Row align="middle" style={{ marginTop: 5, marginBottom: 5 }}>
|
||||
<Col span={24}>
|
||||
<InputNumber
|
||||
min={MIN_TOOL_CALLS}
|
||||
max={MAX_TOOL_CALLS}
|
||||
step={1}
|
||||
value={maxToolCalls}
|
||||
onChange={(value) => {
|
||||
if (!isNull(value)) {
|
||||
setMaxToolCalls(value)
|
||||
setTimeoutTimer('maxToolCalls_onChange', () => updateAssistantSettings({ maxToolCalls: value }), 500)
|
||||
}
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
<Divider style={{ margin: '10px 0' }} />
|
||||
<SettingRow style={{ minHeight: 30 }}>
|
||||
<Label>{t('models.custom_parameters')}</Label>
|
||||
<Button icon={<PlusIcon size={18} />} onClick={onAddCustomParameter}>
|
||||
|
||||
@@ -55,7 +55,9 @@ export const DEFAULT_ASSISTANT_SETTINGS = {
|
||||
reasoning_effort_cache: undefined,
|
||||
qwenThinkMode: undefined,
|
||||
// It would gracefully fallback to prompt if not supported by model.
|
||||
toolUseMode: 'function'
|
||||
toolUseMode: 'function',
|
||||
maxToolCalls: 20,
|
||||
enableMaxToolCalls: true
|
||||
} as const satisfies AssistantSettings
|
||||
|
||||
/**
|
||||
@@ -248,6 +250,8 @@ export const getAssistantSettings = (assistant: Assistant): AssistantSettings =>
|
||||
maxTokens: getAssistantMaxTokens(),
|
||||
streamOutput: assistant?.settings?.streamOutput ?? DEFAULT_ASSISTANT_SETTINGS.streamOutput,
|
||||
toolUseMode: assistant?.settings?.toolUseMode ?? DEFAULT_ASSISTANT_SETTINGS.toolUseMode,
|
||||
maxToolCalls: assistant?.settings?.maxToolCalls ?? DEFAULT_ASSISTANT_SETTINGS.maxToolCalls,
|
||||
enableMaxToolCalls: assistant?.settings?.enableMaxToolCalls ?? DEFAULT_ASSISTANT_SETTINGS.enableMaxToolCalls,
|
||||
defaultModel: assistant?.defaultModel ?? DEFAULT_ASSISTANT_SETTINGS.defaultModel,
|
||||
reasoning_effort: assistant?.settings?.reasoning_effort ?? DEFAULT_ASSISTANT_SETTINGS.reasoning_effort,
|
||||
customParameters: assistant?.settings?.customParameters ?? DEFAULT_ASSISTANT_SETTINGS.customParameters
|
||||
|
||||
@@ -188,6 +188,8 @@ export type AssistantSettings = {
|
||||
reasoning_effort_cache?: ReasoningEffortOption
|
||||
qwenThinkMode?: boolean
|
||||
toolUseMode: 'function' | 'prompt'
|
||||
maxToolCalls?: number
|
||||
enableMaxToolCalls?: boolean
|
||||
}
|
||||
|
||||
export type AssistantPreset = Omit<Assistant, 'model'> & {
|
||||
|
||||
Reference in New Issue
Block a user