@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 @@
1
+ {"version":3,"file":"proposeFormValuesTool.d.ts","sourceRoot":"","sources":["../../src/tools/proposeFormValuesTool.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,yBAAyB,wBAAwB,CAAA;AAE9D;;;GAGG;AACH,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyDpC,CAAA"}
@@ -0,0 +1,56 @@
1
+ import { tool } from 'ai';
2
+ import { z } from 'zod';
3
+ export const proposeFormValuesToolName = 'propose_form_values';
4
+ /**
5
+ * A no-side-effect tool: it exists to force the model to emit structured form fill proposals.
6
+ * The UI will render these proposals and the host app will decide what to apply.
7
+ */
8
+ export const createProposeFormValuesTool = () => tool({
9
+ title: proposeFormValuesToolName,
10
+ description: 'Propose values to fill in a form. Use only allowed select options. Do not apply changes; just propose.',
11
+ inputSchema: z.object({
12
+ formId: z.string().min(1),
13
+ formName: z.string().min(1).optional(),
14
+ fields: z
15
+ .array(z.object({
16
+ name: z.string().min(1),
17
+ label: z.string().min(1),
18
+ value: z.string(),
19
+ inputType: z
20
+ .enum(['text', 'textarea', 'number', 'select', 'radio', 'checkbox', 'date', 'email', 'tel'])
21
+ .optional(),
22
+ options: z
23
+ .array(z.object({
24
+ value: z.string(),
25
+ label: z.string()
26
+ }))
27
+ .optional()
28
+ }))
29
+ .min(1),
30
+ notes: z.string().optional()
31
+ }),
32
+ outputSchema: z.object({
33
+ formId: z.string(),
34
+ formName: z.string().optional(),
35
+ fields: z.array(z.object({
36
+ name: z.string(),
37
+ label: z.string(),
38
+ value: z.string(),
39
+ inputType: z
40
+ .enum(['text', 'textarea', 'number', 'select', 'radio', 'checkbox', 'date', 'email', 'tel'])
41
+ .optional(),
42
+ options: z
43
+ .array(z.object({
44
+ value: z.string(),
45
+ label: z.string()
46
+ }))
47
+ .optional()
48
+ })),
49
+ notes: z.string().optional()
50
+ }),
51
+ execute: async (input) => {
52
+ // Preserve structured output. No I/O, no mutations.
53
+ return input;
54
+ }
55
+ });
56
+ //# sourceMappingURL=proposeFormValuesTool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proposeFormValuesTool.js","sourceRoot":"","sources":["../../src/tools/proposeFormValuesTool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,IAAI,CAAA;AACzB,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,MAAM,CAAC,MAAM,yBAAyB,GAAG,qBAAqB,CAAA;AAE9D;;;GAGG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,GAAG,EAAE,CAC9C,IAAI,CAAC;IACH,KAAK,EAAE,yBAAyB;IAChC,WAAW,EACT,wGAAwG;IAC1G,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC;QACpB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QACzB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;QACtC,MAAM,EAAE,CAAC;aACN,KAAK,CACJ,CAAC,CAAC,MAAM,CAAC;YACP,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;YACvB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;YACxB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;YACjB,SAAS,EAAE,CAAC;iBACT,IAAI,CAAC,CAAC,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;iBAC3F,QAAQ,EAAE;YACb,OAAO,EAAE,CAAC;iBACP,KAAK,CACJ,CAAC,CAAC,MAAM,CAAC;gBACP,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;gBACjB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;aAClB,CAAC,CACH;iBACA,QAAQ,EAAE;SACd,CAAC,CACH;aACA,GAAG,CAAC,CAAC,CAAC;QACT,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;KAC7B,CAAC;IACF,YAAY,EAAE,CAAC,CAAC,MAAM,CAAC;QACrB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;QAClB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAC/B,MAAM,EAAE,CAAC,CAAC,KAAK,CACb,CAAC,CAAC,MAAM,CAAC;YACP,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;YAChB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;YACjB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;YACjB,SAAS,EAAE,CAAC;iBACT,IAAI,CAAC,CAAC,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;iBAC3F,QAAQ,EAAE;YACb,OAAO,EAAE,CAAC;iBACP,KAAK,CACJ,CAAC,CAAC,MAAM,CAAC;gBACP,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;gBACjB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;aAClB,CAAC,CACH;iBACA,QAAQ,EAAE;SACd,CAAC,CACH;QACD,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;KAC7B,CAAC;IACF,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;QACvB,oDAAoD;QACpD,OAAO,KAAK,CAAA;IACd,CAAC;CACF,CAAC,CAAA"}
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@multiplayer-app/ai-agent-node",
3
+ "version": "0.0.1",
4
+ "description": "AI Agent Node.js library for multiplayer ai agents",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "engines": {
9
+ "node": ">=18",
10
+ "npm": ">=8"
11
+ },
12
+ "scripts": {
13
+ "build": "tsc -p tsconfig.json",
14
+ "test": "vitest run",
15
+ "test:watch": "vitest",
16
+ "test:coverage": "vitest run --coverage"
17
+ },
18
+ "author": "Multiplayer",
19
+ "keywords": [
20
+ "ai-agent",
21
+ "library"
22
+ ],
23
+ "license": "MIT",
24
+ "dependencies": {
25
+ "@ai-sdk/anthropic": "^3.0.9",
26
+ "@ai-sdk/google": "^3.0.6",
27
+ "@ai-sdk/openai": "^3.0.12",
28
+ "@multiplayer-app/ai-agent-db": "0.1.0",
29
+ "@multiplayer-app/ai-agent-types": "0.0.1",
30
+ "@openrouter/ai-sdk-provider": "^2.0.0",
31
+ "@socket.io/redis-adapter": "^8.3.0",
32
+ "ai": "6.0.3",
33
+ "dotenv": "^17.2.3",
34
+ "redis": "^5.10.0",
35
+ "socket.io": "^4.8.1",
36
+ "zod": "^4.2.1",
37
+ "kafkajs": "2.2.4",
38
+ "@aws-sdk/client-s3": "3.354.0",
39
+ "@aws-sdk/s3-request-presigner": "3.354.0",
40
+ "@aws-sdk/util-create-request": "3.347.0",
41
+ "@aws-sdk/util-format-url": "3.347.0",
42
+ "@aws-sdk/lib-storage": "3.354.0"
43
+ },
44
+ "peerDependencies": {
45
+ "mammoth": "^1.11.0",
46
+ "pdf-parse": "^2.4.5",
47
+ "ai": "^6.0.3"
48
+ },
49
+ "peerDependenciesMeta": {
50
+ "mammoth": {
51
+ "optional": true
52
+ },
53
+ "pdf-parse": {
54
+ "optional": true
55
+ },
56
+ "ai": {
57
+ "optional": false
58
+ }
59
+ },
60
+ "devDependencies": {
61
+ "@types/express": "^4.17.21",
62
+ "@types/node": "^20.16.0",
63
+ "eslint": "^9.0.0",
64
+ "tsx": "^4.16.0",
65
+ "typescript": "^5.6.0",
66
+ "vitest": "^2.0.0"
67
+ },
68
+ "publishConfig": {
69
+ "access": "public"
70
+ }
71
+ }
package/src/config.ts ADDED
@@ -0,0 +1,46 @@
1
+ import 'dotenv/config';
2
+
3
+ /**
4
+ * Centralized configuration for the AI Agent Service
5
+ * All environment variables are accessed through this file
6
+ */
7
+
8
+ export const config = {
9
+ // Redis configuration
10
+ redis: {
11
+ url: process.env.REDIS_URL,
12
+ host: process.env.REDIS_HOST ?? 'localhost',
13
+ port: Number(process.env.REDIS_PORT ?? 6379),
14
+ password: process.env.REDIS_PASSWORD,
15
+ database: process.env.REDIS_DATABASE ? Number(process.env.REDIS_DATABASE) : undefined,
16
+ },
17
+
18
+ // AI Provider API Keys
19
+ ai: {
20
+ openaiApiKey: process.env.OPENAI_API_KEY,
21
+ anthropicApiKey: process.env.ANTHROPIC_API_KEY,
22
+ googleApiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
23
+ openrouterApiKey: process.env.OPENROUTER_API_KEY,
24
+ maxContextMessages: Number(process.env.MAX_CONTEXT_MESSAGES) || 10,
25
+ defaultModel: process.env.DEFAULT_MODEL,
26
+ },
27
+ s3: {
28
+ bucket: process.env.S3_ATTACHMENTS_BUCKET || 'ai-agent-attachments',
29
+ },
30
+ // Kafka configuration
31
+ kafka: {
32
+ groupId: process.env.KAFKA_GROUP_ID ?? 'ai-agent-node',
33
+ chatTitleGenerationTopic: process.env.KAFKA_CHAT_TITLE_GENERATION_TOPIC ?? 'chat-title-generation',
34
+ backgroundChatProcessingTopic: process.env.KAFKA_BACKGROUND_CHAT_PROCESSING_TOPIC ?? 'background-chat-processing',
35
+ },
36
+ } as const;
37
+
38
+ /**
39
+ * Get Redis connection URL
40
+ */
41
+ export function getRedisUrl(): string {
42
+ if (config.redis.url) {
43
+ return config.redis.url;
44
+ }
45
+ return `redis://${config.redis.host}:${config.redis.port}`;
46
+ }
@@ -0,0 +1,375 @@
1
+ import { describe, it, expect, beforeAll } from 'vitest';
2
+ import { AIHelper } from './AIHelper';
3
+ import { ModelStore } from '../store/ModelStore';
4
+ import { ConfigStore } from '../store/ConfigStore';
5
+ import { MessageRole, AgentToolCallStatus } from '@multiplayer-app/ai-agent-types';
6
+ import type { AgentMessage } from '@multiplayer-app/ai-agent-types';
7
+
8
+ describe('AIHelper.getAssistantResponse', () => {
9
+ let modelStore: ModelStore;
10
+ let configStore: ConfigStore;
11
+
12
+ beforeAll(() => {
13
+ // Setup real ModelStore instance
14
+ modelStore = ModelStore.getInstance();
15
+ modelStore.setModels([
16
+ { id: 'openai/gpt-4o', label: 'GPT-4o Mini', provider: 'openrouter' }
17
+ ]);
18
+ });
19
+
20
+ describe('with tool approval responses', () => {
21
+ it('should handle approved tool with successful execution', async () => {
22
+ // Note: getAssistantResponse accepts Pick<AgentMessage, 'role' | 'content'>[]
23
+ // but convertMessages internally handles toolCalls, so we cast for testing
24
+ const messages = [
25
+ {
26
+ role: MessageRole.User,
27
+ content: 'Please execute the tool'
28
+ },
29
+ {
30
+ role: MessageRole.Assistant,
31
+ content: 'I will execute the tool for you.',
32
+ toolCalls: [
33
+ {
34
+ id: 'tool-call-1',
35
+ name: 'test-tool',
36
+ input: { param: 'value' },
37
+ status: AgentToolCallStatus.Succeeded,
38
+ approved: true,
39
+ output: { result: 'success', data: { executed: true } }
40
+ }
41
+ ]
42
+ }
43
+ ] as Pick<AgentMessage, 'role' | 'content' | 'toolCalls'>[];
44
+
45
+ const response = await AIHelper.getAssistantResponse(messages);
46
+
47
+ // Verify we get a response (actual model call)
48
+ expect(response).toBeDefined();
49
+ expect(typeof response).toBe('string');
50
+ expect(response.length).toBeGreaterThan(0);
51
+
52
+ // The response should acknowledge the tool execution
53
+ // (exact text depends on model, so we just verify it's a valid response)
54
+ }, 30000);
55
+
56
+ it('should handle denied tool with execution-denied output', async () => {
57
+ const messages = [
58
+ {
59
+ role: MessageRole.User,
60
+ content: 'Please execute the tool'
61
+ },
62
+ {
63
+ role: MessageRole.Assistant,
64
+ content: 'I will execute the tool for you.',
65
+ toolCalls: [
66
+ {
67
+ id: 'tool-call-1',
68
+ name: 'test-tool',
69
+ input: { param: 'value' },
70
+ status: AgentToolCallStatus.Succeeded,
71
+ approved: false,
72
+ reason: 'User denied execution',
73
+ output: {
74
+ type: 'execution-denied',
75
+ reason: 'User denied execution'
76
+ }
77
+ }
78
+ ]
79
+ }
80
+ ] as Pick<AgentMessage, 'role' | 'content' | 'toolCalls'>[];
81
+
82
+ const response = await AIHelper.getAssistantResponse(messages);
83
+
84
+ // Verify we get a response acknowledging the denial
85
+ expect(response).toBeDefined();
86
+ expect(typeof response).toBe('string');
87
+ expect(response.length).toBeGreaterThan(0);
88
+
89
+ // The model should respond to the execution-denied output
90
+ }, 30000);
91
+
92
+ it('should handle multiple tools with mixed approvals', async () => {
93
+ const messages = [
94
+ {
95
+ role: MessageRole.User,
96
+ content: 'Execute multiple tools'
97
+ },
98
+ {
99
+ role: MessageRole.Assistant,
100
+ content: 'I will execute the tools.',
101
+ toolCalls: [
102
+ {
103
+ id: 'tool-call-1',
104
+ name: 'approved-tool',
105
+ input: { param: 'value1' },
106
+ status: AgentToolCallStatus.Succeeded,
107
+ approved: true,
108
+ output: { result: 'approved-result' }
109
+ },
110
+ {
111
+ id: 'tool-call-2',
112
+ name: 'denied-tool',
113
+ input: { param: 'value2' },
114
+ status: AgentToolCallStatus.Succeeded,
115
+ approved: false,
116
+ reason: 'Not allowed',
117
+ output: {
118
+ type: 'execution-denied',
119
+ reason: 'Not allowed'
120
+ }
121
+ }
122
+ ]
123
+ }
124
+ ] as Pick<AgentMessage, 'role' | 'content' | 'toolCalls'>[];
125
+
126
+ const response = await AIHelper.getAssistantResponse(messages);
127
+
128
+ // Verify we get a response handling both tool results
129
+ expect(response).toBeDefined();
130
+ expect(typeof response).toBe('string');
131
+ expect(response.length).toBeGreaterThan(0);
132
+
133
+ // The model should process both approved and denied tool results
134
+ }, 30000);
135
+
136
+ it('should handle tool execution failure', async () => {
137
+ const messages = [
138
+ {
139
+ role: MessageRole.User,
140
+ content: 'Execute the tool'
141
+ },
142
+ {
143
+ role: MessageRole.Assistant,
144
+ content: 'I will execute the tool.',
145
+ toolCalls: [
146
+ {
147
+ id: 'tool-call-1',
148
+ name: 'failing-tool',
149
+ input: { param: 'value' },
150
+ status: AgentToolCallStatus.Failed,
151
+ approved: true,
152
+ error: 'Tool execution failed: Network error'
153
+ }
154
+ ]
155
+ }
156
+ ] as Pick<AgentMessage, 'role' | 'content' | 'toolCalls'>[];
157
+
158
+ const response = await AIHelper.getAssistantResponse(messages);
159
+
160
+ // Verify we get a response even when tool execution failed
161
+ expect(response).toBeDefined();
162
+ expect(typeof response).toBe('string');
163
+ expect(response.length).toBeGreaterThan(0);
164
+ }, 30000);
165
+
166
+ it('should handle tool with execution-denied due to tool not found', async () => {
167
+ const messages = [
168
+ {
169
+ role: MessageRole.User,
170
+ content: 'Execute the tool'
171
+ },
172
+ {
173
+ role: MessageRole.Assistant,
174
+ content: 'I will execute the tool.',
175
+ toolCalls: [
176
+ {
177
+ id: 'tool-call-1',
178
+ name: 'missing-tool',
179
+ input: { param: 'value' },
180
+ status: AgentToolCallStatus.Succeeded,
181
+ approved: true,
182
+ output: {
183
+ type: 'execution-denied',
184
+ reason: 'Tool was not found'
185
+ }
186
+ }
187
+ ]
188
+ }
189
+ ] as Pick<AgentMessage, 'role' | 'content' | 'toolCalls'>[];
190
+
191
+ const response = await AIHelper.getAssistantResponse(messages);
192
+
193
+ // Verify we get a response when tool was not found
194
+ expect(response).toBeDefined();
195
+ expect(typeof response).toBe('string');
196
+ expect(response.length).toBeGreaterThan(0);
197
+ }, 30000);
198
+
199
+ it('should handle tool with execution-denied due to tool execution error', async () => {
200
+ const messages = [
201
+ {
202
+ role: MessageRole.User,
203
+ content: 'Execute the tool'
204
+ },
205
+ {
206
+ role: MessageRole.Assistant,
207
+ content: 'I will execute the tool.',
208
+ toolCalls: [
209
+ {
210
+ id: 'tool-call-1',
211
+ name: 'error-tool',
212
+ input: { param: 'value' },
213
+ status: AgentToolCallStatus.Succeeded,
214
+ approved: true,
215
+ output: {
216
+ type: 'execution-denied',
217
+ reason: 'Tool failed with error'
218
+ }
219
+ }
220
+ ]
221
+ }
222
+ ] as Pick<AgentMessage, 'role' | 'content' | 'toolCalls'>[];
223
+
224
+ const response = await AIHelper.getAssistantResponse(messages);
225
+
226
+ // Verify we get a response when tool execution had an error
227
+ expect(response).toBeDefined();
228
+ expect(typeof response).toBe('string');
229
+ expect(response.length).toBeGreaterThan(0);
230
+ }, 30000);
231
+
232
+ it('should handle messages without tool calls', async () => {
233
+ const messages: Pick<AgentMessage, 'role' | 'content'>[] = [
234
+ {
235
+ role: MessageRole.User,
236
+ content: 'Hello, how are you?'
237
+ },
238
+ {
239
+ role: MessageRole.Assistant,
240
+ content: 'I am doing well, thank you!'
241
+ }
242
+ ];
243
+
244
+ const response = await AIHelper.getAssistantResponse(messages);
245
+
246
+ // Verify we get a response for normal conversation
247
+ expect(response).toBeDefined();
248
+ expect(typeof response).toBe('string');
249
+ expect(response.length).toBeGreaterThan(0);
250
+ }, 30000);
251
+
252
+ it('should handle tool calls without output (pending approval)', async () => {
253
+ const messages = [
254
+ {
255
+ role: MessageRole.User,
256
+ content: 'Execute the tool'
257
+ },
258
+ {
259
+ role: MessageRole.Assistant,
260
+ content: 'I need to execute a tool.',
261
+ toolCalls: [
262
+ {
263
+ id: 'tool-call-1',
264
+ name: 'pending-tool',
265
+ input: { param: 'value' },
266
+ status: AgentToolCallStatus.Pending,
267
+ requiresConfirmation: true,
268
+ approvalId: 'approval-123'
269
+ }
270
+ ]
271
+ }
272
+ ] as Pick<AgentMessage, 'role' | 'content' | 'toolCalls'>[];
273
+
274
+ const response = await AIHelper.getAssistantResponse(messages);
275
+
276
+ // Verify we get a response even when tool is pending approval
277
+ // (tool calls without output should not include tool-result in messages)
278
+ expect(response).toBeDefined();
279
+ expect(typeof response).toBe('string');
280
+ expect(response.length).toBeGreaterThan(0);
281
+ }, 30000);
282
+
283
+ it('should handle sequential tool approvals', async () => {
284
+ const messages = [
285
+ {
286
+ role: MessageRole.User,
287
+ content: 'First request'
288
+ },
289
+ {
290
+ role: MessageRole.Assistant,
291
+ content: 'Executing first tool.',
292
+ toolCalls: [
293
+ {
294
+ id: 'tool-call-1',
295
+ name: 'first-tool',
296
+ input: { step: 1 },
297
+ status: AgentToolCallStatus.Succeeded,
298
+ approved: true,
299
+ output: { result: 'first-result' }
300
+ }
301
+ ]
302
+ },
303
+ {
304
+ role: MessageRole.Assistant,
305
+ content: 'Now executing second tool.',
306
+ toolCalls: [
307
+ {
308
+ id: 'tool-call-2',
309
+ name: 'second-tool',
310
+ input: { step: 2 },
311
+ status: AgentToolCallStatus.Succeeded,
312
+ approved: false,
313
+ reason: 'User denied',
314
+ output: {
315
+ type: 'execution-denied',
316
+ reason: 'User denied'
317
+ }
318
+ }
319
+ ]
320
+ }
321
+ ] as Pick<AgentMessage, 'role' | 'content' | 'toolCalls'>[];
322
+
323
+ const response = await AIHelper.getAssistantResponse(messages);
324
+
325
+ // Verify we get a response for sequential tool calls
326
+ expect(response).toBeDefined();
327
+ expect(typeof response).toBe('string');
328
+ expect(response.length).toBeGreaterThan(0);
329
+
330
+ // The model should process both sequential tool results
331
+ }, 30000);
332
+
333
+ it('should pass through options correctly', async () => {
334
+ const messages: Pick<AgentMessage, 'role' | 'content'>[] = [
335
+ {
336
+ role: MessageRole.User,
337
+ content: 'Test message'
338
+ }
339
+ ];
340
+
341
+ const response = await AIHelper.getAssistantResponse(messages, undefined, {
342
+ temperature: 0.9,
343
+ maxOutputTokens: 1000
344
+ });
345
+
346
+ // Verify we get a response with custom options applied
347
+ expect(response).toBeDefined();
348
+ expect(typeof response).toBe('string');
349
+ expect(response.length).toBeGreaterThan(0);
350
+
351
+ // Options are passed through to the model
352
+ }, 30000);
353
+
354
+ it('should handle abort signal', async () => {
355
+ const messages: Pick<AgentMessage, 'role' | 'content'>[] = [
356
+ {
357
+ role: MessageRole.User,
358
+ content: 'Test message'
359
+ }
360
+ ];
361
+
362
+ const abortController = new AbortController();
363
+ const signal = abortController.signal;
364
+
365
+ const response = await AIHelper.getAssistantResponse(messages, signal);
366
+
367
+ // Verify we get a response with abort signal passed through
368
+ expect(response).toBeDefined();
369
+ expect(typeof response).toBe('string');
370
+ expect(response.length).toBeGreaterThan(0);
371
+
372
+ // Signal is passed through to the model for cancellation support
373
+ }, 30000);
374
+ });
375
+ });