mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
feat(ai): add isolated cross-chat agent
This commit is contained in:
1 parent
812440c565
commit
d1a6c711fe
32 files changed
+2443
-243
No files matched your search
@@ -1,7 +1,8 @@
|
||||
import { describe, it } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import type { AIChatManager, AgentStreamChunk, DatabaseManager, LLMConfigStore } from '@openchatlab/node-runtime'
|
||||
import { getChartCapabilityAllowedBuiltinTools } from '@openchatlab/node-runtime'
|
||||
import { getAllowedToolSet, getAvailableToolDefs } from './agent-stream-runner'
|
||||
import { createCliRunAgentStream, getAllowedToolSet, getAvailableToolDefs } from './agent-stream-runner'
|
||||
|
||||
describe('CLI chart capability tool filtering', () => {
|
||||
it('does not expose uncategorized raw SQL in chart-only turns', () => {
|
||||
@@ -64,3 +65,49 @@ describe('CLI chart capability tool filtering', () => {
|
||||
assert.ok(!toolNames.includes('execute_sql'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('CLI agent conversation target validation', () => {
|
||||
it('rejects a global conversation before the default session tool path can run', async () => {
|
||||
let databaseOpenCalls = 0
|
||||
const dbManager = {
|
||||
pathProvider: { getAiDataDir: () => '/tmp/chatlab-agent-runner-test' },
|
||||
open() {
|
||||
databaseOpenCalls++
|
||||
throw new Error('session database must not be opened')
|
||||
},
|
||||
} as unknown as DatabaseManager
|
||||
const aiChatManager = {
|
||||
getAIChat: () => ({
|
||||
id: 'global-chat-1',
|
||||
sessionId: '',
|
||||
kind: 'global',
|
||||
title: null,
|
||||
assistantId: 'default',
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
}),
|
||||
} as unknown as AIChatManager
|
||||
const llmConfigStore = {
|
||||
getDefaultAssistantConfig: () => null,
|
||||
} as unknown as LLMConfigStore
|
||||
const events: AgentStreamChunk[] = []
|
||||
const runAgentStream = createCliRunAgentStream(dbManager, aiChatManager, { llmConfigStore })
|
||||
|
||||
await runAgentStream(
|
||||
{
|
||||
userMessage: '分析一下',
|
||||
aiChatId: 'global-chat-1',
|
||||
sessionId: 'private-session-1',
|
||||
},
|
||||
(event) => events.push(event),
|
||||
new AbortController().signal
|
||||
)
|
||||
|
||||
assert.equal(databaseOpenCalls, 0)
|
||||
assert.deepEqual(
|
||||
events.map((event) => event.type),
|
||||
['error', 'done']
|
||||
)
|
||||
assert.match(String(events[0]?.error && (events[0].error as { message?: string }).message), /does not match/)
|
||||
})
|
||||
})
|
||||
@@ -9,6 +9,8 @@ import type {
|
||||
AgentStreamChunk,
|
||||
LLMConfigStore,
|
||||
SemanticIndexRuntime,
|
||||
CrossChatAnalysisService,
|
||||
SessionRuntimeAdapter,
|
||||
} from '@openchatlab/node-runtime'
|
||||
import {
|
||||
CHART_CAPABILITY_CORE_TOOLS,
|
||||
@@ -23,6 +25,7 @@ import {
|
||||
getChartCapabilitySkill,
|
||||
getSkillConfigWithBuiltinChart,
|
||||
resolveChartRuntimeForRequest,
|
||||
runCrossChatAgent,
|
||||
} from '@openchatlab/node-runtime'
|
||||
import { getChatOverview, normalizeBuiltinToolNames } from '@openchatlab/core'
|
||||
import type { ChartAutoMode } from '@openchatlab/shared-types'
|
||||
@@ -33,6 +36,8 @@ import { buildSemanticSearchGuidance } from '@openchatlab/node-runtime'
|
||||
import { adaptToolsForAgent } from './tool-adapter'
|
||||
import { loadAssistantConfig } from './assistant-loader'
|
||||
import { runServerAgent } from './agent'
|
||||
import { createCliCrossChatTools } from './cross-chat-tool-adapter'
|
||||
import { getServerAiLogger } from './logger'
|
||||
|
||||
function getAiDir(dbManager: DatabaseManager): string {
|
||||
const pathProvider = (dbManager as any)['pathProvider']
|
||||
@@ -76,6 +81,8 @@ export function createCliRunAgentStream(
|
||||
options: {
|
||||
llmConfigStore?: LLMConfigStore
|
||||
semanticIndexService?: SemanticIndexRuntime
|
||||
crossChatAnalysisService?: CrossChatAnalysisService
|
||||
sessionAdapter?: SessionRuntimeAdapter
|
||||
} = {}
|
||||
): (params: AgentStreamRequest, onEvent: (chunk: AgentStreamChunk) => void, abortSignal: AbortSignal) => Promise<void> {
|
||||
const aiDataDir = getAiDir(dbManager)
|
||||
@@ -87,7 +94,9 @@ export function createCliRunAgentStream(
|
||||
userMessage,
|
||||
aiChatId,
|
||||
historyLeafMessageId,
|
||||
chatKind = 'session',
|
||||
sessionId,
|
||||
entityRefs,
|
||||
chatType,
|
||||
locale,
|
||||
assistantId,
|
||||
@@ -99,6 +108,20 @@ export function createCliRunAgentStream(
|
||||
thinkingLevel,
|
||||
} = params
|
||||
|
||||
const conversation = aiChatManager.getAIChat(aiChatId)
|
||||
if (
|
||||
!conversation ||
|
||||
conversation.kind !== chatKind ||
|
||||
(chatKind === 'session' && conversation.sessionId !== sessionId)
|
||||
) {
|
||||
onEvent({
|
||||
type: 'error',
|
||||
error: { name: 'ValidationError', message: 'AI conversation does not match the requested target' },
|
||||
})
|
||||
onEvent({ type: 'done', isFinished: true })
|
||||
return
|
||||
}
|
||||
|
||||
let assistantSystemPrompt: string | undefined
|
||||
let assistantAllowedTools: string[] | undefined
|
||||
if (assistantId) {
|
||||
@@ -114,6 +137,48 @@ export function createCliRunAgentStream(
|
||||
const contextWindow = llmConfig ? (buildPiModel(llmConfig).contextWindow ?? 128000) : 128000
|
||||
const maxToolResultTokens = Math.floor(contextWindow * (maxToolResultPercent / 100))
|
||||
|
||||
if (chatKind === 'global') {
|
||||
if (!llmConfig) {
|
||||
onEvent({ type: 'error', error: { name: 'ConfigError', message: 'LLM service not configured' } })
|
||||
onEvent({ type: 'done', isFinished: true })
|
||||
return
|
||||
}
|
||||
if (!options.crossChatAnalysisService || !options.sessionAdapter) {
|
||||
onEvent({ type: 'error', error: { name: 'RuntimeError', message: 'Cross-chat analysis is unavailable' } })
|
||||
onEvent({ type: 'done', isFinished: true })
|
||||
return
|
||||
}
|
||||
const tools = createCliCrossChatTools({
|
||||
analysisService: options.crossChatAnalysisService,
|
||||
sessionAdapter: options.sessionAdapter,
|
||||
locale,
|
||||
preprocessConfig: params.preprocessConfig,
|
||||
maxToolResultTokens,
|
||||
})
|
||||
await runCrossChatAgent({
|
||||
userMessage,
|
||||
entityRefs,
|
||||
aiChatId,
|
||||
historyLeafMessageId,
|
||||
locale,
|
||||
piModel: buildPiModel(llmConfig),
|
||||
apiKey: llmConfig.apiKey,
|
||||
tools,
|
||||
aiChatManager,
|
||||
onEvent,
|
||||
abortSignal,
|
||||
thinkingLevel: thinkingLevel as import('@openchatlab/core').ThinkingLevel | undefined,
|
||||
logger: getServerAiLogger() ?? undefined,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!sessionId) {
|
||||
onEvent({ type: 'error', error: { name: 'ValidationError', message: 'Session ID is required' } })
|
||||
onEvent({ type: 'done', isFinished: true })
|
||||
return
|
||||
}
|
||||
|
||||
const db = (dbManager as any).open?.(sessionId)
|
||||
const resolvedChartAutoMode: ChartAutoMode = chartAutoMode ?? 'suggest'
|
||||
const chartRuntime = resolveChartRuntimeForRequest({
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import type {
|
||||
AgentTool,
|
||||
CrossChatAnalysisService,
|
||||
PreprocessConfig,
|
||||
SessionRuntimeAdapter,
|
||||
} from '@openchatlab/node-runtime'
|
||||
import { preprocessCrossChatMessages } from '@openchatlab/node-runtime'
|
||||
import {
|
||||
CROSS_CHAT_AGENT_TOOL_REGISTRY,
|
||||
executeToolForAgent,
|
||||
toAgentToolParameters,
|
||||
type CrossChatToolExecutionContext,
|
||||
} from '@openchatlab/tools'
|
||||
|
||||
export function createCliCrossChatTools(options: {
|
||||
analysisService: CrossChatAnalysisService
|
||||
sessionAdapter: SessionRuntimeAdapter
|
||||
locale?: string
|
||||
preprocessConfig?: Record<string, unknown>
|
||||
maxToolResultTokens: number
|
||||
}): AgentTool<any, any>[] {
|
||||
const context: Omit<CrossChatToolExecutionContext, 'abortSignal' | 'reportProgress'> = {
|
||||
locale: options.locale,
|
||||
analysisService: options.analysisService,
|
||||
maxToolResultTokens: options.maxToolResultTokens,
|
||||
preprocessMessagesBySession: (sessionId, messages) =>
|
||||
preprocessCrossChatMessages(
|
||||
options.sessionAdapter,
|
||||
sessionId,
|
||||
messages,
|
||||
options.preprocessConfig as PreprocessConfig | undefined
|
||||
),
|
||||
}
|
||||
|
||||
return CROSS_CHAT_AGENT_TOOL_REGISTRY.map((tool) => ({
|
||||
name: tool.name,
|
||||
label: tool.name,
|
||||
description: tool.description,
|
||||
parameters: toAgentToolParameters(tool.inputSchema) as any,
|
||||
executionMode: tool.executionMode,
|
||||
execute: async (_toolCallId: string, params: unknown, signal, onUpdate) =>
|
||||
executeToolForAgent(tool, params, {
|
||||
...context,
|
||||
abortSignal: signal,
|
||||
reportProgress: (progress) => onUpdate?.({ content: [], details: { progress } }),
|
||||
}),
|
||||
}))
|
||||
}
|
||||
@@ -22,6 +22,9 @@ import {
|
||||
initAppLogger,
|
||||
appLogger,
|
||||
logNativeParserStatus,
|
||||
createContactsService,
|
||||
createCrossChatAnalysisService,
|
||||
createDatabaseManagerAdapter,
|
||||
} from '@openchatlab/node-runtime'
|
||||
import type { SemanticIndexRuntime } from '@openchatlab/node-runtime'
|
||||
import { createServer } from './server'
|
||||
@@ -136,6 +139,17 @@ export async function startHttpServer(options?: HttpServerOptions): Promise<{
|
||||
await migrationRunner.run()
|
||||
const nativeBinding = resolveNativeBinding()
|
||||
dbManager = new DatabaseManager(pathProvider, { nativeBinding, runtime })
|
||||
const sessionAdapter = createDatabaseManagerAdapter(dbManager)
|
||||
const contactsService = createContactsService({
|
||||
adapter: sessionAdapter,
|
||||
pathProvider,
|
||||
runtimeIdentity: runtime,
|
||||
nativeBinding,
|
||||
})
|
||||
const crossChatAnalysisService = createCrossChatAnalysisService({
|
||||
adapter: sessionAdapter,
|
||||
contactsService,
|
||||
})
|
||||
|
||||
const aiDataDir = pathProvider.getAiDataDir()
|
||||
aiChatManager = new AIChatManager(aiDataDir, { nativeBinding })
|
||||
@@ -185,6 +199,7 @@ export async function startHttpServer(options?: HttpServerOptions): Promise<{
|
||||
pathProvider,
|
||||
nativeBinding,
|
||||
runtimeIdentity: runtime,
|
||||
contactsService,
|
||||
semanticIndexService,
|
||||
aiContext: {
|
||||
aiDataDir,
|
||||
@@ -197,6 +212,8 @@ export async function startHttpServer(options?: HttpServerOptions): Promise<{
|
||||
runAgentStream: createCliRunAgentStream(dbManager, aiChatManager, {
|
||||
llmConfigStore,
|
||||
semanticIndexService,
|
||||
crossChatAnalysisService,
|
||||
sessionAdapter,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -29,6 +29,7 @@ import type {
|
||||
LLMConfigStore,
|
||||
CustomProviderStore,
|
||||
CustomModelStore,
|
||||
ContactsService,
|
||||
PendingDataDirMigration,
|
||||
RuntimeIdentity,
|
||||
} from '@openchatlab/node-runtime'
|
||||
@@ -71,6 +72,7 @@ export function registerWebRoutes(
|
||||
pathProvider?: PathProvider
|
||||
nativeBinding?: string
|
||||
runtimeIdentity?: RuntimeIdentity
|
||||
contactsService?: ContactsService
|
||||
aiContext?: AiContextOptions
|
||||
/** 由 server 入口注入的共享语义索引运行时;传入时由调用方管理生命周期 */
|
||||
semanticIndexService?: SemanticIndexRuntime
|
||||
@@ -132,6 +134,7 @@ export function registerWebRoutes(
|
||||
getVersion,
|
||||
nativeBinding: options?.nativeBinding,
|
||||
semanticIndexService,
|
||||
contactsService: options?.contactsService,
|
||||
analyticsService,
|
||||
getCurrentAiLogPath: () => getServerAiLogger()?.getExistingLogPath() ?? null,
|
||||
openDirectory: openDirectoryPath,
|
||||
|
||||
Reference in new issue
Block a user