@lobehub/chat 1.128.0 → 1.128.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/.github/workflows/test.yml +8 -1
  2. package/CHANGELOG.md +25 -0
  3. package/changelog/v1.json +9 -0
  4. package/next.config.ts +8 -1
  5. package/package.json +71 -69
  6. package/packages/context-engine/ARCHITECTURE.md +425 -0
  7. package/packages/context-engine/package.json +40 -0
  8. package/packages/context-engine/src/base/BaseProcessor.ts +87 -0
  9. package/packages/context-engine/src/base/BaseProvider.ts +22 -0
  10. package/packages/context-engine/src/index.ts +32 -0
  11. package/packages/context-engine/src/pipeline.ts +219 -0
  12. package/packages/context-engine/src/processors/HistoryTruncate.ts +76 -0
  13. package/packages/context-engine/src/processors/InputTemplate.ts +83 -0
  14. package/packages/context-engine/src/processors/MessageCleanup.ts +87 -0
  15. package/packages/context-engine/src/processors/MessageContent.ts +298 -0
  16. package/packages/context-engine/src/processors/PlaceholderVariables.ts +196 -0
  17. package/packages/context-engine/src/processors/ToolCall.ts +186 -0
  18. package/packages/context-engine/src/processors/ToolMessageReorder.ts +113 -0
  19. package/packages/context-engine/src/processors/__tests__/HistoryTruncate.test.ts +175 -0
  20. package/packages/context-engine/src/processors/__tests__/InputTemplate.test.ts +243 -0
  21. package/packages/context-engine/src/processors/__tests__/MessageContent.test.ts +394 -0
  22. package/packages/context-engine/src/processors/__tests__/PlaceholderVariables.test.ts +334 -0
  23. package/packages/context-engine/src/processors/__tests__/ToolMessageReorder.test.ts +186 -0
  24. package/packages/context-engine/src/processors/index.ts +15 -0
  25. package/packages/context-engine/src/providers/HistorySummary.ts +102 -0
  26. package/packages/context-engine/src/providers/InboxGuide.ts +102 -0
  27. package/packages/context-engine/src/providers/SystemRoleInjector.ts +64 -0
  28. package/packages/context-engine/src/providers/ToolSystemRole.ts +118 -0
  29. package/packages/context-engine/src/providers/__tests__/HistorySummaryProvider.test.ts +112 -0
  30. package/packages/context-engine/src/providers/__tests__/InboxGuideProvider.test.ts +121 -0
  31. package/packages/context-engine/src/providers/__tests__/SystemRoleInjector.test.ts +200 -0
  32. package/packages/context-engine/src/providers/__tests__/ToolSystemRoleProvider.test.ts +140 -0
  33. package/packages/context-engine/src/providers/index.ts +11 -0
  34. package/packages/context-engine/src/types.ts +201 -0
  35. package/packages/context-engine/vitest.config.mts +10 -0
  36. package/packages/database/package.json +1 -1
  37. package/packages/prompts/src/prompts/systemRole/index.ts +1 -1
  38. package/packages/utils/src/index.ts +2 -0
  39. package/packages/utils/src/uriParser.test.ts +29 -0
  40. package/packages/utils/src/uriParser.ts +24 -0
  41. package/src/services/{__tests__ → chat}/chat.test.ts +22 -1032
  42. package/src/services/chat/clientModelRuntime.test.ts +385 -0
  43. package/src/services/chat/clientModelRuntime.ts +34 -0
  44. package/src/services/chat/contextEngineering.test.ts +848 -0
  45. package/src/services/chat/contextEngineering.ts +123 -0
  46. package/src/services/chat/helper.ts +61 -0
  47. package/src/services/{chat.ts → chat/index.ts} +24 -366
  48. package/src/services/chat/types.ts +9 -0
  49. package/src/services/models.ts +1 -1
  50. package/src/store/aiInfra/slices/aiModel/selectors.ts +2 -2
  51. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +1 -40
  52. /package/src/services/{__tests__ → chat}/__snapshots__/chat.test.ts.snap +0 -0
