illuma-agents 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/LICENSE +25 -21
  2. package/dist/cjs/agents/AgentContext.cjs +222 -0
  3. package/dist/cjs/agents/AgentContext.cjs.map +1 -0
  4. package/dist/cjs/common/enum.cjs +5 -4
  5. package/dist/cjs/common/enum.cjs.map +1 -1
  6. package/dist/cjs/events.cjs +7 -5
  7. package/dist/cjs/events.cjs.map +1 -1
  8. package/dist/cjs/graphs/Graph.cjs +328 -207
  9. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  10. package/dist/cjs/graphs/MultiAgentGraph.cjs +507 -0
  11. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -0
  12. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  13. package/dist/cjs/llm/google/index.cjs.map +1 -1
  14. package/dist/cjs/llm/ollama/index.cjs.map +1 -1
  15. package/dist/cjs/llm/openai/index.cjs +35 -0
  16. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  17. package/dist/cjs/llm/openai/utils/index.cjs +3 -1
  18. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  19. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  20. package/dist/cjs/llm/providers.cjs +0 -2
  21. package/dist/cjs/llm/providers.cjs.map +1 -1
  22. package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
  23. package/dist/cjs/main.cjs +12 -1
  24. package/dist/cjs/main.cjs.map +1 -1
  25. package/dist/cjs/messages/cache.cjs +123 -0
  26. package/dist/cjs/messages/cache.cjs.map +1 -0
  27. package/dist/cjs/messages/content.cjs +53 -0
  28. package/dist/cjs/messages/content.cjs.map +1 -0
  29. package/dist/cjs/messages/format.cjs +17 -29
  30. package/dist/cjs/messages/format.cjs.map +1 -1
  31. package/dist/cjs/run.cjs +119 -74
  32. package/dist/cjs/run.cjs.map +1 -1
  33. package/dist/cjs/stream.cjs +77 -73
  34. package/dist/cjs/stream.cjs.map +1 -1
  35. package/dist/cjs/tools/Calculator.cjs +45 -0
  36. package/dist/cjs/tools/Calculator.cjs.map +1 -0
  37. package/dist/cjs/tools/CodeExecutor.cjs +22 -22
  38. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  39. package/dist/cjs/tools/ToolNode.cjs +5 -3
  40. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  41. package/dist/cjs/tools/handlers.cjs +20 -20
  42. package/dist/cjs/tools/handlers.cjs.map +1 -1
  43. package/dist/cjs/utils/events.cjs +31 -0
  44. package/dist/cjs/utils/events.cjs.map +1 -0
  45. package/dist/cjs/utils/handlers.cjs +70 -0
  46. package/dist/cjs/utils/handlers.cjs.map +1 -0
  47. package/dist/cjs/utils/tokens.cjs +54 -7
  48. package/dist/cjs/utils/tokens.cjs.map +1 -1
  49. package/dist/esm/agents/AgentContext.mjs +220 -0
  50. package/dist/esm/agents/AgentContext.mjs.map +1 -0
  51. package/dist/esm/common/enum.mjs +5 -4
  52. package/dist/esm/common/enum.mjs.map +1 -1
  53. package/dist/esm/events.mjs +7 -5
  54. package/dist/esm/events.mjs.map +1 -1
  55. package/dist/esm/graphs/Graph.mjs +330 -209
  56. package/dist/esm/graphs/Graph.mjs.map +1 -1
  57. package/dist/esm/graphs/MultiAgentGraph.mjs +505 -0
  58. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -0
  59. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  60. package/dist/esm/llm/google/index.mjs.map +1 -1
  61. package/dist/esm/llm/ollama/index.mjs.map +1 -1
  62. package/dist/esm/llm/openai/index.mjs +35 -0
  63. package/dist/esm/llm/openai/index.mjs.map +1 -1
  64. package/dist/esm/llm/openai/utils/index.mjs +3 -1
  65. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  66. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  67. package/dist/esm/llm/providers.mjs +0 -2
  68. package/dist/esm/llm/providers.mjs.map +1 -1
  69. package/dist/esm/llm/vertexai/index.mjs.map +1 -1
  70. package/dist/esm/main.mjs +7 -2
  71. package/dist/esm/main.mjs.map +1 -1
  72. package/dist/esm/messages/cache.mjs +120 -0
  73. package/dist/esm/messages/cache.mjs.map +1 -0
  74. package/dist/esm/messages/content.mjs +51 -0
  75. package/dist/esm/messages/content.mjs.map +1 -0
  76. package/dist/esm/messages/format.mjs +18 -29
  77. package/dist/esm/messages/format.mjs.map +1 -1
  78. package/dist/esm/run.mjs +119 -74
  79. package/dist/esm/run.mjs.map +1 -1
  80. package/dist/esm/stream.mjs +77 -73
  81. package/dist/esm/stream.mjs.map +1 -1
  82. package/dist/esm/tools/Calculator.mjs +24 -0
  83. package/dist/esm/tools/Calculator.mjs.map +1 -0
  84. package/dist/esm/tools/CodeExecutor.mjs +22 -22
  85. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  86. package/dist/esm/tools/ToolNode.mjs +5 -3
  87. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  88. package/dist/esm/tools/handlers.mjs +20 -20
  89. package/dist/esm/tools/handlers.mjs.map +1 -1
  90. package/dist/esm/utils/events.mjs +29 -0
  91. package/dist/esm/utils/events.mjs.map +1 -0
  92. package/dist/esm/utils/handlers.mjs +68 -0
  93. package/dist/esm/utils/handlers.mjs.map +1 -0
  94. package/dist/esm/utils/tokens.mjs +54 -8
  95. package/dist/esm/utils/tokens.mjs.map +1 -1
  96. package/dist/types/agents/AgentContext.d.ts +94 -0
  97. package/dist/types/common/enum.d.ts +7 -5
  98. package/dist/types/events.d.ts +3 -3
  99. package/dist/types/graphs/Graph.d.ts +60 -66
  100. package/dist/types/graphs/MultiAgentGraph.d.ts +47 -0
  101. package/dist/types/graphs/index.d.ts +1 -0
  102. package/dist/types/index.d.ts +1 -0
  103. package/dist/types/llm/openai/index.d.ts +10 -0
  104. package/dist/types/messages/cache.d.ts +20 -0
  105. package/dist/types/messages/content.d.ts +7 -0
  106. package/dist/types/messages/format.d.ts +1 -7
  107. package/dist/types/messages/index.d.ts +2 -0
  108. package/dist/types/messages/reducer.d.ts +9 -0
  109. package/dist/types/run.d.ts +16 -10
  110. package/dist/types/stream.d.ts +4 -3
  111. package/dist/types/tools/Calculator.d.ts +8 -0
  112. package/dist/types/tools/ToolNode.d.ts +1 -1
  113. package/dist/types/tools/handlers.d.ts +9 -7
  114. package/dist/types/tools/search/tool.d.ts +4 -4
  115. package/dist/types/types/graph.d.ts +124 -11
  116. package/dist/types/types/llm.d.ts +13 -9
  117. package/dist/types/types/messages.d.ts +4 -0
  118. package/dist/types/types/run.d.ts +46 -8
  119. package/dist/types/types/stream.d.ts +3 -2
  120. package/dist/types/utils/events.d.ts +6 -0
  121. package/dist/types/utils/handlers.d.ts +34 -0
  122. package/dist/types/utils/index.d.ts +1 -0
  123. package/dist/types/utils/tokens.d.ts +24 -0
  124. package/package.json +162 -145
  125. package/src/agents/AgentContext.ts +323 -0
  126. package/src/common/enum.ts +177 -176
  127. package/src/events.ts +197 -191
  128. package/src/graphs/Graph.ts +1058 -846
  129. package/src/graphs/MultiAgentGraph.ts +598 -0
  130. package/src/graphs/index.ts +2 -1
  131. package/src/index.ts +25 -24
  132. package/src/llm/anthropic/index.ts +413 -413
  133. package/src/llm/google/index.ts +222 -222
  134. package/src/llm/google/utils/zod_to_genai_parameters.ts +86 -88
  135. package/src/llm/ollama/index.ts +92 -92
  136. package/src/llm/openai/index.ts +894 -853
  137. package/src/llm/openai/utils/index.ts +920 -918
  138. package/src/llm/openrouter/index.ts +60 -60
  139. package/src/llm/providers.ts +55 -57
  140. package/src/llm/vertexai/index.ts +360 -360
  141. package/src/messages/cache.test.ts +461 -0
  142. package/src/messages/cache.ts +151 -0
  143. package/src/messages/content.test.ts +362 -0
  144. package/src/messages/content.ts +63 -0
  145. package/src/messages/format.ts +611 -625
  146. package/src/messages/formatAgentMessages.test.ts +1144 -917
  147. package/src/messages/index.ts +6 -4
  148. package/src/messages/reducer.ts +80 -0
  149. package/src/run.ts +447 -381
  150. package/src/scripts/abort.ts +157 -138
  151. package/src/scripts/ant_web_search.ts +158 -158
  152. package/src/scripts/cli.ts +172 -167
  153. package/src/scripts/cli2.ts +133 -125
  154. package/src/scripts/cli3.ts +184 -178
  155. package/src/scripts/cli4.ts +191 -184
  156. package/src/scripts/cli5.ts +191 -184
  157. package/src/scripts/code_exec.ts +213 -214
  158. package/src/scripts/code_exec_simple.ts +147 -129
  159. package/src/scripts/content.ts +138 -120
  160. package/src/scripts/handoff-test.ts +135 -0
  161. package/src/scripts/multi-agent-chain.ts +278 -0
  162. package/src/scripts/multi-agent-conditional.ts +220 -0
  163. package/src/scripts/multi-agent-document-review-chain.ts +197 -0
  164. package/src/scripts/multi-agent-hybrid-flow.ts +310 -0
  165. package/src/scripts/multi-agent-parallel.ts +343 -0
  166. package/src/scripts/multi-agent-sequence.ts +212 -0
  167. package/src/scripts/multi-agent-supervisor.ts +364 -0
  168. package/src/scripts/multi-agent-test.ts +186 -0
  169. package/src/scripts/search.ts +146 -150
  170. package/src/scripts/simple.ts +225 -225
  171. package/src/scripts/stream.ts +140 -122
  172. package/src/scripts/test-custom-prompt-key.ts +145 -0
  173. package/src/scripts/test-handoff-input.ts +170 -0
  174. package/src/scripts/test-multi-agent-list-handoff.ts +261 -0
  175. package/src/scripts/test-tools-before-handoff.ts +222 -0
  176. package/src/scripts/tools.ts +153 -155
  177. package/src/specs/agent-handoffs.test.ts +889 -0
  178. package/src/specs/anthropic.simple.test.ts +320 -317
  179. package/src/specs/azure.simple.test.ts +325 -316
  180. package/src/specs/openai.simple.test.ts +311 -316
  181. package/src/specs/openrouter.simple.test.ts +107 -0
  182. package/src/specs/prune.test.ts +758 -763
  183. package/src/specs/reasoning.test.ts +201 -165
  184. package/src/specs/thinking-prune.test.ts +769 -703
  185. package/src/specs/token-memoization.test.ts +39 -0
  186. package/src/stream.ts +664 -651
  187. package/src/tools/Calculator.test.ts +278 -0
  188. package/src/tools/Calculator.ts +25 -0
  189. package/src/tools/CodeExecutor.ts +220 -220
  190. package/src/tools/ToolNode.ts +170 -170
  191. package/src/tools/handlers.ts +341 -336
  192. package/src/types/graph.ts +372 -185
  193. package/src/types/llm.ts +141 -140
  194. package/src/types/messages.ts +4 -0
  195. package/src/types/run.ts +128 -89
  196. package/src/types/stream.ts +401 -400
  197. package/src/utils/events.ts +32 -0
  198. package/src/utils/handlers.ts +107 -0
  199. package/src/utils/index.ts +6 -5
  200. package/src/utils/llmConfig.ts +183 -183
  201. package/src/utils/tokens.ts +129 -70
  202. package/dist/types/scripts/abort.d.ts +0 -1
  203. package/dist/types/scripts/ant_web_search.d.ts +0 -1
  204. package/dist/types/scripts/args.d.ts +0 -7
  205. package/dist/types/scripts/caching.d.ts +0 -1
  206. package/dist/types/scripts/cli.d.ts +0 -1
  207. package/dist/types/scripts/cli2.d.ts +0 -1
  208. package/dist/types/scripts/cli3.d.ts +0 -1
  209. package/dist/types/scripts/cli4.d.ts +0 -1
  210. package/dist/types/scripts/cli5.d.ts +0 -1
  211. package/dist/types/scripts/code_exec.d.ts +0 -1
  212. package/dist/types/scripts/code_exec_files.d.ts +0 -1
  213. package/dist/types/scripts/code_exec_simple.d.ts +0 -1
  214. package/dist/types/scripts/content.d.ts +0 -1
  215. package/dist/types/scripts/empty_input.d.ts +0 -1
  216. package/dist/types/scripts/image.d.ts +0 -1
  217. package/dist/types/scripts/memory.d.ts +0 -1
  218. package/dist/types/scripts/search.d.ts +0 -1
  219. package/dist/types/scripts/simple.d.ts +0 -1
  220. package/dist/types/scripts/stream.d.ts +0 -1
  221. package/dist/types/scripts/thinking.d.ts +0 -1
  222. package/dist/types/scripts/tools.d.ts +0 -1
  223. package/dist/types/specs/spec.utils.d.ts +0 -1
  224. package/dist/types/tools/example.d.ts +0 -78
  225. package/src/tools/example.ts +0 -129
