@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.
Files changed (210) hide show
  1. package/.env.example +45 -0
  2. package/README.md +611 -0
  3. package/config.example.json +73 -0
  4. package/dist/config.d.ts +35 -0
  5. package/dist/config.d.ts.map +1 -0
  6. package/dist/config.js +44 -0
  7. package/dist/config.js.map +1 -0
  8. package/dist/helpers/AIHelper.d.ts +23 -0
  9. package/dist/helpers/AIHelper.d.ts.map +1 -0
  10. package/dist/helpers/AIHelper.js +326 -0
  11. package/dist/helpers/AIHelper.js.map +1 -0
  12. package/dist/helpers/AIHelper.test.d.ts +2 -0
  13. package/dist/helpers/AIHelper.test.d.ts.map +1 -0
  14. package/dist/helpers/AIHelper.test.js +332 -0
  15. package/dist/helpers/AIHelper.test.js.map +1 -0
  16. package/dist/helpers/ConfigHelper.d.ts +20 -0
  17. package/dist/helpers/ConfigHelper.d.ts.map +1 -0
  18. package/dist/helpers/ConfigHelper.js +118 -0
  19. package/dist/helpers/ConfigHelper.js.map +1 -0
  20. package/dist/helpers/ContextLimiter.d.ts +82 -0
  21. package/dist/helpers/ContextLimiter.d.ts.map +1 -0
  22. package/dist/helpers/ContextLimiter.js +165 -0
  23. package/dist/helpers/ContextLimiter.js.map +1 -0
  24. package/dist/helpers/FileHelper.d.ts +31 -0
  25. package/dist/helpers/FileHelper.d.ts.map +1 -0
  26. package/dist/helpers/FileHelper.js +175 -0
  27. package/dist/helpers/FileHelper.js.map +1 -0
  28. package/dist/helpers/SetupHelper.d.ts +5 -0
  29. package/dist/helpers/SetupHelper.d.ts.map +1 -0
  30. package/dist/helpers/SetupHelper.js +32 -0
  31. package/dist/helpers/SetupHelper.js.map +1 -0
  32. package/dist/helpers/index.d.ts +6 -0
  33. package/dist/helpers/index.d.ts.map +1 -0
  34. package/dist/helpers/index.js +6 -0
  35. package/dist/helpers/index.js.map +1 -0
  36. package/dist/index.d.ts +18 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +17 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/libs/index.d.ts +4 -0
  41. package/dist/libs/index.d.ts.map +1 -0
  42. package/dist/libs/index.js +4 -0
  43. package/dist/libs/index.js.map +1 -0
  44. package/dist/libs/kafka/config.d.ts +5 -0
  45. package/dist/libs/kafka/config.d.ts.map +1 -0
  46. package/dist/libs/kafka/config.js +5 -0
  47. package/dist/libs/kafka/config.js.map +1 -0
  48. package/dist/libs/kafka/consumer.d.ts +16 -0
  49. package/dist/libs/kafka/consumer.d.ts.map +1 -0
  50. package/dist/libs/kafka/consumer.js +126 -0
  51. package/dist/libs/kafka/consumer.js.map +1 -0
  52. package/dist/libs/kafka/index.d.ts +3 -0
  53. package/dist/libs/kafka/index.d.ts.map +1 -0
  54. package/dist/libs/kafka/index.js +3 -0
  55. package/dist/libs/kafka/index.js.map +1 -0
  56. package/dist/libs/kafka/kafka.d.ts +3 -0
  57. package/dist/libs/kafka/kafka.d.ts.map +1 -0
  58. package/dist/libs/kafka/kafka.js +24 -0
  59. package/dist/libs/kafka/kafka.js.map +1 -0
  60. package/dist/libs/kafka/producer.d.ts +11 -0
  61. package/dist/libs/kafka/producer.d.ts.map +1 -0
  62. package/dist/libs/kafka/producer.js +44 -0
  63. package/dist/libs/kafka/producer.js.map +1 -0
  64. package/dist/libs/logger/config.d.ts +5 -0
  65. package/dist/libs/logger/config.d.ts.map +1 -0
  66. package/dist/libs/logger/config.js +6 -0
  67. package/dist/libs/logger/config.js.map +1 -0
  68. package/dist/libs/logger/index.d.ts +10 -0
  69. package/dist/libs/logger/index.d.ts.map +1 -0
  70. package/dist/libs/logger/index.js +20 -0
  71. package/dist/libs/logger/index.js.map +1 -0
  72. package/dist/libs/logger/kafkajs-logger-creator.d.ts +12 -0
  73. package/dist/libs/logger/kafkajs-logger-creator.d.ts.map +1 -0
  74. package/dist/libs/logger/kafkajs-logger-creator.js +29 -0
  75. package/dist/libs/logger/kafkajs-logger-creator.js.map +1 -0
  76. package/dist/libs/logger/logger.d.ts +42 -0
  77. package/dist/libs/logger/logger.d.ts.map +1 -0
  78. package/dist/libs/logger/logger.js +44 -0
  79. package/dist/libs/logger/logger.js.map +1 -0
  80. package/dist/libs/s3/config.d.ts +7 -0
  81. package/dist/libs/s3/config.d.ts.map +1 -0
  82. package/dist/libs/s3/config.js +7 -0
  83. package/dist/libs/s3/config.js.map +1 -0
  84. package/dist/libs/s3/index.d.ts +4 -0
  85. package/dist/libs/s3/index.d.ts.map +1 -0
  86. package/dist/libs/s3/index.js +4 -0
  87. package/dist/libs/s3/index.js.map +1 -0
  88. package/dist/libs/s3/s3.lib.d.ts +25 -0
  89. package/dist/libs/s3/s3.lib.d.ts.map +1 -0
  90. package/dist/libs/s3/s3.lib.js +202 -0
  91. package/dist/libs/s3/s3.lib.js.map +1 -0
  92. package/dist/processors/ChatProcessor.d.ts +66 -0
  93. package/dist/processors/ChatProcessor.d.ts.map +1 -0
  94. package/dist/processors/ChatProcessor.js +610 -0
  95. package/dist/processors/ChatProcessor.js.map +1 -0
  96. package/dist/processors/ModelsProcessor.d.ts +11 -0
  97. package/dist/processors/ModelsProcessor.d.ts.map +1 -0
  98. package/dist/processors/ModelsProcessor.js +30 -0
  99. package/dist/processors/ModelsProcessor.js.map +1 -0
  100. package/dist/processors/index.d.ts +3 -0
  101. package/dist/processors/index.d.ts.map +1 -0
  102. package/dist/processors/index.js +3 -0
  103. package/dist/processors/index.js.map +1 -0
  104. package/dist/services/AIService.d.ts +48 -0
  105. package/dist/services/AIService.d.ts.map +1 -0
  106. package/dist/services/AIService.js +196 -0
  107. package/dist/services/AIService.js.map +1 -0
  108. package/dist/services/InternalEventsHandler.d.ts +21 -0
  109. package/dist/services/InternalEventsHandler.d.ts.map +1 -0
  110. package/dist/services/InternalEventsHandler.js +56 -0
  111. package/dist/services/InternalEventsHandler.js.map +1 -0
  112. package/dist/services/KafkaService.d.ts +35 -0
  113. package/dist/services/KafkaService.d.ts.map +1 -0
  114. package/dist/services/KafkaService.js +120 -0
  115. package/dist/services/KafkaService.js.map +1 -0
  116. package/dist/services/ModelFetcher.d.ts +54 -0
  117. package/dist/services/ModelFetcher.d.ts.map +1 -0
  118. package/dist/services/ModelFetcher.js +247 -0
  119. package/dist/services/ModelFetcher.js.map +1 -0
  120. package/dist/services/RedisService.d.ts +90 -0
  121. package/dist/services/RedisService.d.ts.map +1 -0
  122. package/dist/services/RedisService.js +236 -0
  123. package/dist/services/RedisService.js.map +1 -0
  124. package/dist/services/SocketService.d.ts +39 -0
  125. package/dist/services/SocketService.d.ts.map +1 -0
  126. package/dist/services/SocketService.js +128 -0
  127. package/dist/services/SocketService.js.map +1 -0
  128. package/dist/services/index.d.ts +7 -0
  129. package/dist/services/index.d.ts.map +1 -0
  130. package/dist/services/index.js +7 -0
  131. package/dist/services/index.js.map +1 -0
  132. package/dist/store/AgentStore.d.ts +48 -0
  133. package/dist/store/AgentStore.d.ts.map +1 -0
  134. package/dist/store/AgentStore.js +98 -0
  135. package/dist/store/AgentStore.js.map +1 -0
  136. package/dist/store/ArtifactStore.d.ts +13 -0
  137. package/dist/store/ArtifactStore.d.ts.map +1 -0
  138. package/dist/store/ArtifactStore.js +27 -0
  139. package/dist/store/ArtifactStore.js.map +1 -0
  140. package/dist/store/ConfigStore.d.ts +89 -0
  141. package/dist/store/ConfigStore.d.ts.map +1 -0
  142. package/dist/store/ConfigStore.js +214 -0
  143. package/dist/store/ConfigStore.js.map +1 -0
  144. package/dist/store/ConfigStore.test.d.ts +2 -0
  145. package/dist/store/ConfigStore.test.d.ts.map +1 -0
  146. package/dist/store/ConfigStore.test.js +259 -0
  147. package/dist/store/ConfigStore.test.js.map +1 -0
  148. package/dist/store/ModelStore.d.ts +44 -0
  149. package/dist/store/ModelStore.d.ts.map +1 -0
  150. package/dist/store/ModelStore.js +81 -0
  151. package/dist/store/ModelStore.js.map +1 -0
  152. package/dist/store/ModelStore.test.d.ts +2 -0
  153. package/dist/store/ModelStore.test.d.ts.map +1 -0
  154. package/dist/store/ModelStore.test.js +390 -0
  155. package/dist/store/ModelStore.test.js.map +1 -0
  156. package/dist/store/index.d.ts +5 -0
  157. package/dist/store/index.d.ts.map +1 -0
  158. package/dist/store/index.js +5 -0
  159. package/dist/store/index.js.map +1 -0
  160. package/dist/tools/generateChartTool.d.ts +24 -0
  161. package/dist/tools/generateChartTool.d.ts.map +1 -0
  162. package/dist/tools/generateChartTool.js +124 -0
  163. package/dist/tools/generateChartTool.js.map +1 -0
  164. package/dist/tools/proposeFormValuesTool.d.ts +35 -0
  165. package/dist/tools/proposeFormValuesTool.d.ts.map +1 -0
  166. package/dist/tools/proposeFormValuesTool.js +56 -0
  167. package/dist/tools/proposeFormValuesTool.js.map +1 -0
  168. package/package.json +71 -0
  169. package/src/config.ts +46 -0
  170. package/src/helpers/AIHelper.test.ts +375 -0
  171. package/src/helpers/AIHelper.ts +353 -0
  172. package/src/helpers/ConfigHelper.ts +130 -0
  173. package/src/helpers/ContextLimiter.ts +228 -0
  174. package/src/helpers/FileHelper.ts +197 -0
  175. package/src/helpers/SetupHelper.ts +35 -0
  176. package/src/helpers/index.ts +5 -0
  177. package/src/index.ts +18 -0
  178. package/src/libs/index.ts +3 -0
  179. package/src/libs/kafka/config.ts +4 -0
  180. package/src/libs/kafka/consumer.ts +161 -0
  181. package/src/libs/kafka/index.ts +2 -0
  182. package/src/libs/kafka/kafka.ts +27 -0
  183. package/src/libs/kafka/producer.ts +48 -0
  184. package/src/libs/logger/config.ts +4 -0
  185. package/src/libs/logger/index.ts +21 -0
  186. package/src/libs/logger/kafkajs-logger-creator.ts +28 -0
  187. package/src/libs/logger/logger.ts +60 -0
  188. package/src/libs/s3/config.ts +7 -0
  189. package/src/libs/s3/index.ts +3 -0
  190. package/src/libs/s3/s3.lib.ts +284 -0
  191. package/src/processors/ChatProcessor.ts +713 -0
  192. package/src/processors/ModelsProcessor.ts +34 -0
  193. package/src/processors/index.ts +2 -0
  194. package/src/services/AIService.ts +241 -0
  195. package/src/services/InternalEventsHandler.ts +61 -0
  196. package/src/services/KafkaService.ts +142 -0
  197. package/src/services/ModelFetcher.ts +286 -0
  198. package/src/services/RedisService.ts +285 -0
  199. package/src/services/SocketService.ts +153 -0
  200. package/src/services/index.ts +6 -0
  201. package/src/store/AgentStore.ts +138 -0
  202. package/src/store/ArtifactStore.ts +29 -0
  203. package/src/store/ConfigStore.test.ts +314 -0
  204. package/src/store/ConfigStore.ts +239 -0
  205. package/src/store/ModelStore.test.ts +473 -0
  206. package/src/store/ModelStore.ts +93 -0
  207. package/src/store/index.ts +4 -0
  208. package/src/tools/generateChartTool.ts +131 -0
  209. package/src/tools/proposeFormValuesTool.ts +67 -0
  210. 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,6 @@
1
+ export * from './AIService';
2
+ export * from './KafkaService';
3
+ export * from './RedisService';
4
+ export * from './SocketService';
5
+ export * from './ModelFetcher';
6
+ export * from './InternalEventsHandler';
@@ -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
+ });