@multiplayer-app/ai-agent-node 0.0.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.
- package/.env.example +45 -0
- package/README.md +611 -0
- package/config.example.json +73 -0
- package/dist/config.d.ts +35 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +44 -0
- package/dist/config.js.map +1 -0
- package/dist/helpers/AIHelper.d.ts +23 -0
- package/dist/helpers/AIHelper.d.ts.map +1 -0
- package/dist/helpers/AIHelper.js +326 -0
- package/dist/helpers/AIHelper.js.map +1 -0
- package/dist/helpers/AIHelper.test.d.ts +2 -0
- package/dist/helpers/AIHelper.test.d.ts.map +1 -0
- package/dist/helpers/AIHelper.test.js +332 -0
- package/dist/helpers/AIHelper.test.js.map +1 -0
- package/dist/helpers/ConfigHelper.d.ts +20 -0
- package/dist/helpers/ConfigHelper.d.ts.map +1 -0
- package/dist/helpers/ConfigHelper.js +118 -0
- package/dist/helpers/ConfigHelper.js.map +1 -0
- package/dist/helpers/ContextLimiter.d.ts +82 -0
- package/dist/helpers/ContextLimiter.d.ts.map +1 -0
- package/dist/helpers/ContextLimiter.js +165 -0
- package/dist/helpers/ContextLimiter.js.map +1 -0
- package/dist/helpers/FileHelper.d.ts +31 -0
- package/dist/helpers/FileHelper.d.ts.map +1 -0
- package/dist/helpers/FileHelper.js +175 -0
- package/dist/helpers/FileHelper.js.map +1 -0
- package/dist/helpers/SetupHelper.d.ts +5 -0
- package/dist/helpers/SetupHelper.d.ts.map +1 -0
- package/dist/helpers/SetupHelper.js +32 -0
- package/dist/helpers/SetupHelper.js.map +1 -0
- package/dist/helpers/index.d.ts +6 -0
- package/dist/helpers/index.d.ts.map +1 -0
- package/dist/helpers/index.js +6 -0
- package/dist/helpers/index.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/libs/index.d.ts +4 -0
- package/dist/libs/index.d.ts.map +1 -0
- package/dist/libs/index.js +4 -0
- package/dist/libs/index.js.map +1 -0
- package/dist/libs/kafka/config.d.ts +5 -0
- package/dist/libs/kafka/config.d.ts.map +1 -0
- package/dist/libs/kafka/config.js +5 -0
- package/dist/libs/kafka/config.js.map +1 -0
- package/dist/libs/kafka/consumer.d.ts +16 -0
- package/dist/libs/kafka/consumer.d.ts.map +1 -0
- package/dist/libs/kafka/consumer.js +126 -0
- package/dist/libs/kafka/consumer.js.map +1 -0
- package/dist/libs/kafka/index.d.ts +3 -0
- package/dist/libs/kafka/index.d.ts.map +1 -0
- package/dist/libs/kafka/index.js +3 -0
- package/dist/libs/kafka/index.js.map +1 -0
- package/dist/libs/kafka/kafka.d.ts +3 -0
- package/dist/libs/kafka/kafka.d.ts.map +1 -0
- package/dist/libs/kafka/kafka.js +24 -0
- package/dist/libs/kafka/kafka.js.map +1 -0
- package/dist/libs/kafka/producer.d.ts +11 -0
- package/dist/libs/kafka/producer.d.ts.map +1 -0
- package/dist/libs/kafka/producer.js +44 -0
- package/dist/libs/kafka/producer.js.map +1 -0
- package/dist/libs/logger/config.d.ts +5 -0
- package/dist/libs/logger/config.d.ts.map +1 -0
- package/dist/libs/logger/config.js +6 -0
- package/dist/libs/logger/config.js.map +1 -0
- package/dist/libs/logger/index.d.ts +10 -0
- package/dist/libs/logger/index.d.ts.map +1 -0
- package/dist/libs/logger/index.js +20 -0
- package/dist/libs/logger/index.js.map +1 -0
- package/dist/libs/logger/kafkajs-logger-creator.d.ts +12 -0
- package/dist/libs/logger/kafkajs-logger-creator.d.ts.map +1 -0
- package/dist/libs/logger/kafkajs-logger-creator.js +29 -0
- package/dist/libs/logger/kafkajs-logger-creator.js.map +1 -0
- package/dist/libs/logger/logger.d.ts +42 -0
- package/dist/libs/logger/logger.d.ts.map +1 -0
- package/dist/libs/logger/logger.js +44 -0
- package/dist/libs/logger/logger.js.map +1 -0
- package/dist/libs/s3/config.d.ts +7 -0
- package/dist/libs/s3/config.d.ts.map +1 -0
- package/dist/libs/s3/config.js +7 -0
- package/dist/libs/s3/config.js.map +1 -0
- package/dist/libs/s3/index.d.ts +4 -0
- package/dist/libs/s3/index.d.ts.map +1 -0
- package/dist/libs/s3/index.js +4 -0
- package/dist/libs/s3/index.js.map +1 -0
- package/dist/libs/s3/s3.lib.d.ts +25 -0
- package/dist/libs/s3/s3.lib.d.ts.map +1 -0
- package/dist/libs/s3/s3.lib.js +202 -0
- package/dist/libs/s3/s3.lib.js.map +1 -0
- package/dist/processors/ChatProcessor.d.ts +66 -0
- package/dist/processors/ChatProcessor.d.ts.map +1 -0
- package/dist/processors/ChatProcessor.js +610 -0
- package/dist/processors/ChatProcessor.js.map +1 -0
- package/dist/processors/ModelsProcessor.d.ts +11 -0
- package/dist/processors/ModelsProcessor.d.ts.map +1 -0
- package/dist/processors/ModelsProcessor.js +30 -0
- package/dist/processors/ModelsProcessor.js.map +1 -0
- package/dist/processors/index.d.ts +3 -0
- package/dist/processors/index.d.ts.map +1 -0
- package/dist/processors/index.js +3 -0
- package/dist/processors/index.js.map +1 -0
- package/dist/services/AIService.d.ts +48 -0
- package/dist/services/AIService.d.ts.map +1 -0
- package/dist/services/AIService.js +196 -0
- package/dist/services/AIService.js.map +1 -0
- package/dist/services/InternalEventsHandler.d.ts +21 -0
- package/dist/services/InternalEventsHandler.d.ts.map +1 -0
- package/dist/services/InternalEventsHandler.js +56 -0
- package/dist/services/InternalEventsHandler.js.map +1 -0
- package/dist/services/KafkaService.d.ts +35 -0
- package/dist/services/KafkaService.d.ts.map +1 -0
- package/dist/services/KafkaService.js +120 -0
- package/dist/services/KafkaService.js.map +1 -0
- package/dist/services/ModelFetcher.d.ts +54 -0
- package/dist/services/ModelFetcher.d.ts.map +1 -0
- package/dist/services/ModelFetcher.js +247 -0
- package/dist/services/ModelFetcher.js.map +1 -0
- package/dist/services/RedisService.d.ts +90 -0
- package/dist/services/RedisService.d.ts.map +1 -0
- package/dist/services/RedisService.js +236 -0
- package/dist/services/RedisService.js.map +1 -0
- package/dist/services/SocketService.d.ts +39 -0
- package/dist/services/SocketService.d.ts.map +1 -0
- package/dist/services/SocketService.js +128 -0
- package/dist/services/SocketService.js.map +1 -0
- package/dist/services/index.d.ts +7 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +7 -0
- package/dist/services/index.js.map +1 -0
- package/dist/store/AgentStore.d.ts +48 -0
- package/dist/store/AgentStore.d.ts.map +1 -0
- package/dist/store/AgentStore.js +98 -0
- package/dist/store/AgentStore.js.map +1 -0
- package/dist/store/ArtifactStore.d.ts +13 -0
- package/dist/store/ArtifactStore.d.ts.map +1 -0
- package/dist/store/ArtifactStore.js +27 -0
- package/dist/store/ArtifactStore.js.map +1 -0
- package/dist/store/ConfigStore.d.ts +89 -0
- package/dist/store/ConfigStore.d.ts.map +1 -0
- package/dist/store/ConfigStore.js +214 -0
- package/dist/store/ConfigStore.js.map +1 -0
- package/dist/store/ConfigStore.test.d.ts +2 -0
- package/dist/store/ConfigStore.test.d.ts.map +1 -0
- package/dist/store/ConfigStore.test.js +259 -0
- package/dist/store/ConfigStore.test.js.map +1 -0
- package/dist/store/ModelStore.d.ts +44 -0
- package/dist/store/ModelStore.d.ts.map +1 -0
- package/dist/store/ModelStore.js +81 -0
- package/dist/store/ModelStore.js.map +1 -0
- package/dist/store/ModelStore.test.d.ts +2 -0
- package/dist/store/ModelStore.test.d.ts.map +1 -0
- package/dist/store/ModelStore.test.js +390 -0
- package/dist/store/ModelStore.test.js.map +1 -0
- package/dist/store/index.d.ts +5 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +5 -0
- package/dist/store/index.js.map +1 -0
- package/dist/tools/generateChartTool.d.ts +24 -0
- package/dist/tools/generateChartTool.d.ts.map +1 -0
- package/dist/tools/generateChartTool.js +124 -0
- package/dist/tools/generateChartTool.js.map +1 -0
- package/dist/tools/proposeFormValuesTool.d.ts +35 -0
- package/dist/tools/proposeFormValuesTool.d.ts.map +1 -0
- package/dist/tools/proposeFormValuesTool.js +56 -0
- package/dist/tools/proposeFormValuesTool.js.map +1 -0
- package/package.json +71 -0
- package/src/config.ts +46 -0
- package/src/helpers/AIHelper.test.ts +375 -0
- package/src/helpers/AIHelper.ts +353 -0
- package/src/helpers/ConfigHelper.ts +130 -0
- package/src/helpers/ContextLimiter.ts +228 -0
- package/src/helpers/FileHelper.ts +197 -0
- package/src/helpers/SetupHelper.ts +35 -0
- package/src/helpers/index.ts +5 -0
- package/src/index.ts +18 -0
- package/src/libs/index.ts +3 -0
- package/src/libs/kafka/config.ts +4 -0
- package/src/libs/kafka/consumer.ts +161 -0
- package/src/libs/kafka/index.ts +2 -0
- package/src/libs/kafka/kafka.ts +27 -0
- package/src/libs/kafka/producer.ts +48 -0
- package/src/libs/logger/config.ts +4 -0
- package/src/libs/logger/index.ts +21 -0
- package/src/libs/logger/kafkajs-logger-creator.ts +28 -0
- package/src/libs/logger/logger.ts +60 -0
- package/src/libs/s3/config.ts +7 -0
- package/src/libs/s3/index.ts +3 -0
- package/src/libs/s3/s3.lib.ts +284 -0
- package/src/processors/ChatProcessor.ts +713 -0
- package/src/processors/ModelsProcessor.ts +34 -0
- package/src/processors/index.ts +2 -0
- package/src/services/AIService.ts +241 -0
- package/src/services/InternalEventsHandler.ts +61 -0
- package/src/services/KafkaService.ts +142 -0
- package/src/services/ModelFetcher.ts +286 -0
- package/src/services/RedisService.ts +285 -0
- package/src/services/SocketService.ts +153 -0
- package/src/services/index.ts +6 -0
- package/src/store/AgentStore.ts +138 -0
- package/src/store/ArtifactStore.ts +29 -0
- package/src/store/ConfigStore.test.ts +314 -0
- package/src/store/ConfigStore.ts +239 -0
- package/src/store/ModelStore.test.ts +473 -0
- package/src/store/ModelStore.ts +93 -0
- package/src/store/index.ts +4 -0
- package/src/tools/generateChartTool.ts +131 -0
- package/src/tools/proposeFormValuesTool.ts +67 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { Server as SocketIOServer } from 'socket.io';
|
|
2
|
+
import type { Server as HTTPServer } from 'http';
|
|
3
|
+
import { createAdapter } from '@socket.io/redis-adapter';
|
|
4
|
+
import type { AgentChatResponse, AgentMessage } from '@multiplayer-app/ai-agent-types';
|
|
5
|
+
import { SocketIOEvents, SocketIOConfig } from '@multiplayer-app/ai-agent-types';
|
|
6
|
+
import { redisService } from './RedisService';
|
|
7
|
+
import { logger } from '../libs/logger';
|
|
8
|
+
export interface SocketError extends Error {
|
|
9
|
+
message: string
|
|
10
|
+
data?: {
|
|
11
|
+
code: number
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class SocketIOError extends Error implements SocketError {
|
|
16
|
+
public data: { code: number }
|
|
17
|
+
|
|
18
|
+
constructor(message: string, code: number) {
|
|
19
|
+
super(message)
|
|
20
|
+
this.data = { code }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type Middleware = <REQUEST = any, RESPONSE = any>(request: REQUEST, response: RESPONSE, next?: (error?: SocketError) => void) => void | Promise<void>;
|
|
25
|
+
|
|
26
|
+
const authWrap = (middleware?: Middleware, reqParams?: Record<string, string>) => async (socket, next) => {
|
|
27
|
+
if (!middleware) {
|
|
28
|
+
return next();
|
|
29
|
+
}
|
|
30
|
+
const req = socket.request as any
|
|
31
|
+
|
|
32
|
+
const _headers = Object.fromEntries(
|
|
33
|
+
Object.entries(socket?.handshake?.auth || {})
|
|
34
|
+
.map(([key, value]) => ([key.toLowerCase(), value])),
|
|
35
|
+
) as any
|
|
36
|
+
|
|
37
|
+
req.headers = {
|
|
38
|
+
...(req.headers || {}),
|
|
39
|
+
...(_headers),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (reqParams) {
|
|
43
|
+
req.params = reqParams
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
await middleware(req, {}, (err: any) => {
|
|
48
|
+
if (err) {
|
|
49
|
+
return next(new SocketIOError(err.message, err?.statusCode || 401))
|
|
50
|
+
}
|
|
51
|
+
return next()
|
|
52
|
+
})
|
|
53
|
+
} catch (error: any) {
|
|
54
|
+
return next(new SocketIOError(error?.message || 'Authorization failed', error?.statusCode || 401))
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
export class SocketService {
|
|
60
|
+
private io: SocketIOServer | null = null;
|
|
61
|
+
|
|
62
|
+
async initialize(httpServer: HTTPServer, authorizationMiddleware?: Middleware): Promise<void> {
|
|
63
|
+
this.io = new SocketIOServer(httpServer, {
|
|
64
|
+
cors: {
|
|
65
|
+
origin: true,
|
|
66
|
+
credentials: true
|
|
67
|
+
},
|
|
68
|
+
path: SocketIOConfig.Path
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Set up Redis adapter for multi-server support
|
|
72
|
+
try {
|
|
73
|
+
const { pubClient, subClient } = await redisService.getPubSubClients('socketio');
|
|
74
|
+
this.io.adapter(createAdapter(pubClient, subClient));
|
|
75
|
+
logger.debug('Socket.IO Redis adapter initialized');
|
|
76
|
+
} catch (error) {
|
|
77
|
+
logger.error('Failed to initialize Redis adapter for Socket.IO:', error);
|
|
78
|
+
// Continue without adapter - single server mode
|
|
79
|
+
logger.warn('Socket.IO running without Redis adapter (single server mode)');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.io.use(authWrap(authorizationMiddleware))
|
|
83
|
+
|
|
84
|
+
this.io.on('connection', (socket) => {
|
|
85
|
+
// Extract userId from handshake auth or query
|
|
86
|
+
const userId = (socket.request as any).userId as string
|
|
87
|
+
|
|
88
|
+
// Reject connection if no userId is provided
|
|
89
|
+
if (!userId) {
|
|
90
|
+
logger.debug(`Socket connection rejected: no userId provided for socket ${socket.id}`);
|
|
91
|
+
socket.disconnect(true);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Join user-specific room
|
|
96
|
+
const userRoom = `${SocketIOConfig.UserRoomPrefix}${userId}`;
|
|
97
|
+
socket.join(userRoom);
|
|
98
|
+
|
|
99
|
+
logger.debug(`Socket connected: ${socket.id} for user: ${userId}`);
|
|
100
|
+
|
|
101
|
+
socket.on('disconnect', () => {
|
|
102
|
+
logger.debug(`Socket disconnected: ${socket.id} for user: ${userId}`);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Emit a new chat event to a specific user
|
|
109
|
+
*/
|
|
110
|
+
emitNewChat(userId: string, chat: AgentChatResponse): void {
|
|
111
|
+
if (!this.io) return;
|
|
112
|
+
const userRoom = `${SocketIOConfig.UserRoomPrefix}${userId}`;
|
|
113
|
+
this.io.to(userRoom).emit(SocketIOEvents.ChatNew, chat);
|
|
114
|
+
logger.debug(`Emitted ${SocketIOEvents.ChatNew} to user ${userId}:`, { chatId: chat.id });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Emit a new message event to a specific user
|
|
119
|
+
*/
|
|
120
|
+
emitMessageUpdate(userId: string | undefined, message: AgentMessage, excludeSocketId?: string): void {
|
|
121
|
+
if (!this.io || !userId) return;
|
|
122
|
+
const userRoom = `${SocketIOConfig.UserRoomPrefix}${userId}`;
|
|
123
|
+
|
|
124
|
+
if (excludeSocketId) {
|
|
125
|
+
this.io.to(userRoom).except(excludeSocketId).emit(SocketIOEvents.MessageNew, message);
|
|
126
|
+
} else {
|
|
127
|
+
this.io.to(userRoom).emit(SocketIOEvents.MessageNew, message);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Emit a chat update event to a specific user
|
|
133
|
+
*/
|
|
134
|
+
emitChatUpdate(userId: string, chat: AgentChatResponse, excludeSocketId?: string): void {
|
|
135
|
+
if (!this.io) return;
|
|
136
|
+
const userRoom = `${SocketIOConfig.UserRoomPrefix}${userId}`;
|
|
137
|
+
if (excludeSocketId) {
|
|
138
|
+
this.io.to(userRoom).except(excludeSocketId).emit(SocketIOEvents.ChatUpdate, chat);
|
|
139
|
+
} else {
|
|
140
|
+
this.io.to(userRoom).emit(SocketIOEvents.ChatUpdate, chat);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get the Socket.IO server instance
|
|
146
|
+
*/
|
|
147
|
+
getIO(): SocketIOServer | null {
|
|
148
|
+
return this.io;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Singleton instance
|
|
153
|
+
export const socketService = new SocketService();
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { InternalEventsHandler } from "../services/InternalEventsHandler";
|
|
2
|
+
import { AgentMessage } from "@multiplayer-app/ai-agent-types";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export type AgentProcessListener = (payload: AgentProcessEvent) => void;
|
|
6
|
+
|
|
7
|
+
export interface AgentProcess {
|
|
8
|
+
chatId: string;
|
|
9
|
+
abortController: AbortController;
|
|
10
|
+
listeners: AgentProcessListener[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export enum AgentProcessEventType {
|
|
14
|
+
Stop = 'stop',
|
|
15
|
+
Finished = 'finished',
|
|
16
|
+
Error = 'error',
|
|
17
|
+
Aborted = 'aborted',
|
|
18
|
+
Update = 'update',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type AgentProcessEvent = {
|
|
22
|
+
type: AgentProcessEventType.Update;
|
|
23
|
+
data: AgentMessage;
|
|
24
|
+
} | {
|
|
25
|
+
type: AgentProcessEventType.Finished;
|
|
26
|
+
data: AgentMessage;
|
|
27
|
+
} | {
|
|
28
|
+
type: AgentProcessEventType.Error;
|
|
29
|
+
data: Error;
|
|
30
|
+
} | {
|
|
31
|
+
type: AgentProcessEventType.Aborted;
|
|
32
|
+
data: void;
|
|
33
|
+
} | {
|
|
34
|
+
type: AgentProcessEventType.Stop;
|
|
35
|
+
data: void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AgentProcessEventPayload {
|
|
39
|
+
chatId: string;
|
|
40
|
+
event: AgentProcessEvent;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class AgentStore {
|
|
44
|
+
private readonly internalEventsHandler = new InternalEventsHandler('agent-events');
|
|
45
|
+
private readonly agentProcesses = new Map<string, AgentProcess>();
|
|
46
|
+
|
|
47
|
+
async initialize() {
|
|
48
|
+
await this.internalEventsHandler.listen<AgentProcessEventPayload>((payload: AgentProcessEventPayload) => {
|
|
49
|
+
switch (payload.event.type) {
|
|
50
|
+
case AgentProcessEventType.Stop:
|
|
51
|
+
this.stopAgentProcess(payload.chatId);
|
|
52
|
+
break;
|
|
53
|
+
case AgentProcessEventType.Finished:
|
|
54
|
+
case AgentProcessEventType.Error:
|
|
55
|
+
case AgentProcessEventType.Aborted:
|
|
56
|
+
this.publishEvent(payload.chatId, payload.event);
|
|
57
|
+
this.unregisterAgentProcess(payload.chatId);
|
|
58
|
+
break;
|
|
59
|
+
default:
|
|
60
|
+
this.publishEvent(payload.chatId, payload.event);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async shareAgentProcessEvent(chatId: string, event: AgentProcessEvent) {
|
|
67
|
+
try {
|
|
68
|
+
await this.internalEventsHandler.publish<AgentProcessEventPayload>({ chatId, event });
|
|
69
|
+
} catch (error) {
|
|
70
|
+
// Fallback: publish locally if Redis fails
|
|
71
|
+
if (event.type === AgentProcessEventType.Stop) {
|
|
72
|
+
// Stop must actually abort the running controller, not just notify listeners.
|
|
73
|
+
this.stopAgentProcess(chatId);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
this.publishEvent(chatId, event);
|
|
77
|
+
// If this is a terminal event, also unregister locally
|
|
78
|
+
if (
|
|
79
|
+
event.type === AgentProcessEventType.Finished ||
|
|
80
|
+
event.type === AgentProcessEventType.Error ||
|
|
81
|
+
event.type === AgentProcessEventType.Aborted
|
|
82
|
+
) {
|
|
83
|
+
this.unregisterAgentProcess(chatId);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
registerAgentProcess(chatId: string): AbortController {
|
|
89
|
+
if (!this.agentProcesses.has(chatId)) {
|
|
90
|
+
this.agentProcesses.set(chatId, {
|
|
91
|
+
chatId,
|
|
92
|
+
abortController: new AbortController(),
|
|
93
|
+
listeners: [],
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return this.agentProcesses.get(chatId)!.abortController;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
unregisterAgentProcess(chatId: string) {
|
|
100
|
+
this.agentProcesses.delete(chatId);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
addListener(chatId: string, listener: AgentProcessListener): boolean {
|
|
104
|
+
const agentProcess = this.agentProcesses.get(chatId);
|
|
105
|
+
if (agentProcess) {
|
|
106
|
+
agentProcess.listeners.push(listener);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
removeListener(chatId: string, listener: AgentProcessListener) {
|
|
113
|
+
const agentProcess = this.agentProcesses.get(chatId);
|
|
114
|
+
if (agentProcess) {
|
|
115
|
+
agentProcess.listeners = agentProcess.listeners.filter((l) => l !== listener);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
publishEvent(chatId: string, event: AgentProcessEvent) {
|
|
120
|
+
const agentProcess = this.agentProcesses.get(chatId);
|
|
121
|
+
if (agentProcess) {
|
|
122
|
+
agentProcess.listeners.forEach((listener) => listener(event));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private stopAgentProcess(chatId: string) {
|
|
127
|
+
const agentProcess = this.agentProcesses.get(chatId);
|
|
128
|
+
if (agentProcess) {
|
|
129
|
+
agentProcess.abortController.abort();
|
|
130
|
+
this.publishEvent(chatId, { type: AgentProcessEventType.Aborted, data: undefined });
|
|
131
|
+
// This is terminal. If we keep the process registered, the next send reuses an already-aborted controller
|
|
132
|
+
// and immediately aborts again, which looks like "next messages not processing".
|
|
133
|
+
this.unregisterAgentProcess(chatId);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const agentStore = new AgentStore();
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AgentArtifact } from '@multiplayer-app/ai-agent-types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Simple in-memory store for artifacts.
|
|
5
|
+
* In the future, this could be moved to MongoDB or a separate collection.
|
|
6
|
+
*/
|
|
7
|
+
export class ArtifactStore {
|
|
8
|
+
private artifacts = new Map<string, AgentArtifact[]>();
|
|
9
|
+
|
|
10
|
+
listArtifacts(chatId: string): AgentArtifact[] {
|
|
11
|
+
return this.artifacts.get(chatId) ?? [];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
setArtifacts(chatId: string, artifacts: AgentArtifact[]): void {
|
|
15
|
+
this.artifacts.set(chatId, artifacts);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
addArtifact(chatId: string, artifact: AgentArtifact): void {
|
|
19
|
+
const artifacts = this.artifacts.get(chatId) ?? [];
|
|
20
|
+
artifacts.unshift(artifact);
|
|
21
|
+
// Keep only the last 5 artifacts
|
|
22
|
+
this.artifacts.set(chatId, artifacts.slice(0, 5));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
deleteArtifacts(chatId: string): void {
|
|
26
|
+
this.artifacts.delete(chatId);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { ConfigStore } from './ConfigStore';
|
|
3
|
+
|
|
4
|
+
describe('ConfigStore', () => {
|
|
5
|
+
let configStore: ConfigStore;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
configStore = ConfigStore.getInstance();
|
|
9
|
+
configStore.clear();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('isModelAllowed', () => {
|
|
13
|
+
describe('when no allowedModels are configured', () => {
|
|
14
|
+
it('should allow all models', () => {
|
|
15
|
+
configStore.loadConfig({ agents: [] });
|
|
16
|
+
|
|
17
|
+
expect(configStore.isModelAllowed('openai/gpt-4o')).toBe(true);
|
|
18
|
+
expect(configStore.isModelAllowed('anthropic/claude-3-5-sonnet')).toBe(true);
|
|
19
|
+
expect(configStore.isModelAllowed('any-model')).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('wildcard patterns', () => {
|
|
24
|
+
it('should allow all models if no provider is specified', () => {
|
|
25
|
+
configStore.loadConfig({ agents: [], models: ['gpt-4'] });
|
|
26
|
+
|
|
27
|
+
expect(configStore.isModelAllowed('gpt-4')).toBe(true);
|
|
28
|
+
expect(configStore.isModelAllowed('openai/gpt-4')).toBe(true);
|
|
29
|
+
expect(configStore.isModelAllowed('anyprovider/gpt-4')).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
it('should restrict by provider if it is specified', () => {
|
|
32
|
+
configStore.loadConfig({ agents: [], models: ['openai/gpt-4'] });
|
|
33
|
+
|
|
34
|
+
expect(configStore.isModelAllowed('gpt-4')).toBe(false);
|
|
35
|
+
expect(configStore.isModelAllowed('openai/gpt-4')).toBe(true);
|
|
36
|
+
expect(configStore.isModelAllowed('anyprovider/gpt-4')).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should use provider parameter when model id is provided separately', () => {
|
|
40
|
+
configStore.loadConfig({ agents: [], models: ['openai/gpt-4'] });
|
|
41
|
+
|
|
42
|
+
// When provider is provided and model doesn't have provider prefix, construct as 'provider/model'
|
|
43
|
+
expect(configStore.isModelAllowed('gpt-4', 'openai')).toBe(true);
|
|
44
|
+
expect(configStore.isModelAllowed('gpt-4', 'anthropic')).toBe(false);
|
|
45
|
+
|
|
46
|
+
// When model already has provider prefix, use it as-is (provider parameter is ignored)
|
|
47
|
+
expect(configStore.isModelAllowed('openai/gpt-4', 'openrouter')).toBe(true);
|
|
48
|
+
expect(configStore.isModelAllowed('openrouter/openai/gpt-4', 'openrouter')).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should handle OpenRouter provider with full model path in id', () => {
|
|
52
|
+
configStore.loadConfig({ agents: [], models: ['openai/gpt-4'] });
|
|
53
|
+
|
|
54
|
+
// For OpenRouter, the model id already contains the full path
|
|
55
|
+
expect(configStore.isModelAllowed('openai/gpt-4', 'openrouter')).toBe(true);
|
|
56
|
+
expect(configStore.isModelAllowed('anthropic/claude-3-5-sonnet', 'openrouter')).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should match patterns with wildcard at the end', () => {
|
|
60
|
+
configStore.loadConfig({
|
|
61
|
+
models: ['openai/gpt-4*'],
|
|
62
|
+
agents: []
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(configStore.isModelAllowed('openai/gpt-4o')).toBe(true);
|
|
66
|
+
expect(configStore.isModelAllowed('openai/gpt-4-turbo')).toBe(true);
|
|
67
|
+
expect(configStore.isModelAllowed('openai/gpt-4')).toBe(true);
|
|
68
|
+
expect(configStore.isModelAllowed('openai/gpt-4o-mini')).toBe(true);
|
|
69
|
+
expect(configStore.isModelAllowed('openai/gpt-3.5-turbo')).toBe(false);
|
|
70
|
+
expect(configStore.isModelAllowed('anthropic/claude-3-5-sonnet')).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should match patterns with wildcard at the beginning', () => {
|
|
74
|
+
configStore.loadConfig({
|
|
75
|
+
models: ['*/gpt-4o'],
|
|
76
|
+
agents: []
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(configStore.isModelAllowed('openai/gpt-4o')).toBe(true);
|
|
80
|
+
expect(configStore.isModelAllowed('openrouter/gpt-4o')).toBe(true);
|
|
81
|
+
expect(configStore.isModelAllowed('custom/gpt-4o')).toBe(true);
|
|
82
|
+
expect(configStore.isModelAllowed('openai/gpt-4-turbo')).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should match patterns with wildcard in the middle', () => {
|
|
86
|
+
configStore.loadConfig({
|
|
87
|
+
models: ['openai/gpt-*o'],
|
|
88
|
+
agents: []
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(configStore.isModelAllowed('openai/gpt-4o')).toBe(true);
|
|
92
|
+
expect(configStore.isModelAllowed('openai/gpt-3.5-turbo')).toBe(true);
|
|
93
|
+
expect(configStore.isModelAllowed('openai/gpt-4-turbo')).toBe(true);
|
|
94
|
+
expect(configStore.isModelAllowed('openai/gpt-4')).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should match patterns with multiple wildcards', () => {
|
|
98
|
+
configStore.loadConfig({
|
|
99
|
+
models: ['openai/*-*'],
|
|
100
|
+
agents: []
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(configStore.isModelAllowed('openai/gpt-4o')).toBe(true);
|
|
104
|
+
expect(configStore.isModelAllowed('openai/gpt-4-turbo')).toBe(true);
|
|
105
|
+
expect(configStore.isModelAllowed('openai/gpt4')).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should match provider wildcard patterns', () => {
|
|
109
|
+
configStore.loadConfig({
|
|
110
|
+
models: ['anthropic/*'],
|
|
111
|
+
agents: []
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(configStore.isModelAllowed('anthropic/claude-3-5-sonnet')).toBe(true);
|
|
115
|
+
expect(configStore.isModelAllowed('anthropic/claude-3-opus')).toBe(true);
|
|
116
|
+
expect(configStore.isModelAllowed('anthropic/claude-2')).toBe(true);
|
|
117
|
+
expect(configStore.isModelAllowed('openai/gpt-4o')).toBe(false);
|
|
118
|
+
expect(configStore.isModelAllowed('openrouter/gpt-4o')).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should match openrouter wildcard patterns', () => {
|
|
122
|
+
configStore.loadConfig({
|
|
123
|
+
models: ['openrouter/*'],
|
|
124
|
+
agents: []
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(configStore.isModelAllowed('openrouter/gpt-4o')).toBe(true);
|
|
128
|
+
expect(configStore.isModelAllowed('openrouter/claude-3-5-sonnet')).toBe(true);
|
|
129
|
+
expect(configStore.isModelAllowed('openrouter/any-model')).toBe(true);
|
|
130
|
+
expect(configStore.isModelAllowed('openai/gpt-4o')).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('exact matches', () => {
|
|
135
|
+
it('should match exact model identifiers', () => {
|
|
136
|
+
configStore.loadConfig({
|
|
137
|
+
models: ['openai/gpt-4o', 'anthropic/claude-3-5-sonnet'],
|
|
138
|
+
agents: []
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(configStore.isModelAllowed('openai/gpt-4o')).toBe(true);
|
|
142
|
+
expect(configStore.isModelAllowed('anthropic/claude-3-5-sonnet')).toBe(true);
|
|
143
|
+
expect(configStore.isModelAllowed('openai/gpt-4-turbo')).toBe(false);
|
|
144
|
+
expect(configStore.isModelAllowed('anthropic/claude-3-opus')).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('multiple patterns', () => {
|
|
149
|
+
it('should allow models matching any pattern', () => {
|
|
150
|
+
configStore.loadConfig({
|
|
151
|
+
models: ['openai/gpt-4*', 'anthropic/*', 'openrouter/*'],
|
|
152
|
+
agents: []
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(configStore.isModelAllowed('openai/gpt-4o')).toBe(true);
|
|
156
|
+
expect(configStore.isModelAllowed('openai/gpt-4-turbo')).toBe(true);
|
|
157
|
+
expect(configStore.isModelAllowed('anthropic/claude-3-5-sonnet')).toBe(true);
|
|
158
|
+
expect(configStore.isModelAllowed('openrouter/gpt-4o')).toBe(true);
|
|
159
|
+
expect(configStore.isModelAllowed('google/gemini-pro')).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should handle overlapping patterns correctly', () => {
|
|
163
|
+
configStore.loadConfig({
|
|
164
|
+
models: ['openai/*', 'openai/gpt-4*'],
|
|
165
|
+
agents: []
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(configStore.isModelAllowed('openai/gpt-4o')).toBe(true);
|
|
169
|
+
expect(configStore.isModelAllowed('openai/gpt-3.5-turbo')).toBe(true);
|
|
170
|
+
expect(configStore.isModelAllowed('openai/davinci')).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('edge cases', () => {
|
|
175
|
+
it('should handle empty model string', () => {
|
|
176
|
+
configStore.loadConfig({
|
|
177
|
+
models: ['openai/*'],
|
|
178
|
+
agents: []
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
expect(configStore.isModelAllowed('')).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should handle patterns with special regex characters', () => {
|
|
185
|
+
configStore.loadConfig({
|
|
186
|
+
models: ['openai/gpt-4.5*', 'model.with.dots*'],
|
|
187
|
+
agents: []
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
expect(configStore.isModelAllowed('openai/gpt-4.5')).toBe(true);
|
|
191
|
+
expect(configStore.isModelAllowed('openai/gpt-4.5-turbo')).toBe(true);
|
|
192
|
+
expect(configStore.isModelAllowed('model.with.dots')).toBe(true);
|
|
193
|
+
expect(configStore.isModelAllowed('model.with.dots-v2')).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should escape special characters correctly', () => {
|
|
197
|
+
configStore.loadConfig({
|
|
198
|
+
models: ['model+plus*', 'model(version)*'],
|
|
199
|
+
agents: []
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
expect(configStore.isModelAllowed('model+plus')).toBe(true);
|
|
203
|
+
expect(configStore.isModelAllowed('model+plus-v2')).toBe(true);
|
|
204
|
+
expect(configStore.isModelAllowed('model(version)')).toBe(true);
|
|
205
|
+
expect(configStore.isModelAllowed('model(version)-v2')).toBe(true);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should handle patterns with question marks and other special chars', () => {
|
|
209
|
+
configStore.loadConfig({
|
|
210
|
+
models: ['model?test*'],
|
|
211
|
+
agents: []
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
expect(configStore.isModelAllowed('model?test')).toBe(true);
|
|
215
|
+
expect(configStore.isModelAllowed('model?test-v2')).toBe(true);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should handle patterns with brackets', () => {
|
|
219
|
+
configStore.loadConfig({
|
|
220
|
+
models: ['model[version]*'],
|
|
221
|
+
agents: []
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
expect(configStore.isModelAllowed('model[version]')).toBe(true);
|
|
225
|
+
expect(configStore.isModelAllowed('model[version]-v2')).toBe(true);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should handle patterns with dollar signs and caret', () => {
|
|
229
|
+
configStore.loadConfig({
|
|
230
|
+
models: ['model$test*', 'model^test*'],
|
|
231
|
+
agents: []
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
expect(configStore.isModelAllowed('model$test')).toBe(true);
|
|
235
|
+
expect(configStore.isModelAllowed('model$test-v2')).toBe(true);
|
|
236
|
+
expect(configStore.isModelAllowed('model^test')).toBe(true);
|
|
237
|
+
expect(configStore.isModelAllowed('model^test-v2')).toBe(true);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('real-world examples', () => {
|
|
242
|
+
it('should match examples from config.json', () => {
|
|
243
|
+
configStore.loadConfig({
|
|
244
|
+
models: ['openai/gpt-4*', 'anthropic/*'],
|
|
245
|
+
agents: []
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// OpenAI GPT-4 variants
|
|
249
|
+
expect(configStore.isModelAllowed('openai/gpt-4o')).toBe(true);
|
|
250
|
+
expect(configStore.isModelAllowed('openai/gpt-4-turbo')).toBe(true);
|
|
251
|
+
expect(configStore.isModelAllowed('openai/gpt-4')).toBe(true);
|
|
252
|
+
expect(configStore.isModelAllowed('openai/gpt-4o-mini')).toBe(true);
|
|
253
|
+
|
|
254
|
+
// Anthropic models
|
|
255
|
+
expect(configStore.isModelAllowed('anthropic/claude-3-5-sonnet-20241022')).toBe(true);
|
|
256
|
+
expect(configStore.isModelAllowed('anthropic/claude-3-opus-20240229')).toBe(true);
|
|
257
|
+
expect(configStore.isModelAllowed('anthropic/claude-2.1')).toBe(true);
|
|
258
|
+
|
|
259
|
+
// Should not match
|
|
260
|
+
expect(configStore.isModelAllowed('openai/gpt-3.5-turbo')).toBe(false);
|
|
261
|
+
expect(configStore.isModelAllowed('google/gemini-pro')).toBe(false);
|
|
262
|
+
expect(configStore.isModelAllowed('openrouter/gpt-4o')).toBe(false);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should match openrouter pattern', () => {
|
|
266
|
+
configStore.loadConfig({
|
|
267
|
+
models: ['openrouter/*'],
|
|
268
|
+
agents: []
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
expect(configStore.isModelAllowed('openrouter/openai/gpt-4o')).toBe(true);
|
|
272
|
+
expect(configStore.isModelAllowed('openrouter/anthropic/claude-3-5-sonnet')).toBe(true);
|
|
273
|
+
expect(configStore.isModelAllowed('openrouter/meta-llama/llama-3-70b-instruct')).toBe(true);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe('getAllowedModels', () => {
|
|
278
|
+
it('should return a copy of allowed models', () => {
|
|
279
|
+
const models = ['openai/gpt-4*', 'anthropic/*'];
|
|
280
|
+
configStore.loadConfig({
|
|
281
|
+
models,
|
|
282
|
+
agents: []
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const allowed = configStore.getAllowedModels();
|
|
286
|
+
expect(allowed).toEqual(models);
|
|
287
|
+
expect(allowed).not.toBe(configStore.getAllowedModels()); // Should be a new array
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('should return empty array when no models configured', () => {
|
|
291
|
+
configStore.loadConfig({ agents: [] });
|
|
292
|
+
expect(configStore.getAllowedModels()).toEqual([]);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
describe('clear method', () => {
|
|
298
|
+
it('should clear allowedModels when clear is called', () => {
|
|
299
|
+
configStore.loadConfig({
|
|
300
|
+
models: ['openai/gpt-4*'],
|
|
301
|
+
agents: []
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
expect(configStore.isModelAllowed('openai/gpt-4o')).toBe(true);
|
|
305
|
+
|
|
306
|
+
configStore.clear();
|
|
307
|
+
|
|
308
|
+
// After clear, should allow all (no patterns configured)
|
|
309
|
+
expect(configStore.isModelAllowed('openai/gpt-4o')).toBe(true);
|
|
310
|
+
expect(configStore.getAllowedModels()).toEqual([]);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
});
|