@hed-hog/core 0.0.349 → 0.0.351

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 (350) hide show
  1. package/dist/access-log/access-log.controller.d.ts +16 -0
  2. package/dist/access-log/access-log.controller.d.ts.map +1 -0
  3. package/dist/access-log/access-log.controller.js +44 -0
  4. package/dist/access-log/access-log.controller.js.map +1 -0
  5. package/dist/access-log/access-log.interceptor.d.ts +9 -0
  6. package/dist/access-log/access-log.interceptor.d.ts.map +1 -0
  7. package/dist/access-log/access-log.interceptor.js +46 -0
  8. package/dist/access-log/access-log.interceptor.js.map +1 -0
  9. package/dist/access-log/access-log.module.d.ts +3 -0
  10. package/dist/access-log/access-log.module.d.ts.map +1 -0
  11. package/dist/access-log/access-log.module.js +29 -0
  12. package/dist/access-log/access-log.module.js.map +1 -0
  13. package/dist/access-log/access-log.service.d.ts +47 -0
  14. package/dist/access-log/access-log.service.d.ts.map +1 -0
  15. package/dist/access-log/access-log.service.js +113 -0
  16. package/dist/access-log/access-log.service.js.map +1 -0
  17. package/dist/access-log/dto/query-access-log.dto.d.ts +8 -0
  18. package/dist/access-log/dto/query-access-log.dto.d.ts.map +1 -0
  19. package/dist/access-log/dto/query-access-log.dto.js +42 -0
  20. package/dist/access-log/dto/query-access-log.dto.js.map +1 -0
  21. package/dist/auth/auth.controller.d.ts +12 -12
  22. package/dist/auth/auth.service.d.ts +27 -27
  23. package/dist/auth/auth.service.d.ts.map +1 -1
  24. package/dist/auth/auth.service.js +0 -4
  25. package/dist/auth/auth.service.js.map +1 -1
  26. package/dist/challenge/challenge.service.d.ts +2 -2
  27. package/dist/challenge/challenge.service.d.ts.map +1 -1
  28. package/dist/challenge/challenge.service.js +0 -1
  29. package/dist/challenge/challenge.service.js.map +1 -1
  30. package/dist/core/system.module.d.ts.map +1 -1
  31. package/dist/core/system.module.js +1 -0
  32. package/dist/core/system.module.js.map +1 -1
  33. package/dist/core.module.d.ts.map +1 -1
  34. package/dist/core.module.js +15 -0
  35. package/dist/core.module.js.map +1 -1
  36. package/dist/dashboard/dashboard-user/dashboard-user.controller.d.ts +3 -3
  37. package/dist/dashboard/dashboard-user/dashboard-user.service.d.ts +3 -3
  38. package/dist/dashboard/dashboard.module.d.ts.map +1 -1
  39. package/dist/dashboard/dashboard.module.js +4 -1
  40. package/dist/dashboard/dashboard.module.js.map +1 -1
  41. package/dist/file/file.controller.js +0 -1
  42. package/dist/file/file.controller.js.map +1 -1
  43. package/dist/file/file.service.d.ts +65 -0
  44. package/dist/file/file.service.d.ts.map +1 -1
  45. package/dist/file/file.service.js +151 -3
  46. package/dist/file/file.service.js.map +1 -1
  47. package/dist/file/provider/abstract.provider.d.ts +8 -0
  48. package/dist/file/provider/abstract.provider.d.ts.map +1 -1
  49. package/dist/file/provider/abstract.provider.js.map +1 -1
  50. package/dist/file/provider/azure.provider.d.ts +4 -0
  51. package/dist/file/provider/azure.provider.d.ts.map +1 -1
  52. package/dist/file/provider/azure.provider.js +14 -0
  53. package/dist/file/provider/azure.provider.js.map +1 -1
  54. package/dist/file/provider/gcs.provider.d.ts +4 -0
  55. package/dist/file/provider/gcs.provider.d.ts.map +1 -1
  56. package/dist/file/provider/gcs.provider.js +14 -0
  57. package/dist/file/provider/gcs.provider.js.map +1 -1
  58. package/dist/file/provider/local.provider.d.ts +4 -0
  59. package/dist/file/provider/local.provider.d.ts.map +1 -1
  60. package/dist/file/provider/local.provider.js +19 -0
  61. package/dist/file/provider/local.provider.js.map +1 -1
  62. package/dist/file/provider/provider.factory.d.ts.map +1 -1
  63. package/dist/file/provider/provider.factory.js +3 -0
  64. package/dist/file/provider/provider.factory.js.map +1 -1
  65. package/dist/file/provider/s3.provider.d.ts +4 -0
  66. package/dist/file/provider/s3.provider.d.ts.map +1 -1
  67. package/dist/file/provider/s3.provider.js +17 -0
  68. package/dist/file/provider/s3.provider.js.map +1 -1
  69. package/dist/index.d.ts +3 -0
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +4 -0
  72. package/dist/index.js.map +1 -1
  73. package/dist/mcp/guards/mcp-auth.guard.d.ts +6 -1
  74. package/dist/mcp/guards/mcp-auth.guard.d.ts.map +1 -1
  75. package/dist/mcp/guards/mcp-auth.guard.js +29 -2
  76. package/dist/mcp/guards/mcp-auth.guard.js.map +1 -1
  77. package/dist/mcp/mcp-api-key.controller.d.ts +32 -0
  78. package/dist/mcp/mcp-api-key.controller.d.ts.map +1 -0
  79. package/dist/mcp/mcp-api-key.controller.js +64 -0
  80. package/dist/mcp/mcp-api-key.controller.js.map +1 -0
  81. package/dist/mcp/mcp-api-key.service.d.ts +27 -0
  82. package/dist/mcp/mcp-api-key.service.d.ts.map +1 -0
  83. package/dist/mcp/mcp-api-key.service.js +74 -0
  84. package/dist/mcp/mcp-api-key.service.js.map +1 -0
  85. package/dist/mcp/mcp-audit.service.d.ts +3 -1
  86. package/dist/mcp/mcp-audit.service.d.ts.map +1 -1
  87. package/dist/mcp/mcp-audit.service.js +14 -2
  88. package/dist/mcp/mcp-audit.service.js.map +1 -1
  89. package/dist/mcp/mcp-registry.service.d.ts +1 -2
  90. package/dist/mcp/mcp-registry.service.d.ts.map +1 -1
  91. package/dist/mcp/mcp-registry.service.js +6 -2
  92. package/dist/mcp/mcp-registry.service.js.map +1 -1
  93. package/dist/mcp/mcp.controller.d.ts +3 -1
  94. package/dist/mcp/mcp.controller.d.ts.map +1 -1
  95. package/dist/mcp/mcp.controller.js +10 -2
  96. package/dist/mcp/mcp.controller.js.map +1 -1
  97. package/dist/mcp/mcp.module.d.ts.map +1 -1
  98. package/dist/mcp/mcp.module.js +80 -1
  99. package/dist/mcp/mcp.module.js.map +1 -1
  100. package/dist/mcp/tools/core-ai.mcp-tools.d.ts +45 -0
  101. package/dist/mcp/tools/core-ai.mcp-tools.d.ts.map +1 -0
  102. package/dist/mcp/tools/core-ai.mcp-tools.js +214 -0
  103. package/dist/mcp/tools/core-ai.mcp-tools.js.map +1 -0
  104. package/dist/mcp/tools/core-dashboard-items.mcp-tools.d.ts +21 -0
  105. package/dist/mcp/tools/core-dashboard-items.mcp-tools.d.ts.map +1 -0
  106. package/dist/mcp/tools/core-dashboard-items.mcp-tools.js +89 -0
  107. package/dist/mcp/tools/core-dashboard-items.mcp-tools.js.map +1 -0
  108. package/dist/mcp/tools/core-dashboard.mcp-tools.d.ts +29 -0
  109. package/dist/mcp/tools/core-dashboard.mcp-tools.d.ts.map +1 -0
  110. package/dist/mcp/tools/core-dashboard.mcp-tools.js +149 -0
  111. package/dist/mcp/tools/core-dashboard.mcp-tools.js.map +1 -0
  112. package/dist/mcp/tools/core-event-webhooks.mcp-tools.d.ts +30 -0
  113. package/dist/mcp/tools/core-event-webhooks.mcp-tools.d.ts.map +1 -0
  114. package/dist/mcp/tools/core-event-webhooks.mcp-tools.js +185 -0
  115. package/dist/mcp/tools/core-event-webhooks.mcp-tools.js.map +1 -0
  116. package/dist/mcp/tools/core-files.mcp-tools.d.ts +26 -0
  117. package/dist/mcp/tools/core-files.mcp-tools.d.ts.map +1 -0
  118. package/dist/mcp/tools/core-files.mcp-tools.js +140 -0
  119. package/dist/mcp/tools/core-files.mcp-tools.js.map +1 -0
  120. package/dist/mcp/tools/core-integration-events.mcp-tools.d.ts +26 -0
  121. package/dist/mcp/tools/core-integration-events.mcp-tools.d.ts.map +1 -0
  122. package/dist/mcp/tools/core-integration-events.mcp-tools.js +168 -0
  123. package/dist/mcp/tools/core-integration-events.mcp-tools.js.map +1 -0
  124. package/dist/mcp/tools/core-mail-profile.mcp-tools.d.ts +32 -0
  125. package/dist/mcp/tools/core-mail-profile.mcp-tools.d.ts.map +1 -0
  126. package/dist/mcp/tools/core-mail-profile.mcp-tools.js +178 -0
  127. package/dist/mcp/tools/core-mail-profile.mcp-tools.js.map +1 -0
  128. package/dist/mcp/tools/core-mail-sent.mcp-tools.d.ts +14 -0
  129. package/dist/mcp/tools/core-mail-sent.mcp-tools.d.ts.map +1 -0
  130. package/dist/mcp/tools/core-mail-sent.mcp-tools.js +106 -0
  131. package/dist/mcp/tools/core-mail-sent.mcp-tools.js.map +1 -0
  132. package/dist/mcp/tools/core-mail.mcp-tools.d.ts +36 -0
  133. package/dist/mcp/tools/core-mail.mcp-tools.d.ts.map +1 -0
  134. package/dist/mcp/tools/core-mail.mcp-tools.js +184 -0
  135. package/dist/mcp/tools/core-mail.mcp-tools.js.map +1 -0
  136. package/dist/mcp/tools/core-mcp-chat.mcp-tools.d.ts +21 -0
  137. package/dist/mcp/tools/core-mcp-chat.mcp-tools.d.ts.map +1 -0
  138. package/dist/mcp/tools/core-mcp-chat.mcp-tools.js +118 -0
  139. package/dist/mcp/tools/core-mcp-chat.mcp-tools.js.map +1 -0
  140. package/dist/mcp/tools/core-menus.mcp-tools.d.ts +40 -0
  141. package/dist/mcp/tools/core-menus.mcp-tools.d.ts.map +1 -0
  142. package/dist/mcp/tools/core-menus.mcp-tools.js +268 -0
  143. package/dist/mcp/tools/core-menus.mcp-tools.js.map +1 -0
  144. package/dist/mcp/tools/core-notifications.mcp-tools.d.ts +31 -0
  145. package/dist/mcp/tools/core-notifications.mcp-tools.d.ts.map +1 -0
  146. package/dist/mcp/tools/core-notifications.mcp-tools.js +178 -0
  147. package/dist/mcp/tools/core-notifications.mcp-tools.js.map +1 -0
  148. package/dist/mcp/tools/core-profile.mcp-tools.d.ts +20 -0
  149. package/dist/mcp/tools/core-profile.mcp-tools.d.ts.map +1 -0
  150. package/dist/mcp/tools/core-profile.mcp-tools.js +111 -0
  151. package/dist/mcp/tools/core-profile.mcp-tools.js.map +1 -0
  152. package/dist/mcp/tools/core-roles.mcp-tools.d.ts +70 -0
  153. package/dist/mcp/tools/core-roles.mcp-tools.d.ts.map +1 -0
  154. package/dist/mcp/tools/core-roles.mcp-tools.js +355 -0
  155. package/dist/mcp/tools/core-roles.mcp-tools.js.map +1 -0
  156. package/dist/mcp/tools/core-routes.mcp-tools.d.ts +52 -0
  157. package/dist/mcp/tools/core-routes.mcp-tools.d.ts.map +1 -0
  158. package/dist/mcp/tools/core-routes.mcp-tools.js +254 -0
  159. package/dist/mcp/tools/core-routes.mcp-tools.js.map +1 -0
  160. package/dist/mcp/tools/core-screens.mcp-tools.d.ts +47 -0
  161. package/dist/mcp/tools/core-screens.mcp-tools.d.ts.map +1 -0
  162. package/dist/mcp/tools/core-screens.mcp-tools.js +250 -0
  163. package/dist/mcp/tools/core-screens.mcp-tools.js.map +1 -0
  164. package/dist/mcp/tools/core-sessions.mcp-tools.d.ts +20 -0
  165. package/dist/mcp/tools/core-sessions.mcp-tools.d.ts.map +1 -0
  166. package/dist/mcp/tools/core-sessions.mcp-tools.js +128 -0
  167. package/dist/mcp/tools/core-sessions.mcp-tools.js.map +1 -0
  168. package/dist/mcp/tools/core-settings.mcp-tools.d.ts +25 -0
  169. package/dist/mcp/tools/core-settings.mcp-tools.d.ts.map +1 -1
  170. package/dist/mcp/tools/core-settings.mcp-tools.js +138 -0
  171. package/dist/mcp/tools/core-settings.mcp-tools.js.map +1 -1
  172. package/dist/mcp/tools/core-system.mcp-tools.d.ts +8 -0
  173. package/dist/mcp/tools/core-system.mcp-tools.d.ts.map +1 -0
  174. package/dist/mcp/tools/core-system.mcp-tools.js +40 -0
  175. package/dist/mcp/tools/core-system.mcp-tools.js.map +1 -0
  176. package/dist/mcp/tools/core-webhooks.mcp-tools.d.ts +46 -0
  177. package/dist/mcp/tools/core-webhooks.mcp-tools.d.ts.map +1 -0
  178. package/dist/mcp/tools/core-webhooks.mcp-tools.js +266 -0
  179. package/dist/mcp/tools/core-webhooks.mcp-tools.js.map +1 -0
  180. package/dist/mcp-chat/mcp-chat.controller.d.ts +5 -1
  181. package/dist/mcp-chat/mcp-chat.controller.d.ts.map +1 -1
  182. package/dist/mcp-chat/mcp-chat.controller.js +46 -0
  183. package/dist/mcp-chat/mcp-chat.controller.js.map +1 -1
  184. package/dist/mcp-chat/mcp-chat.service.d.ts +31 -3
  185. package/dist/mcp-chat/mcp-chat.service.d.ts.map +1 -1
  186. package/dist/mcp-chat/mcp-chat.service.js +776 -38
  187. package/dist/mcp-chat/mcp-chat.service.js.map +1 -1
  188. package/dist/notification/dto/create-notification.dto.d.ts +26 -0
  189. package/dist/notification/dto/create-notification.dto.d.ts.map +1 -0
  190. package/dist/notification/dto/create-notification.dto.js +86 -0
  191. package/dist/notification/dto/create-notification.dto.js.map +1 -0
  192. package/dist/notification/dto/update-progress.dto.d.ts +6 -0
  193. package/dist/notification/dto/update-progress.dto.d.ts.map +1 -0
  194. package/dist/notification/dto/update-progress.dto.js +33 -0
  195. package/dist/notification/dto/update-progress.dto.js.map +1 -0
  196. package/dist/notification/notification.controller.d.ts +112 -0
  197. package/dist/notification/notification.controller.d.ts.map +1 -0
  198. package/dist/notification/notification.controller.js +130 -0
  199. package/dist/notification/notification.controller.js.map +1 -0
  200. package/dist/notification/notification.module.d.ts +3 -0
  201. package/dist/notification/notification.module.d.ts.map +1 -0
  202. package/dist/notification/notification.module.js +29 -0
  203. package/dist/notification/notification.module.js.map +1 -0
  204. package/dist/notification/notification.service.d.ts +112 -0
  205. package/dist/notification/notification.service.d.ts.map +1 -0
  206. package/dist/notification/notification.service.js +269 -0
  207. package/dist/notification/notification.service.js.map +1 -0
  208. package/dist/pdf/pdf.service.js +1 -1
  209. package/dist/pdf/pdf.service.js.map +1 -1
  210. package/dist/profile/profile.controller.d.ts +4 -4
  211. package/dist/profile/profile.controller.js +0 -1
  212. package/dist/profile/profile.controller.js.map +1 -1
  213. package/dist/profile/profile.module.d.ts.map +1 -1
  214. package/dist/profile/profile.module.js +1 -0
  215. package/dist/profile/profile.module.js.map +1 -1
  216. package/dist/profile/profile.service.d.ts +4 -4
  217. package/dist/session/session.controller.d.ts +3 -3
  218. package/dist/session/session.service.d.ts +9 -9
  219. package/dist/setting/dto/update-setting-list.dto.d.ts +9 -0
  220. package/dist/setting/dto/update-setting-list.dto.d.ts.map +1 -0
  221. package/dist/setting/dto/update-setting-list.dto.js +44 -0
  222. package/dist/setting/dto/update-setting-list.dto.js.map +1 -0
  223. package/dist/setting/setting.controller.d.ts +20 -1
  224. package/dist/setting/setting.controller.d.ts.map +1 -1
  225. package/dist/setting/setting.controller.js +12 -0
  226. package/dist/setting/setting.controller.js.map +1 -1
  227. package/dist/setting/setting.service.d.ts +20 -1
  228. package/dist/setting/setting.service.d.ts.map +1 -1
  229. package/dist/setting/setting.service.js +80 -4
  230. package/dist/setting/setting.service.js.map +1 -1
  231. package/dist/task/task.service.d.ts +13 -0
  232. package/dist/task/task.service.d.ts.map +1 -1
  233. package/dist/task/task.service.js +85 -0
  234. package/dist/task/task.service.js.map +1 -1
  235. package/dist/token/token.service.d.ts.map +1 -1
  236. package/dist/token/token.service.js +0 -1
  237. package/dist/token/token.service.js.map +1 -1
  238. package/dist/user/user.controller.d.ts +11 -11
  239. package/dist/user/user.service.d.ts +27 -27
  240. package/dist/webhook-integration/dto/webhook-action.dto.d.ts +22 -0
  241. package/dist/webhook-integration/dto/webhook-action.dto.d.ts.map +1 -0
  242. package/dist/webhook-integration/dto/webhook-action.dto.js +120 -0
  243. package/dist/webhook-integration/dto/webhook-action.dto.js.map +1 -0
  244. package/dist/webhook-integration/webhook-integration.controller.d.ts +7 -5
  245. package/dist/webhook-integration/webhook-integration.controller.d.ts.map +1 -1
  246. package/dist/webhook-integration/webhook-integration.controller.js +18 -5
  247. package/dist/webhook-integration/webhook-integration.controller.js.map +1 -1
  248. package/dist/webhook-integration/webhook-integration.service.d.ts +18 -5
  249. package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
  250. package/dist/webhook-integration/webhook-integration.service.js +342 -59
  251. package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
  252. package/dist/webhook-integration/webhook-utils.d.ts +1 -0
  253. package/dist/webhook-integration/webhook-utils.d.ts.map +1 -1
  254. package/dist/webhook-integration/webhook-utils.js +38 -0
  255. package/dist/webhook-integration/webhook-utils.js.map +1 -1
  256. package/hedhog/data/menu.yaml +63 -4
  257. package/hedhog/data/route.yaml +1437 -0
  258. package/hedhog/data/setting_group.yaml +112 -0
  259. package/hedhog/data/setting_subgroup.yaml +32 -0
  260. package/hedhog/frontend/app/access-log/page.tsx.ejs +772 -0
  261. package/hedhog/frontend/app/account/components/mcp-tokens.tsx.ejs +285 -0
  262. package/hedhog/frontend/app/account/layout.tsx.ejs +32 -55
  263. package/hedhog/frontend/app/account/mcp-tokens/page.tsx.ejs +5 -0
  264. package/hedhog/frontend/app/ai_agent/page.tsx.ejs +10 -3
  265. package/hedhog/frontend/app/dashboard/dashboard-home-tabs.tsx.ejs +33 -13
  266. package/hedhog/frontend/app/dashboard/management/tabs/components-tab.tsx.ejs +12 -5
  267. package/hedhog/frontend/app/dashboard/management/tabs/dashboards-tab.tsx.ejs +12 -5
  268. package/hedhog/frontend/app/integrations-webhooks/page.tsx.ejs +3559 -1835
  269. package/hedhog/frontend/app/mcp_chat/chat-components.tsx.ejs +633 -0
  270. package/hedhog/frontend/app/mcp_chat/chat-types.ts.ejs +16 -0
  271. package/hedhog/frontend/app/mcp_chat/page.tsx.ejs +583 -384
  272. package/hedhog/frontend/app/menu/page.tsx.ejs +9 -3
  273. package/hedhog/frontend/app/roles/page.tsx.ejs +18 -6
  274. package/hedhog/frontend/app/routes/page.tsx.ejs +9 -3
  275. package/hedhog/frontend/app/setting-list/page.tsx.ejs +822 -0
  276. package/hedhog/frontend/app/users/page.tsx.ejs +445 -203
  277. package/hedhog/frontend/app/users/user-access-sheet.tsx.ejs +310 -304
  278. package/hedhog/frontend/app/users/user-sheet.tsx.ejs +41 -44
  279. package/hedhog/frontend/messages/en.json +343 -3
  280. package/hedhog/frontend/messages/pt.json +343 -3
  281. package/hedhog/table/access_log.yaml +31 -0
  282. package/hedhog/table/mcp_api_key.yaml +30 -0
  283. package/hedhog/table/notification.yaml +72 -0
  284. package/hedhog/table/webhook_action_log.yaml +49 -0
  285. package/package.json +5 -4
  286. package/src/access-log/access-log.controller.ts +29 -0
  287. package/src/access-log/access-log.interceptor.ts +38 -0
  288. package/src/access-log/access-log.module.ts +16 -0
  289. package/src/access-log/access-log.service.ts +129 -0
  290. package/src/access-log/dto/query-access-log.dto.ts +23 -0
  291. package/src/auth/auth.service.ts +0 -7
  292. package/src/challenge/challenge.service.ts +0 -2
  293. package/src/core/system.module.ts +1 -0
  294. package/src/core.module.ts +15 -0
  295. package/src/dashboard/dashboard.module.ts +4 -1
  296. package/src/file/file.controller.ts +14 -14
  297. package/src/file/file.service.ts +198 -4
  298. package/src/file/provider/abstract.provider.ts +10 -0
  299. package/src/file/provider/azure.provider.ts +24 -0
  300. package/src/file/provider/gcs.provider.ts +20 -0
  301. package/src/file/provider/local.provider.ts +30 -2
  302. package/src/file/provider/provider.factory.ts +3 -0
  303. package/src/file/provider/s3.provider.ts +22 -0
  304. package/src/index.ts +5 -0
  305. package/src/mcp/guards/mcp-auth.guard.ts +64 -32
  306. package/src/mcp/mcp-api-key.controller.ts +42 -0
  307. package/src/mcp/mcp-api-key.service.ts +66 -0
  308. package/src/mcp/mcp-audit.service.ts +47 -36
  309. package/src/mcp/mcp-registry.service.ts +75 -69
  310. package/src/mcp/mcp.controller.ts +40 -30
  311. package/src/mcp/mcp.module.ts +115 -36
  312. package/src/mcp/tools/core-ai.mcp-tools.ts +156 -0
  313. package/src/mcp/tools/core-dashboard-items.mcp-tools.ts +63 -0
  314. package/src/mcp/tools/core-dashboard.mcp-tools.ts +103 -0
  315. package/src/mcp/tools/core-event-webhooks.mcp-tools.ts +134 -0
  316. package/src/mcp/tools/core-files.mcp-tools.ts +105 -0
  317. package/src/mcp/tools/core-integration-events.mcp-tools.ts +118 -0
  318. package/src/mcp/tools/core-mail-profile.mcp-tools.ts +128 -0
  319. package/src/mcp/tools/core-mail-sent.mcp-tools.ts +76 -0
  320. package/src/mcp/tools/core-mail.mcp-tools.ts +130 -0
  321. package/src/mcp/tools/core-mcp-chat.mcp-tools.ts +81 -0
  322. package/src/mcp/tools/core-menus.mcp-tools.ts +197 -0
  323. package/src/mcp/tools/core-notifications.mcp-tools.ts +128 -0
  324. package/src/mcp/tools/core-profile.mcp-tools.ts +74 -0
  325. package/src/mcp/tools/core-roles.mcp-tools.ts +288 -0
  326. package/src/mcp/tools/core-routes.mcp-tools.ts +196 -0
  327. package/src/mcp/tools/core-screens.mcp-tools.ts +186 -0
  328. package/src/mcp/tools/core-sessions.mcp-tools.ts +97 -0
  329. package/src/mcp/tools/core-settings.mcp-tools.ts +171 -53
  330. package/src/mcp/tools/core-system.mcp-tools.ts +19 -0
  331. package/src/mcp/tools/core-webhooks.mcp-tools.ts +199 -0
  332. package/src/mcp-chat/mcp-chat.controller.ts +113 -60
  333. package/src/mcp-chat/mcp-chat.service.ts +1343 -332
  334. package/src/notification/dto/create-notification.dto.ts +71 -0
  335. package/src/notification/dto/update-progress.dto.ts +16 -0
  336. package/src/notification/notification.controller.ts +83 -0
  337. package/src/notification/notification.module.ts +16 -0
  338. package/src/notification/notification.service.ts +324 -0
  339. package/src/pdf/pdf.service.ts +1 -1
  340. package/src/profile/profile.controller.ts +1 -1
  341. package/src/profile/profile.module.ts +1 -0
  342. package/src/setting/dto/update-setting-list.dto.ts +32 -0
  343. package/src/setting/setting.controller.ts +9 -0
  344. package/src/setting/setting.service.ts +99 -11
  345. package/src/task/task.service.ts +103 -0
  346. package/src/token/token.service.ts +0 -2
  347. package/src/webhook-integration/dto/webhook-action.dto.ts +96 -0
  348. package/src/webhook-integration/webhook-integration.controller.ts +124 -103
  349. package/src/webhook-integration/webhook-integration.service.ts +1137 -735
  350. package/src/webhook-integration/webhook-utils.ts +136 -79