@@ -0,0 +1,219 @@
1
+ import debug from 'debug';
2
+
3
+ import type {
4
+ AgentState,
5
+ ContextProcessor,
6
+ PipelineContext,
7
+ PipelineResult,
8
+ ProcessorOptions,
9
+ } from './types';
10
+ import { PipelineError } from './types';
11
+
12
+ const log = debug('context-engine:ContextEngine');
13
+
14
+ /**
15
+ * Context Engine Configuration
16
+ */
17
+ export interface ContextEngineConfig extends ProcessorOptions {
18
+ /** Processor pipeline */
19
+ pipeline: ContextProcessor[];
20
+ }
21
+
22
+ /**
23
+ * Context Engine - Core orchestrator that executes processors sequentially
24
+ */
25
+ export class ContextEngine {
26
+ private processors: ContextProcessor[] = [];
27
+ private options: ProcessorOptions;
28
+
29
+ constructor(config: ContextEngineConfig) {
30
+ const { pipeline, ...options } = config;
31
+ this.processors = [...pipeline];
32
+ this.options = {
33
+ debug: false,
34
+ logger: console.log,
35
+ ...options,
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Add processor to pipeline
41
+ */
42
+ addProcessor(processor: ContextProcessor): this {
43
+ this.processors.push(processor);
44
+ return this;
45
+ }
46
+
47
+ /**
48
+ * Remove processor
49
+ */
50
+ removeProcessor(name: string): this {
51
+ this.processors = this.processors.filter((p) => p.name !== name);
52
+ return this;
53
+ }
54
+
55
+ /**
56
+ * Get processor list
57
+ */
58
+ getProcessors(): ContextProcessor[] {
59
+ return [...this.processors];
60
+ }
61
+
62
+ /**
63
+ * Clear all processors
64
+ */
65
+ clear(): this {
66
+ this.processors = [];
67
+ return this;
68
+ }
69
+
70
+ /**
71
+ * Execute pipeline processing
72
+ */
73
+ async process(input: {
74
+ initialState: AgentState;
75
+ maxTokens: number;
76
+ messages?: Array<any>;
77
+ metadata?: Record<string, any>;
78
+ model: string;
79
+ }): Promise<PipelineResult> {
80
+ const startTime = Date.now();
81
+ const processorDurations: Record<string, number> = {};
82
+
83
+ // Create initial pipeline context
84
+ let context: PipelineContext = {
85
+ initialState: input.initialState,
86
+ isAborted: false,
87
+ messages: Array.isArray(input.messages) ? [...input.messages] : [],
88
+ metadata: {
89
+ maxTokens: input.maxTokens,
90
+ model: input.model,
91
+ ...input.metadata,
92
+ },
93
+ };
94
+
95
+ log('Starting pipeline processing');
96
+ log('Number of processors:', this.processors.length);
97
+
98
+ let processedCount = 0;
99
+
100
+ try {
101
+ // Execute each processor in sequence
102
+ for (const processor of this.processors) {
103
+ if (context.isAborted) {
104
+ log('Pipeline aborted before processor', processor.name, 'reason:', context.abortReason);
105
+ break;
106
+ }
107
+
108
+ const processorStartTime = Date.now();
109
+ log('Executing processor:', processor.name);
110
+
111
+ try {
112
+ context = await processor.process(context);
113
+ processedCount++;
114
+
115
+ const duration = Date.now() - processorStartTime;
116
+ processorDurations[processor.name] = duration;
117
+
118
+ log('Processor', processor.name, 'completed in', duration + 'ms');
119
+
120
+ if (context.isAborted) {
121
+ log('Pipeline aborted by processor', processor.name, 'reason:', context.abortReason);
122
+ break;
123
+ }
124
+ } catch (error) {
125
+ const duration = Date.now() - processorStartTime;
126
+ processorDurations[processor.name] = duration;
127
+
128
+ log('Processor', processor.name, 'execution failed:', error);
129
+ throw new PipelineError(
130
+ `Processor [${processor.name}] execution failed`,
131
+ processor.name,
132
+ error instanceof Error ? error : new Error(String(error)),
133
+ );
134
+ }
135
+ }
136
+
137
+ const totalDuration = Date.now() - startTime;
138
+ log('Pipeline processing completed in', totalDuration + 'ms');
139
+
140
+ return {
141
+ abortReason: context.abortReason,
142
+ isAborted: context.isAborted,
143
+ messages: context.messages,
144
+ metadata: context.metadata,
145
+ stats: {
146
+ processedCount,
147
+ processorDurations,
148
+ totalDuration,
149
+ },
150
+ };
151
+ } catch (error) {
152
+ log('Pipeline processing failed:', error);
153
+
154
+ if (error instanceof PipelineError) {
155
+ throw error;
156
+ }
157
+
158
+ throw new PipelineError(
159
+ 'Unknown error occurred during pipeline processing',
160
+ undefined,
161
+ error instanceof Error ? error : new Error(String(error)),
162
+ );
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Get pipeline statistics
168
+ */
169
+ getStats() {
170
+ return {
171
+ processorCount: this.processors.length,
172
+ processorNames: this.processors.map((p) => p.name),
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Clone pipeline (deep copy processor list)
178
+ */
179
+ clone(): ContextEngine {
180
+ return new ContextEngine({
181
+ pipeline: [...this.processors],
182
+ ...this.options,
183
+ });
184
+ }
185
+
186
+ /**
187
+ * Validate pipeline configuration
188
+ */
189
+ validate(): { errors: string[]; valid: boolean } {
190
+ const errors: string[] = [];
191
+
192
+ // Check for duplicate processor names
193
+ const names = this.processors.map((p) => p.name);
194
+ const duplicates = names.filter((name, index) => names.indexOf(name) !== index);
195
+ if (duplicates.length > 0) {
196
+ errors.push(`Found duplicate processor names: ${duplicates.join(', ')}`);
197
+ }
198
+
199
+ // Check if processors are empty
200
+ if (this.processors.length === 0) {
201
+ errors.push('No processors in pipeline');
202
+ }
203
+
204
+ // Check if processors implement required methods
205
+ this.processors.forEach((processor) => {
206
+ if (!processor.name) {
207
+ errors.push('Processor missing name');
208
+ }
209
+ if (typeof processor.process !== 'function') {
210
+ errors.push(`Processor [${processor.name}] missing process method`);
211
+ }
212
+ });
213
+
214
+ return {
215
+ errors,
216
+ valid: errors.length === 0,
217
+ };
218
+ }
219
+ }
@@ -0,0 +1,76 @@
1
+ import debug from 'debug';
2
+
3
+ import { BaseProcessor } from '../base/BaseProcessor';
4
+ import type { PipelineContext, ProcessorOptions } from '../types';
5
+
6
+ const log = debug('context-engine:processor:HistoryTruncateProcessor');
7
+
8
+ export interface HistoryTruncateConfig {
9
+ /** Whether to enable history count limit */
10
+ enableHistoryCount?: boolean;
11
+ /** Maximum number of historical messages to keep */
12
+ historyCount?: number;
13
+ }
14
+
15
+ /**
16
+ * Slice messages based on history count configuration
17
+ * @param messages Original messages array
18
+ * @param options Configuration options for slicing
19
+ * @returns Sliced messages array
20
+ */
21
+ export const getSlicedMessages = (
22
+ messages: any[],
23
+ options: {
24
+ enableHistoryCount?: boolean;
25
+ historyCount?: number;
26
+ },
27
+ ): any[] => {
28
+ // if historyCount is not enabled, return all messages
29
+ if (!options.enableHistoryCount || options.historyCount === undefined) return messages;
30
+
31
+ // if historyCount is negative or set to 0, return empty array
32
+ if (options.historyCount <= 0) return [];
33
+
34
+ // if historyCount is positive, return last N messages
35
+ return messages.slice(-options.historyCount);
36
+ };
37
+
38
+ /**
39
+ * History Truncate Processor
40
+ * Responsible for limiting message history based on configuration
41
+ */
42
+ export class HistoryTruncateProcessor extends BaseProcessor {
43
+ readonly name = 'HistoryTruncateProcessor';
44
+
45
+ constructor(
46
+ private config: HistoryTruncateConfig,
47
+ options: ProcessorOptions = {},
48
+ ) {
49
+ super(options);
50
+ }
51
+
52
+ protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
53
+ const clonedContext = this.cloneContext(context);
54
+
55
+ const originalCount = clonedContext.messages.length;
56
+
57
+ // Apply history truncation
58
+ clonedContext.messages = getSlicedMessages(clonedContext.messages, {
59
+ enableHistoryCount: this.config.enableHistoryCount,
60
+ historyCount: this.config.historyCount,
61
+ });
62
+
63
+ const finalCount = clonedContext.messages.length;
64
+ const truncatedCount = originalCount - finalCount;
65
+
66
+ // Update metadata
67
+ clonedContext.metadata.historyTruncated = truncatedCount;
68
+ clonedContext.metadata.finalMessageCount = finalCount;
69
+
70
+ log(
71
+ `History truncation completed, truncated ${truncatedCount} messages (${originalCount} → ${finalCount})`,
72
+ );
73
+
74
+ return this.markAsExecuted(clonedContext);
75
+ }
76
+ }
@@ -0,0 +1,83 @@
1
+ import debug from 'debug';
2
+ import { template } from 'lodash-es';
3
+
4
+ import { BaseProcessor } from '../base/BaseProcessor';
5
+ import type { PipelineContext, ProcessorOptions } from '../types';
6
+
7
+ const log = debug('context-engine:processor:InputTemplateProcessor');
8
+
9
+ export interface InputTemplateConfig {
10
+ /** Input message template string */
11
+ inputTemplate?: string;
12
+ }
13
+
14
+ /**
15
+ * Input Template Processor
16
+ * Responsible for applying input message templates to user messages
17
+ */
18
+ export class InputTemplateProcessor extends BaseProcessor {
19
+ readonly name = 'InputTemplateProcessor';
20
+
21
+ constructor(
22
+ private config: InputTemplateConfig,
23
+ options: ProcessorOptions = {},
24
+ ) {
25
+ super(options);
26
+ }
27
+
28
+ protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
29
+ const clonedContext = this.cloneContext(context);
30
+
31
+ // Skip processing if no template is configured
32
+ if (!this.config.inputTemplate) {
33
+ log('No input template configured, skipping processing');
34
+ return this.markAsExecuted(clonedContext);
35
+ }
36
+
37
+ let processedCount = 0;
38
+
39
+ try {
40
+ // Compile the template
41
+ const compiler = template(this.config.inputTemplate, {
42
+ interpolate: /{{\s*(text)\s*}}/g,
43
+ });
44
+
45
+ log(`Applying input template: ${this.config.inputTemplate}`);
46
+
47
+ // Process each message
48
+ for (let i = 0; i < clonedContext.messages.length; i++) {
49
+ const message = clonedContext.messages[i];
50
+
51
+ // Only apply template to user messages
52
+ if (message.role === 'user') {
53
+ try {
54
+ const originalContent = message.content;
55
+ const processedContent = compiler({ text: originalContent });
56
+
57
+ if (processedContent !== originalContent) {
58
+ clonedContext.messages[i] = {
59
+ ...message,
60
+ content: processedContent,
61
+ };
62
+ processedCount++;
63
+ log(`Applied template to message ${message.id}`);
64
+ }
65
+ } catch (error) {
66
+ log.extend('error')(`Error applying template to message ${message.id}: ${error}`);
67
+ // Keep original message on error
68
+ }
69
+ }
70
+ }
71
+ } catch (error) {
72
+ log.extend('error')(`Template compilation failed: ${error}`);
73
+ // Skip processing if template compilation fails
74
+ }
75
+
76
+ // Update metadata
77
+ clonedContext.metadata.inputTemplateProcessed = processedCount;
78
+
79
+ log(`Input template processing completed, processed ${processedCount} messages`);
80
+
81
+ return this.markAsExecuted(clonedContext);
82
+ }
83
+ }
@@ -0,0 +1,87 @@
1
+ import debug from 'debug';
2
+
3
+ import { BaseProcessor } from '../base/BaseProcessor';
4
+ import type { PipelineContext, ProcessorOptions } from '../types';
5
+
6
+ const log = debug('context-engine:processor:MessageCleanupProcessor');
7
+
8
+ /**
9
+ * 消息清理处理器
10
+ * 负责清理消息中的多余字段,只保留 OpenAI 格式所需的必要字段
11
+ */
12
+ export class MessageCleanupProcessor extends BaseProcessor {
13
+ readonly name = 'MessageCleanupProcessor';
14
+
15
+ constructor(options: ProcessorOptions = {}) {
16
+ super(options);
17
+ }
18
+
19
+ protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
20
+ const clonedContext = this.cloneContext(context);
21
+
22
+ let cleanedCount = 0;
23
+
24
+ // 清理每条消息,只保留必要字段
25
+ for (let i = 0; i < clonedContext.messages.length; i++) {
26
+ const message = clonedContext.messages[i];
27
+ const cleanedMessage = this.cleanMessage(message);
28
+
29
+ if (cleanedMessage !== message) {
30
+ clonedContext.messages[i] = cleanedMessage;
31
+ cleanedCount++;
32
+ }
33
+ }
34
+
35
+ // 更新元数据
36
+ clonedContext.metadata.messageCleanup = {
37
+ cleanedCount,
38
+ totalMessages: clonedContext.messages.length,
39
+ };
40
+
41
+ log(`Message cleanup completed, cleaned ${cleanedCount} messages`);
42
+ return this.markAsExecuted(clonedContext);
43
+ }
44
+
45
+ /**
46
+ * 清理单条消息,只保留必要字段
47
+ */
48
+ private cleanMessage(message: any): any {
49
+ switch (message.role) {
50
+ case 'system': {
51
+ return {
52
+ content: message.content,
53
+ role: message.role,
54
+ };
55
+ }
56
+
57
+ case 'user': {
58
+ return {
59
+ content: message.content,
60
+ role: message.role,
61
+ };
62
+ }
63
+
64
+ case 'assistant': {
65
+ return {
66
+ content: message.content,
67
+ role: message.role,
68
+ ...(message.tool_calls && { tool_calls: message.tool_calls }),
69
+ };
70
+ }
71
+
72
+ case 'tool': {
73
+ return {
74
+ content: message.content,
75
+ role: message.role,
76
+ tool_call_id: message.tool_call_id,
77
+ ...(message.name && { name: message.name }),
78
+ };
79
+ }
80
+
81
+ default: {
82
+ // 对于未知角色,保持原样
83
+ return message;
84
+ }
85
+ }
86
+ }
87
+ }