@@ -1,625 +1,611 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import {
3
- AIMessage,
4
- ToolMessage,
5
- BaseMessage,
6
- HumanMessage,
7
- SystemMessage,
8
- getBufferString,
9
- } from '@langchain/core/messages';
10
- import type { MessageContentImageUrl } from '@langchain/core/messages';
11
- import type { ToolCall } from '@langchain/core/messages/tool';
12
- import type {
13
- MessageContentComplex,
14
- ToolCallPart,
15
- TPayload,
16
- TMessage,
17
- } from '@/types';
18
- import { Providers, ContentTypes } from '@/common';
19
-
20
- interface MediaMessageParams {
21
- message: {
22
- role: string;
23
- content: string;
24
- name?: string;
25
- [key: string]: any;
26
- };
27
- mediaParts: MessageContentComplex[];
28
- endpoint?: Providers;
29
- }
30
-
31
- /**
32
- * Formats a message with media content (images, documents, videos, audios) to API payload format.
33
- *
34
- * @param params - The parameters for formatting.
35
- * @returns - The formatted message.
36
- */
37
- export const formatMediaMessage = ({
38
- message,
39
- endpoint,
40
- mediaParts,
41
- }: MediaMessageParams): {
42
- role: string;
43
- content: MessageContentComplex[];
44
- name?: string;
45
- [key: string]: any;
46
- } => {
47
- // Create a new object to avoid mutating the input
48
- const result: {
49
- role: string;
50
- content: MessageContentComplex[];
51
- name?: string;
52
- [key: string]: any;
53
- } = {
54
- ...message,
55
- content: [] as MessageContentComplex[],
56
- };
57
-
58
- if (endpoint === Providers.ANTHROPIC) {
59
- result.content = [
60
- ...mediaParts,
61
- { type: ContentTypes.TEXT, text: message.content },
62
- ] as MessageContentComplex[];
63
- return result;
64
- }
65
-
66
- result.content = [
67
- { type: ContentTypes.TEXT, text: message.content },
68
- ...mediaParts,
69
- ] as MessageContentComplex[];
70
-
71
- return result;
72
- };
73
-
74
- interface MessageInput {
75
- role?: string;
76
- _name?: string;
77
- sender?: string;
78
- text?: string;
79
- content?: string | MessageContentComplex[];
80
- image_urls?: MessageContentImageUrl[];
81
- documents?: MessageContentComplex[];
82
- videos?: MessageContentComplex[];
83
- audios?: MessageContentComplex[];
84
- lc_id?: string[];
85
- [key: string]: any;
86
- }
87
-
88
- interface FormatMessageParams {
89
- message: MessageInput;
90
- userName?: string;
91
- assistantName?: string;
92
- endpoint?: Providers;
93
- langChain?: boolean;
94
- }
95
-
96
- interface FormattedMessage {
97
- role: string;
98
- content: string | MessageContentComplex[];
99
- name?: string;
100
- [key: string]: any;
101
- }
102
-
103
- /**
104
- * Formats a message to OpenAI payload format based on the provided options.
105
- *
106
- * @param params - The parameters for formatting.
107
- * @returns - The formatted message.
108
- */
109
- export const formatMessage = ({
110
- message,
111
- userName,
112
- endpoint,
113
- assistantName,
114
- langChain = false,
115
- }: FormatMessageParams):
116
- | FormattedMessage
117
- | HumanMessage
118
- | AIMessage
119
- | SystemMessage => {
120
- // eslint-disable-next-line prefer-const
121
- let { role: _role, _name, sender, text, content: _content, lc_id } = message;
122
- if (lc_id && lc_id[2] && !langChain) {
123
- const roleMapping: Record<string, string> = {
124
- SystemMessage: 'system',
125
- HumanMessage: 'user',
126
- AIMessage: 'assistant',
127
- };
128
- _role = roleMapping[lc_id[2]] || _role;
129
- }
130
- const role =
131
- _role ??
132
- (sender != null && sender && sender.toLowerCase() === 'user'
133
- ? 'user'
134
- : 'assistant');
135
- const content = _content ?? text ?? '';
136
- const formattedMessage: FormattedMessage = {
137
- role,
138
- content,
139
- };
140
-
141
- // Set name fields first
142
- if (_name != null && _name) {
143
- formattedMessage.name = _name;
144
- }
145
-
146
- if (userName != null && userName && formattedMessage.role === 'user') {
147
- formattedMessage.name = userName;
148
- }
149
-
150
- if (
151
- assistantName != null &&
152
- assistantName &&
153
- formattedMessage.role === 'assistant'
154
- ) {
155
- formattedMessage.name = assistantName;
156
- }
157
-
158
- if (formattedMessage.name != null && formattedMessage.name) {
159
- // Conform to API regex: ^[a-zA-Z0-9_-]{1,64}$
160
- // https://community.openai.com/t/the-format-of-the-name-field-in-the-documentation-is-incorrect/175684/2
161
- formattedMessage.name = formattedMessage.name.replace(
162
- /[^a-zA-Z0-9_-]/g,
163
- '_'
164
- );
165
-
166
- if (formattedMessage.name.length > 64) {
167
- formattedMessage.name = formattedMessage.name.substring(0, 64);
168
- }
169
- }
170
-
171
- const { image_urls, documents, videos, audios } = message;
172
- const mediaParts: MessageContentComplex[] = [];
173
-
174
- if (Array.isArray(documents) && documents.length > 0) {
175
- mediaParts.push(...documents);
176
- }
177
-
178
- if (Array.isArray(videos) && videos.length > 0) {
179
- mediaParts.push(...videos);
180
- }
181
-
182
- if (Array.isArray(audios) && audios.length > 0) {
183
- mediaParts.push(...audios);
184
- }
185
-
186
- if (Array.isArray(image_urls) && image_urls.length > 0) {
187
- mediaParts.push(...image_urls);
188
- }
189
-
190
- if (mediaParts.length > 0 && role === 'user') {
191
- const mediaMessage = formatMediaMessage({
192
- message: {
193
- ...formattedMessage,
194
- content:
195
- typeof formattedMessage.content === 'string'
196
- ? formattedMessage.content
197
- : '',
198
- },
199
- mediaParts,
200
- endpoint,
201
- });
202
-
203
- if (!langChain) {
204
- return mediaMessage;
205
- }
206
-
207
- return new HumanMessage(mediaMessage);
208
- }
209
-
210
- if (!langChain) {
211
- return formattedMessage;
212
- }
213
-
214
- if (role === 'user') {
215
- return new HumanMessage(formattedMessage);
216
- } else if (role === 'assistant') {
217
- return new AIMessage(formattedMessage);
218
- } else {
219
- return new SystemMessage(formattedMessage);
220
- }
221
- };
222
-
223
- /**
224
- * Formats an array of messages for LangChain.
225
- *
226
- * @param messages - The array of messages to format.
227
- * @param formatOptions - The options for formatting each message.
228
- * @returns - The array of formatted LangChain messages.
229
- */
230
- export const formatLangChainMessages = (
231
- messages: Array<MessageInput>,
232
- formatOptions: Omit<FormatMessageParams, 'message' | 'langChain'>
233
- ): Array<HumanMessage | AIMessage | SystemMessage> => {
234
- return messages.map((msg) => {
235
- const formatted = formatMessage({
236
- ...formatOptions,
237
- message: msg,
238
- langChain: true,
239
- });
240
- return formatted as HumanMessage | AIMessage | SystemMessage;
241
- });
242
- };
243
-
244
- interface LangChainMessage {
245
- lc_kwargs?: {
246
- additional_kwargs?: Record<string, any>;
247
- [key: string]: any;
248
- };
249
- kwargs?: {
250
- additional_kwargs?: Record<string, any>;
251
- [key: string]: any;
252
- };
253
- [key: string]: any;
254
- }
255
-
256
- /**
257
- * Formats a LangChain message object by merging properties from `lc_kwargs` or `kwargs` and `additional_kwargs`.
258
- *
259
- * @param message - The message object to format.
260
- * @returns - The formatted LangChain message.
261
- */
262
- export const formatFromLangChain = (
263
- message: LangChainMessage
264
- ): Record<string, any> => {
265
- const kwargs = message.lc_kwargs ?? message.kwargs ?? {};
266
- const { additional_kwargs = {}, ...message_kwargs } = kwargs;
267
- return {
268
- ...message_kwargs,
269
- ...additional_kwargs,
270
- };
271
- };
272
-
273
- /**
274
- * Helper function to format an assistant message
275
- * @param message The message to format
276
- * @returns Array of formatted messages
277
- */
278
- function formatAssistantMessage(
279
- message: Partial<TMessage>
280
- ): Array<AIMessage | ToolMessage> {
281
- const formattedMessages: Array<AIMessage | ToolMessage> = [];
282
- let currentContent: MessageContentComplex[] = [];
283
- let lastAIMessage: AIMessage | null = null;
284
- let hasReasoning = false;
285
-
286
- if (Array.isArray(message.content)) {
287
- for (const part of message.content) {
288
- if (part.type === ContentTypes.TEXT && part.tool_call_ids) {
289
- /*
290
- If there's pending content, it needs to be aggregated as a single string to prepare for tool calls.
291
- For Anthropic models, the "tool_calls" field on a message is only respected if content is a string.
292
- */
293
- if (currentContent.length > 0) {
294
- let content = currentContent.reduce((acc, curr) => {
295
- if (curr.type === ContentTypes.TEXT) {
296
- return `${acc}${curr[ContentTypes.TEXT] || ''}\n`;
297
- }
298
- return acc;
299
- }, '');
300
- content =
301
- `${content}\n${part[ContentTypes.TEXT] ?? part.text ?? ''}`.trim();
302
- lastAIMessage = new AIMessage({ content });
303
- formattedMessages.push(lastAIMessage);
304
- currentContent = [];
305
- continue;
306
- }
307
- // Create a new AIMessage with this text and prepare for tool calls
308
- lastAIMessage = new AIMessage({
309
- content: part.text || '',
310
- });
311
- formattedMessages.push(lastAIMessage);
312
- } else if (part.type === ContentTypes.TOOL_CALL) {
313
- if (!lastAIMessage) {
314
- // "Heal" the payload by creating an AIMessage to precede the tool call
315
- lastAIMessage = new AIMessage({ content: '' });
316
- formattedMessages.push(lastAIMessage);
317
- }
318
-
319
- // Note: `tool_calls` list is defined when constructed by `AIMessage` class, and outputs should be excluded from it
320
- const {
321
- output,
322
- args: _args,
323
- ..._tool_call
324
- } = part.tool_call as ToolCallPart;
325
- const tool_call: ToolCallPart = _tool_call;
326
- // TODO: investigate; args as dictionary may need to be providers-or-tool-specific
327
- let args: any = _args;
328
- try {
329
- if (typeof _args === 'string') {
330
- args = JSON.parse(_args);
331
- }
332
- } catch {
333
- if (typeof _args === 'string') {
334
- args = { input: _args };
335
- }
336
- }
337
-
338
- tool_call.args = args;
339
- if (!lastAIMessage.tool_calls) {
340
- lastAIMessage.tool_calls = [];
341
- }
342
- lastAIMessage.tool_calls.push(tool_call as ToolCall);
343
-
344
- formattedMessages.push(
345
- new ToolMessage({
346
- tool_call_id: tool_call.id ?? '',
347
- name: tool_call.name,
348
- content: output || '',
349
- })
350
- );
351
- } else if (part.type === ContentTypes.THINK) {
352
- hasReasoning = true;
353
- continue;
354
- } else if (
355
- part.type === ContentTypes.ERROR ||
356
- part.type === ContentTypes.AGENT_UPDATE
357
- ) {
358
- continue;
359
- } else {
360
- currentContent.push(part);
361
- }
362
- }
363
- }
364
-
365
- if (hasReasoning && currentContent.length > 0) {
366
- const content = currentContent
367
- .reduce((acc, curr) => {
368
- if (curr.type === ContentTypes.TEXT) {
369
- return `${acc}${curr[ContentTypes.TEXT] || ''}\n`;
370
- }
371
- return acc;
372
- }, '')
373
- .trim();
374
-
375
- if (content) {
376
- formattedMessages.push(new AIMessage({ content }));
377
- }
378
- } else if (currentContent.length > 0) {
379
- formattedMessages.push(new AIMessage({ content: currentContent }));
380
- }
381
-
382
- return formattedMessages;
383
- }
384
-
385
- /**
386
- * Formats an array of messages for LangChain, handling tool calls and creating ToolMessage instances.
387
- *
388
- * @param payload - The array of messages to format.
389
- * @param indexTokenCountMap - Optional map of message indices to token counts.
390
- * @param tools - Optional set of tool names that are allowed in the request.
391
- * @returns - Object containing formatted messages and updated indexTokenCountMap if provided.
392
- */
393
- export const formatAgentMessages = (
394
- payload: TPayload,
395
- indexTokenCountMap?: Record<number, number>,
396
- tools?: Set<string>
397
- ): {
398
- messages: Array<HumanMessage | AIMessage | SystemMessage | ToolMessage>;
399
- indexTokenCountMap?: Record<number, number>;
400
- } => {
401
- const messages: Array<
402
- HumanMessage | AIMessage | SystemMessage | ToolMessage
403
- > = [];
404
- // If indexTokenCountMap is provided, create a new map to track the updated indices
405
- const updatedIndexTokenCountMap: Record<number, number> = {};
406
- // Keep track of the mapping from original payload indices to result indices
407
- const indexMapping: Record<number, number[]> = {};
408
-
409
- // Process messages with tool conversion if tools set is provided
410
- for (let i = 0; i < payload.length; i++) {
411
- const message = payload[i];
412
- // Q: Store the current length of messages to track where this payload message starts in the result?
413
- // const startIndex = messages.length;
414
- if (typeof message.content === 'string') {
415
- message.content = [
416
- { type: ContentTypes.TEXT, [ContentTypes.TEXT]: message.content },
417
- ];
418
- }
419
- if (message.role !== 'assistant') {
420
- messages.push(
421
- formatMessage({
422
- message: message as MessageInput,
423
- langChain: true,
424
- }) as HumanMessage | AIMessage | SystemMessage
425
- );
426
-
427
- // Update the index mapping for this message
428
- indexMapping[i] = [messages.length - 1];
429
- continue;
430
- }
431
-
432
- // For assistant messages, track the starting index before processing
433
- const startMessageIndex = messages.length;
434
-
435
- // If tools set is provided, we need to check if we need to convert tool messages to a string
436
- if (tools) {
437
- // First, check if this message contains tool calls
438
- let hasToolCalls = false;
439
- let hasInvalidTool = false;
440
- const toolNames: string[] = [];
441
-
442
- const content = message.content;
443
- if (content && Array.isArray(content)) {
444
- for (const part of content) {
445
- if (part.type === ContentTypes.TOOL_CALL) {
446
- hasToolCalls = true;
447
- if (tools.size === 0) {
448
- hasInvalidTool = true;
449
- break;
450
- }
451
- const toolName = part.tool_call.name;
452
- toolNames.push(toolName);
453
- if (!tools.has(toolName)) {
454
- hasInvalidTool = true;
455
- }
456
- }
457
- }
458
- }
459
-
460
- // If this message has tool calls and at least one is invalid, we need to convert it
461
- if (hasToolCalls && hasInvalidTool) {
462
- // We need to collect all related messages (this message and any subsequent tool messages)
463
- const toolSequence: BaseMessage[] = [];
464
- let sequenceEndIndex = i;
465
-
466
- // Process the current assistant message to get the AIMessage with tool calls
467
- const formattedMessages = formatAssistantMessage(message);
468
- toolSequence.push(...formattedMessages);
469
-
470
- // Look ahead for any subsequent assistant messages that might be part of this tool sequence
471
- let j = i + 1;
472
- while (j < payload.length && payload[j].role === 'assistant') {
473
- // Check if this is a continuation of the tool sequence
474
- let isToolResponse = false;
475
- const content = payload[j].content;
476
- if (content && Array.isArray(content)) {
477
- for (const part of content) {
478
- if (part.type === ContentTypes.TOOL_CALL) {
479
- isToolResponse = true;
480
- break;
481
- }
482
- }
483
- }
484
-
485
- if (isToolResponse) {
486
- // This is part of the tool sequence, add it
487
- const nextMessages = formatAssistantMessage(payload[j]);
488
- toolSequence.push(...nextMessages);
489
- sequenceEndIndex = j;
490
- j++;
491
- } else {
492
- // This is not part of the tool sequence, stop looking
493
- break;
494
- }
495
- }
496
-
497
- // Convert the sequence to a string
498
- const bufferString = getBufferString(toolSequence);
499
- messages.push(new AIMessage({ content: bufferString }));
500
-
501
- // Skip the messages we've already processed
502
- i = sequenceEndIndex;
503
-
504
- // Update the index mapping for this sequence
505
- const resultIndices = [messages.length - 1];
506
- for (let k = i; k >= i && k <= sequenceEndIndex; k++) {
507
- indexMapping[k] = resultIndices;
508
- }
509
-
510
- continue;
511
- }
512
- }
513
-
514
- // Process the assistant message using the helper function
515
- const formattedMessages = formatAssistantMessage(message);
516
- messages.push(...formattedMessages);
517
-
518
- // Update the index mapping for this assistant message
519
- // Store all indices that were created from this original message
520
- const endMessageIndex = messages.length;
521
- const resultIndices = [];
522
- for (let j = startMessageIndex; j < endMessageIndex; j++) {
523
- resultIndices.push(j);
524
- }
525
- indexMapping[i] = resultIndices;
526
- }
527
-
528
- // Update the token count map if it was provided
529
- if (indexTokenCountMap) {
530
- for (
531
- let originalIndex = 0;
532
- originalIndex < payload.length;
533
- originalIndex++
534
- ) {
535
- const resultIndices = indexMapping[originalIndex] || [];
536
- const tokenCount = indexTokenCountMap[originalIndex];
537
-
538
- if (tokenCount !== undefined) {
539
- if (resultIndices.length === 1) {
540
- // Simple 1:1 mapping
541
- updatedIndexTokenCountMap[resultIndices[0]] = tokenCount;
542
- } else if (resultIndices.length > 1) {
543
- // If one message was split into multiple, distribute the token count
544
- // This is a simplification - in reality, you might want a more sophisticated distribution
545
- const countPerMessage = Math.floor(tokenCount / resultIndices.length);
546
- resultIndices.forEach((resultIndex, idx) => {
547
- if (idx === resultIndices.length - 1) {
548
- // Give any remainder to the last message
549
- updatedIndexTokenCountMap[resultIndex] =
550
- tokenCount - countPerMessage * (resultIndices.length - 1);
551
- } else {
552
- updatedIndexTokenCountMap[resultIndex] = countPerMessage;
553
- }
554
- });
555
- }
556
- }
557
- }
558
- }
559
-
560
- return {
561
- messages,
562
- indexTokenCountMap: indexTokenCountMap
563
- ? updatedIndexTokenCountMap
564
- : undefined,
565
- };
566
- };
567
-
568
- /**
569
- * Formats an array of messages for LangChain, making sure all content fields are strings
570
- * @param payload - The array of messages to format.
571
- * @returns - The array of formatted LangChain messages, including ToolMessages for tool calls.
572
- */
573
- export const formatContentStrings = (
574
- payload: Array<BaseMessage>
575
- ): Array<BaseMessage> => {
576
- // Create a copy of the payload to avoid modifying the original
577
- const result = [...payload];
578
-
579
- for (const message of result) {
580
- if (typeof message.content === 'string') {
581
- continue;
582
- }
583
-
584
- if (!Array.isArray(message.content)) {
585
- continue;
586
- }
587
-
588
- // Reduce text types to a single string, ignore all other types
589
- const content = message.content.reduce((acc, curr) => {
590
- if (curr.type === ContentTypes.TEXT) {
591
- return `${acc}${curr[ContentTypes.TEXT] || ''}\n`;
592
- }
593
- return acc;
594
- }, '');
595
-
596
- message.content = content.trim();
597
- }
598
-
599
- return result;
600
- };
601
-
602
- /**
603
- * Adds a value at key 0 for system messages and shifts all key indices by one in an indexTokenCountMap.
604
- * This is useful when adding a system message at the beginning of a conversation.
605
- *
606
- * @param indexTokenCountMap - The original map of message indices to token counts
607
- * @param instructionsTokenCount - The token count for the system message to add at index 0
608
- * @returns A new map with the system message at index 0 and all other indices shifted by 1
609
- */
610
- export function shiftIndexTokenCountMap(
611
- indexTokenCountMap: Record<number, number>,
612
- instructionsTokenCount: number
613
- ): Record<number, number> {
614
- // Create a new map to avoid modifying the original
615
- const shiftedMap: Record<number, number> = {};
616
- shiftedMap[0] = instructionsTokenCount;
617
-
618
- // Shift all existing indices by 1
619
- for (const [indexStr, tokenCount] of Object.entries(indexTokenCountMap)) {
620
- const index = Number(indexStr);
621
- shiftedMap[index + 1] = tokenCount;
622
- }
623
-
624
- return shiftedMap;
625
- }
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import {
3
+ AIMessage,
4
+ ToolMessage,
5
+ BaseMessage,
6
+ HumanMessage,
7
+ SystemMessage,
8
+ getBufferString,
9
+ } from '@langchain/core/messages';
10
+ import type { MessageContentImageUrl } from '@langchain/core/messages';
11
+ import type { ToolCall } from '@langchain/core/messages/tool';
12
+ import type {
13
+ MessageContentComplex,
14
+ ToolCallPart,
15
+ TPayload,
16
+ TMessage,
17
+ } from '@/types';
18
+ import { Providers, ContentTypes } from '@/common';
19
+
20
+ interface MediaMessageParams {
21
+ message: {
22
+ role: string;
23
+ content: string;
24
+ name?: string;
25
+ [key: string]: any;
26
+ };
27
+ mediaParts: MessageContentComplex[];
28
+ endpoint?: Providers;
29
+ }
30
+
31
+ /**
32
+ * Formats a message with media content (images, documents, videos, audios) to API payload format.
33
+ *
34
+ * @param params - The parameters for formatting.
35
+ * @returns - The formatted message.
36
+ */
37
+ export const formatMediaMessage = ({
38
+ message,
39
+ endpoint,
40
+ mediaParts,
41
+ }: MediaMessageParams): {
42
+ role: string;
43
+ content: MessageContentComplex[];
44
+ name?: string;
45
+ [key: string]: any;
46
+ } => {
47
+ // Create a new object to avoid mutating the input
48
+ const result: {
49
+ role: string;
50
+ content: MessageContentComplex[];
51
+ name?: string;
52
+ [key: string]: any;
53
+ } = {
54
+ ...message,
55
+ content: [] as MessageContentComplex[],
56
+ };
57
+
58
+ if (endpoint === Providers.ANTHROPIC) {
59
+ result.content = [
60
+ ...mediaParts,
61
+ { type: ContentTypes.TEXT, text: message.content },
62
+ ] as MessageContentComplex[];
63
+ return result;
64
+ }
65
+
66
+ result.content = [
67
+ { type: ContentTypes.TEXT, text: message.content },
68
+ ...mediaParts,
69
+ ] as MessageContentComplex[];
70
+
71
+ return result;
72
+ };
73
+
74
+ interface MessageInput {
75
+ role?: string;
76
+ _name?: string;
77
+ sender?: string;
78
+ text?: string;
79
+ content?: string | MessageContentComplex[];
80
+ image_urls?: MessageContentImageUrl[];
81
+ documents?: MessageContentComplex[];
82
+ videos?: MessageContentComplex[];
83
+ audios?: MessageContentComplex[];
84
+ lc_id?: string[];
85
+ [key: string]: any;
86
+ }
87
+
88
+ interface FormatMessageParams {
89
+ message: MessageInput;
90
+ userName?: string;
91
+ assistantName?: string;
92
+ endpoint?: Providers;
93
+ langChain?: boolean;
94
+ }
95
+
96
+ interface FormattedMessage {
97
+ role: string;
98
+ content: string | MessageContentComplex[];
99
+ name?: string;
100
+ [key: string]: any;
101
+ }
102
+
103
+ /**
104
+ * Formats a message to OpenAI payload format based on the provided options.
105
+ *
106
+ * @param params - The parameters for formatting.
107
+ * @returns - The formatted message.
108
+ */
109
+ export const formatMessage = ({
110
+ message,
111
+ userName,
112
+ endpoint,
113
+ assistantName,
114
+ langChain = false,
115
+ }: FormatMessageParams):
116
+ | FormattedMessage
117
+ | HumanMessage
118
+ | AIMessage
119
+ | SystemMessage => {
120
+ // eslint-disable-next-line prefer-const
121
+ let { role: _role, _name, sender, text, content: _content, lc_id } = message;
122
+ if (lc_id && lc_id[2] && !langChain) {
123
+ const roleMapping: Record<string, string> = {
124
+ SystemMessage: 'system',
125
+ HumanMessage: 'user',
126
+ AIMessage: 'assistant',
127
+ };
128
+ _role = roleMapping[lc_id[2]] || _role;
129
+ }
130
+ const role =
131
+ _role ??
132
+ (sender != null && sender && sender.toLowerCase() === 'user'
133
+ ? 'user'
134
+ : 'assistant');
135
+ const content = _content ?? text ?? '';
136
+ const formattedMessage: FormattedMessage = {
137
+ role,
138
+ content,
139
+ };
140
+
141
+ // Set name fields first
142
+ if (_name != null && _name) {
143
+ formattedMessage.name = _name;
144
+ }
145
+
146
+ if (userName != null && userName && formattedMessage.role === 'user') {
147
+ formattedMessage.name = userName;
148
+ }
149
+
150
+ if (
151
+ assistantName != null &&
152
+ assistantName &&
153
+ formattedMessage.role === 'assistant'
154
+ ) {
155
+ formattedMessage.name = assistantName;
156
+ }
157
+
158
+ if (formattedMessage.name != null && formattedMessage.name) {
159
+ // Conform to API regex: ^[a-zA-Z0-9_-]{1,64}$
160
+ // https://community.openai.com/t/the-format-of-the-name-field-in-the-documentation-is-incorrect/175684/2
161
+ formattedMessage.name = formattedMessage.name.replace(
162
+ /[^a-zA-Z0-9_-]/g,
163
+ '_'
164
+ );
165
+
166
+ if (formattedMessage.name.length > 64) {
167
+ formattedMessage.name = formattedMessage.name.substring(0, 64);
168
+ }
169
+ }
170
+
171
+ const { image_urls, documents, videos, audios } = message;
172
+ const mediaParts: MessageContentComplex[] = [];
173
+
174
+ if (Array.isArray(documents) && documents.length > 0) {
175
+ mediaParts.push(...documents);
176
+ }
177
+
178
+ if (Array.isArray(videos) && videos.length > 0) {
179
+ mediaParts.push(...videos);
180
+ }
181
+
182
+ if (Array.isArray(audios) && audios.length > 0) {
183
+ mediaParts.push(...audios);
184
+ }
185
+
186
+ if (Array.isArray(image_urls) && image_urls.length > 0) {
187
+ mediaParts.push(...image_urls);
188
+ }
189
+
190
+ if (mediaParts.length > 0 && role === 'user') {
191
+ const mediaMessage = formatMediaMessage({
192
+ message: {
193
+ ...formattedMessage,
194
+ content:
195
+ typeof formattedMessage.content === 'string'
196
+ ? formattedMessage.content
197
+ : '',
198
+ },
199
+ mediaParts,
200
+ endpoint,
201
+ });
202
+
203
+ if (!langChain) {
204
+ return mediaMessage;
205
+ }
206
+
207
+ return new HumanMessage(mediaMessage);
208
+ }
209
+
210
+ if (!langChain) {
211
+ return formattedMessage;
212
+ }
213
+
214
+ if (role === 'user') {
215
+ return new HumanMessage(formattedMessage);
216
+ } else if (role === 'assistant') {
217
+ return new AIMessage(formattedMessage);
218
+ } else {
219
+ return new SystemMessage(formattedMessage);
220
+ }
221
+ };
222
+
223
+ /**
224
+ * Formats an array of messages for LangChain.
225
+ *
226
+ * @param messages - The array of messages to format.
227
+ * @param formatOptions - The options for formatting each message.
228
+ * @returns - The array of formatted LangChain messages.
229
+ */
230
+ export const formatLangChainMessages = (
231
+ messages: Array<MessageInput>,
232
+ formatOptions: Omit<FormatMessageParams, 'message' | 'langChain'>
233
+ ): Array<HumanMessage | AIMessage | SystemMessage> => {
234
+ return messages.map((msg) => {
235
+ const formatted = formatMessage({
236
+ ...formatOptions,
237
+ message: msg,
238
+ langChain: true,
239
+ });
240
+ return formatted as HumanMessage | AIMessage | SystemMessage;
241
+ });
242
+ };
243
+
244
+ interface LangChainMessage {
245
+ lc_kwargs?: {
246
+ additional_kwargs?: Record<string, any>;
247
+ [key: string]: any;
248
+ };
249
+ kwargs?: {
250
+ additional_kwargs?: Record<string, any>;
251
+ [key: string]: any;
252
+ };
253
+ [key: string]: any;
254
+ }
255
+
256
+ /**
257
+ * Formats a LangChain message object by merging properties from `lc_kwargs` or `kwargs` and `additional_kwargs`.
258
+ *
259
+ * @param message - The message object to format.
260
+ * @returns - The formatted LangChain message.
261
+ */
262
+ export const formatFromLangChain = (
263
+ message: LangChainMessage
264
+ ): Record<string, any> => {
265
+ const kwargs = message.lc_kwargs ?? message.kwargs ?? {};
266
+ const { additional_kwargs = {}, ...message_kwargs } = kwargs;
267
+ return {
268
+ ...message_kwargs,
269
+ ...additional_kwargs,
270
+ };
271
+ };
272
+
273
+ /**
274
+ * Helper function to format an assistant message
275
+ * @param message The message to format
276
+ * @returns Array of formatted messages
277
+ */
278
+ function formatAssistantMessage(
279
+ message: Partial<TMessage>
280
+ ): Array<AIMessage | ToolMessage> {
281
+ const formattedMessages: Array<AIMessage | ToolMessage> = [];
282
+ let currentContent: MessageContentComplex[] = [];
283
+ let lastAIMessage: AIMessage | null = null;
284
+ let hasReasoning = false;
285
+
286
+ if (Array.isArray(message.content)) {
287
+ for (const part of message.content) {
288
+ if (part.type === ContentTypes.TEXT && part.tool_call_ids) {
289
+ /*
290
+ If there's pending content, it needs to be aggregated as a single string to prepare for tool calls.
291
+ For Anthropic models, the "tool_calls" field on a message is only respected if content is a string.
292
+ */
293
+ if (currentContent.length > 0) {
294
+ let content = currentContent.reduce((acc, curr) => {
295
+ if (curr.type === ContentTypes.TEXT) {
296
+ return `${acc}${curr[ContentTypes.TEXT] || ''}\n`;
297
+ }
298
+ return acc;
299
+ }, '');
300
+ content =
301
+ `${content}\n${part[ContentTypes.TEXT] ?? part.text ?? ''}`.trim();
302
+ lastAIMessage = new AIMessage({ content });
303
+ formattedMessages.push(lastAIMessage);
304
+ currentContent = [];
305
+ continue;
306
+ }
307
+ // Create a new AIMessage with this text and prepare for tool calls
308
+ lastAIMessage = new AIMessage({
309
+ content: part.text || '',
310
+ });
311
+ formattedMessages.push(lastAIMessage);
312
+ } else if (part.type === ContentTypes.TOOL_CALL) {
313
+ // Skip malformed tool call entries without tool_call property
314
+ if (!part.tool_call) {
315
+ continue;
316
+ }
317
+
318
+ // Note: `tool_calls` list is defined when constructed by `AIMessage` class, and outputs should be excluded from it
319
+ const {
320
+ output,
321
+ args: _args,
322
+ ..._tool_call
323
+ } = part.tool_call as ToolCallPart;
324
+
325
+ // Skip invalid tool calls that have no name AND no output
326
+ if (!_tool_call.name && (output == null || output === '')) {
327
+ continue;
328
+ }
329
+
330
+ if (!lastAIMessage) {
331
+ // "Heal" the payload by creating an AIMessage to precede the tool call
332
+ lastAIMessage = new AIMessage({ content: '' });
333
+ formattedMessages.push(lastAIMessage);
334
+ }
335
+
336
+ const tool_call: ToolCallPart = _tool_call;
337
+ // TODO: investigate; args as dictionary may need to be providers-or-tool-specific
338
+ let args: any = _args;
339
+ try {
340
+ if (typeof _args === 'string') {
341
+ args = JSON.parse(_args);
342
+ }
343
+ } catch {
344
+ if (typeof _args === 'string') {
345
+ args = { input: _args };
346
+ }
347
+ }
348
+
349
+ tool_call.args = args;
350
+ if (!lastAIMessage.tool_calls) {
351
+ lastAIMessage.tool_calls = [];
352
+ }
353
+ lastAIMessage.tool_calls.push(tool_call as ToolCall);
354
+
355
+ formattedMessages.push(
356
+ new ToolMessage({
357
+ tool_call_id: tool_call.id ?? '',
358
+ name: tool_call.name,
359
+ content: output || '',
360
+ })
361
+ );
362
+ } else if (part.type === ContentTypes.THINK) {
363
+ hasReasoning = true;
364
+ continue;
365
+ } else if (
366
+ part.type === ContentTypes.ERROR ||
367
+ part.type === ContentTypes.AGENT_UPDATE
368
+ ) {
369
+ continue;
370
+ } else {
371
+ currentContent.push(part);
372
+ }
373
+ }
374
+ }
375
+
376
+ if (hasReasoning && currentContent.length > 0) {
377
+ const content = currentContent
378
+ .reduce((acc, curr) => {
379
+ if (curr.type === ContentTypes.TEXT) {
380
+ return `${acc}${curr[ContentTypes.TEXT] || ''}\n`;
381
+ }
382
+ return acc;
383
+ }, '')
384
+ .trim();
385
+
386
+ if (content) {
387
+ formattedMessages.push(new AIMessage({ content }));
388
+ }
389
+ } else if (currentContent.length > 0) {
390
+ formattedMessages.push(new AIMessage({ content: currentContent }));
391
+ }
392
+
393
+ return formattedMessages;
394
+ }
395
+
396
+ /**
397
+ * Formats an array of messages for LangChain, handling tool calls and creating ToolMessage instances.
398
+ *
399
+ * @param payload - The array of messages to format.
400
+ * @param indexTokenCountMap - Optional map of message indices to token counts.
401
+ * @param tools - Optional set of tool names that are allowed in the request.
402
+ * @returns - Object containing formatted messages and updated indexTokenCountMap if provided.
403
+ */
404
+ export const formatAgentMessages = (
405
+ payload: TPayload,
406
+ indexTokenCountMap?: Record<number, number>,
407
+ tools?: Set<string>
408
+ ): {
409
+ messages: Array<HumanMessage | AIMessage | SystemMessage | ToolMessage>;
410
+ indexTokenCountMap?: Record<number, number>;
411
+ } => {
412
+ const messages: Array<
413
+ HumanMessage | AIMessage | SystemMessage | ToolMessage
414
+ > = [];
415
+ // If indexTokenCountMap is provided, create a new map to track the updated indices
416
+ const updatedIndexTokenCountMap: Record<number, number> = {};
417
+ // Keep track of the mapping from original payload indices to result indices
418
+ const indexMapping: Record<number, number[]> = {};
419
+
420
+ // Process messages with tool conversion if tools set is provided
421
+ for (let i = 0; i < payload.length; i++) {
422
+ const message = payload[i];
423
+ // Q: Store the current length of messages to track where this payload message starts in the result?
424
+ // const startIndex = messages.length;
425
+ if (typeof message.content === 'string') {
426
+ message.content = [
427
+ { type: ContentTypes.TEXT, [ContentTypes.TEXT]: message.content },
428
+ ];
429
+ }
430
+ if (message.role !== 'assistant') {
431
+ messages.push(
432
+ formatMessage({
433
+ message: message as MessageInput,
434
+ langChain: true,
435
+ }) as HumanMessage | AIMessage | SystemMessage
436
+ );
437
+
438
+ // Update the index mapping for this message
439
+ indexMapping[i] = [messages.length - 1];
440
+ continue;
441
+ }
442
+
443
+ // For assistant messages, track the starting index before processing
444
+ const startMessageIndex = messages.length;
445
+
446
+ // If tools set is provided, we need to check if we need to convert tool messages to a string
447
+ if (tools) {
448
+ // First, check if this message contains tool calls
449
+ let hasToolCalls = false;
450
+ let hasInvalidTool = false;
451
+ const toolNames: string[] = [];
452
+
453
+ const content = message.content;
454
+ if (content && Array.isArray(content)) {
455
+ for (const part of content) {
456
+ if (part.type === ContentTypes.TOOL_CALL) {
457
+ hasToolCalls = true;
458
+ if (tools.size === 0) {
459
+ hasInvalidTool = true;
460
+ break;
461
+ }
462
+ // Protect against malformed tool call entries
463
+ if (
464
+ !part.tool_call ||
465
+ part.tool_call.name == null ||
466
+ part.tool_call.name === ''
467
+ ) {
468
+ hasInvalidTool = true;
469
+ continue;
470
+ }
471
+ const toolName = part.tool_call.name;
472
+ toolNames.push(toolName);
473
+ if (!tools.has(toolName)) {
474
+ hasInvalidTool = true;
475
+ }
476
+ }
477
+ }
478
+ }
479
+
480
+ // If this message has tool calls and at least one is invalid, we need to convert it
481
+ if (hasToolCalls && hasInvalidTool) {
482
+ // We need to collect all related messages (this message and any subsequent tool messages)
483
+ const toolSequence: BaseMessage[] = [];
484
+ let sequenceEndIndex = i;
485
+
486
+ // Process the current assistant message to get the AIMessage with tool calls
487
+ const formattedMessages = formatAssistantMessage(message);
488
+ toolSequence.push(...formattedMessages);
489
+
490
+ // Look ahead for any subsequent assistant messages that might be part of this tool sequence
491
+ let j = i + 1;
492
+ while (j < payload.length && payload[j].role === 'assistant') {
493
+ // Check if this is a continuation of the tool sequence
494
+ let isToolResponse = false;
495
+ const content = payload[j].content;
496
+ if (content && Array.isArray(content)) {
497
+ for (const part of content) {
498
+ if (part.type === ContentTypes.TOOL_CALL) {
499
+ isToolResponse = true;
500
+ break;
501
+ }
502
+ }
503
+ }
504
+
505
+ if (isToolResponse) {
506
+ // This is part of the tool sequence, add it
507
+ const nextMessages = formatAssistantMessage(payload[j]);
508
+ toolSequence.push(...nextMessages);
509
+ sequenceEndIndex = j;
510
+ j++;
511
+ } else {
512
+ // This is not part of the tool sequence, stop looking
513
+ break;
514
+ }
515
+ }
516
+
517
+ // Convert the sequence to a string
518
+ const bufferString = getBufferString(toolSequence);
519
+ messages.push(new AIMessage({ content: bufferString }));
520
+
521
+ // Skip the messages we've already processed
522
+ i = sequenceEndIndex;
523
+
524
+ // Update the index mapping for this sequence
525
+ const resultIndices = [messages.length - 1];
526
+ for (let k = i; k >= i && k <= sequenceEndIndex; k++) {
527
+ indexMapping[k] = resultIndices;
528
+ }
529
+
530
+ continue;
531
+ }
532
+ }
533
+
534
+ // Process the assistant message using the helper function
535
+ const formattedMessages = formatAssistantMessage(message);
536
+ messages.push(...formattedMessages);
537
+
538
+ // Update the index mapping for this assistant message
539
+ // Store all indices that were created from this original message
540
+ const endMessageIndex = messages.length;
541
+ const resultIndices = [];
542
+ for (let j = startMessageIndex; j < endMessageIndex; j++) {
543
+ resultIndices.push(j);
544
+ }
545
+ indexMapping[i] = resultIndices;
546
+ }
547
+
548
+ // Update the token count map if it was provided
549
+ if (indexTokenCountMap) {
550
+ for (
551
+ let originalIndex = 0;
552
+ originalIndex < payload.length;
553
+ originalIndex++
554
+ ) {
555
+ const resultIndices = indexMapping[originalIndex] || [];
556
+ const tokenCount = indexTokenCountMap[originalIndex];
557
+
558
+ if (tokenCount !== undefined) {
559
+ if (resultIndices.length === 1) {
560
+ // Simple 1:1 mapping
561
+ updatedIndexTokenCountMap[resultIndices[0]] = tokenCount;
562
+ } else if (resultIndices.length > 1) {
563
+ // If one message was split into multiple, distribute the token count
564
+ // This is a simplification - in reality, you might want a more sophisticated distribution
565
+ const countPerMessage = Math.floor(tokenCount / resultIndices.length);
566
+ resultIndices.forEach((resultIndex, idx) => {
567
+ if (idx === resultIndices.length - 1) {
568
+ // Give any remainder to the last message
569
+ updatedIndexTokenCountMap[resultIndex] =
570
+ tokenCount - countPerMessage * (resultIndices.length - 1);
571
+ } else {
572
+ updatedIndexTokenCountMap[resultIndex] = countPerMessage;
573
+ }
574
+ });
575
+ }
576
+ }
577
+ }
578
+ }
579
+
580
+ return {
581
+ messages,
582
+ indexTokenCountMap: indexTokenCountMap
583
+ ? updatedIndexTokenCountMap
584
+ : undefined,
585
+ };
586
+ };
587
+
588
+ /**
589
+ * Adds a value at key 0 for system messages and shifts all key indices by one in an indexTokenCountMap.
590
+ * This is useful when adding a system message at the beginning of a conversation.
591
+ *
592
+ * @param indexTokenCountMap - The original map of message indices to token counts
593
+ * @param instructionsTokenCount - The token count for the system message to add at index 0
594
+ * @returns A new map with the system message at index 0 and all other indices shifted by 1
595
+ */
596
+ export function shiftIndexTokenCountMap(
597
+ indexTokenCountMap: Record<number, number>,
598
+ instructionsTokenCount: number
599
+ ): Record<number, number> {
600
+ // Create a new map to avoid modifying the original
601
+ const shiftedMap: Record<number, number> = {};
602
+ shiftedMap[0] = instructionsTokenCount;
603
+
604
+ // Shift all existing indices by 1
605
+ for (const [indexStr, tokenCount] of Object.entries(indexTokenCountMap)) {
606
+ const index = Number(indexStr);
607
+ shiftedMap[index + 1] = tokenCount;
608
+ }
609
+
610
+ return shiftedMap;
611
+ }