@zhin.js/agent 0.0.2 → 0.0.4
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.
- package/CHANGELOG.md +17 -0
- package/lib/agent.d.ts +4 -129
- package/lib/agent.d.ts.map +1 -1
- package/lib/agent.js +3 -733
- package/lib/agent.js.map +1 -1
- package/lib/compaction.d.ts +3 -129
- package/lib/compaction.d.ts.map +1 -1
- package/lib/compaction.js +2 -367
- package/lib/compaction.js.map +1 -1
- package/lib/context-manager.d.ts +3 -210
- package/lib/context-manager.d.ts.map +1 -1
- package/lib/context-manager.js +2 -310
- package/lib/context-manager.js.map +1 -1
- package/lib/conversation-memory.d.ts +3 -189
- package/lib/conversation-memory.d.ts.map +1 -1
- package/lib/conversation-memory.js +2 -616
- package/lib/conversation-memory.js.map +1 -1
- package/lib/init/create-zhin-agent.d.ts.map +1 -1
- package/lib/init/create-zhin-agent.js +1 -3
- package/lib/init/create-zhin-agent.js.map +1 -1
- package/lib/output.d.ts +3 -90
- package/lib/output.d.ts.map +1 -1
- package/lib/output.js +2 -173
- package/lib/output.js.map +1 -1
- package/lib/rate-limiter.d.ts +3 -35
- package/lib/rate-limiter.d.ts.map +1 -1
- package/lib/rate-limiter.js +2 -83
- package/lib/rate-limiter.js.map +1 -1
- package/lib/session.d.ts +3 -190
- package/lib/session.d.ts.map +1 -1
- package/lib/session.js +2 -462
- package/lib/session.js.map +1 -1
- package/lib/storage.d.ts +3 -65
- package/lib/storage.d.ts.map +1 -1
- package/lib/storage.js +2 -102
- package/lib/storage.js.map +1 -1
- package/lib/tone-detector.d.ts +3 -16
- package/lib/tone-detector.d.ts.map +1 -1
- package/lib/tone-detector.js +2 -69
- package/lib/tone-detector.js.map +1 -1
- package/package.json +3 -2
- package/src/agent.ts +4 -852
- package/src/compaction.ts +27 -528
- package/src/context-manager.ts +14 -439
- package/src/conversation-memory.ts +3 -814
- package/src/init/create-zhin-agent.ts +1 -3
- package/src/output.ts +14 -260
- package/src/rate-limiter.ts +3 -127
- package/src/session.ts +12 -565
- package/src/storage.ts +8 -134
- package/src/tone-detector.ts +3 -87
- package/tests/ai/setup.ts +20 -84
- package/tests/ai/agent.test.ts +0 -565
- package/tests/ai/context-manager.test.ts +0 -413
- package/tests/ai/conversation-memory.test.ts +0 -128
- package/tests/ai/output.test.ts +0 -128
- package/tests/ai/rate-limiter.test.ts +0 -108
- package/tests/ai/session.test.ts +0 -334
- package/tests/ai/tone-detector.test.ts +0 -80
package/src/storage.ts
CHANGED
|
@@ -1,135 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* // ... later, when DB is ready:
|
|
11
|
-
* const dbBackend = new DatabaseStorageBackend<MyRecord>(model, { keyField: 'session_id' });
|
|
12
|
-
* service.upgradeBackend(dbBackend);
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
export interface StorageBackend<T extends Record<string, any>> {
|
|
16
|
-
get(key: string): Promise<T | null>;
|
|
17
|
-
set(key: string, value: T): Promise<void>;
|
|
18
|
-
delete(key: string): Promise<boolean>;
|
|
19
|
-
list(filter?: Partial<T>): Promise<T[]>;
|
|
20
|
-
clear(): Promise<void>;
|
|
21
|
-
readonly type: 'memory' | 'database';
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* In-memory storage backend.
|
|
26
|
-
*/
|
|
27
|
-
export class MemoryStorageBackend<T extends Record<string, any>> implements StorageBackend<T> {
|
|
28
|
-
readonly type = 'memory' as const;
|
|
29
|
-
private data = new Map<string, T>();
|
|
30
|
-
|
|
31
|
-
async get(key: string): Promise<T | null> {
|
|
32
|
-
return this.data.get(key) ?? null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async set(key: string, value: T): Promise<void> {
|
|
36
|
-
this.data.set(key, value);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async delete(key: string): Promise<boolean> {
|
|
40
|
-
return this.data.delete(key);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async list(filter?: Partial<T>): Promise<T[]> {
|
|
44
|
-
const all = Array.from(this.data.values());
|
|
45
|
-
if (!filter) return all;
|
|
46
|
-
return all.filter(item => {
|
|
47
|
-
for (const [k, v] of Object.entries(filter)) {
|
|
48
|
-
if (item[k] !== v) return false;
|
|
49
|
-
}
|
|
50
|
-
return true;
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async clear(): Promise<void> {
|
|
55
|
-
this.data.clear();
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Database model interface aligned with @zhin.js/database's RelatedModel API.
|
|
61
|
-
*/
|
|
62
|
-
export interface DbModel {
|
|
63
|
-
select(...fields: string[]): any;
|
|
64
|
-
create(data: Record<string, any>): Promise<any>;
|
|
65
|
-
update(data: Partial<any>): any;
|
|
66
|
-
delete(condition: Record<string, any>): any;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Database-backed storage backend.
|
|
71
|
-
*/
|
|
72
|
-
export class DatabaseStorageBackend<T extends Record<string, any>> implements StorageBackend<T> {
|
|
73
|
-
readonly type = 'database' as const;
|
|
74
|
-
|
|
75
|
-
constructor(
|
|
76
|
-
private model: DbModel,
|
|
77
|
-
private options: {
|
|
78
|
-
/** The field name used as lookup key (e.g. 'session_id', 'user_id') */
|
|
79
|
-
keyField: string;
|
|
80
|
-
},
|
|
81
|
-
) {}
|
|
82
|
-
|
|
83
|
-
async get(key: string): Promise<T | null> {
|
|
84
|
-
const rows: T[] = await this.model
|
|
85
|
-
.select()
|
|
86
|
-
.where({ [this.options.keyField]: key });
|
|
87
|
-
return rows[0] ?? null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async set(key: string, value: T): Promise<void> {
|
|
91
|
-
const existing = await this.get(key);
|
|
92
|
-
if (existing) {
|
|
93
|
-
await this.model
|
|
94
|
-
.update(value)
|
|
95
|
-
.where({ [this.options.keyField]: key });
|
|
96
|
-
} else {
|
|
97
|
-
await this.model.create({ ...value, [this.options.keyField]: key });
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async delete(key: string): Promise<boolean> {
|
|
102
|
-
try {
|
|
103
|
-
await this.model.delete({ [this.options.keyField]: key });
|
|
104
|
-
return true;
|
|
105
|
-
} catch {
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async list(filter?: Partial<T>): Promise<T[]> {
|
|
111
|
-
if (filter) {
|
|
112
|
-
return this.model.select().where(filter);
|
|
113
|
-
}
|
|
114
|
-
return this.model.select();
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async clear(): Promise<void> {
|
|
118
|
-
const all = await this.list();
|
|
119
|
-
for (const item of all) {
|
|
120
|
-
const key = (item as Record<string, unknown>)[this.options.keyField] as string | undefined;
|
|
121
|
-
if (key) await this.delete(key);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Helper to create a swappable backend ref.
|
|
128
|
-
* Call `swap(newBackend)` to atomically upgrade from memory to database.
|
|
129
|
-
*/
|
|
130
|
-
export function createSwappableBackend<T extends Record<string, any>>(
|
|
131
|
-
initial: StorageBackend<T>,
|
|
132
|
-
): { backend: StorageBackend<T>; swap: (next: StorageBackend<T>) => void } {
|
|
133
|
-
const ref = { backend: initial, swap: (next: StorageBackend<T>) => { ref.backend = next; } };
|
|
134
|
-
return ref;
|
|
135
|
-
}
|
|
2
|
+
* Re-export from @zhin.js/ai for backward compatibility.
|
|
3
|
+
*/
|
|
4
|
+
export {
|
|
5
|
+
MemoryStorageBackend,
|
|
6
|
+
DatabaseStorageBackend,
|
|
7
|
+
createSwappableBackend,
|
|
8
|
+
} from '@zhin.js/ai';
|
|
9
|
+
export type { StorageBackend, DbModel } from '@zhin.js/ai';
|
package/src/tone-detector.ts
CHANGED
|
@@ -1,89 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* 通过标点符号、emoji 密度、关键词分析用户语气,
|
|
5
|
-
* 生成一条 hint 注入 system prompt,让 AI 的回复匹配用户情绪。
|
|
6
|
-
*
|
|
7
|
-
* 零 LLM 开销,纯正则/统计分析。
|
|
2
|
+
* Re-export from @zhin.js/ai for backward compatibility.
|
|
8
3
|
*/
|
|
9
|
-
|
|
10
|
-
export type Tone
|
|
11
|
-
|
|
12
|
-
interface ToneResult {
|
|
13
|
-
tone: Tone;
|
|
14
|
-
hint: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// 常见负面情绪词
|
|
18
|
-
const FRUSTRATED_WORDS = /不行|不对|又错|还是不|怎么回事|搞不定|烦死|崩溃|无语|什么鬼|bug|报错|失败|出问题/;
|
|
19
|
-
const SAD_WORDS = /难过|伤心|失落|遗憾|可惜|唉|哎|不开心|郁闷|心累/;
|
|
20
|
-
const URGENT_WORDS = /急|赶紧|马上|立刻|紧急|尽快|快点|asap|hurry/i;
|
|
21
|
-
const EXCITED_WORDS = /太好了|太棒了|厉害|牛|可以|成功|搞定|完美|赞|nice|amazing|awesome|cool/i;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* 检测用户消息的情绪语气
|
|
25
|
-
*/
|
|
26
|
-
export function detectTone(message: string): ToneResult {
|
|
27
|
-
const len = message.length;
|
|
28
|
-
if (len === 0) return { tone: 'neutral', hint: '' };
|
|
29
|
-
|
|
30
|
-
// 统计特征
|
|
31
|
-
const exclamations = (message.match(/!/g) || []).length + (message.match(/!/g) || []).length;
|
|
32
|
-
const questions = (message.match(/\?/g) || []).length + (message.match(/?/g) || []).length;
|
|
33
|
-
const ellipsis = (message.match(/\.\.\./g) || []).length + (message.match(/…/g) || []).length;
|
|
34
|
-
const capsRatio = len > 5 ? (message.match(/[A-Z]/g) || []).length / len : 0;
|
|
35
|
-
|
|
36
|
-
// emoji 检测(常见 Unicode 范围)
|
|
37
|
-
const emojiCount = (message.match(/[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/gu) || []).length;
|
|
38
|
-
|
|
39
|
-
// 关键词检测
|
|
40
|
-
const isFrustrated = FRUSTRATED_WORDS.test(message);
|
|
41
|
-
const isSad = SAD_WORDS.test(message);
|
|
42
|
-
const isUrgent = URGENT_WORDS.test(message);
|
|
43
|
-
const isExcited = EXCITED_WORDS.test(message);
|
|
44
|
-
|
|
45
|
-
// 判定优先级: frustrated > urgent > sad > excited > questioning > neutral
|
|
46
|
-
if (isFrustrated || (exclamations >= 3 && !isExcited)) {
|
|
47
|
-
return {
|
|
48
|
-
tone: 'frustrated',
|
|
49
|
-
hint: '用户似乎有些沮丧或受挫,请用耐心、理解的语气回复,先表示共情再提供帮助。',
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (isUrgent) {
|
|
54
|
-
return {
|
|
55
|
-
tone: 'urgent',
|
|
56
|
-
hint: '用户似乎很着急,请直接给出解决方案,减少寒暄,优先效率。',
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (isSad || ellipsis >= 2) {
|
|
61
|
-
return {
|
|
62
|
-
tone: 'sad',
|
|
63
|
-
hint: '用户的语气似乎有些低落,请用温暖、关心的语气回复。',
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (isExcited || (emojiCount >= 2 && exclamations >= 1)) {
|
|
68
|
-
return {
|
|
69
|
-
tone: 'excited',
|
|
70
|
-
hint: '用户的心情不错,可以用更活泼、热情的语气回复。',
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (questions >= 2 || (questions >= 1 && len < 20)) {
|
|
75
|
-
return {
|
|
76
|
-
tone: 'questioning',
|
|
77
|
-
hint: '', // 提问是正常的,不需要特殊 hint
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (capsRatio > 0.5 && len > 10) {
|
|
82
|
-
return {
|
|
83
|
-
tone: 'frustrated',
|
|
84
|
-
hint: '用户使用了大量大写字母,可能在表达强烈情绪,请注意语气。',
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return { tone: 'neutral', hint: '' };
|
|
89
|
-
}
|
|
4
|
+
export { detectTone } from '@zhin.js/ai';
|
|
5
|
+
export type { Tone } from '@zhin.js/ai';
|
package/tests/ai/setup.ts
CHANGED
|
@@ -1,17 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent 模块测试环境设置
|
|
3
3
|
* 提供 Mock 与测试辅助,类型与规范从 @zhin.js/core 引用
|
|
4
|
+
* 通用 AI mocks 从 @zhin.js/ai 测试 setup 导入
|
|
4
5
|
*/
|
|
5
6
|
import { vi } from 'vitest';
|
|
6
7
|
import type { Message, MessageElement, Tool, ToolContext } from '@zhin.js/core';
|
|
7
|
-
import type { AIConfig
|
|
8
|
-
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
import type { AIConfig } from '@zhin.js/core';
|
|
9
|
+
|
|
10
|
+
// Import and re-export generic AI mocks from ai package tests
|
|
11
|
+
import {
|
|
12
|
+
createMockLogger,
|
|
13
|
+
createMockProvider,
|
|
14
|
+
createChatMessage,
|
|
15
|
+
delay,
|
|
16
|
+
waitFor,
|
|
17
|
+
collectAsyncGenerator,
|
|
18
|
+
} from '../../../ai/tests/setup.js';
|
|
19
|
+
export {
|
|
20
|
+
createMockLogger,
|
|
21
|
+
createMockProvider,
|
|
22
|
+
createChatMessage,
|
|
23
|
+
delay,
|
|
24
|
+
waitFor,
|
|
25
|
+
collectAsyncGenerator,
|
|
26
|
+
};
|
|
15
27
|
|
|
16
28
|
export const createMockPlugin = (name = 'test-plugin') => ({
|
|
17
29
|
name,
|
|
@@ -134,54 +146,6 @@ export const createMockTool = (options: MockToolOptions): Tool => {
|
|
|
134
146
|
return tool;
|
|
135
147
|
};
|
|
136
148
|
|
|
137
|
-
export interface MockProviderOptions {
|
|
138
|
-
name?: string;
|
|
139
|
-
response?: string | AsyncGenerator<string>;
|
|
140
|
-
toolCalls?: Array<{ name: string; arguments: Record<string, any> }>;
|
|
141
|
-
error?: Error;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export const createMockProvider = (options: MockProviderOptions = {}) => {
|
|
145
|
-
const {
|
|
146
|
-
name = 'mock',
|
|
147
|
-
response = '这是 AI 的回复',
|
|
148
|
-
toolCalls = [],
|
|
149
|
-
error,
|
|
150
|
-
} = options;
|
|
151
|
-
|
|
152
|
-
if (error) {
|
|
153
|
-
return {
|
|
154
|
-
name,
|
|
155
|
-
chat: vi.fn().mockRejectedValue(error),
|
|
156
|
-
healthCheck: vi.fn().mockResolvedValue(false),
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const generateResponse = async function* (): AsyncGenerator<{
|
|
161
|
-
content?: string;
|
|
162
|
-
toolCalls?: Array<{ name: string; arguments: Record<string, any> }>;
|
|
163
|
-
done: boolean;
|
|
164
|
-
}> {
|
|
165
|
-
if (toolCalls.length > 0) {
|
|
166
|
-
yield { toolCalls, done: false };
|
|
167
|
-
}
|
|
168
|
-
if (typeof response === 'string') {
|
|
169
|
-
yield { content: response, done: true };
|
|
170
|
-
} else {
|
|
171
|
-
for await (const chunk of response) {
|
|
172
|
-
yield { content: chunk, done: false };
|
|
173
|
-
}
|
|
174
|
-
yield { done: true };
|
|
175
|
-
}
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
name,
|
|
180
|
-
chat: vi.fn().mockImplementation(() => generateResponse()),
|
|
181
|
-
healthCheck: vi.fn().mockResolvedValue(true),
|
|
182
|
-
};
|
|
183
|
-
};
|
|
184
|
-
|
|
185
149
|
export const createToolContext = (options: Partial<ToolContext> = {}): ToolContext => ({
|
|
186
150
|
platform: 'test',
|
|
187
151
|
scope: 'group',
|
|
@@ -209,34 +173,6 @@ export const createMockAIConfig = (overrides: Partial<AIConfig> = {}): AIConfig
|
|
|
209
173
|
...overrides,
|
|
210
174
|
});
|
|
211
175
|
|
|
212
|
-
export const waitFor = async <T>(promise: Promise<T>, timeout = 5000): Promise<T> => {
|
|
213
|
-
return Promise.race([
|
|
214
|
-
promise,
|
|
215
|
-
new Promise<T>((_, reject) =>
|
|
216
|
-
setTimeout(() => reject(new Error('Timeout')), timeout)
|
|
217
|
-
),
|
|
218
|
-
]);
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
export const collectAsyncGenerator = async <T>(generator: AsyncGenerator<T>): Promise<T[]> => {
|
|
222
|
-
const results: T[] = [];
|
|
223
|
-
for await (const item of generator) {
|
|
224
|
-
results.push(item);
|
|
225
|
-
}
|
|
226
|
-
return results;
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
export const delay = (ms: number): Promise<void> =>
|
|
230
|
-
new Promise((resolve) => setTimeout(resolve, ms));
|
|
231
|
-
|
|
232
|
-
export const createChatMessage = (
|
|
233
|
-
role: 'user' | 'assistant' | 'system',
|
|
234
|
-
content: string
|
|
235
|
-
): ChatMessage => ({
|
|
236
|
-
role,
|
|
237
|
-
content,
|
|
238
|
-
});
|
|
239
|
-
|
|
240
176
|
export const assertToolParameters = (
|
|
241
177
|
tool: Tool,
|
|
242
178
|
expectedProperties: string[],
|