mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
feat(ai): enable automatic context compression
This commit is contained in:
1 parent
5c7ad41b23
commit
17320045dc
43 files changed
+546
-309
No files matched your search
@@ -12,6 +12,7 @@ import type {
|
|||||||
} from '@openchatlab/node-runtime'
|
} from '@openchatlab/node-runtime'
|
||||||
import {
|
import {
|
||||||
CHART_CAPABILITY_CORE_TOOLS,
|
CHART_CAPABILITY_CORE_TOOLS,
|
||||||
|
DEFAULT_CONTEXT_COMPRESSION_CONFIG,
|
||||||
SkillManager,
|
SkillManager,
|
||||||
buildPiModel,
|
buildPiModel,
|
||||||
buildSkillMenuWithBuiltinChart,
|
buildSkillMenuWithBuiltinChart,
|
||||||
@@ -93,7 +94,6 @@ export function createCliRunAgentStream(
|
|||||||
skillId,
|
skillId,
|
||||||
enableAutoSkill,
|
enableAutoSkill,
|
||||||
chartAutoMode,
|
chartAutoMode,
|
||||||
compressionConfig,
|
|
||||||
ownerInfo,
|
ownerInfo,
|
||||||
mentionedMembers,
|
mentionedMembers,
|
||||||
thinkingLevel,
|
thinkingLevel,
|
||||||
@@ -110,7 +110,7 @@ export function createCliRunAgentStream(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const llmConfig = llmConfigStore.getDefaultAssistantConfig()
|
const llmConfig = llmConfigStore.getDefaultAssistantConfig()
|
||||||
const maxToolResultPercent = compressionConfig?.maxToolResultPercent ?? 50
|
const maxToolResultPercent = DEFAULT_CONTEXT_COMPRESSION_CONFIG.maxToolResultPercent ?? 50
|
||||||
const contextWindow = llmConfig ? (buildPiModel(llmConfig).contextWindow ?? 128000) : 128000
|
const contextWindow = llmConfig ? (buildPiModel(llmConfig).contextWindow ?? 128000) : 128000
|
||||||
const maxToolResultTokens = Math.floor(contextWindow * (maxToolResultPercent / 100))
|
const maxToolResultTokens = Math.floor(contextWindow * (maxToolResultPercent / 100))
|
||||||
|
|
||||||
@@ -196,15 +196,6 @@ export function createCliRunAgentStream(
|
|||||||
agentTools.push(activateSkillTool as any)
|
agentTools.push(activateSkillTool as any)
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedCompression = compressionConfig?.enabled
|
|
||||||
? {
|
|
||||||
enabled: true as const,
|
|
||||||
tokenThresholdPercent: compressionConfig.tokenThresholdPercent ?? 75,
|
|
||||||
bufferSizePercent: compressionConfig.bufferSizePercent ?? 20,
|
|
||||||
maxToolResultPercent: compressionConfig.maxToolResultPercent,
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
let dataSnapshot: DataSnapshot | undefined
|
let dataSnapshot: DataSnapshot | undefined
|
||||||
if (db) {
|
if (db) {
|
||||||
try {
|
try {
|
||||||
@@ -223,7 +214,6 @@ export function createCliRunAgentStream(
|
|||||||
assistantSystemPrompt,
|
assistantSystemPrompt,
|
||||||
skillMenu: resolvedSkillMenu,
|
skillMenu: resolvedSkillMenu,
|
||||||
skillDef: resolvedSkillDef,
|
skillDef: resolvedSkillDef,
|
||||||
compressionConfig: resolvedCompression,
|
|
||||||
tools: agentTools,
|
tools: agentTools,
|
||||||
llmConfig,
|
llmConfig,
|
||||||
aiChatManager,
|
aiChatManager,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_MAX_TOOL_ROUNDS,
|
DEFAULT_MAX_TOOL_ROUNDS,
|
||||||
|
DEFAULT_CONTEXT_COMPRESSION_CONFIG,
|
||||||
buildPiModel,
|
buildPiModel,
|
||||||
buildPlanGuidance,
|
buildPlanGuidance,
|
||||||
createAnalysisPlanner,
|
createAnalysisPlanner,
|
||||||
@@ -27,7 +28,6 @@ import {
|
|||||||
type PiMessage,
|
type PiMessage,
|
||||||
type SimpleHistoryMessage,
|
type SimpleHistoryMessage,
|
||||||
type AIChatManager,
|
type AIChatManager,
|
||||||
type CompressionConfig,
|
|
||||||
type AgentTool,
|
type AgentTool,
|
||||||
type DataSnapshot,
|
type DataSnapshot,
|
||||||
type AIServiceConfig,
|
type AIServiceConfig,
|
||||||
@@ -49,7 +49,6 @@ export interface RunAgentOptions {
|
|||||||
assistantSystemPrompt?: string
|
assistantSystemPrompt?: string
|
||||||
skillMenu?: string | null
|
skillMenu?: string | null
|
||||||
skillDef?: { name: string; prompt: string }
|
skillDef?: { name: string; prompt: string }
|
||||||
compressionConfig?: CompressionConfig
|
|
||||||
tools?: AgentTool[]
|
tools?: AgentTool[]
|
||||||
llmConfig: AIServiceConfig | null
|
llmConfig: AIServiceConfig | null
|
||||||
aiChatManager: AIChatManager
|
aiChatManager: AIChatManager
|
||||||
@@ -72,7 +71,6 @@ export async function runServerAgent(options: RunAgentOptions): Promise<void> {
|
|||||||
assistantSystemPrompt,
|
assistantSystemPrompt,
|
||||||
skillMenu,
|
skillMenu,
|
||||||
skillDef,
|
skillDef,
|
||||||
compressionConfig,
|
|
||||||
tools = [],
|
tools = [],
|
||||||
llmConfig,
|
llmConfig,
|
||||||
aiChatManager,
|
aiChatManager,
|
||||||
@@ -123,7 +121,7 @@ export async function runServerAgent(options: RunAgentOptions): Promise<void> {
|
|||||||
systemPrompt,
|
systemPrompt,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (compressionConfig?.enabled && historyLeafMessageId === undefined) {
|
if (historyLeafMessageId === undefined) {
|
||||||
const llmAdapter = createCompressionLlmAdapter({
|
const llmAdapter = createCompressionLlmAdapter({
|
||||||
piModel,
|
piModel,
|
||||||
apiKey: llmConfig.apiKey,
|
apiKey: llmConfig.apiKey,
|
||||||
@@ -131,7 +129,7 @@ export async function runServerAgent(options: RunAgentOptions): Promise<void> {
|
|||||||
})
|
})
|
||||||
const compressionResult = await checkAndCompress(
|
const compressionResult = await checkAndCompress(
|
||||||
aiChatId,
|
aiChatId,
|
||||||
compressionConfig,
|
DEFAULT_CONTEXT_COMPRESSION_CONFIG,
|
||||||
systemPrompt,
|
systemPrompt,
|
||||||
llmAdapter,
|
llmAdapter,
|
||||||
aiChatManager,
|
aiChatManager,
|
||||||
@@ -148,7 +146,7 @@ export async function runServerAgent(options: RunAgentOptions): Promise<void> {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else if (compressionConfig?.enabled && historyLeafMessageId !== undefined) {
|
} else {
|
||||||
aiLogger?.info?.('Compression', 'Skipping compression for edited branch request', {
|
aiLogger?.info?.('Compression', 'Skipping compression for edited branch request', {
|
||||||
aiChatId,
|
aiChatId,
|
||||||
historyLeafMessageId,
|
historyLeafMessageId,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
buildSkillMenuWithBuiltinChart,
|
buildSkillMenuWithBuiltinChart,
|
||||||
CHART_CAPABILITY_ANALYSIS_TOOLS,
|
CHART_CAPABILITY_ANALYSIS_TOOLS,
|
||||||
checkAndCompress,
|
checkAndCompress,
|
||||||
|
DEFAULT_CONTEXT_COMPRESSION_CONFIG,
|
||||||
createCompressionLlmAdapter,
|
createCompressionLlmAdapter,
|
||||||
createDataSnapshotFromOverview,
|
createDataSnapshotFromOverview,
|
||||||
formatAIError,
|
formatAIError,
|
||||||
@@ -27,7 +28,7 @@ import {
|
|||||||
resolveChartRuntimeForRequest,
|
resolveChartRuntimeForRequest,
|
||||||
buildSemanticSearchGuidance,
|
buildSemanticSearchGuidance,
|
||||||
} from '@openchatlab/node-runtime'
|
} from '@openchatlab/node-runtime'
|
||||||
import type { CompressionConfig, CompressionLlmAdapter, AgentRuntimeStatus } from '@openchatlab/node-runtime'
|
import type { CompressionLlmAdapter, AgentRuntimeStatus } from '@openchatlab/node-runtime'
|
||||||
import { Agent, type AgentStreamChunk, type SkillContext } from './agent'
|
import { Agent, type AgentStreamChunk, type SkillContext } from './agent'
|
||||||
import type { ToolContext } from './tools/types'
|
import type { ToolContext } from './tools/types'
|
||||||
import { buildPiModel, findModelDefinition } from './llm'
|
import { buildPiModel, findModelDefinition } from './llm'
|
||||||
@@ -85,7 +86,6 @@ export function createElectronRunAgentStream(
|
|||||||
skillId,
|
skillId,
|
||||||
enableAutoSkill,
|
enableAutoSkill,
|
||||||
chartAutoMode,
|
chartAutoMode,
|
||||||
compressionConfig,
|
|
||||||
ownerInfo,
|
ownerInfo,
|
||||||
mentionedMembers,
|
mentionedMembers,
|
||||||
thinkingLevel,
|
thinkingLevel,
|
||||||
@@ -113,7 +113,7 @@ export function createElectronRunAgentStream(
|
|||||||
}
|
}
|
||||||
const piModel = buildPiModel(activeAIConfig)
|
const piModel = buildPiModel(activeAIConfig)
|
||||||
|
|
||||||
if (compressionConfig?.enabled && aiChatId && historyLeafMessageId === undefined) {
|
if (aiChatId && historyLeafMessageId === undefined) {
|
||||||
try {
|
try {
|
||||||
const tempAssistantConfig = assistantId
|
const tempAssistantConfig = assistantId
|
||||||
? (assistantManager.getAssistantConfig(assistantId) ?? undefined)
|
? (assistantManager.getAssistantConfig(assistantId) ?? undefined)
|
||||||
@@ -122,7 +122,7 @@ export function createElectronRunAgentStream(
|
|||||||
|
|
||||||
const compressionResult = await checkAndCompress(
|
const compressionResult = await checkAndCompress(
|
||||||
aiChatId,
|
aiChatId,
|
||||||
compressionConfig as CompressionConfig,
|
DEFAULT_CONTEXT_COMPRESSION_CONFIG,
|
||||||
systemPromptForCompression,
|
systemPromptForCompression,
|
||||||
buildCompressionAdapter(activeAIConfig, () => {
|
buildCompressionAdapter(activeAIConfig, () => {
|
||||||
onEvent({
|
onEvent({
|
||||||
@@ -242,7 +242,7 @@ export function createElectronRunAgentStream(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxToolResultPercent = compressionConfig?.maxToolResultPercent ?? 50
|
const maxToolResultPercent = DEFAULT_CONTEXT_COMPRESSION_CONFIG.maxToolResultPercent ?? 50
|
||||||
const modelDef = findModelDefinition(activeAIConfig.provider, activeAIConfig.model || '')
|
const modelDef = findModelDefinition(activeAIConfig.provider, activeAIConfig.model || '')
|
||||||
const resolvedContextWindow = modelDef?.contextWindow || DEFAULT_CONTEXT_WINDOW
|
const resolvedContextWindow = modelDef?.contextWindow || DEFAULT_CONTEXT_WINDOW
|
||||||
const maxToolResultTokens = Math.floor(resolvedContextWindow * (maxToolResultPercent / 100))
|
const maxToolResultTokens = Math.floor(resolvedContextWindow * (maxToolResultPercent / 100))
|
||||||
|
|||||||
@@ -288,6 +288,14 @@ function handleEvent(event: AgentStreamEvent) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (event.type === 'context-compression-start') {
|
||||||
|
processText.value = t('webAI.compressingContext')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.type === 'context-compression-finish') {
|
||||||
|
processText.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
const message = streamingMessage.value
|
const message = streamingMessage.value
|
||||||
if (!message) return
|
if (!message) return
|
||||||
if (event.type === 'text-delta') {
|
if (event.type === 'text-delta') {
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import { BUILTIN_MODELS, getBuiltinModelById } from '@openchatlab/core'
|
||||||
|
import { generateText } from 'ai'
|
||||||
|
|
||||||
|
import type { RunAgentInput, RuntimeContextSummary, RuntimeMessage } from './types'
|
||||||
|
|
||||||
|
const DEFAULT_CONTEXT_WINDOW = 32_768
|
||||||
|
const COMPRESSION_THRESHOLD_PERCENT = 75
|
||||||
|
const BUFFER_SIZE_PERCENT = 20
|
||||||
|
const MAX_SUMMARY_TOKENS = 4_096
|
||||||
|
const textEncoder = new TextEncoder()
|
||||||
|
|
||||||
|
const COMPRESSION_PROMPT = `You are compressing a long AI conversation so it can continue within the model context window.
|
||||||
|
|
||||||
|
Return only a concise structured summary in the same language as the conversation.
|
||||||
|
Preserve important facts, conclusions, user preferences, names, dates, unresolved questions, and next actions.
|
||||||
|
Merge the previous summary when present. Do not copy long passages verbatim.
|
||||||
|
|
||||||
|
{history}`
|
||||||
|
|
||||||
|
export function resolveRuntimeContextWindow(providerId: string, modelId: string): number {
|
||||||
|
const exact = getBuiltinModelById(providerId, modelId)
|
||||||
|
const knownById = exact ?? BUILTIN_MODELS.find((model) => model.id === modelId)
|
||||||
|
return knownById?.contextWindow ?? DEFAULT_CONTEXT_WINDOW
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function prepareContextMessages(
|
||||||
|
input: RunAgentInput,
|
||||||
|
storedMessages: RuntimeMessage[]
|
||||||
|
): Promise<RuntimeMessage[]> {
|
||||||
|
const previousSummary = await input.repository.getContextSummary(input.conversationId)
|
||||||
|
const { summary, recentMessages } = resolveCurrentHistory(storedMessages, previousSummary)
|
||||||
|
const effectiveMessages = summary ? [toSummaryMessage(summary), ...recentMessages] : recentMessages
|
||||||
|
const contextWindow = input.model.contextWindow ?? DEFAULT_CONTEXT_WINDOW
|
||||||
|
const thresholdTokens = Math.floor(contextWindow * (COMPRESSION_THRESHOLD_PERCENT / 100))
|
||||||
|
const currentTokens = estimateMessagesTokens(effectiveMessages, input.systemPrompt, input.userMessage)
|
||||||
|
|
||||||
|
if (currentTokens < thresholdTokens || recentMessages.length === 0) return effectiveMessages
|
||||||
|
|
||||||
|
const bufferTokenBudget = Math.floor(contextWindow * (BUFFER_SIZE_PERCENT / 100))
|
||||||
|
const { bufferMessages, messagesToCompress } = splitMessages(recentMessages, bufferTokenBudget)
|
||||||
|
if (messagesToCompress.length === 0) return effectiveMessages
|
||||||
|
|
||||||
|
input.onEvent({ type: 'context-compression-start' })
|
||||||
|
let compressed = false
|
||||||
|
try {
|
||||||
|
const history = formatCompressionHistory(summary, messagesToCompress)
|
||||||
|
const maxOutputTokens = Math.max(64, Math.min(MAX_SUMMARY_TOKENS, Math.floor(contextWindow * 0.1)))
|
||||||
|
const result = await generateText({
|
||||||
|
model: input.model.model,
|
||||||
|
prompt: COMPRESSION_PROMPT.replace('{history}', history),
|
||||||
|
maxOutputTokens,
|
||||||
|
abortSignal: input.signal,
|
||||||
|
})
|
||||||
|
const content = result.text.trim()
|
||||||
|
if (!content) return effectiveMessages
|
||||||
|
|
||||||
|
const boundaryMessageId = messagesToCompress.at(-1)!.id
|
||||||
|
const nextSummary = await input.repository.saveContextSummary({
|
||||||
|
conversationId: input.conversationId,
|
||||||
|
content,
|
||||||
|
boundaryMessageId,
|
||||||
|
compressedMessageCount: (summary?.compressedMessageCount ?? 0) + messagesToCompress.length,
|
||||||
|
})
|
||||||
|
compressed = true
|
||||||
|
return [toSummaryMessage(nextSummary), ...bufferMessages]
|
||||||
|
} catch (error) {
|
||||||
|
if (input.signal.aborted) throw error
|
||||||
|
return effectiveMessages
|
||||||
|
} finally {
|
||||||
|
input.onEvent({ type: 'context-compression-finish', compressed })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveCurrentHistory(
|
||||||
|
messages: RuntimeMessage[],
|
||||||
|
summary: RuntimeContextSummary | null
|
||||||
|
): { summary: RuntimeContextSummary | null; recentMessages: RuntimeMessage[] } {
|
||||||
|
if (!summary) return { summary: null, recentMessages: messages }
|
||||||
|
const boundaryIndex = messages.findIndex((message) => message.id === summary.boundaryMessageId)
|
||||||
|
if (boundaryIndex < 0) return { summary: null, recentMessages: messages }
|
||||||
|
return { summary, recentMessages: messages.slice(boundaryIndex + 1) }
|
||||||
|
}
|
||||||
|
|
||||||
|
function toSummaryMessage(summary: RuntimeContextSummary): RuntimeMessage {
|
||||||
|
return {
|
||||||
|
id: `context-summary:${summary.conversationId}`,
|
||||||
|
conversationId: summary.conversationId,
|
||||||
|
role: 'assistant',
|
||||||
|
content: summary.content,
|
||||||
|
createdAt: summary.updatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitMessages(
|
||||||
|
messages: RuntimeMessage[],
|
||||||
|
bufferTokenBudget: number
|
||||||
|
): { bufferMessages: RuntimeMessage[]; messagesToCompress: RuntimeMessage[] } {
|
||||||
|
let bufferTokens = 0
|
||||||
|
let splitIndex = messages.length
|
||||||
|
for (let index = messages.length - 1; index >= 0; index--) {
|
||||||
|
const messageTokens = estimateMessageTokens(messages[index])
|
||||||
|
if (bufferTokens + messageTokens > bufferTokenBudget) {
|
||||||
|
splitIndex = index + 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
bufferTokens += messageTokens
|
||||||
|
if (index === 0) splitIndex = 0
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
bufferMessages: messages.slice(splitIndex),
|
||||||
|
messagesToCompress: messages.slice(0, splitIndex),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function estimateMessagesTokens(messages: RuntimeMessage[], systemPrompt: string, userMessage: string): number {
|
||||||
|
return (
|
||||||
|
estimateTextTokens(systemPrompt) +
|
||||||
|
estimateTextTokens(userMessage) +
|
||||||
|
messages.reduce((total, message) => total + estimateMessageTokens(message), 0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function estimateMessageTokens(message: RuntimeMessage): number {
|
||||||
|
return estimateTextTokens(message.content) + 4
|
||||||
|
}
|
||||||
|
|
||||||
|
function estimateTextTokens(text: string): number {
|
||||||
|
// UTF-8 bytes / 3 is deliberately conservative for mixed Chinese and Latin
|
||||||
|
// text while keeping this browser runtime free of a tokenizer dependency.
|
||||||
|
return Math.ceil(textEncoder.encode(text).byteLength / 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCompressionHistory(summary: RuntimeContextSummary | null, messages: RuntimeMessage[]): string {
|
||||||
|
const sections: string[] = []
|
||||||
|
if (summary) sections.push(`[PREVIOUS SUMMARY]\n${summary.content}`)
|
||||||
|
sections.push(
|
||||||
|
`[MESSAGES]\n${messages.map((message) => `[${message.role.toUpperCase()}]\n${message.content}`).join('\n\n')}`
|
||||||
|
)
|
||||||
|
return sections.join('\n\n')
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from './default-assistant'
|
export * from './default-assistant'
|
||||||
|
export * from './context-compression'
|
||||||
export * from './errors'
|
export * from './errors'
|
||||||
export * from './runtime'
|
export * from './runtime'
|
||||||
export * from './token-budget'
|
export * from './token-budget'
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import type {
|
|||||||
AppendRuntimeMessageInput,
|
AppendRuntimeMessageInput,
|
||||||
ConversationRepository,
|
ConversationRepository,
|
||||||
RuntimeConversation,
|
RuntimeConversation,
|
||||||
|
RuntimeContextSummary,
|
||||||
RuntimeMessage,
|
RuntimeMessage,
|
||||||
|
RuntimeModel,
|
||||||
|
SaveRuntimeContextSummaryInput,
|
||||||
RuntimeToolDefinition,
|
RuntimeToolDefinition,
|
||||||
RuntimeToolResult,
|
RuntimeToolResult,
|
||||||
ToolExecutionContext,
|
ToolExecutionContext,
|
||||||
@@ -23,6 +26,7 @@ const EMPTY_USAGE = {
|
|||||||
|
|
||||||
class MemoryRepository implements ConversationRepository {
|
class MemoryRepository implements ConversationRepository {
|
||||||
readonly messages: RuntimeMessage[] = []
|
readonly messages: RuntimeMessage[] = []
|
||||||
|
contextSummary: RuntimeContextSummary | null = null
|
||||||
readonly conversation: RuntimeConversation = {
|
readonly conversation: RuntimeConversation = {
|
||||||
id: 'conversation-1',
|
id: 'conversation-1',
|
||||||
sessionId: 'session-1',
|
sessionId: 'session-1',
|
||||||
@@ -57,6 +61,15 @@ class MemoryRepository implements ConversationRepository {
|
|||||||
const message = this.messages.find((item) => item.id === id)
|
const message = this.messages.find((item) => item.id === id)
|
||||||
if (message) Object.assign(message, patch)
|
if (message) Object.assign(message, patch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getContextSummary(conversationId: string): Promise<RuntimeContextSummary | null> {
|
||||||
|
return this.contextSummary?.conversationId === conversationId ? this.contextSummary : null
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveContextSummary(input: SaveRuntimeContextSummaryInput): Promise<RuntimeContextSummary> {
|
||||||
|
this.contextSummary = { ...input, updatedAt: Date.now() }
|
||||||
|
return this.contextSummary
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FakeToolExecutor implements ToolExecutor {
|
class FakeToolExecutor implements ToolExecutor {
|
||||||
@@ -296,4 +309,181 @@ describe('runAgent', () => {
|
|||||||
assert.equal(result.message.blocks?.[0]?.type === 'text' ? result.message.blocks[0].text : null, '先确认数据。')
|
assert.equal(result.message.blocks?.[0]?.type === 'text' ? result.message.blocks[0].text : null, '先确认数据。')
|
||||||
assert.equal(result.message.blocks?.[3]?.type === 'text' ? result.message.blocks[3].text : null, '最终结论。')
|
assert.equal(result.message.blocks?.[3]?.type === 'text' ? result.message.blocks[3].text : null, '最终结论。')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('automatically compresses oversized browser history without deleting original messages', async () => {
|
||||||
|
const model = new MockLanguageModelV3({
|
||||||
|
doGenerate: {
|
||||||
|
content: [{ type: 'text', text: 'COMPRESSED CONTEXT' }],
|
||||||
|
finishReason: { unified: 'stop', raw: 'stop' },
|
||||||
|
usage: EMPTY_USAGE,
|
||||||
|
warnings: [],
|
||||||
|
},
|
||||||
|
doStream: {
|
||||||
|
stream: simulateReadableStream({
|
||||||
|
chunks: [
|
||||||
|
{ type: 'stream-start', warnings: [] },
|
||||||
|
{ type: 'text-start', id: 'text-1' },
|
||||||
|
{ type: 'text-delta', id: 'text-1', delta: '继续回答' },
|
||||||
|
{ type: 'text-end', id: 'text-1' },
|
||||||
|
{ type: 'finish', finishReason: { unified: 'stop', raw: 'stop' }, usage: EMPTY_USAGE },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const repository = new MemoryRepository()
|
||||||
|
for (let index = 0; index < 8; index++) {
|
||||||
|
await repository.appendMessage({
|
||||||
|
conversationId: 'conversation-1',
|
||||||
|
role: index % 2 === 0 ? 'user' : 'assistant',
|
||||||
|
content: `old-marker-${index} ${'history '.repeat(20)}`,
|
||||||
|
createdAt: index + 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const originalMessageIds = repository.messages.map((message) => message.id)
|
||||||
|
const events: AgentStreamEvent[] = []
|
||||||
|
|
||||||
|
await runAgent({
|
||||||
|
conversationId: 'conversation-1',
|
||||||
|
sessionId: 'session-1',
|
||||||
|
systemPrompt: 'You are a test assistant.',
|
||||||
|
userMessage: 'continue',
|
||||||
|
model: { model, contextWindow: 240 } as RuntimeModel,
|
||||||
|
repository,
|
||||||
|
tools: new FakeToolExecutor(),
|
||||||
|
signal: new AbortController().signal,
|
||||||
|
onEvent: (event) => events.push(event),
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(repository.contextSummary?.content, 'COMPRESSED CONTEXT')
|
||||||
|
assert.ok(repository.contextSummary?.compressedMessageCount)
|
||||||
|
assert.deepEqual(
|
||||||
|
repository.messages.slice(0, originalMessageIds.length).map((message) => message.id),
|
||||||
|
originalMessageIds
|
||||||
|
)
|
||||||
|
const modelPrompt = JSON.stringify(model.doStreamCalls[0]?.prompt)
|
||||||
|
assert.match(modelPrompt, /COMPRESSED CONTEXT/)
|
||||||
|
assert.doesNotMatch(modelPrompt, /old-marker-0/)
|
||||||
|
assert.equal(
|
||||||
|
events.some((event) => event.type === 'context-compression-start'),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
events.some((event) => event.type === 'context-compression-finish'),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('merges an existing browser summary into the next automatic compression', async () => {
|
||||||
|
const model = new MockLanguageModelV3({
|
||||||
|
doGenerate: {
|
||||||
|
content: [{ type: 'text', text: 'UPDATED SUMMARY' }],
|
||||||
|
finishReason: { unified: 'stop', raw: 'stop' },
|
||||||
|
usage: EMPTY_USAGE,
|
||||||
|
warnings: [],
|
||||||
|
},
|
||||||
|
doStream: {
|
||||||
|
stream: simulateReadableStream({
|
||||||
|
chunks: [
|
||||||
|
{ type: 'stream-start', warnings: [] },
|
||||||
|
{ type: 'text-start', id: 'text-1' },
|
||||||
|
{ type: 'text-delta', id: 'text-1', delta: '继续回答' },
|
||||||
|
{ type: 'text-end', id: 'text-1' },
|
||||||
|
{ type: 'finish', finishReason: { unified: 'stop', raw: 'stop' }, usage: EMPTY_USAGE },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const repository = new MemoryRepository()
|
||||||
|
const boundary = await repository.appendMessage({
|
||||||
|
conversationId: 'conversation-1',
|
||||||
|
role: 'assistant',
|
||||||
|
content: 'already summarized',
|
||||||
|
createdAt: 1,
|
||||||
|
})
|
||||||
|
repository.contextSummary = {
|
||||||
|
conversationId: 'conversation-1',
|
||||||
|
content: 'PREVIOUS SUMMARY',
|
||||||
|
boundaryMessageId: boundary.id,
|
||||||
|
compressedMessageCount: 4,
|
||||||
|
updatedAt: 2,
|
||||||
|
}
|
||||||
|
for (let index = 0; index < 8; index++) {
|
||||||
|
await repository.appendMessage({
|
||||||
|
conversationId: 'conversation-1',
|
||||||
|
role: index % 2 === 0 ? 'user' : 'assistant',
|
||||||
|
content: `new-marker-${index} ${'history '.repeat(20)}`,
|
||||||
|
createdAt: index + 3,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await runAgent({
|
||||||
|
conversationId: 'conversation-1',
|
||||||
|
sessionId: 'session-1',
|
||||||
|
systemPrompt: 'You are a test assistant.',
|
||||||
|
userMessage: 'continue',
|
||||||
|
model: { model, contextWindow: 240 },
|
||||||
|
repository,
|
||||||
|
tools: new FakeToolExecutor(),
|
||||||
|
signal: new AbortController().signal,
|
||||||
|
onEvent: () => undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
const compressionPrompt = JSON.stringify(model.doGenerateCalls[0]?.prompt)
|
||||||
|
assert.match(compressionPrompt, /PREVIOUS SUMMARY/)
|
||||||
|
assert.match(compressionPrompt, /new-marker-0/)
|
||||||
|
assert.equal(repository.contextSummary?.content, 'UPDATED SUMMARY')
|
||||||
|
assert.ok((repository.contextSummary?.compressedMessageCount ?? 0) > 4)
|
||||||
|
const modelPrompt = JSON.stringify(model.doStreamCalls[0]?.prompt)
|
||||||
|
assert.match(modelPrompt, /UPDATED SUMMARY/)
|
||||||
|
assert.doesNotMatch(modelPrompt, /PREVIOUS SUMMARY/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('continues with the original browser history when automatic compression fails', async () => {
|
||||||
|
const model = new MockLanguageModelV3({
|
||||||
|
doGenerate: async () => {
|
||||||
|
throw new Error('compression failed')
|
||||||
|
},
|
||||||
|
doStream: {
|
||||||
|
stream: simulateReadableStream({
|
||||||
|
chunks: [
|
||||||
|
{ type: 'stream-start', warnings: [] },
|
||||||
|
{ type: 'text-start', id: 'text-1' },
|
||||||
|
{ type: 'text-delta', id: 'text-1', delta: 'fallback answer' },
|
||||||
|
{ type: 'text-end', id: 'text-1' },
|
||||||
|
{ type: 'finish', finishReason: { unified: 'stop', raw: 'stop' }, usage: EMPTY_USAGE },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const repository = new MemoryRepository()
|
||||||
|
for (let index = 0; index < 8; index++) {
|
||||||
|
await repository.appendMessage({
|
||||||
|
conversationId: 'conversation-1',
|
||||||
|
role: index % 2 === 0 ? 'user' : 'assistant',
|
||||||
|
content: `fallback-marker-${index} ${'history '.repeat(20)}`,
|
||||||
|
createdAt: index + 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const events: AgentStreamEvent[] = []
|
||||||
|
|
||||||
|
const result = await runAgent({
|
||||||
|
conversationId: 'conversation-1',
|
||||||
|
sessionId: 'session-1',
|
||||||
|
systemPrompt: 'You are a test assistant.',
|
||||||
|
userMessage: 'continue',
|
||||||
|
model: { model, contextWindow: 240 },
|
||||||
|
repository,
|
||||||
|
tools: new FakeToolExecutor(),
|
||||||
|
signal: new AbortController().signal,
|
||||||
|
onEvent: (event) => events.push(event),
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(result.message.content, 'fallback answer')
|
||||||
|
assert.equal(repository.contextSummary, null)
|
||||||
|
assert.match(JSON.stringify(model.doStreamCalls[0]?.prompt), /fallback-marker-0/)
|
||||||
|
assert.deepEqual(
|
||||||
|
events.find((event) => event.type === 'context-compression-finish'),
|
||||||
|
{ type: 'context-compression-finish', compressed: false }
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@@ -2,6 +2,7 @@ import type { ModelMessage, ToolSet } from 'ai'
|
|||||||
import { jsonSchema, stepCountIs, streamText, tool } from 'ai'
|
import { jsonSchema, stepCountIs, streamText, tool } from 'ai'
|
||||||
|
|
||||||
import { normalizeRuntimeError } from './errors'
|
import { normalizeRuntimeError } from './errors'
|
||||||
|
import { prepareContextMessages } from './context-compression'
|
||||||
import { truncateToolResult } from './token-budget'
|
import { truncateToolResult } from './token-budget'
|
||||||
import type {
|
import type {
|
||||||
AgentStreamEvent,
|
AgentStreamEvent,
|
||||||
@@ -86,6 +87,7 @@ export async function runAgent(input: RunAgentInput): Promise<RunAgentResult> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const storedMessages = await input.repository.getMessages(input.conversationId)
|
const storedMessages = await input.repository.getMessages(input.conversationId)
|
||||||
|
const contextMessages = await prepareContextMessages(input, storedMessages)
|
||||||
|
|
||||||
const blocks: RuntimeContentBlock[] = []
|
const blocks: RuntimeContentBlock[] = []
|
||||||
const toolsUsed = new Set<string>()
|
const toolsUsed = new Set<string>()
|
||||||
@@ -156,7 +158,7 @@ export async function runAgent(input: RunAgentInput): Promise<RunAgentResult> {
|
|||||||
const result = streamText({
|
const result = streamText({
|
||||||
model: input.model.model,
|
model: input.model.model,
|
||||||
instructions: input.systemPrompt,
|
instructions: input.systemPrompt,
|
||||||
messages: toModelMessages(storedMessages, input.userMessage),
|
messages: toModelMessages(contextMessages, input.userMessage),
|
||||||
tools: toolSet,
|
tools: toolSet,
|
||||||
stopWhen: stepCountIs(input.maxToolSteps ?? 8),
|
stopWhen: stepCountIs(input.maxToolSteps ?? 8),
|
||||||
maxOutputTokens: input.maxOutputTokens ?? 4_096,
|
maxOutputTokens: input.maxOutputTokens ?? 4_096,
|
||||||
|
|||||||
@@ -21,6 +21,16 @@ export interface RuntimeMessage {
|
|||||||
usage?: TokenUsage
|
usage?: TokenUsage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RuntimeContextSummary {
|
||||||
|
conversationId: string
|
||||||
|
content: string
|
||||||
|
boundaryMessageId: string
|
||||||
|
compressedMessageCount: number
|
||||||
|
updatedAt: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SaveRuntimeContextSummaryInput = Omit<RuntimeContextSummary, 'updatedAt'>
|
||||||
|
|
||||||
export type RuntimeContentBlock =
|
export type RuntimeContentBlock =
|
||||||
| { type: 'text'; text: string }
|
| { type: 'text'; text: string }
|
||||||
| { type: 'reasoning'; text: string }
|
| { type: 'reasoning'; text: string }
|
||||||
@@ -48,6 +58,8 @@ export interface AppendRuntimeMessageInput {
|
|||||||
export interface ConversationRepository {
|
export interface ConversationRepository {
|
||||||
getConversation(id: string): Promise<RuntimeConversation | null>
|
getConversation(id: string): Promise<RuntimeConversation | null>
|
||||||
getMessages(conversationId: string): Promise<RuntimeMessage[]>
|
getMessages(conversationId: string): Promise<RuntimeMessage[]>
|
||||||
|
getContextSummary(conversationId: string): Promise<RuntimeContextSummary | null>
|
||||||
|
saveContextSummary(input: SaveRuntimeContextSummaryInput): Promise<RuntimeContextSummary>
|
||||||
appendMessage(input: AppendRuntimeMessageInput): Promise<RuntimeMessage>
|
appendMessage(input: AppendRuntimeMessageInput): Promise<RuntimeMessage>
|
||||||
updateMessage(id: string, patch: Pick<RuntimeMessage, 'content' | 'blocks' | 'usage'>): Promise<void>
|
updateMessage(id: string, patch: Pick<RuntimeMessage, 'content' | 'blocks' | 'usage'>): Promise<void>
|
||||||
}
|
}
|
||||||
@@ -83,12 +95,15 @@ export interface ToolExecutor {
|
|||||||
|
|
||||||
export interface RuntimeModel {
|
export interface RuntimeModel {
|
||||||
model: LanguageModel
|
model: LanguageModel
|
||||||
|
contextWindow?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FinishReason = 'stop' | 'length' | 'content-filter' | 'tool-calls' | 'error' | 'aborted' | 'unknown'
|
export type FinishReason = 'stop' | 'length' | 'content-filter' | 'tool-calls' | 'error' | 'aborted' | 'unknown'
|
||||||
|
|
||||||
export type AgentStreamEvent =
|
export type AgentStreamEvent =
|
||||||
| { type: 'start'; requestId: string; messageId: string }
|
| { type: 'start'; requestId: string; messageId: string }
|
||||||
|
| { type: 'context-compression-start' }
|
||||||
|
| { type: 'context-compression-finish'; compressed: boolean }
|
||||||
| { type: 'text-delta'; delta: string }
|
| { type: 'text-delta'; delta: string }
|
||||||
| { type: 'reasoning-delta'; delta: string }
|
| { type: 'reasoning-delta'; delta: string }
|
||||||
| { type: 'tool-start'; callId: string; name: string; input: unknown }
|
| { type: 'tool-start'; callId: string; name: string; input: unknown }
|
||||||
|
|||||||
@@ -39,12 +39,6 @@ export interface AgentStreamRequest {
|
|||||||
skillId?: string | null
|
skillId?: string | null
|
||||||
enableAutoSkill?: boolean
|
enableAutoSkill?: boolean
|
||||||
chartAutoMode?: ChartAutoMode
|
chartAutoMode?: ChartAutoMode
|
||||||
compressionConfig?: {
|
|
||||||
enabled: boolean
|
|
||||||
tokenThresholdPercent?: number
|
|
||||||
bufferSizePercent?: number
|
|
||||||
maxToolResultPercent?: number
|
|
||||||
}
|
|
||||||
ownerInfo?: { platformId: string; displayName: string }
|
ownerInfo?: { platformId: string; displayName: string }
|
||||||
mentionedMembers?: Array<{
|
mentionedMembers?: Array<{
|
||||||
memberId: number
|
memberId: number
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ function cleanup(dir: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CONFIG: CompressionConfig = {
|
const CONFIG: CompressionConfig = {
|
||||||
enabled: true,
|
|
||||||
tokenThresholdPercent: 75,
|
tokenThresholdPercent: 75,
|
||||||
bufferSizePercent: 20,
|
bufferSizePercent: 20,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,10 +70,6 @@ export async function checkAndCompress(
|
|||||||
convManager: AIChatManager,
|
convManager: AIChatManager,
|
||||||
logger: CompressionLogger = defaultLogger
|
logger: CompressionLogger = defaultLogger
|
||||||
): Promise<CompressionResult> {
|
): Promise<CompressionResult> {
|
||||||
if (!config.enabled) {
|
|
||||||
return { compressed: false, reason: 'skipped_disabled' }
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const contextWindow = llmAdapter.contextWindow || DEFAULT_CONTEXT_WINDOW
|
const contextWindow = llmAdapter.contextWindow || DEFAULT_CONTEXT_WINDOW
|
||||||
const thresholdTokens = Math.floor(contextWindow * (config.tokenThresholdPercent / 100) * 0.95)
|
const thresholdTokens = Math.floor(contextWindow * (config.tokenThresholdPercent / 100) * 0.95)
|
||||||
@@ -198,7 +194,7 @@ export async function manualCompress(
|
|||||||
return { compressed: false, reason: 'skipped_idempotent' }
|
return { compressed: false, reason: 'skipped_idempotent' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const overrideConfig = { ...config, enabled: true, tokenThresholdPercent: 0 }
|
const overrideConfig = { ...config, tokenThresholdPercent: 0 }
|
||||||
return checkAndCompress(aiChatId, overrideConfig, systemPrompt, llmAdapter, convManager, logger)
|
return checkAndCompress(aiChatId, overrideConfig, systemPrompt, llmAdapter, convManager, logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,5 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export type { CompressionConfig, CompressionResult, CompressionLogger, CompressionLlmAdapter } from './types'
|
export type { CompressionConfig, CompressionResult, CompressionLogger, CompressionLlmAdapter } from './types'
|
||||||
|
export { DEFAULT_CONTEXT_COMPRESSION_CONFIG } from './types'
|
||||||
export { checkAndCompress, manualCompress } from './compressor'
|
export { checkAndCompress, manualCompress } from './compressor'
|
||||||
export { createCompressionLlmAdapter, type CreateCompressionLlmAdapterOptions } from './adapter-factory'
|
export { createCompressionLlmAdapter, type CreateCompressionLlmAdapterOptions } from './adapter-factory'
|
||||||
@@ -1,23 +1,21 @@
|
|||||||
export interface CompressionConfig {
|
export interface CompressionConfig {
|
||||||
enabled: boolean
|
|
||||||
/** 触发压缩的 token 阈值百分比(相对于 context window),默认 75 */
|
/** 触发压缩的 token 阈值百分比(相对于 context window),默认 75 */
|
||||||
tokenThresholdPercent: number
|
tokenThresholdPercent: number
|
||||||
/** 保留最近消息的缓冲区大小(相对于 context window 的百分比),默认 20 */
|
/** 保留最近消息的缓冲区大小(相对于 context window 的百分比),默认 20 */
|
||||||
bufferSizePercent: number
|
bufferSizePercent: number
|
||||||
/** 单次工具返回的最大上下文占比(相对于 context window 的百分比),默认 35 */
|
/** 单次工具返回的最大上下文占比(相对于 context window 的百分比),默认 50 */
|
||||||
maxToolResultPercent?: number
|
maxToolResultPercent?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_CONTEXT_COMPRESSION_CONFIG: Readonly<CompressionConfig> = Object.freeze({
|
||||||
|
tokenThresholdPercent: 75,
|
||||||
|
bufferSizePercent: 20,
|
||||||
|
maxToolResultPercent: 50,
|
||||||
|
})
|
||||||
|
|
||||||
export interface CompressionResult {
|
export interface CompressionResult {
|
||||||
compressed: boolean
|
compressed: boolean
|
||||||
reason:
|
reason: 'skipped_below_threshold' | 'skipped_idempotent' | 'success' | 'fallback_truncated' | 'thrashing' | 'error'
|
||||||
| 'skipped_disabled'
|
|
||||||
| 'skipped_below_threshold'
|
|
||||||
| 'skipped_idempotent'
|
|
||||||
| 'success'
|
|
||||||
| 'fallback_truncated'
|
|
||||||
| 'thrashing'
|
|
||||||
| 'error'
|
|
||||||
tokensBefore?: number
|
tokensBefore?: number
|
||||||
tokensAfter?: number
|
tokensAfter?: number
|
||||||
summaryContent?: string
|
summaryContent?: string
|
||||||
|
|||||||
@@ -76,7 +76,12 @@ export type { ChartSchemaGateState } from './chart-schema-gate'
|
|||||||
|
|
||||||
// Compression
|
// Compression
|
||||||
export type { CompressionConfig, CompressionResult, CompressionLogger, CompressionLlmAdapter } from './compression'
|
export type { CompressionConfig, CompressionResult, CompressionLogger, CompressionLlmAdapter } from './compression'
|
||||||
export { checkAndCompress, manualCompress, createCompressionLlmAdapter } from './compression'
|
export {
|
||||||
|
DEFAULT_CONTEXT_COMPRESSION_CONFIG,
|
||||||
|
checkAndCompress,
|
||||||
|
manualCompress,
|
||||||
|
createCompressionLlmAdapter,
|
||||||
|
} from './compression'
|
||||||
export type { CreateCompressionLlmAdapterOptions } from './compression'
|
export type { CreateCompressionLlmAdapterOptions } from './compression'
|
||||||
|
|
||||||
// Preprocessor
|
// Preprocessor
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ export type {
|
|||||||
|
|
||||||
// Compression
|
// Compression
|
||||||
export type { CompressionConfig, CompressionResult, CompressionLogger, CompressionLlmAdapter } from './ai'
|
export type { CompressionConfig, CompressionResult, CompressionLogger, CompressionLlmAdapter } from './ai'
|
||||||
export { checkAndCompress, manualCompress, createCompressionLlmAdapter } from './ai'
|
export { DEFAULT_CONTEXT_COMPRESSION_CONFIG, checkAndCompress, manualCompress, createCompressionLlmAdapter } from './ai'
|
||||||
export type { CreateCompressionLlmAdapterOptions } from './ai'
|
export type { CreateCompressionLlmAdapterOptions } from './ai'
|
||||||
|
|
||||||
// SkillManager
|
// SkillManager
|
||||||
@@ -398,7 +398,6 @@ export type {
|
|||||||
AIPreprocessConfig,
|
AIPreprocessConfig,
|
||||||
WordFilterScheme,
|
WordFilterScheme,
|
||||||
KeywordTemplate,
|
KeywordTemplate,
|
||||||
ContextCompressionSettings,
|
|
||||||
} from './preferences'
|
} from './preferences'
|
||||||
|
|
||||||
// Merger orchestration
|
// Merger orchestration
|
||||||
|
|||||||
@@ -190,6 +190,33 @@ test('loads thinkingLevels as empty object when field is absent in legacy prefer
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('ignores legacy context compression preferences now that compression is automatic', () => {
|
||||||
|
const systemDir = createTempSystemDir()
|
||||||
|
try {
|
||||||
|
writeFileSync(
|
||||||
|
join(systemDir, 'preferences.json'),
|
||||||
|
JSON.stringify({
|
||||||
|
aiGlobalSettings: {
|
||||||
|
maxMessagesPerRequest: 500,
|
||||||
|
contextCompression: {
|
||||||
|
enabled: false,
|
||||||
|
tokenThresholdPercent: 95,
|
||||||
|
bufferSizePercent: 50,
|
||||||
|
maxToolResultPercent: 80,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const loaded = new PreferencesManager(systemDir).load()
|
||||||
|
|
||||||
|
assert.equal(loaded.aiGlobalSettings.maxMessagesPerRequest, 500)
|
||||||
|
assert.equal('contextCompression' in loaded.aiGlobalSettings, false)
|
||||||
|
} finally {
|
||||||
|
rmSync(systemDir, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test('persists and normalizes skipped assistant upgrade versions', () => {
|
test('persists and normalizes skipped assistant upgrade versions', () => {
|
||||||
const systemDir = createTempSystemDir()
|
const systemDir = createTempSystemDir()
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import type {
|
|||||||
AIPreprocessConfig,
|
AIPreprocessConfig,
|
||||||
WordFilterScheme,
|
WordFilterScheme,
|
||||||
KeywordTemplate,
|
KeywordTemplate,
|
||||||
ContextCompressionSettings,
|
|
||||||
DesensitizeRule,
|
DesensitizeRule,
|
||||||
FilterHistoryItem,
|
FilterHistoryItem,
|
||||||
} from '@openchatlab/shared-types'
|
} from '@openchatlab/shared-types'
|
||||||
@@ -25,7 +24,6 @@ export type {
|
|||||||
AIPreprocessConfig,
|
AIPreprocessConfig,
|
||||||
WordFilterScheme,
|
WordFilterScheme,
|
||||||
KeywordTemplate,
|
KeywordTemplate,
|
||||||
ContextCompressionSettings,
|
|
||||||
DesensitizeRule,
|
DesensitizeRule,
|
||||||
FilterHistoryItem,
|
FilterHistoryItem,
|
||||||
}
|
}
|
||||||
@@ -53,12 +51,6 @@ const DEFAULTS: Preferences = {
|
|||||||
chartAutoMode: 'suggest',
|
chartAutoMode: 'suggest',
|
||||||
searchContextBefore: 2,
|
searchContextBefore: 2,
|
||||||
searchContextAfter: 2,
|
searchContextAfter: 2,
|
||||||
contextCompression: {
|
|
||||||
enabled: true,
|
|
||||||
tokenThresholdPercent: 75,
|
|
||||||
bufferSizePercent: 20,
|
|
||||||
maxToolResultPercent: 50,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
customKeywordTemplates: [],
|
customKeywordTemplates: [],
|
||||||
deletedPresetTemplateIds: [],
|
deletedPresetTemplateIds: [],
|
||||||
@@ -141,16 +133,7 @@ export class PreferencesManager {
|
|||||||
aiPreprocessConfig: partial.aiPreprocessConfig
|
aiPreprocessConfig: partial.aiPreprocessConfig
|
||||||
? this.normalizeAiPreprocessConfig(partial.aiPreprocessConfig)
|
? this.normalizeAiPreprocessConfig(partial.aiPreprocessConfig)
|
||||||
: { ...DEFAULTS.aiPreprocessConfig },
|
: { ...DEFAULTS.aiPreprocessConfig },
|
||||||
aiGlobalSettings: partial.aiGlobalSettings
|
aiGlobalSettings: this.normalizeAiGlobalSettings(partial.aiGlobalSettings),
|
||||||
? {
|
|
||||||
...DEFAULTS.aiGlobalSettings,
|
|
||||||
...partial.aiGlobalSettings,
|
|
||||||
contextCompression: {
|
|
||||||
...DEFAULTS.aiGlobalSettings.contextCompression,
|
|
||||||
...(partial.aiGlobalSettings.contextCompression ?? {}),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: { ...DEFAULTS.aiGlobalSettings },
|
|
||||||
customKeywordTemplates: partial.customKeywordTemplates ?? DEFAULTS.customKeywordTemplates,
|
customKeywordTemplates: partial.customKeywordTemplates ?? DEFAULTS.customKeywordTemplates,
|
||||||
deletedPresetTemplateIds: partial.deletedPresetTemplateIds ?? DEFAULTS.deletedPresetTemplateIds,
|
deletedPresetTemplateIds: partial.deletedPresetTemplateIds ?? DEFAULTS.deletedPresetTemplateIds,
|
||||||
wordFilter: partial.wordFilter ? { ...DEFAULTS.wordFilter, ...partial.wordFilter } : { ...DEFAULTS.wordFilter },
|
wordFilter: partial.wordFilter ? { ...DEFAULTS.wordFilter, ...partial.wordFilter } : { ...DEFAULTS.wordFilter },
|
||||||
@@ -161,6 +144,15 @@ export class PreferencesManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private normalizeAiGlobalSettings(partial: Partial<AIGlobalSettings> | undefined): AIGlobalSettings {
|
||||||
|
const normalized = { ...(partial ?? {}) } as Record<string, unknown>
|
||||||
|
// Context compression became an automatic runtime capability. Ignore the
|
||||||
|
// legacy user preference so a previously disabled value cannot survive
|
||||||
|
// after the setting is removed.
|
||||||
|
delete normalized.contextCompression
|
||||||
|
return { ...DEFAULTS.aiGlobalSettings, ...(normalized as Partial<AIGlobalSettings>) }
|
||||||
|
}
|
||||||
|
|
||||||
private normalizeAiPreprocessConfig(partial: Partial<AIPreprocessConfig> | undefined): AIPreprocessConfig {
|
private normalizeAiPreprocessConfig(partial: Partial<AIPreprocessConfig> | undefined): AIPreprocessConfig {
|
||||||
const defaults = DEFAULTS.aiPreprocessConfig
|
const defaults = DEFAULTS.aiPreprocessConfig
|
||||||
const rules = Array.isArray(partial?.desensitizeRules) ? partial.desensitizeRules : []
|
const rules = Array.isArray(partial?.desensitizeRules) ? partial.desensitizeRules : []
|
||||||
|
|||||||
@@ -195,13 +195,6 @@ export interface WordFilterScheme {
|
|||||||
createdAt: number
|
createdAt: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContextCompressionSettings {
|
|
||||||
enabled: boolean
|
|
||||||
tokenThresholdPercent: number
|
|
||||||
bufferSizePercent: number
|
|
||||||
maxToolResultPercent: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ChartAutoMode = 'explicit' | 'suggest' | 'aggressive'
|
export type ChartAutoMode = 'explicit' | 'suggest' | 'aggressive'
|
||||||
|
|
||||||
export interface AIGlobalSettings {
|
export interface AIGlobalSettings {
|
||||||
@@ -212,7 +205,6 @@ export interface AIGlobalSettings {
|
|||||||
chartAutoMode: ChartAutoMode
|
chartAutoMode: ChartAutoMode
|
||||||
searchContextBefore: number
|
searchContextBefore: number
|
||||||
searchContextAfter: number
|
searchContextAfter: number
|
||||||
contextCompression: ContextCompressionSettings
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KeywordTemplate {
|
export interface KeywordTemplate {
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import type { LanguageModel } from 'ai'
|
import type { LanguageModel } from 'ai'
|
||||||
|
import { resolveRuntimeContextWindow } from '@openchatlab/ai-runtime'
|
||||||
|
|
||||||
import type { WebModelConfig } from './types'
|
import type { WebModelConfig } from './types'
|
||||||
|
|
||||||
export interface CreatedWebModel {
|
export interface CreatedWebModel {
|
||||||
model: LanguageModel
|
model: LanguageModel
|
||||||
|
contextWindow?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createWebAIModel(config: WebModelConfig, apiKey: string): Promise<CreatedWebModel> {
|
export async function createWebAIModel(config: WebModelConfig, apiKey: string): Promise<CreatedWebModel> {
|
||||||
@@ -20,5 +22,6 @@ export async function createWebAIModel(config: WebModelConfig, apiKey: string):
|
|||||||
apiKey,
|
apiKey,
|
||||||
baseURL,
|
baseURL,
|
||||||
}).chatModel(config.model),
|
}).chatModel(config.model),
|
||||||
|
contextWindow: resolveRuntimeContextWindow(config.provider, config.model),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,9 @@ import type {
|
|||||||
AppendRuntimeMessageInput,
|
AppendRuntimeMessageInput,
|
||||||
ConversationRepository,
|
ConversationRepository,
|
||||||
RuntimeConversation,
|
RuntimeConversation,
|
||||||
|
RuntimeContextSummary,
|
||||||
RuntimeMessage,
|
RuntimeMessage,
|
||||||
|
SaveRuntimeContextSummaryInput,
|
||||||
RuntimeToolDefinition,
|
RuntimeToolDefinition,
|
||||||
ToolExecutionContext,
|
ToolExecutionContext,
|
||||||
ToolExecutor,
|
ToolExecutor,
|
||||||
@@ -28,6 +30,14 @@ export class RpcConversationRepository implements ConversationRepository {
|
|||||||
return this.rpc.request('ai.message.list', { conversationId })
|
return this.rpc.request('ai.message.list', { conversationId })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getContextSummary(conversationId: string): Promise<RuntimeContextSummary | null> {
|
||||||
|
return this.rpc.request('ai.context-summary.get', { conversationId })
|
||||||
|
}
|
||||||
|
|
||||||
|
saveContextSummary(input: SaveRuntimeContextSummaryInput): Promise<RuntimeContextSummary> {
|
||||||
|
return this.rpc.request('ai.context-summary.save', input)
|
||||||
|
}
|
||||||
|
|
||||||
appendMessage(input: AppendRuntimeMessageInput): Promise<RuntimeMessage> {
|
appendMessage(input: AppendRuntimeMessageInput): Promise<RuntimeMessage> {
|
||||||
return this.rpc.request('ai.message.append', input)
|
return this.rpc.request('ai.message.append', input)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ import assert from 'node:assert/strict'
|
|||||||
import { webcrypto } from 'node:crypto'
|
import { webcrypto } from 'node:crypto'
|
||||||
import { describe, it } from 'node:test'
|
import { describe, it } from 'node:test'
|
||||||
|
|
||||||
import type { RuntimeConversation, RuntimeMessage } from '@openchatlab/ai-runtime'
|
import type {
|
||||||
|
RuntimeContextSummary,
|
||||||
|
RuntimeConversation,
|
||||||
|
RuntimeMessage,
|
||||||
|
SaveRuntimeContextSummaryInput,
|
||||||
|
} from '@openchatlab/ai-runtime'
|
||||||
import { MockLanguageModelV3, simulateReadableStream } from 'ai/test'
|
import { MockLanguageModelV3, simulateReadableStream } from 'ai/test'
|
||||||
|
|
||||||
import { WebModelConfigStore, type BrowserKeyValueStore } from './model-config-store'
|
import { WebModelConfigStore, type BrowserKeyValueStore } from './model-config-store'
|
||||||
@@ -31,6 +36,7 @@ class MemoryKeyValueStore implements BrowserKeyValueStore {
|
|||||||
|
|
||||||
function createRpc(toolListPayloads: unknown[] = [], messageListGate?: Promise<void>): WebRuntimeRpcPort {
|
function createRpc(toolListPayloads: unknown[] = [], messageListGate?: Promise<void>): WebRuntimeRpcPort {
|
||||||
const conversations = new Map<string, RuntimeConversation>()
|
const conversations = new Map<string, RuntimeConversation>()
|
||||||
|
const contextSummaries = new Map<string, RuntimeContextSummary>()
|
||||||
const messages: RuntimeMessage[] = []
|
const messages: RuntimeMessage[] = []
|
||||||
let sequence = 0
|
let sequence = 0
|
||||||
return {
|
return {
|
||||||
@@ -76,6 +82,14 @@ function createRpc(toolListPayloads: unknown[] = [], messageListGate?: Promise<v
|
|||||||
if (index >= 0) messages.splice(index, 1)
|
if (index >= 0) messages.splice(index, 1)
|
||||||
return { deleted: index >= 0 } as never
|
return { deleted: index >= 0 } as never
|
||||||
}
|
}
|
||||||
|
case 'ai.context-summary.get':
|
||||||
|
return (contextSummaries.get((payload as { conversationId: string }).conversationId) ?? null) as never
|
||||||
|
case 'ai.context-summary.save': {
|
||||||
|
const input = payload as SaveRuntimeContextSummaryInput
|
||||||
|
const summary: RuntimeContextSummary = { ...input, updatedAt: Date.now() }
|
||||||
|
contextSummaries.set(summary.conversationId, summary)
|
||||||
|
return summary as never
|
||||||
|
}
|
||||||
case 'ai.tool.list':
|
case 'ai.tool.list':
|
||||||
toolListPayloads.push(payload)
|
toolListPayloads.push(payload)
|
||||||
return [] as never
|
return [] as never
|
||||||
|
|||||||
@@ -221,6 +221,8 @@ function filterMessagesFromHistory(
|
|||||||
getConversation: (id) => repository.getConversation(id),
|
getConversation: (id) => repository.getConversation(id),
|
||||||
getMessages: async (conversationId) =>
|
getMessages: async (conversationId) =>
|
||||||
(await repository.getMessages(conversationId)).filter((message) => !messageIds.has(message.id)),
|
(await repository.getMessages(conversationId)).filter((message) => !messageIds.has(message.id)),
|
||||||
|
getContextSummary: (conversationId) => repository.getContextSummary(conversationId),
|
||||||
|
saveContextSummary: (input) => repository.saveContextSummary(input),
|
||||||
appendMessage: (input) => repository.appendMessage(input),
|
appendMessage: (input) => repository.appendMessage(input),
|
||||||
updateMessage: (id, patch) => repository.updateMessage(id, patch),
|
updateMessage: (id, patch) => repository.updateMessage(id, patch),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ describe('BrowserAIConversationRepository', () => {
|
|||||||
const repository = new BrowserAIConversationRepository(database)
|
const repository = new BrowserAIConversationRepository(database)
|
||||||
try {
|
try {
|
||||||
const conversation = await repository.createConversation('session-1', 'First')
|
const conversation = await repository.createConversation('session-1', 'First')
|
||||||
await repository.appendMessage({
|
const firstMessage = await repository.appendMessage({
|
||||||
conversationId: conversation.id,
|
conversationId: conversation.id,
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: 'hello',
|
content: 'hello',
|
||||||
@@ -73,6 +73,12 @@ describe('BrowserAIConversationRepository', () => {
|
|||||||
content: 'world',
|
content: 'world',
|
||||||
usage: { totalTokens: 3 },
|
usage: { totalTokens: 3 },
|
||||||
})
|
})
|
||||||
|
await repository.saveContextSummary({
|
||||||
|
conversationId: conversation.id,
|
||||||
|
content: 'Earlier context summary',
|
||||||
|
boundaryMessageId: firstMessage.id,
|
||||||
|
compressedMessageCount: 1,
|
||||||
|
})
|
||||||
assert.equal((await repository.listConversations('session-1')).length, 1)
|
assert.equal((await repository.listConversations('session-1')).length, 1)
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
(await repository.getMessages(conversation.id)).map((message) => [message.role, message.content]),
|
(await repository.getMessages(conversation.id)).map((message) => [message.role, message.content]),
|
||||||
@@ -81,9 +87,11 @@ describe('BrowserAIConversationRepository', () => {
|
|||||||
['assistant', 'world'],
|
['assistant', 'world'],
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
assert.equal((await repository.getContextSummary(conversation.id))?.content, 'Earlier context summary')
|
||||||
assert.equal(await repository.deleteBySession('session-1'), 1)
|
assert.equal(await repository.deleteBySession('session-1'), 1)
|
||||||
assert.equal(await repository.getConversation(conversation.id), null)
|
assert.equal(await repository.getConversation(conversation.id), null)
|
||||||
assert.deepEqual(await repository.getMessages(conversation.id), [])
|
assert.deepEqual(await repository.getMessages(conversation.id), [])
|
||||||
|
assert.equal(await repository.getContextSummary(conversation.id), null)
|
||||||
} finally {
|
} finally {
|
||||||
database.dispose()
|
database.dispose()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import type {
|
|||||||
AppendRuntimeMessageInput,
|
AppendRuntimeMessageInput,
|
||||||
ConversationRepository,
|
ConversationRepository,
|
||||||
RuntimeConversation,
|
RuntimeConversation,
|
||||||
|
RuntimeContextSummary,
|
||||||
RuntimeMessage,
|
RuntimeMessage,
|
||||||
|
SaveRuntimeContextSummaryInput,
|
||||||
} from '@openchatlab/ai-runtime'
|
} from '@openchatlab/ai-runtime'
|
||||||
import type { DatabaseAdapter } from '@openchatlab/core'
|
import type { DatabaseAdapter } from '@openchatlab/core'
|
||||||
|
|
||||||
@@ -33,6 +35,15 @@ export const WEB_AI_SCHEMA = `
|
|||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_ai_message_conversation_created
|
CREATE INDEX IF NOT EXISTS idx_ai_message_conversation_created
|
||||||
ON ai_message(conversation_id, created_at ASC, id ASC);
|
ON ai_message(conversation_id, created_at ASC, id ASC);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ai_context_summary (
|
||||||
|
conversation_id TEXT PRIMARY KEY,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
boundary_message_id TEXT NOT NULL,
|
||||||
|
compressed_message_count INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY(conversation_id) REFERENCES ai_conversation(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
`
|
`
|
||||||
|
|
||||||
interface ConversationRow {
|
interface ConversationRow {
|
||||||
@@ -53,6 +64,14 @@ interface MessageRow {
|
|||||||
usage_json: string | null
|
usage_json: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ContextSummaryRow {
|
||||||
|
conversation_id: string
|
||||||
|
content: string
|
||||||
|
boundary_message_id: string
|
||||||
|
compressed_message_count: number
|
||||||
|
updated_at: number
|
||||||
|
}
|
||||||
|
|
||||||
function createId(prefix: string): string {
|
function createId(prefix: string): string {
|
||||||
return `${prefix}_${crypto.randomUUID()}`
|
return `${prefix}_${crypto.randomUUID()}`
|
||||||
}
|
}
|
||||||
@@ -88,6 +107,16 @@ function mapMessage(row: MessageRow): RuntimeMessage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mapContextSummary(row: ContextSummaryRow): RuntimeContextSummary {
|
||||||
|
return {
|
||||||
|
conversationId: row.conversation_id,
|
||||||
|
content: row.content,
|
||||||
|
boundaryMessageId: row.boundary_message_id,
|
||||||
|
compressedMessageCount: row.compressed_message_count,
|
||||||
|
updatedAt: row.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class BrowserAIConversationRepository implements ConversationRepository {
|
export class BrowserAIConversationRepository implements ConversationRepository {
|
||||||
constructor(private readonly database: WorkspaceDatabasePort) {}
|
constructor(private readonly database: WorkspaceDatabasePort) {}
|
||||||
|
|
||||||
@@ -157,6 +186,38 @@ export class BrowserAIConversationRepository implements ConversationRepository {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getContextSummary(conversationId: string): Promise<RuntimeContextSummary | null> {
|
||||||
|
return this.withDatabase((db) => {
|
||||||
|
const row = db.prepare('SELECT * FROM ai_context_summary WHERE conversation_id = ?').get(conversationId) as
|
||||||
|
| ContextSummaryRow
|
||||||
|
| undefined
|
||||||
|
return row ? mapContextSummary(row) : null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
saveContextSummary(input: SaveRuntimeContextSummaryInput): Promise<RuntimeContextSummary> {
|
||||||
|
const summary: RuntimeContextSummary = { ...input, updatedAt: Date.now() }
|
||||||
|
return this.withDatabase((db) => {
|
||||||
|
db.prepare(
|
||||||
|
`INSERT INTO ai_context_summary (
|
||||||
|
conversation_id, content, boundary_message_id, compressed_message_count, updated_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(conversation_id) DO UPDATE SET
|
||||||
|
content = excluded.content,
|
||||||
|
boundary_message_id = excluded.boundary_message_id,
|
||||||
|
compressed_message_count = excluded.compressed_message_count,
|
||||||
|
updated_at = excluded.updated_at`
|
||||||
|
).run(
|
||||||
|
summary.conversationId,
|
||||||
|
summary.content,
|
||||||
|
summary.boundaryMessageId,
|
||||||
|
summary.compressedMessageCount,
|
||||||
|
summary.updatedAt
|
||||||
|
)
|
||||||
|
return summary
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
appendMessage(input: AppendRuntimeMessageInput): Promise<RuntimeMessage> {
|
appendMessage(input: AppendRuntimeMessageInput): Promise<RuntimeMessage> {
|
||||||
const message: RuntimeMessage = {
|
const message: RuntimeMessage = {
|
||||||
id: input.id ?? createId('message'),
|
id: input.id ?? createId('message'),
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import type { BrowserSessionCatalogItem } from '../import/session-catalog'
|
|||||||
import type {
|
import type {
|
||||||
AppendRuntimeMessageInput,
|
AppendRuntimeMessageInput,
|
||||||
RuntimeConversation,
|
RuntimeConversation,
|
||||||
|
RuntimeContextSummary,
|
||||||
RuntimeMessage,
|
RuntimeMessage,
|
||||||
|
SaveRuntimeContextSummaryInput,
|
||||||
RuntimeToolDefinition,
|
RuntimeToolDefinition,
|
||||||
RuntimeToolResult,
|
RuntimeToolResult,
|
||||||
} from '@openchatlab/ai-runtime'
|
} from '@openchatlab/ai-runtime'
|
||||||
@@ -141,6 +143,14 @@ export interface WebRuntimeTaskMap {
|
|||||||
payload: { messageId: string }
|
payload: { messageId: string }
|
||||||
result: { deleted: boolean }
|
result: { deleted: boolean }
|
||||||
}
|
}
|
||||||
|
'ai.context-summary.get': {
|
||||||
|
payload: { conversationId: string }
|
||||||
|
result: RuntimeContextSummary | null
|
||||||
|
}
|
||||||
|
'ai.context-summary.save': {
|
||||||
|
payload: SaveRuntimeContextSummaryInput
|
||||||
|
result: RuntimeContextSummary
|
||||||
|
}
|
||||||
'ai.tool.list': {
|
'ai.tool.list': {
|
||||||
payload: { locale?: string }
|
payload: { locale?: string }
|
||||||
result: RuntimeToolDefinition[]
|
result: RuntimeToolDefinition[]
|
||||||
@@ -350,6 +360,8 @@ const WEB_RUNTIME_TASK_TYPES: Record<WebRuntimeTaskType, true> = {
|
|||||||
'ai.message.append': true,
|
'ai.message.append': true,
|
||||||
'ai.message.update': true,
|
'ai.message.update': true,
|
||||||
'ai.message.delete': true,
|
'ai.message.delete': true,
|
||||||
|
'ai.context-summary.get': true,
|
||||||
|
'ai.context-summary.save': true,
|
||||||
'ai.tool.list': true,
|
'ai.tool.list': true,
|
||||||
'ai.tool.execute': true,
|
'ai.tool.execute': true,
|
||||||
'analysis.hourly': true,
|
'analysis.hourly': true,
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ export type WorkerAIRepository = Pick<
|
|||||||
| 'appendMessage'
|
| 'appendMessage'
|
||||||
| 'updateMessage'
|
| 'updateMessage'
|
||||||
| 'deleteMessage'
|
| 'deleteMessage'
|
||||||
|
| 'getContextSummary'
|
||||||
|
| 'saveContextSummary'
|
||||||
>
|
>
|
||||||
|
|
||||||
export type WorkerAIToolRuntime = Pick<BrowserAIToolRuntime, 'listTools' | 'execute'>
|
export type WorkerAIToolRuntime = Pick<BrowserAIToolRuntime, 'listTools' | 'execute'>
|
||||||
@@ -283,6 +285,10 @@ export class WebRuntimeWorkerController {
|
|||||||
return { updated: true }
|
return { updated: true }
|
||||||
case 'ai.message.delete':
|
case 'ai.message.delete':
|
||||||
return { deleted: await this.aiRepository.deleteMessage(request.payload.messageId) }
|
return { deleted: await this.aiRepository.deleteMessage(request.payload.messageId) }
|
||||||
|
case 'ai.context-summary.get':
|
||||||
|
return this.aiRepository.getContextSummary(request.payload.conversationId)
|
||||||
|
case 'ai.context-summary.save':
|
||||||
|
return this.aiRepository.saveContextSummary(request.payload)
|
||||||
case 'ai.tool.list':
|
case 'ai.tool.list':
|
||||||
return this.aiToolRuntime.listTools(request.payload.locale)
|
return this.aiToolRuntime.listTools(request.payload.locale)
|
||||||
case 'ai.tool.execute': {
|
case 'ai.tool.execute': {
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import assert from 'node:assert/strict'
|
|
||||||
import { describe, it } from 'node:test'
|
|
||||||
import { MAX_TOOL_RESULT_PERCENT, normalizeMaxToolResultPercent } from './promptConfigLimits'
|
|
||||||
|
|
||||||
describe('AIPromptConfigTab tool result budget controls', () => {
|
|
||||||
it('allows users to raise tool result budget up to 80 percent', () => {
|
|
||||||
assert.equal(MAX_TOOL_RESULT_PERCENT, 80)
|
|
||||||
assert.equal(normalizeMaxToolResultPercent(80), 80)
|
|
||||||
assert.equal(normalizeMaxToolResultPercent(81), 80)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -4,7 +4,6 @@ import { storeToRefs } from 'pinia'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { usePromptStore } from '@/stores/prompt'
|
import { usePromptStore } from '@/stores/prompt'
|
||||||
import type { ChartAutoMode } from '@openchatlab/shared-types'
|
import type { ChartAutoMode } from '@openchatlab/shared-types'
|
||||||
import { MAX_TOOL_RESULT_PERCENT, MIN_TOOL_RESULT_PERCENT, normalizeMaxToolResultPercent } from './promptConfigLimits'
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
@@ -77,50 +76,6 @@ const searchContextAfter = computed({
|
|||||||
emit('config-changed')
|
emit('config-changed')
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// 上下文压缩配置
|
|
||||||
const compressionEnabled = computed({
|
|
||||||
get: () => aiGlobalSettings.value.contextCompression?.enabled ?? false,
|
|
||||||
set: (val: boolean) => {
|
|
||||||
promptStore.updateAIGlobalSettings({
|
|
||||||
contextCompression: { ...aiGlobalSettings.value.contextCompression, enabled: val },
|
|
||||||
})
|
|
||||||
emit('config-changed')
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const compressionThreshold = computed({
|
|
||||||
get: () => aiGlobalSettings.value.contextCompression?.tokenThresholdPercent ?? 75,
|
|
||||||
set: (val: number) => {
|
|
||||||
const clampedVal = Math.max(30, Math.min(95, val || 75))
|
|
||||||
promptStore.updateAIGlobalSettings({
|
|
||||||
contextCompression: { ...aiGlobalSettings.value.contextCompression, tokenThresholdPercent: clampedVal },
|
|
||||||
})
|
|
||||||
emit('config-changed')
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const compressionBuffer = computed({
|
|
||||||
get: () => aiGlobalSettings.value.contextCompression?.bufferSizePercent ?? 20,
|
|
||||||
set: (val: number) => {
|
|
||||||
const clampedVal = Math.max(5, Math.min(50, val || 20))
|
|
||||||
promptStore.updateAIGlobalSettings({
|
|
||||||
contextCompression: { ...aiGlobalSettings.value.contextCompression, bufferSizePercent: clampedVal },
|
|
||||||
})
|
|
||||||
emit('config-changed')
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const maxToolResultPercent = computed({
|
|
||||||
get: () => aiGlobalSettings.value.contextCompression?.maxToolResultPercent ?? 50,
|
|
||||||
set: (val: number) => {
|
|
||||||
const clampedVal = normalizeMaxToolResultPercent(val)
|
|
||||||
promptStore.updateAIGlobalSettings({
|
|
||||||
contextCompression: { ...aiGlobalSettings.value.contextCompression, maxToolResultPercent: clampedVal },
|
|
||||||
})
|
|
||||||
emit('config-changed')
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -210,82 +165,5 @@ const maxToolResultPercent = computed({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 上下文压缩设置 -->
|
|
||||||
<div :ref="(el) => setPromptSectionRef('compression', el as HTMLElement | null)">
|
|
||||||
<h4 class="mb-3 flex items-center gap-2 text-sm font-semibold text-gray-900 dark:text-white">
|
|
||||||
<UIcon name="i-heroicons-archive-box-arrow-down" class="h-4 w-4 text-purple-500" />
|
|
||||||
{{ t('settings.aiPrompt.compression.title') }}
|
|
||||||
</h4>
|
|
||||||
<div class="space-y-4 rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800/50">
|
|
||||||
<!-- 压缩开关 -->
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex-1 pr-4">
|
|
||||||
<p class="text-sm font-medium text-gray-900 dark:text-white">
|
|
||||||
{{ t('settings.aiPrompt.compression.enable') }}
|
|
||||||
</p>
|
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{{ t('settings.aiPrompt.compression.enableDesc') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<USwitch v-model="compressionEnabled" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 工具结果上限(始终显示,不依赖压缩开关) -->
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex-1 pr-4">
|
|
||||||
<p class="text-sm font-medium text-gray-900 dark:text-white">
|
|
||||||
{{ t('settings.aiPrompt.compression.maxToolResultPercent') }}
|
|
||||||
</p>
|
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{{ t('settings.aiPrompt.compression.maxToolResultPercentDesc') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<UInputNumber
|
|
||||||
v-model="maxToolResultPercent"
|
|
||||||
:min="MIN_TOOL_RESULT_PERCENT"
|
|
||||||
:max="MAX_TOOL_RESULT_PERCENT"
|
|
||||||
class="w-24"
|
|
||||||
/>
|
|
||||||
<span class="text-xs text-gray-400">%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template v-if="compressionEnabled">
|
|
||||||
<!-- 压缩阈值 -->
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex-1 pr-4">
|
|
||||||
<p class="text-sm font-medium text-gray-900 dark:text-white">
|
|
||||||
{{ t('settings.aiPrompt.compression.threshold') }}
|
|
||||||
</p>
|
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{{ t('settings.aiPrompt.compression.thresholdDesc') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<UInputNumber v-model="compressionThreshold" :min="30" :max="95" class="w-24" />
|
|
||||||
<span class="text-xs text-gray-400">%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 缓冲区大小 -->
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex-1 pr-4">
|
|
||||||
<p class="text-sm font-medium text-gray-900 dark:text-white">
|
|
||||||
{{ t('settings.aiPrompt.compression.buffer') }}
|
|
||||||
</p>
|
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{{ t('settings.aiPrompt.compression.bufferDesc') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<UInputNumber v-model="compressionBuffer" :min="5" :max="50" class="w-24" />
|
|
||||||
<span class="text-xs text-gray-400">%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export const MAX_TOOL_RESULT_PERCENT = 80
|
|
||||||
export const MIN_TOOL_RESULT_PERCENT = 10
|
|
||||||
export const DEFAULT_TOOL_RESULT_PERCENT = 50
|
|
||||||
|
|
||||||
export function normalizeMaxToolResultPercent(value: number): number {
|
|
||||||
return Math.max(MIN_TOOL_RESULT_PERCENT, Math.min(MAX_TOOL_RESULT_PERCENT, value || DEFAULT_TOOL_RESULT_PERCENT))
|
|
||||||
}
|
|
||||||
@@ -25,7 +25,6 @@ const navItems = computed(() => [
|
|||||||
{ id: 'semanticIndex', label: t('settings.tabs.semanticIndex') },
|
{ id: 'semanticIndex', label: t('settings.tabs.semanticIndex') },
|
||||||
{ id: 'skill', label: t('settings.tabs.chatPreferences') },
|
{ id: 'skill', label: t('settings.tabs.chatPreferences') },
|
||||||
{ id: 'chat', label: t('settings.aiPrompt.chatSettings.title') },
|
{ id: 'chat', label: t('settings.aiPrompt.chatSettings.title') },
|
||||||
{ id: 'compression', label: t('settings.tabs.compression') },
|
|
||||||
{ id: 'preprocess', label: t('settings.tabs.aiPreprocess') },
|
{ id: 'preprocess', label: t('settings.tabs.aiPreprocess') },
|
||||||
{ id: 'sessionIndex', label: t('settings.tabs.sessionManage') },
|
{ id: 'sessionIndex', label: t('settings.tabs.sessionManage') },
|
||||||
{ id: 'export', label: t('settings.aiPrompt.exportSettings.title') },
|
{ id: 'export', label: t('settings.aiPrompt.exportSettings.title') },
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
"aiDefaultModel": "Default Model",
|
"aiDefaultModel": "Default Model",
|
||||||
"semanticIndex": "Semantic Index",
|
"semanticIndex": "Semantic Index",
|
||||||
"chatPreferences": "Preferences",
|
"chatPreferences": "Preferences",
|
||||||
"compression": "Compression",
|
|
||||||
"aiPreprocess": "Preprocess",
|
"aiPreprocess": "Preprocess",
|
||||||
"dataManage": "Data Management",
|
"dataManage": "Data Management",
|
||||||
"storage": "Storage",
|
"storage": "Storage",
|
||||||
@@ -377,17 +376,6 @@
|
|||||||
"sqlExportFormat": {
|
"sqlExportFormat": {
|
||||||
"title": "SQL Lab Export Format",
|
"title": "SQL Lab Export Format",
|
||||||
"description": "File format for exporting SQL query results"
|
"description": "File format for exporting SQL query results"
|
||||||
},
|
|
||||||
"compression": {
|
|
||||||
"title": "Context Compression",
|
|
||||||
"enable": "Auto-compress context",
|
|
||||||
"enableDesc": "Automatically compress long conversation history into summaries to reduce token usage",
|
|
||||||
"threshold": "Compression threshold",
|
|
||||||
"thresholdDesc": "Trigger compression when context tokens reach this percentage of the model's context window",
|
|
||||||
"buffer": "Buffer size",
|
|
||||||
"bufferDesc": "Percentage of recent messages to keep uncompressed",
|
|
||||||
"maxToolResultPercent": "Tool result limit",
|
|
||||||
"maxToolResultPercentDesc": "Maximum data percentage per tool call sent to AI (based on model context window). Excess results are intelligently truncated"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"regenerate": "Regenerate",
|
"regenerate": "Regenerate",
|
||||||
"footer": "AI can make mistakes. Verify important conclusions.",
|
"footer": "AI can make mistakes. Verify important conclusions.",
|
||||||
"usingTool": "Using {name}",
|
"usingTool": "Using {name}",
|
||||||
|
"compressingContext": "Compressing context",
|
||||||
"conversations": {
|
"conversations": {
|
||||||
"title": "Conversations",
|
"title": "Conversations",
|
||||||
"deleteConfirm": "Delete this AI conversation?"
|
"deleteConfirm": "Delete this AI conversation?"
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
"aiDefaultModel": "デフォルトモデル",
|
"aiDefaultModel": "デフォルトモデル",
|
||||||
"semanticIndex": "セマンティックインデックス",
|
"semanticIndex": "セマンティックインデックス",
|
||||||
"chatPreferences": "会話設定",
|
"chatPreferences": "会話設定",
|
||||||
"compression": "圧縮",
|
|
||||||
"aiPreprocess": "前処理",
|
"aiPreprocess": "前処理",
|
||||||
"dataManage": "データ管理",
|
"dataManage": "データ管理",
|
||||||
"storage": "ストレージ管理",
|
"storage": "ストレージ管理",
|
||||||
@@ -377,17 +376,6 @@
|
|||||||
"sqlExportFormat": {
|
"sqlExportFormat": {
|
||||||
"title": "SQL Lab エクスポート形式",
|
"title": "SQL Lab エクスポート形式",
|
||||||
"description": "SQL クエリ結果をエクスポートする際のファイル形式"
|
"description": "SQL クエリ結果をエクスポートする際のファイル形式"
|
||||||
},
|
|
||||||
"compression": {
|
|
||||||
"title": "コンテキスト圧縮",
|
|
||||||
"enable": "コンテキストの自動圧縮",
|
|
||||||
"enableDesc": "会話が長くなった際に履歴を自動的に要約に圧縮し、トークン消費を削減します",
|
|
||||||
"threshold": "圧縮閾値",
|
|
||||||
"thresholdDesc": "コンテキストトークンがモデルウィンドウのこの割合に達したら圧縮を開始",
|
|
||||||
"buffer": "バッファサイズ",
|
|
||||||
"bufferDesc": "圧縮せずに保持する最新メッセージの割合",
|
|
||||||
"maxToolResultPercent": "ツール結果の上限",
|
|
||||||
"maxToolResultPercentDesc": "1回のツール呼び出しでAIに送信される最大データ割合(モデルのコンテキストウィンドウに基づいて計算)。超過分はスマートに切り捨てられます"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"regenerate": "再生成",
|
"regenerate": "再生成",
|
||||||
"footer": "AI は誤ることがあります。重要な結論は確認してください。",
|
"footer": "AI は誤ることがあります。重要な結論は確認してください。",
|
||||||
"usingTool": "{name} を使用中",
|
"usingTool": "{name} を使用中",
|
||||||
|
"compressingContext": "コンテキストを圧縮中",
|
||||||
"conversations": {
|
"conversations": {
|
||||||
"title": "会話",
|
"title": "会話",
|
||||||
"deleteConfirm": "この AI 会話を削除しますか?"
|
"deleteConfirm": "この AI 会話を削除しますか?"
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
"aiDefaultModel": "默认模型",
|
"aiDefaultModel": "默认模型",
|
||||||
"semanticIndex": "语义索引",
|
"semanticIndex": "语义索引",
|
||||||
"chatPreferences": "对话偏好",
|
"chatPreferences": "对话偏好",
|
||||||
"compression": "上下文压缩",
|
|
||||||
"aiPreprocess": "预处理",
|
"aiPreprocess": "预处理",
|
||||||
"dataManage": "数据管理",
|
"dataManage": "数据管理",
|
||||||
"storage": "存储管理",
|
"storage": "存储管理",
|
||||||
@@ -377,17 +376,6 @@
|
|||||||
"sqlExportFormat": {
|
"sqlExportFormat": {
|
||||||
"title": "SQL Lab 导出格式",
|
"title": "SQL Lab 导出格式",
|
||||||
"description": "导出 SQL 查询结果时使用的文件格式"
|
"description": "导出 SQL 查询结果时使用的文件格式"
|
||||||
},
|
|
||||||
"compression": {
|
|
||||||
"title": "上下文压缩",
|
|
||||||
"enable": "自动压缩上下文",
|
|
||||||
"enableDesc": "对话过长时自动压缩历史为摘要,保留关键信息的同时降低 Token 消耗",
|
|
||||||
"threshold": "压缩阈值",
|
|
||||||
"thresholdDesc": "上下文 Token 达到模型窗口的百分比时触发压缩",
|
|
||||||
"buffer": "缓冲区大小",
|
|
||||||
"bufferDesc": "保留最近消息原文的比例,不参与压缩",
|
|
||||||
"maxToolResultPercent": "工具结果上限",
|
|
||||||
"maxToolResultPercentDesc": "单次工具返回给 AI 的最大数据量占比(基于模型上下文窗口计算),超出部分会被智能截断"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"regenerate": "重新生成",
|
"regenerate": "重新生成",
|
||||||
"footer": "AI 可能会犯错,请核对重要结论",
|
"footer": "AI 可能会犯错,请核对重要结论",
|
||||||
"usingTool": "正在调用 {name}",
|
"usingTool": "正在调用 {name}",
|
||||||
|
"compressingContext": "正在压缩上下文",
|
||||||
"conversations": {
|
"conversations": {
|
||||||
"title": "对话",
|
"title": "对话",
|
||||||
"deleteConfirm": "确定删除这条 AI 对话吗?"
|
"deleteConfirm": "确定删除这条 AI 对话吗?"
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
"aiDefaultModel": "預設模型",
|
"aiDefaultModel": "預設模型",
|
||||||
"semanticIndex": "語意索引",
|
"semanticIndex": "語意索引",
|
||||||
"chatPreferences": "對話偏好",
|
"chatPreferences": "對話偏好",
|
||||||
"compression": "上下文壓縮",
|
|
||||||
"aiPreprocess": "前處理",
|
"aiPreprocess": "前處理",
|
||||||
"dataManage": "資料管理",
|
"dataManage": "資料管理",
|
||||||
"storage": "儲存管理",
|
"storage": "儲存管理",
|
||||||
@@ -377,17 +376,6 @@
|
|||||||
"sqlExportFormat": {
|
"sqlExportFormat": {
|
||||||
"title": "SQL Lab 匯出格式",
|
"title": "SQL Lab 匯出格式",
|
||||||
"description": "匯出 SQL 查詢結果時使用的檔案格式"
|
"description": "匯出 SQL 查詢結果時使用的檔案格式"
|
||||||
},
|
|
||||||
"compression": {
|
|
||||||
"title": "上下文壓縮",
|
|
||||||
"enable": "自動壓縮上下文",
|
|
||||||
"enableDesc": "對話過長時自動壓縮歷史為摘要,保留關鍵資訊的同時降低 Token 消耗",
|
|
||||||
"threshold": "壓縮閾值",
|
|
||||||
"thresholdDesc": "上下文 Token 達到模型視窗的百分比時觸發壓縮",
|
|
||||||
"buffer": "緩衝區大小",
|
|
||||||
"bufferDesc": "保留最近訊息原文的比例,不參與壓縮",
|
|
||||||
"maxToolResultPercent": "工具結果上限",
|
|
||||||
"maxToolResultPercentDesc": "單次工具回傳給 AI 的最大資料量占比(基於模型上下文視窗計算),超出部分會被智慧截斷"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"regenerate": "重新產生",
|
"regenerate": "重新產生",
|
||||||
"footer": "AI 可能會出錯,請核對重要結論",
|
"footer": "AI 可能會出錯,請核對重要結論",
|
||||||
"usingTool": "正在呼叫 {name}",
|
"usingTool": "正在呼叫 {name}",
|
||||||
|
"compressingContext": "正在壓縮上下文",
|
||||||
"conversations": {
|
"conversations": {
|
||||||
"title": "對話",
|
"title": "對話",
|
||||||
"deleteConfirm": "確定刪除這條 AI 對話嗎?"
|
"deleteConfirm": "確定刪除這條 AI 對話嗎?"
|
||||||
|
|||||||
@@ -100,13 +100,6 @@ export interface AgentStreamResult {
|
|||||||
error?: SerializedErrorInfo | Record<string, unknown>
|
error?: SerializedErrorInfo | Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CompressionConfig {
|
|
||||||
enabled: boolean
|
|
||||||
tokenThresholdPercent: number
|
|
||||||
bufferSizePercent: number
|
|
||||||
maxToolResultPercent?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AgentStreamParams {
|
export interface AgentStreamParams {
|
||||||
userMessage: string
|
userMessage: string
|
||||||
sessionId: string
|
sessionId: string
|
||||||
@@ -118,7 +111,6 @@ export interface AgentStreamParams {
|
|||||||
skillId?: string | null
|
skillId?: string | null
|
||||||
enableAutoSkill?: boolean
|
enableAutoSkill?: boolean
|
||||||
chartAutoMode?: ChartAutoMode
|
chartAutoMode?: ChartAutoMode
|
||||||
compressionConfig?: CompressionConfig
|
|
||||||
ownerInfo?: { platformId: string; displayName: string }
|
ownerInfo?: { platformId: string; displayName: string }
|
||||||
mentionedMembers?: Array<{
|
mentionedMembers?: Array<{
|
||||||
memberId: number
|
memberId: number
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export type {
|
|||||||
AIPreprocessConfig,
|
AIPreprocessConfig,
|
||||||
WordFilterScheme,
|
WordFilterScheme,
|
||||||
KeywordTemplate,
|
KeywordTemplate,
|
||||||
ContextCompressionSettings,
|
|
||||||
DesensitizeRule,
|
DesensitizeRule,
|
||||||
UiConfig,
|
UiConfig,
|
||||||
} from '@openchatlab/shared-types'
|
} from '@openchatlab/shared-types'
|
||||||
|
|||||||
@@ -1020,12 +1020,6 @@ export const useAIChatStore = defineStore('aiChatRuntime', () => {
|
|||||||
skillId: currentSkillId,
|
skillId: currentSkillId,
|
||||||
enableAutoSkill: !currentSkillId ? autoSkillEnabled : undefined,
|
enableAutoSkill: !currentSkillId ? autoSkillEnabled : undefined,
|
||||||
chartAutoMode: !currentSkillId ? chartAutoMode : undefined,
|
chartAutoMode: !currentSkillId ? chartAutoMode : undefined,
|
||||||
compressionConfig: {
|
|
||||||
enabled: aiGlobalSettings.value.contextCompression?.enabled ?? false,
|
|
||||||
tokenThresholdPercent: aiGlobalSettings.value.contextCompression?.tokenThresholdPercent ?? 75,
|
|
||||||
bufferSizePercent: aiGlobalSettings.value.contextCompression?.bufferSizePercent ?? 20,
|
|
||||||
maxToolResultPercent: aiGlobalSettings.value.contextCompression?.maxToolResultPercent ?? 50,
|
|
||||||
},
|
|
||||||
thinkingLevel: (() => {
|
thinkingLevel: (() => {
|
||||||
const cfg = llmStore.defaultAssistant
|
const cfg = llmStore.defaultAssistant
|
||||||
if (!cfg?.configId || !cfg?.modelId) return undefined
|
if (!cfg?.configId || !cfg?.modelId) return undefined
|
||||||
@@ -1619,12 +1613,6 @@ export const useAIChatStore = defineStore('aiChatRuntime', () => {
|
|||||||
skillId: currentSkillId,
|
skillId: currentSkillId,
|
||||||
enableAutoSkill: !currentSkillId ? autoSkillEnabled : undefined,
|
enableAutoSkill: !currentSkillId ? autoSkillEnabled : undefined,
|
||||||
chartAutoMode: !currentSkillId ? chartAutoMode : undefined,
|
chartAutoMode: !currentSkillId ? chartAutoMode : undefined,
|
||||||
compressionConfig: {
|
|
||||||
enabled: aiGlobalSettings.value.contextCompression?.enabled ?? false,
|
|
||||||
tokenThresholdPercent: aiGlobalSettings.value.contextCompression?.tokenThresholdPercent ?? 75,
|
|
||||||
bufferSizePercent: aiGlobalSettings.value.contextCompression?.bufferSizePercent ?? 20,
|
|
||||||
maxToolResultPercent: aiGlobalSettings.value.contextCompression?.maxToolResultPercent ?? 50,
|
|
||||||
},
|
|
||||||
thinkingLevel: (() => {
|
thinkingLevel: (() => {
|
||||||
const cfg = llmStore.defaultAssistant
|
const cfg = llmStore.defaultAssistant
|
||||||
if (!cfg?.configId || !cfg?.modelId) return undefined
|
if (!cfg?.configId || !cfg?.modelId) return undefined
|
||||||
|
|||||||
+2
-22
@@ -4,13 +4,6 @@ import type { KeywordTemplate } from '@/types/analysis'
|
|||||||
import type { ThinkingLevel } from '@openchatlab/core'
|
import type { ThinkingLevel } from '@openchatlab/core'
|
||||||
import type { ChartAutoMode } from '@openchatlab/shared-types'
|
import type { ChartAutoMode } from '@openchatlab/shared-types'
|
||||||
|
|
||||||
interface ContextCompressionSettings {
|
|
||||||
enabled: boolean
|
|
||||||
tokenThresholdPercent: number
|
|
||||||
bufferSizePercent: number
|
|
||||||
maxToolResultPercent: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AIGlobalSettings {
|
interface AIGlobalSettings {
|
||||||
maxMessagesPerRequest: number
|
maxMessagesPerRequest: number
|
||||||
exportFormat: 'markdown' | 'txt'
|
exportFormat: 'markdown' | 'txt'
|
||||||
@@ -19,12 +12,9 @@ interface AIGlobalSettings {
|
|||||||
chartAutoMode: ChartAutoMode
|
chartAutoMode: ChartAutoMode
|
||||||
searchContextBefore: number
|
searchContextBefore: number
|
||||||
searchContextAfter: number
|
searchContextAfter: number
|
||||||
contextCompression: ContextCompressionSettings
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AIGlobalSettingsUpdate = Partial<Omit<AIGlobalSettings, 'contextCompression'>> & {
|
type AIGlobalSettingsUpdate = Partial<AIGlobalSettings>
|
||||||
contextCompression?: Partial<ContextCompressionSettings>
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AI 配置与关键词模板相关的全局状态
|
* AI 配置与关键词模板相关的全局状态
|
||||||
@@ -41,12 +31,6 @@ export const usePromptStore = defineStore(
|
|||||||
chartAutoMode: 'suggest',
|
chartAutoMode: 'suggest',
|
||||||
searchContextBefore: 2,
|
searchContextBefore: 2,
|
||||||
searchContextAfter: 2,
|
searchContextAfter: 2,
|
||||||
contextCompression: {
|
|
||||||
enabled: true,
|
|
||||||
tokenThresholdPercent: 75,
|
|
||||||
bufferSizePercent: 20,
|
|
||||||
maxToolResultPercent: 50,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
const customKeywordTemplates = ref<KeywordTemplate[]>([])
|
const customKeywordTemplates = ref<KeywordTemplate[]>([])
|
||||||
const deletedPresetTemplateIds = ref<string[]>([])
|
const deletedPresetTemplateIds = ref<string[]>([])
|
||||||
@@ -69,13 +53,9 @@ export const usePromptStore = defineStore(
|
|||||||
* 更新 AI 全局设置
|
* 更新 AI 全局设置
|
||||||
*/
|
*/
|
||||||
function updateAIGlobalSettings(settings: AIGlobalSettingsUpdate) {
|
function updateAIGlobalSettings(settings: AIGlobalSettingsUpdate) {
|
||||||
const { contextCompression, ...baseSettings } = settings
|
|
||||||
aiGlobalSettings.value = {
|
aiGlobalSettings.value = {
|
||||||
...aiGlobalSettings.value,
|
...aiGlobalSettings.value,
|
||||||
...baseSettings,
|
...settings,
|
||||||
contextCompression: contextCompression
|
|
||||||
? { ...aiGlobalSettings.value.contextCompression, ...contextCompression }
|
|
||||||
: aiGlobalSettings.value.contextCompression,
|
|
||||||
}
|
}
|
||||||
notifyAIConfigChanged()
|
notifyAIConfigChanged()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user