@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.
Files changed (59) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/lib/agent.d.ts +4 -129
  3. package/lib/agent.d.ts.map +1 -1
  4. package/lib/agent.js +3 -733
  5. package/lib/agent.js.map +1 -1
  6. package/lib/compaction.d.ts +3 -129
  7. package/lib/compaction.d.ts.map +1 -1
  8. package/lib/compaction.js +2 -367
  9. package/lib/compaction.js.map +1 -1
  10. package/lib/context-manager.d.ts +3 -210
  11. package/lib/context-manager.d.ts.map +1 -1
  12. package/lib/context-manager.js +2 -310
  13. package/lib/context-manager.js.map +1 -1
  14. package/lib/conversation-memory.d.ts +3 -189
  15. package/lib/conversation-memory.d.ts.map +1 -1
  16. package/lib/conversation-memory.js +2 -616
  17. package/lib/conversation-memory.js.map +1 -1
  18. package/lib/init/create-zhin-agent.d.ts.map +1 -1
  19. package/lib/init/create-zhin-agent.js +1 -3
  20. package/lib/init/create-zhin-agent.js.map +1 -1
  21. package/lib/output.d.ts +3 -90
  22. package/lib/output.d.ts.map +1 -1
  23. package/lib/output.js +2 -173
  24. package/lib/output.js.map +1 -1
  25. package/lib/rate-limiter.d.ts +3 -35
  26. package/lib/rate-limiter.d.ts.map +1 -1
  27. package/lib/rate-limiter.js +2 -83
  28. package/lib/rate-limiter.js.map +1 -1
  29. package/lib/session.d.ts +3 -190
  30. package/lib/session.d.ts.map +1 -1
  31. package/lib/session.js +2 -462
  32. package/lib/session.js.map +1 -1
  33. package/lib/storage.d.ts +3 -65
  34. package/lib/storage.d.ts.map +1 -1
  35. package/lib/storage.js +2 -102
  36. package/lib/storage.js.map +1 -1
  37. package/lib/tone-detector.d.ts +3 -16
  38. package/lib/tone-detector.d.ts.map +1 -1
  39. package/lib/tone-detector.js +2 -69
  40. package/lib/tone-detector.js.map +1 -1
  41. package/package.json +3 -2
  42. package/src/agent.ts +4 -852
  43. package/src/compaction.ts +27 -528
  44. package/src/context-manager.ts +14 -439
  45. package/src/conversation-memory.ts +3 -814
  46. package/src/init/create-zhin-agent.ts +1 -3
  47. package/src/output.ts +14 -260
  48. package/src/rate-limiter.ts +3 -127
  49. package/src/session.ts +12 -565
  50. package/src/storage.ts +8 -134
  51. package/src/tone-detector.ts +3 -87
  52. package/tests/ai/setup.ts +20 -84
  53. package/tests/ai/agent.test.ts +0 -565
  54. package/tests/ai/context-manager.test.ts +0 -413
  55. package/tests/ai/conversation-memory.test.ts +0 -128
  56. package/tests/ai/output.test.ts +0 -128
  57. package/tests/ai/rate-limiter.test.ts +0 -108
  58. package/tests/ai/session.test.ts +0 -334
  59. package/tests/ai/tone-detector.test.ts +0 -80
package/src/storage.ts CHANGED
@@ -1,135 +1,9 @@
1
1
  /**
2
- * StorageBackend Unified storage abstraction layer
3
- *
4
- * Provides a common interface for AI sub-systems (session, context, memory,
5
- * user profile, follow-up) to switch between in-memory and database backends
6
- * without changing business logic.
7
- *
8
- * Usage:
9
- * const backend = new MemoryStorageBackend<MyRecord>();
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';
@@ -1,89 +1,5 @@
1
1
  /**
2
- * ToneDetector 轻量级情绪/语气检测
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 = 'neutral' | 'frustrated' | 'excited' | 'questioning' | 'sad' | 'urgent';
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, ChatMessage } from '@zhin.js/core';
8
-
9
- export const createMockLogger = () => ({
10
- debug: vi.fn(),
11
- info: vi.fn(),
12
- warn: vi.fn(),
13
- error: vi.fn(),
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[],