@@ -1,332 +1,1343 @@
1
- import { PrismaService } from '@hed-hog/api-prisma';
2
- import {
3
- BadRequestException,
4
- ForbiddenException,
5
- forwardRef,
6
- HttpException,
7
- HttpStatus,
8
- Inject,
9
- Injectable,
10
- NotFoundException,
11
- } from '@nestjs/common';
12
- import axios from 'axios';
13
- import { SettingService } from '../setting/setting.service';
14
- import { McpContext } from '../mcp/types/mcp-context.type';
15
- import { McpRegistryService } from '../mcp/mcp-registry.service';
16
- import { CreateConversationDTO } from './dto/create-conversation.dto';
17
-
18
- type OpenAiMessage =
19
- | { role: 'system'; content: string }
20
- | { role: 'user'; content: string }
21
- | { role: 'assistant'; content: string | null; tool_calls?: OpenAiToolCall[] }
22
- | { role: 'tool'; tool_call_id: string; content: string };
23
-
24
- interface OpenAiToolCall {
25
- id: string;
26
- type: 'function';
27
- function: { name: string; arguments: string };
28
- }
29
-
30
- const SYSTEM_PROMPT =
31
- 'You are a helpful assistant with access to tools from the HedHog system. ' +
32
- 'Use the available tools to answer the user\'s questions accurately. ' +
33
- 'Always respond in the same language the user writes in.';
34
-
35
- const sanitize = (n: string) => n.replace(/\./g, '__');
36
- const restore = (n: string) => n.replace(/__/g, '.');
37
-
38
- @Injectable()
39
- export class McpChatService {
40
- constructor(
41
- @Inject(forwardRef(() => PrismaService))
42
- private readonly prisma: PrismaService,
43
- @Inject(forwardRef(() => SettingService))
44
- private readonly setting: SettingService,
45
- @Inject(forwardRef(() => McpRegistryService))
46
- private readonly registry: McpRegistryService,
47
- ) {}
48
-
49
- async listConversations(userId: number) {
50
- return this.prisma.mcp_conversation.findMany({
51
- where: { user_id: userId },
52
- orderBy: { created_at: 'desc' },
53
- select: { id: true, title: true, created_at: true, updated_at: true },
54
- });
55
- }
56
-
57
- async createConversation(userId: number, dto: CreateConversationDTO) {
58
- return this.prisma.mcp_conversation.create({
59
- data: {
60
- user_id: userId,
61
- title: dto.title ?? null,
62
- },
63
- });
64
- }
65
-
66
- async deleteConversations(userId: number, ids: number[]) {
67
- await this.prisma.mcp_conversation.deleteMany({
68
- where: { id: { in: ids }, user_id: userId },
69
- });
70
- return { count: ids.length };
71
- }
72
-
73
- async getMessages(conversationId: number, userId: number) {
74
- await this.assertOwnership(conversationId, userId);
75
- return this.prisma.mcp_conversation_message.findMany({
76
- where: { conversation_id: conversationId },
77
- orderBy: { created_at: 'asc' },
78
- });
79
- }
80
-
81
- async sendMessage(
82
- conversationId: number,
83
- userId: number,
84
- locale: string,
85
- message: string,
86
- ) {
87
- await this.assertOwnership(conversationId, userId);
88
-
89
- const apiKey = await this.getOpenAiKey();
90
-
91
- // Save user message
92
- await this.prisma.mcp_conversation_message.create({
93
- data: {
94
- conversation_id: conversationId,
95
- role: 'user',
96
- content: message,
97
- },
98
- });
99
-
100
- // Auto-set title on first message
101
- const conv = await this.prisma.mcp_conversation.findUnique({
102
- where: { id: conversationId },
103
- });
104
- if (!conv!.title) {
105
- await this.prisma.mcp_conversation.update({
106
- where: { id: conversationId },
107
- data: { title: message.slice(0, 60) },
108
- });
109
- }
110
-
111
- // Build OpenAI messages from history
112
- const history = await this.prisma.mcp_conversation_message.findMany({
113
- where: { conversation_id: conversationId },
114
- orderBy: { created_at: 'asc' },
115
- });
116
-
117
- const openAiMessages: OpenAiMessage[] = [
118
- { role: 'system', content: SYSTEM_PROMPT },
119
- ...this.toOpenAiMessages(history),
120
- ];
121
-
122
- // Build tool definitions
123
- const tools = this.registry.getAll().map((entry) => ({
124
- type: 'function' as const,
125
- function: {
126
- name: sanitize(entry.definition.name),
127
- description: entry.definition.description,
128
- parameters: entry.definition.inputSchema ?? {
129
- type: 'object',
130
- properties: {},
131
- },
132
- },
133
- }));
134
-
135
- // Tool-calling loop
136
- let maxIterations = 10;
137
- while (maxIterations-- > 0) {
138
- let response: any;
139
- try {
140
- response = await axios.post(
141
- 'https://api.openai.com/v1/chat/completions',
142
- {
143
- model: 'gpt-4o-mini',
144
- messages: openAiMessages,
145
- tools: tools.length > 0 ? tools : undefined,
146
- tool_choice: tools.length > 0 ? 'auto' : undefined,
147
- },
148
- {
149
- headers: {
150
- Authorization: `Bearer ${apiKey}`,
151
- 'Content-Type': 'application/json',
152
- },
153
- },
154
- );
155
- } catch (err: any) {
156
- const status = err?.response?.status;
157
- const errBody = err?.response?.data?.error;
158
- if (status === 429) {
159
- throw new HttpException(
160
- errBody?.message ?? 'OpenAI rate limit exceeded. Please try again later.',
161
- HttpStatus.TOO_MANY_REQUESTS,
162
- );
163
- }
164
- if (status === 401) {
165
- throw new BadRequestException('Invalid OpenAI API key.');
166
- }
167
- throw new HttpException(
168
- errBody?.message ?? 'OpenAI API error.',
169
- status ?? HttpStatus.BAD_GATEWAY,
170
- );
171
- }
172
-
173
- const choice = response.data?.choices?.[0];
174
- const assistantMsg = choice?.message;
175
-
176
- if (assistantMsg?.tool_calls?.length) {
177
- // Add assistant tool-call message to context
178
- openAiMessages.push({
179
- role: 'assistant',
180
- content: null,
181
- tool_calls: assistantMsg.tool_calls,
182
- });
183
-
184
- // Execute each tool call
185
- for (const toolCall of assistantMsg.tool_calls as OpenAiToolCall[]) {
186
- const originalName = restore(toolCall.function.name);
187
- const argsJson = toolCall.function.arguments;
188
- let args: Record<string, any> = {};
189
- try {
190
- args = JSON.parse(argsJson);
191
- } catch {
192
- args = {};
193
- }
194
-
195
- // Persist tool_call message
196
- await this.prisma.mcp_conversation_message.create({
197
- data: {
198
- conversation_id: conversationId,
199
- role: 'tool_call',
200
- content: argsJson,
201
- tool_name: originalName,
202
- tool_call_id: toolCall.id,
203
- },
204
- });
205
-
206
- // Execute the tool
207
- let resultContent: string;
208
- const entry = this.registry.get(originalName);
209
- if (entry) {
210
- const context: McpContext = {
211
- userId,
212
- sessionId: '',
213
- locale,
214
- toolName: originalName,
215
- };
216
- try {
217
- const result = await entry.handler(args, context);
218
- resultContent = JSON.stringify(result);
219
- } catch (e: any) {
220
- resultContent = JSON.stringify({ error: e.message });
221
- }
222
- } else {
223
- resultContent = JSON.stringify({ error: `Tool '${originalName}' not found` });
224
- }
225
-
226
- // Persist tool_result message
227
- await this.prisma.mcp_conversation_message.create({
228
- data: {
229
- conversation_id: conversationId,
230
- role: 'tool_result',
231
- content: resultContent,
232
- tool_name: originalName,
233
- tool_call_id: toolCall.id,
234
- },
235
- });
236
-
237
- // Add tool result to context for next iteration
238
- openAiMessages.push({
239
- role: 'tool',
240
- tool_call_id: toolCall.id,
241
- content: resultContent,
242
- });
243
- }
244
-
245
- continue; // next iteration with tool results
246
- }
247
-
248
- // Final assistant response
249
- const finalContent: string = assistantMsg?.content || '';
250
- await this.prisma.mcp_conversation_message.create({
251
- data: {
252
- conversation_id: conversationId,
253
- role: 'assistant',
254
- content: finalContent,
255
- },
256
- });
257
-
258
- return this.prisma.mcp_conversation_message.findMany({
259
- where: { conversation_id: conversationId },
260
- orderBy: { created_at: 'asc' },
261
- });
262
- }
263
-
264
- throw new BadRequestException('Maximum tool call iterations exceeded.');
265
- }
266
-
267
- private toOpenAiMessages(
268
- history: Array<{
269
- role: string;
270
- content: string;
271
- tool_name: string | null;
272
- tool_call_id: string | null;
273
- }>,
274
- ): OpenAiMessage[] {
275
- const messages: OpenAiMessage[] = [];
276
- const toolCallMap = new Map<string, string>(); // tool_call_id → sanitized name
277
-
278
- for (const msg of history) {
279
- if (msg.role === 'user') {
280
- messages.push({ role: 'user', content: msg.content });
281
- } else if (msg.role === 'assistant') {
282
- messages.push({ role: 'assistant', content: msg.content });
283
- } else if (msg.role === 'tool_call' && msg.tool_call_id && msg.tool_name) {
284
- const sanitizedName = sanitize(msg.tool_name);
285
- toolCallMap.set(msg.tool_call_id, sanitizedName);
286
- messages.push({
287
- role: 'assistant',
288
- content: null,
289
- tool_calls: [
290
- {
291
- id: msg.tool_call_id,
292
- type: 'function',
293
- function: { name: sanitizedName, arguments: msg.content },
294
- },
295
- ],
296
- });
297
- } else if (msg.role === 'tool_result' && msg.tool_call_id) {
298
- messages.push({
299
- role: 'tool',
300
- tool_call_id: msg.tool_call_id,
301
- content: msg.content,
302
- });
303
- }
304
- }
305
-
306
- return messages;
307
- }
308
-
309
- private async assertOwnership(conversationId: number, userId: number) {
310
- const conv = await this.prisma.mcp_conversation.findUnique({
311
- where: { id: conversationId },
312
- select: { user_id: true },
313
- });
314
- if (!conv) {
315
- throw new NotFoundException(`Conversation ${conversationId} not found.`);
316
- }
317
- if (conv.user_id !== userId) {
318
- throw new ForbiddenException('Access denied to this conversation.');
319
- }
320
- }
321
-
322
- private async getOpenAiKey(): Promise<string> {
323
- const settings = await this.setting.getSettingValues(['ai-openai-api-key']);
324
- const key = settings['ai-openai-api-key'];
325
- if (!key) {
326
- throw new BadRequestException(
327
- 'OpenAI API key is not configured (setting "ai-openai-api-key").',
328
- );
329
- }
330
- return key;
331
- }
332
- }
1
+ import { PrismaService } from '@hed-hog/api-prisma';
2
+ import {
3
+ BadRequestException,
4
+ ForbiddenException,
5
+ forwardRef,
6
+ HttpException,
7
+ HttpStatus,
8
+ Inject,
9
+ Injectable,
10
+ Logger,
11
+ NotFoundException,
12
+ } from '@nestjs/common';
13
+ import axios from 'axios';
14
+ import { McpRegistryService, McpToolEntry } from '../mcp/mcp-registry.service';
15
+ import { McpContext } from '../mcp/types/mcp-context.type';
16
+ import { SettingService } from '../setting/setting.service';
17
+ import { CreateConversationDTO } from './dto/create-conversation.dto';
18
+
19
+ type OpenAiMessage =
20
+ | { role: 'system'; content: string }
21
+ | { role: 'user'; content: string }
22
+ | { role: 'assistant'; content: string | null; tool_calls?: OpenAiToolCall[] }
23
+ | { role: 'tool'; tool_call_id: string; content: string };
24
+
25
+ interface OpenAiToolCall {
26
+ id: string;
27
+ type: 'function';
28
+ function: { name: string; arguments: string };
29
+ }
30
+
31
+ interface OpenAiToolDefinition {
32
+ type: 'function';
33
+ function: {
34
+ name: string;
35
+ description: string;
36
+ parameters: Record<string, any>;
37
+ };
38
+ }
39
+
40
+ interface GeminiFunctionDeclaration {
41
+ name: string;
42
+ description: string;
43
+ parameters: Record<string, any>;
44
+ }
45
+
46
+ interface GeminiPart {
47
+ text?: string;
48
+ functionCall?: { name: string; args: Record<string, any> };
49
+ functionResponse?: { name: string; response: { output: string } };
50
+ }
51
+
52
+ interface GeminiContent {
53
+ role: 'user' | 'model';
54
+ parts: GeminiPart[];
55
+ }
56
+
57
+ interface ScoredTool {
58
+ name: string;
59
+ domain: string;
60
+ readOnly: boolean;
61
+ priorityBucket: number;
62
+ keywordBoost: number;
63
+ matchScore: number;
64
+ index: number;
65
+ tool: OpenAiToolDefinition;
66
+ }
67
+
68
+ interface ToolSelectionResult<TTool> {
69
+ tools: TTool[];
70
+ clarification?: string;
71
+ }
72
+
73
+ type ConversationHistoryItem = {
74
+ role: string;
75
+ content: string;
76
+ tool_name: string | null;
77
+ tool_call_id: string | null;
78
+ };
79
+
80
+ const MAX_OPENAI_TOOLS = 128;
81
+ const TARGET_SELECTED_TOOLS = 112;
82
+ const MAX_ITERATIONS = 20;
83
+ const FORCE_FINISH_THRESHOLD = 2;
84
+ const MIN_DOMAIN_CONFIDENCE = 2;
85
+ const AMBIGUOUS_DOMAIN_DELTA = 1;
86
+
87
+ const SYSTEM_PROMPT =
88
+ 'You are a helpful assistant with access to tools from the HedHog system. ' +
89
+ 'Use the available tools to answer the user\'s questions accurately. ' +
90
+ 'Always respond in the same language the user writes in.';
91
+
92
+ const sanitize = (n: string) => n.replace(/\./g, '__');
93
+ const restore = (n: string) => n.replace(/__/g, '.');
94
+
95
+ const DOMAIN_KEYWORDS: Record<string, string[]> = {
96
+ finance: [
97
+ 'finance',
98
+ 'financeiro',
99
+ 'financeira',
100
+ 'financas',
101
+ 'finanças',
102
+ 'banco',
103
+ 'bancos',
104
+ 'bank',
105
+ 'banks',
106
+ 'conta',
107
+ 'contas',
108
+ 'account',
109
+ 'accounts',
110
+ 'conta bancaria',
111
+ 'contas bancarias',
112
+ 'conta bancária',
113
+ 'contas bancárias',
114
+ 'extrato',
115
+ 'extratos',
116
+ 'statement',
117
+ 'statements',
118
+ 'pagamento',
119
+ 'pagamentos',
120
+ 'payment',
121
+ 'payments',
122
+ 'recebimento',
123
+ 'recebimentos',
124
+ 'receivable',
125
+ 'payable',
126
+ 'currency',
127
+ 'moeda',
128
+ 'transferencia',
129
+ 'transferência',
130
+ ],
131
+ operations: [
132
+ 'operations',
133
+ 'operacoes',
134
+ 'operações',
135
+ 'tarefa',
136
+ 'tarefas',
137
+ 'task',
138
+ 'tasks',
139
+ 'projeto',
140
+ 'projetos',
141
+ 'project',
142
+ 'projects',
143
+ 'timesheet',
144
+ 'apontamento',
145
+ 'aprovacao',
146
+ 'aprovação',
147
+ 'approval',
148
+ 'colaborador',
149
+ 'collaborator',
150
+ ],
151
+ lms: [
152
+ 'lms',
153
+ 'curso',
154
+ 'cursos',
155
+ 'course',
156
+ 'courses',
157
+ 'aula',
158
+ 'aulas',
159
+ 'lesson',
160
+ 'lessons',
161
+ 'treinamento',
162
+ 'training',
163
+ 'turma',
164
+ 'class',
165
+ 'aluno',
166
+ 'student',
167
+ 'prova',
168
+ 'exam',
169
+ ],
170
+ core: [
171
+ 'core',
172
+ 'usuario',
173
+ 'usuário',
174
+ 'usuarios',
175
+ 'usuários',
176
+ 'user',
177
+ 'users',
178
+ 'perfil',
179
+ 'profile',
180
+ 'role',
181
+ 'roles',
182
+ 'menu',
183
+ 'menus',
184
+ 'rota',
185
+ 'rotas',
186
+ 'route',
187
+ 'routes',
188
+ 'configuracao',
189
+ 'configuração',
190
+ 'setting',
191
+ 'settings',
192
+ ],
193
+ contact: [
194
+ 'contato',
195
+ 'contatos',
196
+ 'contact',
197
+ 'contacts',
198
+ 'pessoa',
199
+ 'pessoas',
200
+ 'person',
201
+ 'proposal',
202
+ 'proposta',
203
+ ],
204
+ };
205
+
206
+ const ACTION_KEYWORDS = [
207
+ 'listar',
208
+ 'lista',
209
+ 'buscar',
210
+ 'procurar',
211
+ 'consultar',
212
+ 'mostrar',
213
+ 'ver',
214
+ 'criar',
215
+ 'adicionar',
216
+ 'atualizar',
217
+ 'alterar',
218
+ 'remover',
219
+ 'deletar',
220
+ 'excluir',
221
+ 'list',
222
+ 'search',
223
+ 'find',
224
+ 'show',
225
+ 'get',
226
+ 'create',
227
+ 'add',
228
+ 'update',
229
+ 'delete',
230
+ 'remove',
231
+ ];
232
+
233
+ @Injectable()
234
+ export class McpChatService {
235
+ private readonly logger = new Logger(McpChatService.name);
236
+
237
+ constructor(
238
+ @Inject(forwardRef(() => PrismaService))
239
+ private readonly prisma: PrismaService,
240
+ @Inject(forwardRef(() => SettingService))
241
+ private readonly setting: SettingService,
242
+ @Inject(forwardRef(() => McpRegistryService))
243
+ private readonly registry: McpRegistryService,
244
+ ) {}
245
+
246
+ async listConversations(userId: number) {
247
+ return this.prisma.mcp_conversation.findMany({
248
+ where: { user_id: userId },
249
+ orderBy: { created_at: 'desc' },
250
+ select: { id: true, title: true, created_at: true, updated_at: true },
251
+ });
252
+ }
253
+
254
+ async createConversation(userId: number, dto: CreateConversationDTO) {
255
+ return this.prisma.mcp_conversation.create({
256
+ data: {
257
+ user_id: userId,
258
+ title: dto.title ?? null,
259
+ },
260
+ });
261
+ }
262
+
263
+ async deleteConversations(userId: number, ids: number[]) {
264
+ await this.prisma.mcp_conversation.deleteMany({
265
+ where: { id: { in: ids }, user_id: userId },
266
+ });
267
+ return { count: ids.length };
268
+ }
269
+
270
+ async getMessages(conversationId: number, userId: number) {
271
+ await this.assertOwnership(conversationId, userId);
272
+ return this.prisma.mcp_conversation_message.findMany({
273
+ where: { conversation_id: conversationId },
274
+ orderBy: { created_at: 'asc' },
275
+ });
276
+ }
277
+
278
+ async sendMessage(
279
+ conversationId: number,
280
+ userId: number,
281
+ locale: string,
282
+ message: string,
283
+ ) {
284
+ const cfg = await this.setting.getSettingValues(['mcp-enabled']);
285
+ if (cfg['mcp-enabled'] === false) {
286
+ throw new BadRequestException('MCP server is disabled.');
287
+ }
288
+
289
+ await this.assertOwnership(conversationId, userId);
290
+
291
+ const { provider, apiKey } = await this.getProviderKey();
292
+
293
+ // Save user message
294
+ await this.prisma.mcp_conversation_message.create({
295
+ data: {
296
+ conversation_id: conversationId,
297
+ role: 'user',
298
+ content: message,
299
+ },
300
+ });
301
+
302
+ // Auto-set title on first message
303
+ const conv = await this.prisma.mcp_conversation.findUnique({
304
+ where: { id: conversationId },
305
+ });
306
+ if (!conv!.title) {
307
+ await this.prisma.mcp_conversation.update({
308
+ where: { id: conversationId },
309
+ data: { title: message.slice(0, 60) },
310
+ });
311
+ }
312
+
313
+ if (provider === 'gemini') {
314
+ return this.runGeminiLoop(conversationId, userId, locale, message, apiKey);
315
+ }
316
+ return this.runOpenAiLoop(conversationId, userId, locale, message, apiKey);
317
+ }
318
+
319
+ // ── OpenAI loop ──────────────────────────────────────────────────────────
320
+
321
+ private async runOpenAiLoop(
322
+ conversationId: number,
323
+ userId: number,
324
+ locale: string,
325
+ message: string,
326
+ apiKey: string,
327
+ ) {
328
+ const history = await this.prisma.mcp_conversation_message.findMany({
329
+ where: { conversation_id: conversationId },
330
+ orderBy: { created_at: 'asc' },
331
+ });
332
+
333
+ const openAiMessages: OpenAiMessage[] = [
334
+ { role: 'system', content: SYSTEM_PROMPT },
335
+ ...this.toOpenAiMessages(history),
336
+ ];
337
+
338
+ const toolSelection = await this.buildOpenAiTools(
339
+ message,
340
+ conversationId,
341
+ userId,
342
+ history,
343
+ );
344
+ if (toolSelection.clarification) {
345
+ return this.recordAssistantAndReturnHistory(
346
+ conversationId,
347
+ toolSelection.clarification,
348
+ );
349
+ }
350
+ const tools = toolSelection.tools;
351
+
352
+ const callSignatures = new Set<string>();
353
+ let maxIterations = MAX_ITERATIONS;
354
+ while (maxIterations-- > 0) {
355
+ const forceFinish = maxIterations < FORCE_FINISH_THRESHOLD;
356
+ let response: any;
357
+ try {
358
+ response = await axios.post(
359
+ 'https://api.openai.com/v1/chat/completions',
360
+ {
361
+ model: 'gpt-4o-mini',
362
+ messages: openAiMessages,
363
+ tools: !forceFinish && tools.length > 0 ? tools : undefined,
364
+ tool_choice: !forceFinish && tools.length > 0 ? 'auto' : undefined,
365
+ },
366
+ {
367
+ headers: {
368
+ Authorization: `Bearer ${apiKey}`,
369
+ 'Content-Type': 'application/json',
370
+ },
371
+ },
372
+ );
373
+ } catch (err: any) {
374
+ const status = err?.response?.status;
375
+ const errBody = err?.response?.data?.error;
376
+ if (status === 429) {
377
+ throw new HttpException(
378
+ errBody?.message ?? 'OpenAI rate limit exceeded. Please try again later.',
379
+ HttpStatus.TOO_MANY_REQUESTS,
380
+ );
381
+ }
382
+ if (status === 401) {
383
+ throw new BadRequestException('Invalid OpenAI API key.');
384
+ }
385
+ throw new HttpException(
386
+ errBody?.message ?? 'OpenAI API error.',
387
+ status ?? HttpStatus.BAD_GATEWAY,
388
+ );
389
+ }
390
+
391
+ const choice = response.data?.choices?.[0];
392
+ const assistantMsg = choice?.message;
393
+
394
+ if (assistantMsg?.tool_calls?.length && !forceFinish) {
395
+ openAiMessages.push({
396
+ role: 'assistant',
397
+ content: null,
398
+ tool_calls: assistantMsg.tool_calls,
399
+ });
400
+
401
+ for (const toolCall of assistantMsg.tool_calls as OpenAiToolCall[]) {
402
+ const originalName = restore(toolCall.function.name);
403
+ const argsJson = toolCall.function.arguments;
404
+ let args: Record<string, any> = {};
405
+ try {
406
+ args = JSON.parse(argsJson);
407
+ } catch {
408
+ args = {};
409
+ }
410
+
411
+ await this.prisma.mcp_conversation_message.create({
412
+ data: {
413
+ conversation_id: conversationId,
414
+ role: 'tool_call',
415
+ content: argsJson,
416
+ tool_name: originalName,
417
+ tool_call_id: toolCall.id,
418
+ },
419
+ });
420
+
421
+ const sig = `${toolCall.function.name}:${argsJson}`;
422
+ if (callSignatures.has(sig)) {
423
+ this.logger.warn(
424
+ `MCP chat duplicate tool call detected — aborting re-execution. ` +
425
+ `conversationId=${conversationId} tool=${originalName}`,
426
+ );
427
+ const dupResult = JSON.stringify({ error: 'Duplicate tool call detected, aborting loop.' });
428
+ await this.prisma.mcp_conversation_message.create({
429
+ data: {
430
+ conversation_id: conversationId,
431
+ role: 'tool_result',
432
+ content: dupResult,
433
+ tool_name: originalName,
434
+ tool_call_id: toolCall.id,
435
+ },
436
+ });
437
+ openAiMessages.push({
438
+ role: 'tool',
439
+ tool_call_id: toolCall.id,
440
+ content: dupResult,
441
+ });
442
+ continue;
443
+ }
444
+ callSignatures.add(sig);
445
+
446
+ let resultContent: string;
447
+ const entry = this.registry.get(originalName);
448
+ if (entry) {
449
+ const context: McpContext = {
450
+ userId,
451
+ sessionId: '',
452
+ locale,
453
+ toolName: originalName,
454
+ };
455
+ try {
456
+ const result = await entry.handler(args, context);
457
+ resultContent = JSON.stringify(result);
458
+ } catch (e: any) {
459
+ resultContent = JSON.stringify({ error: e.message });
460
+ }
461
+ } else {
462
+ resultContent = JSON.stringify({ error: `Tool '${originalName}' not found` });
463
+ }
464
+
465
+ await this.prisma.mcp_conversation_message.create({
466
+ data: {
467
+ conversation_id: conversationId,
468
+ role: 'tool_result',
469
+ content: resultContent,
470
+ tool_name: originalName,
471
+ tool_call_id: toolCall.id,
472
+ },
473
+ });
474
+
475
+ openAiMessages.push({
476
+ role: 'tool',
477
+ tool_call_id: toolCall.id,
478
+ content: resultContent,
479
+ });
480
+ }
481
+
482
+ continue;
483
+ }
484
+
485
+ const finalContent: string = assistantMsg?.content || '';
486
+ await this.prisma.mcp_conversation_message.create({
487
+ data: {
488
+ conversation_id: conversationId,
489
+ role: 'assistant',
490
+ content: finalContent,
491
+ },
492
+ });
493
+
494
+ return this.prisma.mcp_conversation_message.findMany({
495
+ where: { conversation_id: conversationId },
496
+ orderBy: { created_at: 'asc' },
497
+ });
498
+ }
499
+
500
+ throw new BadRequestException('Maximum tool call iterations exceeded.');
501
+ }
502
+
503
+ // ── Gemini loop ──────────────────────────────────────────────────────────
504
+
505
+ private async runGeminiLoop(
506
+ conversationId: number,
507
+ userId: number,
508
+ locale: string,
509
+ message: string,
510
+ apiKey: string,
511
+ ) {
512
+ const history = await this.prisma.mcp_conversation_message.findMany({
513
+ where: { conversation_id: conversationId },
514
+ orderBy: { created_at: 'asc' },
515
+ });
516
+
517
+ const geminiContents: GeminiContent[] = this.toGeminiContents(history);
518
+ const toolSelection = await this.buildGeminiTools(
519
+ message,
520
+ conversationId,
521
+ userId,
522
+ history,
523
+ );
524
+ if (toolSelection.clarification) {
525
+ return this.recordAssistantAndReturnHistory(
526
+ conversationId,
527
+ toolSelection.clarification,
528
+ );
529
+ }
530
+ const functionDeclarations = toolSelection.tools;
531
+
532
+ let maxIterations = MAX_ITERATIONS;
533
+ let callIndex = 0;
534
+
535
+ while (maxIterations-- > 0) {
536
+ const forceFinish = maxIterations < FORCE_FINISH_THRESHOLD;
537
+ let response: any;
538
+ try {
539
+ response = await axios.post(
540
+ `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`,
541
+ {
542
+ systemInstruction: { parts: [{ text: SYSTEM_PROMPT }] },
543
+ contents: geminiContents,
544
+ tools:
545
+ !forceFinish && functionDeclarations.length > 0
546
+ ? [{ functionDeclarations }]
547
+ : undefined,
548
+ },
549
+ { headers: { 'Content-Type': 'application/json' } },
550
+ );
551
+ } catch (err: any) {
552
+ const status = err?.response?.status;
553
+ const errData = err?.response?.data;
554
+ if (status === 429) {
555
+ throw new HttpException(
556
+ errData?.error?.message ?? 'Gemini rate limit exceeded. Please try again later.',
557
+ HttpStatus.TOO_MANY_REQUESTS,
558
+ );
559
+ }
560
+ if (status === 400) {
561
+ throw new BadRequestException(
562
+ errData?.error?.message ?? 'Invalid Gemini API request.',
563
+ );
564
+ }
565
+ if (status === 401 || status === 403) {
566
+ throw new BadRequestException('Invalid Gemini API key.');
567
+ }
568
+ throw new HttpException(
569
+ errData?.error?.message ?? 'Gemini API error.',
570
+ status ?? HttpStatus.BAD_GATEWAY,
571
+ );
572
+ }
573
+
574
+ const candidate = response.data?.candidates?.[0];
575
+ const parts: GeminiPart[] = candidate?.content?.parts ?? [];
576
+
577
+ const functionCallParts = parts.filter((p) => p.functionCall);
578
+ const textParts = parts.filter((p) => p.text);
579
+
580
+ if (functionCallParts.length > 0 && !forceFinish) {
581
+ geminiContents.push({ role: 'model', parts: functionCallParts });
582
+
583
+ const functionResponseParts: GeminiPart[] = [];
584
+
585
+ for (const part of functionCallParts) {
586
+ const { name, args } = part.functionCall!;
587
+ const originalName = restore(name);
588
+ const argsJson = JSON.stringify(args ?? {});
589
+ const toolCallId = `gemini-${callIndex++}`;
590
+
591
+ await this.prisma.mcp_conversation_message.create({
592
+ data: {
593
+ conversation_id: conversationId,
594
+ role: 'tool_call',
595
+ content: argsJson,
596
+ tool_name: originalName,
597
+ tool_call_id: toolCallId,
598
+ },
599
+ });
600
+
601
+ let resultContent: string;
602
+ const entry = this.registry.get(originalName);
603
+ if (entry) {
604
+ const context: McpContext = {
605
+ userId,
606
+ sessionId: '',
607
+ locale,
608
+ toolName: originalName,
609
+ };
610
+ try {
611
+ const result = await entry.handler(args ?? {}, context);
612
+ resultContent = JSON.stringify(result);
613
+ } catch (e: any) {
614
+ resultContent = JSON.stringify({ error: e.message });
615
+ }
616
+ } else {
617
+ resultContent = JSON.stringify({ error: `Tool '${originalName}' not found` });
618
+ }
619
+
620
+ await this.prisma.mcp_conversation_message.create({
621
+ data: {
622
+ conversation_id: conversationId,
623
+ role: 'tool_result',
624
+ content: resultContent,
625
+ tool_name: originalName,
626
+ tool_call_id: toolCallId,
627
+ },
628
+ });
629
+
630
+ functionResponseParts.push({
631
+ functionResponse: {
632
+ name,
633
+ response: { output: resultContent },
634
+ },
635
+ });
636
+ }
637
+
638
+ geminiContents.push({ role: 'user', parts: functionResponseParts });
639
+ continue;
640
+ }
641
+
642
+ const finalContent = textParts.map((p) => p.text).join('');
643
+ await this.prisma.mcp_conversation_message.create({
644
+ data: {
645
+ conversation_id: conversationId,
646
+ role: 'assistant',
647
+ content: finalContent,
648
+ },
649
+ });
650
+
651
+ return this.prisma.mcp_conversation_message.findMany({
652
+ where: { conversation_id: conversationId },
653
+ orderBy: { created_at: 'asc' },
654
+ });
655
+ }
656
+
657
+ throw new BadRequestException('Maximum tool call iterations exceeded.');
658
+ }
659
+
660
+ // ── Helpers ──────────────────────────────────────────────────────────────
661
+
662
+ private toOpenAiMessages(
663
+ history: Array<{
664
+ role: string;
665
+ content: string;
666
+ tool_name: string | null;
667
+ tool_call_id: string | null;
668
+ }>,
669
+ ): OpenAiMessage[] {
670
+ const messages: OpenAiMessage[] = [];
671
+ const toolCallMap = new Map<string, string>(); // tool_call_id → sanitized name
672
+
673
+ for (const msg of history) {
674
+ if (msg.role === 'user') {
675
+ messages.push({ role: 'user', content: msg.content });
676
+ } else if (msg.role === 'assistant') {
677
+ messages.push({ role: 'assistant', content: msg.content });
678
+ } else if (msg.role === 'tool_call' && msg.tool_call_id && msg.tool_name) {
679
+ const sanitizedName = sanitize(msg.tool_name);
680
+ toolCallMap.set(msg.tool_call_id, sanitizedName);
681
+ messages.push({
682
+ role: 'assistant',
683
+ content: null,
684
+ tool_calls: [
685
+ {
686
+ id: msg.tool_call_id,
687
+ type: 'function',
688
+ function: { name: sanitizedName, arguments: msg.content },
689
+ },
690
+ ],
691
+ });
692
+ } else if (msg.role === 'tool_result' && msg.tool_call_id) {
693
+ messages.push({
694
+ role: 'tool',
695
+ tool_call_id: msg.tool_call_id,
696
+ content: msg.content,
697
+ });
698
+ }
699
+ }
700
+
701
+ return messages;
702
+ }
703
+
704
+ private toGeminiContents(
705
+ history: Array<{
706
+ role: string;
707
+ content: string;
708
+ tool_name: string | null;
709
+ tool_call_id: string | null;
710
+ }>,
711
+ ): GeminiContent[] {
712
+ const contents: GeminiContent[] = [];
713
+ let i = 0;
714
+
715
+ while (i < history.length) {
716
+ const msg = history[i];
717
+
718
+ if (msg.role === 'user') {
719
+ contents.push({ role: 'user', parts: [{ text: msg.content }] });
720
+ i++;
721
+ } else if (msg.role === 'assistant') {
722
+ contents.push({ role: 'model', parts: [{ text: msg.content }] });
723
+ i++;
724
+ } else if (msg.role === 'tool_call') {
725
+ const functionCallParts: GeminiPart[] = [];
726
+ while (i < history.length && history[i].role === 'tool_call') {
727
+ const m = history[i];
728
+ let args: Record<string, any> = {};
729
+ try {
730
+ args = JSON.parse(m.content);
731
+ } catch {
732
+ args = {};
733
+ }
734
+ functionCallParts.push({
735
+ functionCall: { name: sanitize(m.tool_name ?? ''), args },
736
+ });
737
+ i++;
738
+ }
739
+ contents.push({ role: 'model', parts: functionCallParts });
740
+ } else if (msg.role === 'tool_result') {
741
+ const functionResponseParts: GeminiPart[] = [];
742
+ while (i < history.length && history[i].role === 'tool_result') {
743
+ const m = history[i];
744
+ functionResponseParts.push({
745
+ functionResponse: {
746
+ name: sanitize(m.tool_name ?? ''),
747
+ response: { output: m.content },
748
+ },
749
+ });
750
+ i++;
751
+ }
752
+ contents.push({ role: 'user', parts: functionResponseParts });
753
+ } else {
754
+ i++;
755
+ }
756
+ }
757
+
758
+ return contents;
759
+ }
760
+
761
+ private async assertOwnership(conversationId: number, userId: number) {
762
+ const conv = await this.prisma.mcp_conversation.findUnique({
763
+ where: { id: conversationId },
764
+ select: { user_id: true },
765
+ });
766
+ if (!conv) {
767
+ throw new NotFoundException(`Conversation ${conversationId} not found.`);
768
+ }
769
+ if (conv.user_id !== userId) {
770
+ throw new ForbiddenException('Access denied to this conversation.');
771
+ }
772
+ }
773
+
774
+ private async getProviderKey(): Promise<{ provider: 'openai' | 'gemini'; apiKey: string }> {
775
+ const settings = await this.setting.getSettingValues([
776
+ 'mcp-ai-provider',
777
+ 'ai-openai-api-key',
778
+ 'ai-gemini-api-key',
779
+ ]);
780
+ const provider = ((settings['mcp-ai-provider'] as string) || 'openai') as 'openai' | 'gemini';
781
+
782
+ if (provider === 'gemini') {
783
+ const key = settings['ai-gemini-api-key'] as string;
784
+ if (!key) {
785
+ throw new BadRequestException(
786
+ 'Gemini API key is not configured. Please set it in Settings → AI → Gemini API Key.',
787
+ );
788
+ }
789
+ return { provider: 'gemini', apiKey: key };
790
+ }
791
+
792
+ const key = settings['ai-openai-api-key'] as string;
793
+ if (!key) {
794
+ throw new BadRequestException(
795
+ 'OpenAI API key is not configured. Please set it in Settings → AI → OpenAI API Key.',
796
+ );
797
+ }
798
+ return { provider: 'openai', apiKey: key };
799
+ }
800
+
801
+ private async buildOpenAiTools(
802
+ message: string,
803
+ conversationId: number,
804
+ userId: number,
805
+ history: ConversationHistoryItem[],
806
+ ): Promise<ToolSelectionResult<OpenAiToolDefinition>> {
807
+ return this.selectOpenAiTools(message, conversationId, userId, history);
808
+ }
809
+
810
+ private async buildGeminiTools(
811
+ message: string,
812
+ conversationId: number,
813
+ userId: number,
814
+ history: ConversationHistoryItem[],
815
+ ): Promise<ToolSelectionResult<GeminiFunctionDeclaration>> {
816
+ const selection = await this.selectOpenAiTools(
817
+ message,
818
+ conversationId,
819
+ userId,
820
+ history,
821
+ );
822
+
823
+ return {
824
+ clarification: selection.clarification,
825
+ tools: selection.tools.map((t) => t.function),
826
+ };
827
+ }
828
+
829
+ private async selectOpenAiTools(
830
+ message: string,
831
+ conversationId: number,
832
+ userId: number,
833
+ history: ConversationHistoryItem[],
834
+ ): Promise<ToolSelectionResult<OpenAiToolDefinition>> {
835
+ const allEntries = this.registry.getAll();
836
+ const allowedToolNames = await this.getAllowedMcpToolNames(userId);
837
+ const allowedEntries = allEntries.filter((entry) =>
838
+ allowedToolNames.has(entry.definition.name),
839
+ );
840
+
841
+ const dedupedBySanitized = new Map<string, ScoredTool>();
842
+ const excludedDuplicates: string[] = [];
843
+ const routerText = this.buildToolRouterText(message, history);
844
+ const normalizedMessage = this.normalizeText(routerText);
845
+ const messageTerms = this.getMessageKeywordSet(normalizedMessage);
846
+ const domainScores = this.scoreToolDomains(normalizedMessage, allowedEntries);
847
+ const candidateDomains = this.getCandidateDomains(domainScores);
848
+ const clarification = this.getToolSelectionClarification(
849
+ message,
850
+ normalizedMessage,
851
+ domainScores,
852
+ candidateDomains,
853
+ allowedEntries,
854
+ );
855
+
856
+ if (clarification) {
857
+ this.logger.log(
858
+ `MCP tools need clarification: conversationId=${conversationId} userId=${userId} ` +
859
+ `registered=${allEntries.length} authorized=${allowedEntries.length} ` +
860
+ `domains=${this.formatDomainScores(domainScores)} selected=0`,
861
+ );
862
+ return { tools: [], clarification };
863
+ }
864
+
865
+ const entriesForRanking =
866
+ candidateDomains.length > 0
867
+ ? allowedEntries.filter((entry) =>
868
+ candidateDomains.includes(this.getToolDomain(entry.definition.name)),
869
+ )
870
+ : allowedEntries;
871
+
872
+ for (const [index, entry] of entriesForRanking.entries()) {
873
+ const originalName = entry.definition.name;
874
+ const sanitizedName = sanitize(originalName);
875
+ const readOnly = Boolean(entry.definition.readOnly);
876
+ const domain = this.getToolDomain(originalName);
877
+ const matchScore = this.scoreToolMatch(
878
+ normalizedMessage,
879
+ messageTerms,
880
+ entry,
881
+ candidateDomains,
882
+ );
883
+
884
+ if (dedupedBySanitized.has(sanitizedName)) {
885
+ excludedDuplicates.push(originalName);
886
+ continue;
887
+ }
888
+
889
+ const scored: ScoredTool = {
890
+ name: originalName,
891
+ domain,
892
+ readOnly,
893
+ priorityBucket: this.getDomainPriorityBucket(originalName, candidateDomains),
894
+ keywordBoost: this.getKeywordBoost(normalizedMessage, originalName),
895
+ matchScore,
896
+ index,
897
+ tool: {
898
+ type: 'function',
899
+ function: {
900
+ name: sanitizedName,
901
+ description: entry.definition.description,
902
+ parameters: entry.definition.inputSchema ?? {
903
+ type: 'object',
904
+ properties: {},
905
+ },
906
+ },
907
+ },
908
+ };
909
+
910
+ dedupedBySanitized.set(sanitizedName, scored);
911
+ }
912
+
913
+ const scoredTools = Array.from(dedupedBySanitized.values());
914
+ scoredTools.sort((a, b) => {
915
+ if (a.matchScore !== b.matchScore) {
916
+ return b.matchScore - a.matchScore;
917
+ }
918
+ if (a.priorityBucket !== b.priorityBucket) {
919
+ return a.priorityBucket - b.priorityBucket;
920
+ }
921
+ if (a.keywordBoost !== b.keywordBoost) {
922
+ return b.keywordBoost - a.keywordBoost;
923
+ }
924
+ if (a.readOnly !== b.readOnly) {
925
+ return a.readOnly ? -1 : 1;
926
+ }
927
+ return a.index - b.index;
928
+ });
929
+
930
+ const selected = scoredTools.slice(0, TARGET_SELECTED_TOOLS);
931
+ const excludedByLimit = scoredTools
932
+ .slice(TARGET_SELECTED_TOOLS)
933
+ .map((tool) => tool.name);
934
+
935
+ if (excludedDuplicates.length > 0) {
936
+ this.logger.warn(
937
+ `MCP tools duplicate sanitize names ignored: ${excludedDuplicates.length}. ` +
938
+ `Examples: ${excludedDuplicates.slice(0, 10).join(', ')}`,
939
+ );
940
+ }
941
+
942
+ if (excludedByLimit.length > 0) {
943
+ this.logger.warn(
944
+ `MCP tools truncated for provider budget (${TARGET_SELECTED_TOOLS}/${MAX_OPENAI_TOOLS}). ` +
945
+ `conversationId=${conversationId} userId=${userId} ` +
946
+ `available=${scoredTools.length} selected=${selected.length} excluded=${excludedByLimit.length}. ` +
947
+ `Top excluded: ${excludedByLimit.slice(0, 12).join(', ')}`,
948
+ );
949
+ }
950
+
951
+ this.logger.log(
952
+ `MCP tools prepared: conversationId=${conversationId} userId=${userId} ` +
953
+ `registered=${allEntries.length} authorized=${allowedEntries.length} ` +
954
+ `domains=${this.formatDomainScores(domainScores)} ranked=${scoredTools.length} ` +
955
+ `selected=${selected.length}`,
956
+ );
957
+
958
+ return { tools: selected.map((entry) => entry.tool) };
959
+ }
960
+
961
+ private async getAllowedMcpToolNames(userId: number): Promise<Set<string>> {
962
+ const routes = await this.prisma.route.findMany({
963
+ where: {
964
+ type: 'MCP' as any,
965
+ tool_name: { not: null },
966
+ role_route: {
967
+ some: {
968
+ role: {
969
+ role_user: {
970
+ some: { user_id: userId },
971
+ },
972
+ },
973
+ },
974
+ },
975
+ },
976
+ select: { tool_name: true },
977
+ });
978
+
979
+ return new Set(
980
+ routes
981
+ .map((route) => route.tool_name)
982
+ .filter((toolName): toolName is string => typeof toolName === 'string'),
983
+ );
984
+ }
985
+
986
+ private getDomainPriorityBucket(toolName: string, candidateDomains: string[]): number {
987
+ const domain = this.getToolDomain(toolName);
988
+ const candidateIndex = candidateDomains.indexOf(domain);
989
+ if (candidateIndex >= 0) {
990
+ return candidateIndex;
991
+ }
992
+ if (toolName.startsWith('operations.')) return 10;
993
+ if (toolName.startsWith('finance.')) return 11;
994
+ if (toolName.startsWith('lms.')) return 12;
995
+ if (toolName.startsWith('core.')) return 13;
996
+ return 99;
997
+ }
998
+
999
+ private getKeywordBoost(message: string, toolName: string): number {
1000
+ const terms = this.getMessageKeywordSet(message);
1001
+ if (terms.size === 0) {
1002
+ return 0;
1003
+ }
1004
+
1005
+ let boost = 0;
1006
+ if (
1007
+ terms.has('task') &&
1008
+ (toolName.includes('.task') || toolName.includes('.tasks.'))
1009
+ ) {
1010
+ boost += 4;
1011
+ }
1012
+ if (
1013
+ terms.has('project') &&
1014
+ (toolName.includes('.project') || toolName.includes('.projects.'))
1015
+ ) {
1016
+ boost += 3;
1017
+ }
1018
+ if (
1019
+ terms.has('class') &&
1020
+ (toolName.includes('.class') ||
1021
+ toolName.includes('.course') ||
1022
+ toolName.includes('.training'))
1023
+ ) {
1024
+ boost += 3;
1025
+ }
1026
+ if (
1027
+ terms.has('pending') &&
1028
+ (toolName.includes('.task') ||
1029
+ toolName.includes('.approval') ||
1030
+ toolName.includes('.time-off'))
1031
+ ) {
1032
+ boost += 2;
1033
+ }
1034
+
1035
+ return boost;
1036
+ }
1037
+
1038
+ private buildToolRouterText(
1039
+ message: string,
1040
+ history: ConversationHistoryItem[],
1041
+ ): string {
1042
+ const recentContext = history
1043
+ .slice(-8)
1044
+ .map((item) =>
1045
+ [item.role === 'user' ? item.content : undefined, item.tool_name]
1046
+ .filter(Boolean)
1047
+ .join(' '),
1048
+ )
1049
+ .join(' ');
1050
+
1051
+ return `${recentContext} ${message}`;
1052
+ }
1053
+
1054
+ private normalizeText(value: string): string {
1055
+ return value
1056
+ .toLowerCase()
1057
+ .normalize('NFD')
1058
+ .replace(/[\u0300-\u036f]/g, '')
1059
+ .replace(/[^a-z0-9]+/g, ' ')
1060
+ .replace(/\s+/g, ' ')
1061
+ .trim();
1062
+ }
1063
+
1064
+ private getToolDomain(toolName: string): string {
1065
+ return toolName.split('.')[0] || 'unknown';
1066
+ }
1067
+
1068
+ private scoreToolDomains(
1069
+ normalizedMessage: string,
1070
+ entries: McpToolEntry[],
1071
+ ): Map<string, number> {
1072
+ const scores = new Map<string, number>();
1073
+
1074
+ for (const [domain, keywords] of Object.entries(DOMAIN_KEYWORDS)) {
1075
+ let score = 0;
1076
+ if (this.hasTerm(normalizedMessage, domain)) {
1077
+ score += 3;
1078
+ }
1079
+ for (const keyword of keywords) {
1080
+ const normalizedKeyword = this.normalizeText(keyword);
1081
+ if (normalizedKeyword && normalizedMessage.includes(normalizedKeyword)) {
1082
+ score += normalizedKeyword.includes(' ') ? 3 : 2;
1083
+ }
1084
+ }
1085
+ if (score > 0 && entries.some((entry) => this.getToolDomain(entry.definition.name) === domain)) {
1086
+ scores.set(domain, score);
1087
+ }
1088
+ }
1089
+
1090
+ for (const entry of entries) {
1091
+ const domain = this.getToolDomain(entry.definition.name);
1092
+ const toolText = this.normalizeText(
1093
+ `${entry.definition.name} ${entry.definition.description ?? ''}`,
1094
+ );
1095
+ let score = scores.get(domain) ?? 0;
1096
+ for (const term of normalizedMessage.split(' ')) {
1097
+ if (term.length >= 4 && toolText.includes(term)) {
1098
+ score += 1;
1099
+ }
1100
+ }
1101
+ if (score > 0) {
1102
+ scores.set(domain, score);
1103
+ }
1104
+ }
1105
+
1106
+ return scores;
1107
+ }
1108
+
1109
+ private getCandidateDomains(domainScores: Map<string, number>): string[] {
1110
+ const ranked = Array.from(domainScores.entries())
1111
+ .filter(([, score]) => score >= MIN_DOMAIN_CONFIDENCE)
1112
+ .sort((a, b) => b[1] - a[1]);
1113
+
1114
+ if (ranked.length === 0) {
1115
+ return [];
1116
+ }
1117
+
1118
+ const topScore = ranked[0][1];
1119
+ return ranked
1120
+ .filter(([, score]) => topScore - score <= AMBIGUOUS_DOMAIN_DELTA)
1121
+ .map(([domain]) => domain);
1122
+ }
1123
+
1124
+ private getToolSelectionClarification(
1125
+ originalMessage: string,
1126
+ normalizedMessage: string,
1127
+ domainScores: Map<string, number>,
1128
+ candidateDomains: string[],
1129
+ entries: McpToolEntry[],
1130
+ ): string | undefined {
1131
+ if (!this.messageLooksLikeToolRequest(normalizedMessage)) {
1132
+ return undefined;
1133
+ }
1134
+
1135
+ const rankedDomains = Array.from(domainScores.entries()).sort((a, b) => b[1] - a[1]);
1136
+ if (rankedDomains.length === 0 || rankedDomains[0][1] < MIN_DOMAIN_CONFIDENCE) {
1137
+ return this.isLikelyPortuguese(originalMessage)
1138
+ ? 'Você quer consultar qual área do sistema? Por exemplo: financeiro, operações, LMS ou configurações.'
1139
+ : 'Which system area do you want to query? For example: finance, operations, LMS, or settings.';
1140
+ }
1141
+
1142
+ if (candidateDomains.length > 1) {
1143
+ const options = candidateDomains
1144
+ .slice(0, 3)
1145
+ .map((domain) => this.getDomainLabel(domain, originalMessage))
1146
+ .join(', ');
1147
+ return this.isLikelyPortuguese(originalMessage)
1148
+ ? `Você quer consultar qual área: ${options}?`
1149
+ : `Which area do you want to query: ${options}?`;
1150
+ }
1151
+
1152
+ if (
1153
+ candidateDomains[0] === 'finance' &&
1154
+ this.isAmbiguousFinanceAccountsRequest(normalizedMessage, entries)
1155
+ ) {
1156
+ return this.isLikelyPortuguese(originalMessage)
1157
+ ? 'Você quer consultar contas bancárias, contas a pagar ou contas a receber?'
1158
+ : 'Do you want bank accounts, accounts payable, or accounts receivable?';
1159
+ }
1160
+
1161
+ return undefined;
1162
+ }
1163
+
1164
+ private scoreToolMatch(
1165
+ normalizedMessage: string,
1166
+ terms: Set<string>,
1167
+ entry: McpToolEntry,
1168
+ candidateDomains: string[],
1169
+ ): number {
1170
+ const toolName = entry.definition.name;
1171
+ const toolText = this.normalizeText(
1172
+ `${toolName} ${entry.definition.description ?? ''} ${this.getSchemaText(entry.definition.inputSchema)}`,
1173
+ );
1174
+ const domain = this.getToolDomain(toolName);
1175
+ let score = candidateDomains.includes(domain) ? 8 : 0;
1176
+
1177
+ for (const term of normalizedMessage.split(' ')) {
1178
+ if (term.length < 3) continue;
1179
+ if (toolText.includes(term)) {
1180
+ score += term.length >= 5 ? 2 : 1;
1181
+ }
1182
+ }
1183
+
1184
+ if (terms.has('list') && entry.definition.readOnly) score += 4;
1185
+ if (terms.has('write') && !entry.definition.readOnly) score += 4;
1186
+
1187
+ score += this.getFinanceSpecificBoost(normalizedMessage, toolName);
1188
+ score += this.getKeywordBoost(normalizedMessage, toolName);
1189
+
1190
+ return score;
1191
+ }
1192
+
1193
+ private getSchemaText(schema: Record<string, any> | undefined): string {
1194
+ if (!schema || typeof schema !== 'object') {
1195
+ return '';
1196
+ }
1197
+ const properties = schema.properties;
1198
+ if (!properties || typeof properties !== 'object') {
1199
+ return '';
1200
+ }
1201
+ return Object.keys(properties).join(' ');
1202
+ }
1203
+
1204
+ private getFinanceSpecificBoost(message: string, toolName: string): number {
1205
+ if (!toolName.startsWith('finance.')) {
1206
+ return 0;
1207
+ }
1208
+
1209
+ let boost = 0;
1210
+ if (
1211
+ /banco|bancaria|bancarias|bank/.test(message) &&
1212
+ toolName.includes('bank-accounts')
1213
+ ) {
1214
+ boost += 10;
1215
+ }
1216
+ if (/extrato|statement/.test(message) && toolName.includes('statements')) {
1217
+ boost += 10;
1218
+ }
1219
+ if (/pagar|payable/.test(message) && toolName.includes('accounts-payable')) {
1220
+ boost += 10;
1221
+ }
1222
+ if (/receber|receivable/.test(message) && toolName.includes('accounts-receivable')) {
1223
+ boost += 10;
1224
+ }
1225
+ if (/moeda|currency/.test(message) && toolName.includes('currencies')) {
1226
+ boost += 8;
1227
+ }
1228
+ if (/transferencia|transfer|transferencias/.test(message) && toolName.includes('transfers')) {
1229
+ boost += 8;
1230
+ }
1231
+ return boost;
1232
+ }
1233
+
1234
+ private messageLooksLikeToolRequest(normalizedMessage: string): boolean {
1235
+ return ACTION_KEYWORDS.some((keyword) =>
1236
+ this.hasTerm(normalizedMessage, this.normalizeText(keyword)),
1237
+ );
1238
+ }
1239
+
1240
+ private isAmbiguousFinanceAccountsRequest(
1241
+ normalizedMessage: string,
1242
+ entries: McpToolEntry[],
1243
+ ): boolean {
1244
+ const mentionsGenericAccounts = /\b(conta|contas|account|accounts)\b/.test(normalizedMessage);
1245
+ const hasQualifier =
1246
+ /banco|bancaria|bancarias|bank|extrato|statement|pagar|payable|receber|receivable/.test(
1247
+ normalizedMessage,
1248
+ );
1249
+ if (!mentionsGenericAccounts || hasQualifier) {
1250
+ return false;
1251
+ }
1252
+
1253
+ const financeToolNames = entries
1254
+ .map((entry) => entry.definition.name)
1255
+ .filter((name) => name.startsWith('finance.'));
1256
+
1257
+ return (
1258
+ financeToolNames.some((name) => name.includes('bank-accounts')) &&
1259
+ (financeToolNames.some((name) => name.includes('accounts-payable')) ||
1260
+ financeToolNames.some((name) => name.includes('accounts-receivable')))
1261
+ );
1262
+ }
1263
+
1264
+ private hasTerm(text: string, term: string): boolean {
1265
+ return new RegExp(`(^|\\s)${this.escapeRegex(term)}($|\\s)`).test(text);
1266
+ }
1267
+
1268
+ private escapeRegex(value: string): string {
1269
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1270
+ }
1271
+
1272
+ private getDomainLabel(domain: string, message: string): string {
1273
+ const labels: Record<string, { pt: string; en: string }> = {
1274
+ finance: { pt: 'financeiro', en: 'finance' },
1275
+ operations: { pt: 'operações', en: 'operations' },
1276
+ lms: { pt: 'LMS/cursos', en: 'LMS/courses' },
1277
+ core: { pt: 'configurações/core', en: 'settings/core' },
1278
+ contact: { pt: 'contatos', en: 'contacts' },
1279
+ };
1280
+ const label = labels[domain];
1281
+ if (!label) {
1282
+ return domain;
1283
+ }
1284
+ return this.isLikelyPortuguese(message) ? label.pt : label.en;
1285
+ }
1286
+
1287
+ private isLikelyPortuguese(message: string): boolean {
1288
+ return /(voce|você|qual|consultar|listar|conta|contas|bancaria|bancária|extrato|tarefas|curso|cursos)/i.test(
1289
+ message,
1290
+ );
1291
+ }
1292
+
1293
+ private formatDomainScores(domainScores: Map<string, number>): string {
1294
+ const formatted = Array.from(domainScores.entries())
1295
+ .sort((a, b) => b[1] - a[1])
1296
+ .slice(0, 5)
1297
+ .map(([domain, score]) => `${domain}:${score}`)
1298
+ .join(',');
1299
+
1300
+ return formatted || 'none';
1301
+ }
1302
+
1303
+ private async recordAssistantAndReturnHistory(
1304
+ conversationId: number,
1305
+ content: string,
1306
+ ) {
1307
+ await this.prisma.mcp_conversation_message.create({
1308
+ data: {
1309
+ conversation_id: conversationId,
1310
+ role: 'assistant',
1311
+ content,
1312
+ },
1313
+ });
1314
+
1315
+ return this.prisma.mcp_conversation_message.findMany({
1316
+ where: { conversation_id: conversationId },
1317
+ orderBy: { created_at: 'asc' },
1318
+ });
1319
+ }
1320
+
1321
+ private getMessageKeywordSet(message: string): Set<string> {
1322
+ const keywords = new Set<string>();
1323
+ if (/(tarefa|tarefas|task|tasks)/i.test(message)) {
1324
+ keywords.add('task');
1325
+ }
1326
+ if (/(projeto|projetos|project|projects)/i.test(message)) {
1327
+ keywords.add('project');
1328
+ }
1329
+ if (/(turma|class|curso|cursos|course|training)/i.test(message)) {
1330
+ keywords.add('class');
1331
+ }
1332
+ if (/(pendente|pendentes|pending|open|aberta|abertas)/i.test(message)) {
1333
+ keywords.add('pending');
1334
+ }
1335
+ if (/(listar|lista|buscar|procurar|consultar|mostrar|ver|list|search|find|show|get)/i.test(message)) {
1336
+ keywords.add('list');
1337
+ }
1338
+ if (/(criar|adicionar|atualizar|alterar|remover|deletar|excluir|create|add|update|delete|remove)/i.test(message)) {
1339
+ keywords.add('write');
1340
+ }
1341
+ return keywords;
1342
+ }
1343
+ }