@librechat/agents 2.4.51 → 2.4.53

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 (66) hide show
  1. package/dist/cjs/graphs/Graph.cjs +7 -6
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/llm/anthropic/index.cjs +8 -8
  4. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  5. package/dist/cjs/llm/anthropic/types.cjs.map +1 -1
  6. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +15 -0
  7. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  8. package/dist/cjs/main.cjs +2 -0
  9. package/dist/cjs/main.cjs.map +1 -1
  10. package/dist/cjs/stream.cjs +8 -0
  11. package/dist/cjs/stream.cjs.map +1 -1
  12. package/dist/cjs/tools/ToolNode.cjs +8 -2
  13. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  14. package/dist/cjs/tools/handlers.cjs +101 -1
  15. package/dist/cjs/tools/handlers.cjs.map +1 -1
  16. package/dist/cjs/tools/search/anthropic.cjs +40 -0
  17. package/dist/cjs/tools/search/anthropic.cjs.map +1 -0
  18. package/dist/cjs/tools/search/search.cjs +82 -15
  19. package/dist/cjs/tools/search/search.cjs.map +1 -1
  20. package/dist/esm/graphs/Graph.mjs +7 -6
  21. package/dist/esm/graphs/Graph.mjs.map +1 -1
  22. package/dist/esm/llm/anthropic/index.mjs +8 -8
  23. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  24. package/dist/esm/llm/anthropic/types.mjs.map +1 -1
  25. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +15 -0
  26. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  27. package/dist/esm/main.mjs +1 -1
  28. package/dist/esm/stream.mjs +9 -1
  29. package/dist/esm/stream.mjs.map +1 -1
  30. package/dist/esm/tools/ToolNode.mjs +8 -2
  31. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  32. package/dist/esm/tools/handlers.mjs +101 -3
  33. package/dist/esm/tools/handlers.mjs.map +1 -1
  34. package/dist/esm/tools/search/anthropic.mjs +37 -0
  35. package/dist/esm/tools/search/anthropic.mjs.map +1 -0
  36. package/dist/esm/tools/search/search.mjs +82 -15
  37. package/dist/esm/tools/search/search.mjs.map +1 -1
  38. package/dist/types/graphs/Graph.d.ts +2 -0
  39. package/dist/types/llm/anthropic/types.d.ts +2 -0
  40. package/dist/types/llm/anthropic/utils/message_inputs.d.ts +1 -1
  41. package/dist/types/llm/anthropic/utils/output_parsers.d.ts +3 -3
  42. package/dist/types/scripts/ant_web_search.d.ts +1 -0
  43. package/dist/types/tools/CodeExecutor.d.ts +2 -2
  44. package/dist/types/tools/ToolNode.d.ts +1 -1
  45. package/dist/types/tools/handlers.d.ts +11 -0
  46. package/dist/types/tools/search/anthropic.d.ts +16 -0
  47. package/dist/types/tools/search/types.d.ts +9 -0
  48. package/dist/types/types/llm.d.ts +3 -2
  49. package/dist/types/types/stream.d.ts +9 -1
  50. package/package.json +5 -3
  51. package/src/graphs/Graph.ts +8 -6
  52. package/src/llm/anthropic/Jacob_Lee_Resume_2023.pdf +0 -0
  53. package/src/llm/anthropic/index.ts +7 -12
  54. package/src/llm/anthropic/llm.spec.ts +447 -115
  55. package/src/llm/anthropic/types.ts +16 -0
  56. package/src/llm/anthropic/utils/message_inputs.ts +17 -2
  57. package/src/llm/anthropic/utils/output_parsers.ts +4 -4
  58. package/src/scripts/ant_web_search.ts +158 -0
  59. package/src/stream.ts +16 -5
  60. package/src/tools/ToolNode.ts +17 -3
  61. package/src/tools/handlers.ts +163 -1
  62. package/src/tools/search/anthropic.ts +51 -0
  63. package/src/tools/search/search.ts +92 -14
  64. package/src/tools/search/types.ts +9 -0
  65. package/src/types/llm.ts +4 -2
  66. package/src/types/stream.ts +14 -0
@@ -49,6 +49,8 @@ export type AnthropicWebSearchToolResultBlockParam =
49
49
  Anthropic.Messages.WebSearchToolResultBlockParam;
50
50
  export type AnthropicWebSearchResultBlockParam =
51
51
  Anthropic.Messages.WebSearchResultBlockParam;
52
+ export type AnthropicSearchResultBlockParam =
53
+ Anthropic.Beta.BetaSearchResultBlockParam;
52
54
 
53
55
  // Union of all possible content block types including server tool use
54
56
  export type AnthropicContentBlock =
@@ -63,6 +65,20 @@ export type AnthropicContentBlock =
63
65
  | AnthropicWebSearchToolResultBlockParam
64
66
  | AnthropicWebSearchResultBlockParam;
65
67
 
68
+ // Union of all possible content block types including server tool use
69
+ export type ChatAnthropicContentBlock =
70
+ | AnthropicTextBlockParam
71
+ | AnthropicImageBlockParam
72
+ | AnthropicToolUseBlockParam
73
+ | AnthropicToolResultBlockParam
74
+ | AnthropicDocumentBlockParam
75
+ | AnthropicThinkingBlockParam
76
+ | AnthropicRedactedThinkingBlockParam
77
+ | AnthropicServerToolUseBlockParam
78
+ | AnthropicWebSearchToolResultBlockParam
79
+ | AnthropicWebSearchResultBlockParam
80
+ | AnthropicSearchResultBlockParam;
81
+
66
82
  export function isAnthropicImageBlockParam(
67
83
  block: unknown
68
84
  ): block is AnthropicImageBlockParam {
@@ -25,7 +25,6 @@ import {
25
25
  AnthropicImageBlockParam,
26
26
  AnthropicMessageCreateParams,
27
27
  AnthropicTextBlockParam,
28
- AnthropicToolResponse,
29
28
  AnthropicToolResultBlockParam,
30
29
  AnthropicToolUseBlockParam,
31
30
  AnthropicDocumentBlockParam,
@@ -34,7 +33,9 @@ import {
34
33
  AnthropicServerToolUseBlockParam,
35
34
  AnthropicWebSearchToolResultBlockParam,
36
35
  isAnthropicImageBlockParam,
37
- } from '@/llm/anthropic/types';
36
+ AnthropicSearchResultBlockParam,
37
+ AnthropicToolResponse,
38
+ } from '../types';
38
39
 
39
40
  function _formatImage(imageUrl: string) {
40
41
  const parsed = parseBase64DataUrl({ dataUrl: imageUrl });
@@ -407,6 +408,20 @@ function _formatContent(content: MessageContent) {
407
408
  ...(cacheControl ? { cache_control: cacheControl } : {}),
408
409
  };
409
410
  return block;
411
+ } else if (contentPart.type === 'search_result') {
412
+ const block: AnthropicSearchResultBlockParam = {
413
+ type: 'search_result' as const, // Explicitly setting the type as "search_result"
414
+ title: contentPart.title,
415
+ source: contentPart.source,
416
+ ...('cache_control' in contentPart && contentPart.cache_control
417
+ ? { cache_control: contentPart.cache_control }
418
+ : {}),
419
+ ...('citations' in contentPart && contentPart.citations
420
+ ? { citations: contentPart.citations }
421
+ : {}),
422
+ content: contentPart.content,
423
+ };
424
+ return block;
410
425
  } else if (
411
426
  textTypes.find((t) => t === contentPart.type) &&
412
427
  'text' in contentPart
@@ -4,17 +4,17 @@ import {
4
4
  BaseLLMOutputParser,
5
5
  OutputParserException,
6
6
  } from '@langchain/core/output_parsers';
7
- import { JsonOutputKeyToolsParserParams } from '@langchain/core/output_parsers/openai_tools';
7
+ import { JsonOutputKeyToolsParserParamsInterop } from '@langchain/core/output_parsers/openai_tools';
8
+ import { ChatGeneration } from '@langchain/core/outputs';
9
+ import { ToolCall } from '@langchain/core/messages/tool';
8
10
  import {
9
11
  interopSafeParseAsync,
10
12
  InteropZodType,
11
13
  } from '@langchain/core/utils/types';
12
- import { ChatGeneration } from '@langchain/core/outputs';
13
- import { ToolCall } from '@langchain/core/messages/tool';
14
14
 
15
15
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
16
  interface AnthropicToolsOutputParserParams<T extends Record<string, any>>
17
- extends JsonOutputKeyToolsParserParams<T> {}
17
+ extends JsonOutputKeyToolsParserParamsInterop<T> {}
18
18
 
19
19
  export class AnthropicToolsOutputParser<
20
20
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -0,0 +1,158 @@
1
+ /* eslint-disable no-console */
2
+ // src/scripts/cli.ts
3
+ import { config } from 'dotenv';
4
+ config();
5
+ import { HumanMessage, BaseMessage } from '@langchain/core/messages';
6
+ import type * as t from '@/types';
7
+ import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
8
+ import { ToolEndHandler, ModelEndHandler } from '@/events';
9
+
10
+ import { getArgs } from '@/scripts/args';
11
+ import { Run } from '@/run';
12
+ import { GraphEvents, Callback, Providers } from '@/common';
13
+ import { getLLMConfig } from '@/utils/llmConfig';
14
+
15
+ const conversationHistory: BaseMessage[] = [];
16
+ let _contentParts: (t.MessageContentComplex | undefined)[] = [];
17
+ async function testStandardStreaming(): Promise<void> {
18
+ const { userName, location, currentDate } = await getArgs();
19
+ const { contentParts, aggregateContent } = createContentAggregator();
20
+ _contentParts = contentParts;
21
+ const customHandlers = {
22
+ [GraphEvents.TOOL_END]: new ToolEndHandler(),
23
+ [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
24
+ [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
25
+ [GraphEvents.ON_RUN_STEP_COMPLETED]: {
26
+ handle: (
27
+ event: GraphEvents.ON_RUN_STEP_COMPLETED,
28
+ data: t.StreamEventData
29
+ ): void => {
30
+ console.log('====== ON_RUN_STEP_COMPLETED ======');
31
+ // console.dir(data, { depth: null });
32
+ aggregateContent({
33
+ event,
34
+ data: data as unknown as { result: t.ToolEndEvent },
35
+ });
36
+ },
37
+ },
38
+ [GraphEvents.ON_RUN_STEP]: {
39
+ handle: (
40
+ event: GraphEvents.ON_RUN_STEP,
41
+ data: t.StreamEventData
42
+ ): void => {
43
+ console.log('====== ON_RUN_STEP ======');
44
+ console.dir(data, { depth: null });
45
+ aggregateContent({ event, data: data as t.RunStep });
46
+ },
47
+ },
48
+ [GraphEvents.ON_RUN_STEP_DELTA]: {
49
+ handle: (
50
+ event: GraphEvents.ON_RUN_STEP_DELTA,
51
+ data: t.StreamEventData
52
+ ): void => {
53
+ console.log('====== ON_RUN_STEP_DELTA ======');
54
+ console.dir(data, { depth: null });
55
+ aggregateContent({ event, data: data as t.RunStepDeltaEvent });
56
+ },
57
+ },
58
+ [GraphEvents.ON_MESSAGE_DELTA]: {
59
+ handle: (
60
+ event: GraphEvents.ON_MESSAGE_DELTA,
61
+ data: t.StreamEventData
62
+ ): void => {
63
+ console.log('====== ON_MESSAGE_DELTA ======');
64
+ console.dir(data, { depth: null });
65
+ aggregateContent({ event, data: data as t.MessageDeltaEvent });
66
+ },
67
+ },
68
+ [GraphEvents.TOOL_START]: {
69
+ handle: (
70
+ _event: string,
71
+ data: t.StreamEventData,
72
+ metadata?: Record<string, unknown>
73
+ ): void => {
74
+ console.log('====== TOOL_START ======');
75
+ // console.dir(data, { depth: null });
76
+ },
77
+ },
78
+ };
79
+
80
+ const llmConfig = getLLMConfig(
81
+ Providers.ANTHROPIC
82
+ ) as t.AnthropicClientOptions & t.SharedLLMConfig;
83
+ llmConfig.model = 'claude-3-5-sonnet-latest';
84
+
85
+ const run = await Run.create<t.IState>({
86
+ runId: 'test-run-id',
87
+ graphConfig: {
88
+ type: 'standard',
89
+ llmConfig,
90
+ tools: [
91
+ {
92
+ type: 'web_search_20250305',
93
+ name: 'web_search',
94
+ max_uses: 5,
95
+ },
96
+ ],
97
+ instructions: 'You are a friendly AI assistant.',
98
+ // additional_instructions: `Always address the user by their name. The user's name is ${userName} and they are located in ${location}.`,
99
+ },
100
+ returnContent: true,
101
+ customHandlers,
102
+ });
103
+
104
+ const config = {
105
+ configurable: {
106
+ provider: Providers.ANTHROPIC,
107
+ thread_id: 'conversation-num-1',
108
+ },
109
+ streamMode: 'values',
110
+ version: 'v2' as const,
111
+ };
112
+
113
+ console.log('Test 1: Search query (search tool test)');
114
+
115
+ // const userMessage = `
116
+ // Make a search for the weather in ${location} today, which is ${currentDate}.
117
+ // Before making the search, please let me know what you're about to do, then immediately start searching without hesitation.
118
+ // Make sure to always refer to me by name, which is ${userName}.
119
+ // After giving me a thorough summary, tell me a joke about the weather forecast we went over.
120
+ // `;
121
+ // const userMessage = 'Are massage guns good?';
122
+ // const userMessage = 'What is functional programming?';
123
+ const userMessage = "Get me today's trending news.";
124
+ // const userMessage = "search recent italy earthquake volcano activity";
125
+ // const userMessage =
126
+ // "use 'Trump' as the exact search query and tell me what you find.";
127
+
128
+ conversationHistory.push(new HumanMessage(userMessage));
129
+
130
+ const inputs = {
131
+ messages: conversationHistory,
132
+ };
133
+ const finalContentParts = await run.processStream(inputs, config);
134
+ const finalMessages = run.getRunMessages();
135
+ if (finalMessages) {
136
+ conversationHistory.push(...finalMessages);
137
+ console.dir(conversationHistory, { depth: null });
138
+ }
139
+ // console.dir(finalContentParts, { depth: null });
140
+ console.log('\n\n====================\n\n');
141
+ // console.dir(contentParts, { depth: null });
142
+ }
143
+
144
+ process.on('unhandledRejection', (reason, promise) => {
145
+ console.error('Unhandled Rejection at:', promise, 'reason:', reason);
146
+ console.log('Content Parts:');
147
+ console.dir(_contentParts, { depth: null });
148
+ process.exit(1);
149
+ });
150
+
151
+ testStandardStreaming().catch((err) => {
152
+ console.error(err);
153
+ console.log('Conversation history:');
154
+ console.dir(conversationHistory, { depth: null });
155
+ console.log('Content Parts:');
156
+ console.dir(_contentParts, { depth: null });
157
+ process.exit(1);
158
+ });
package/src/stream.ts CHANGED
@@ -1,17 +1,21 @@
1
1
  // src/stream.ts
2
+ import type { ChatOpenAIReasoningSummary } from '@langchain/openai';
2
3
  import type { AIMessageChunk } from '@langchain/core/messages';
3
4
  import type { ToolCall } from '@langchain/core/messages/tool';
4
5
  import type { Graph } from '@/graphs';
5
- import type { ChatOpenAIReasoningSummary } from '@langchain/openai';
6
6
  import type * as t from '@/types';
7
7
  import {
8
- StepTypes,
8
+ ToolCallTypes,
9
9
  ContentTypes,
10
10
  GraphEvents,
11
- ToolCallTypes,
11
+ StepTypes,
12
12
  Providers,
13
13
  } from '@/common';
14
- import { handleToolCalls, handleToolCallChunks } from '@/tools/handlers';
14
+ import {
15
+ handleServerToolResult,
16
+ handleToolCallChunks,
17
+ handleToolCalls,
18
+ } from '@/tools/handlers';
15
19
  import { getMessageId } from '@/messages';
16
20
 
17
21
  /**
@@ -132,8 +136,15 @@ export class ChatModelStreamHandler implements t.EventHandler {
132
136
  reasoningKey: graph.reasoningKey,
133
137
  provider: metadata?.provider as Providers,
134
138
  });
139
+ const skipHandling = handleServerToolResult({
140
+ content,
141
+ metadata,
142
+ graph,
143
+ });
144
+ if (skipHandling) {
145
+ return;
146
+ }
135
147
  this.handleReasoning(chunk, graph, metadata?.provider as Providers);
136
-
137
148
  let hasToolCalls = false;
138
149
  if (
139
150
  chunk.tool_calls &&
@@ -138,16 +138,30 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
138
138
  }
139
139
  }
140
140
 
141
+ function areToolCallsInvoked(
142
+ message: AIMessage,
143
+ invokedToolIds?: Set<string>
144
+ ): boolean {
145
+ if (!invokedToolIds || invokedToolIds.size === 0) return false;
146
+ return (
147
+ message.tool_calls?.every(
148
+ (toolCall) => toolCall.id != null && invokedToolIds.has(toolCall.id)
149
+ ) ?? false
150
+ );
151
+ }
152
+
141
153
  export function toolsCondition(
142
- state: BaseMessage[] | typeof MessagesAnnotation.State
154
+ state: BaseMessage[] | typeof MessagesAnnotation.State,
155
+ invokedToolIds?: Set<string>
143
156
  ): 'tools' | typeof END {
144
- const message = Array.isArray(state)
157
+ const message: AIMessage = Array.isArray(state)
145
158
  ? state[state.length - 1]
146
159
  : state.messages[state.messages.length - 1];
147
160
 
148
161
  if (
149
162
  'tool_calls' in message &&
150
- ((message as AIMessage).tool_calls?.length ?? 0) > 0
163
+ (message.tool_calls?.length ?? 0) > 0 &&
164
+ !areToolCallsInvoked(message, invokedToolIds)
151
165
  ) {
152
166
  return GraphNodeKeys.TOOLS;
153
167
  } else {
@@ -1,10 +1,22 @@
1
1
  /* eslint-disable no-console */
2
2
  // src/tools/handlers.ts
3
3
  import { nanoid } from 'nanoid';
4
+ import { ToolMessage } from '@langchain/core/messages';
5
+ import type { AnthropicWebSearchResultBlockParam } from '@/llm/anthropic/types';
4
6
  import type { ToolCall, ToolCallChunk } from '@langchain/core/messages/tool';
5
7
  import type { Graph } from '@/graphs';
6
8
  import type * as t from '@/types';
7
- import { StepTypes, ContentTypes, ToolCallTypes } from '@/common';
9
+ import {
10
+ coerceAnthropicSearchResults,
11
+ isAnthropicWebSearchResult,
12
+ } from '@/tools/search/anthropic';
13
+ import {
14
+ StepTypes,
15
+ ContentTypes,
16
+ ToolCallTypes,
17
+ Providers,
18
+ Constants,
19
+ } from '@/common';
8
20
  import { getMessageId } from '@/messages';
9
21
 
10
22
  export function handleToolCallChunks({
@@ -165,3 +177,153 @@ export const handleToolCalls = (
165
177
  });
166
178
  }
167
179
  };
180
+
181
+ export const toolResultTypes = new Set([
182
+ // 'tool_use',
183
+ // 'server_tool_use',
184
+ // 'input_json_delta',
185
+ 'tool_result',
186
+ 'web_search_result',
187
+ 'web_search_tool_result',
188
+ ]);
189
+
190
+ /**
191
+ * Handles the result of a server tool call; in other words, a provider's built-in tool.
192
+ * As of 2025-07-06, only Anthropic handles server tool calls with this pattern.
193
+ */
194
+ export function handleServerToolResult({
195
+ content,
196
+ metadata,
197
+ graph,
198
+ }: {
199
+ content?: string | t.MessageContentComplex[];
200
+ metadata?: Record<string, unknown>;
201
+ graph: Graph;
202
+ }): boolean {
203
+ let skipHandling = false;
204
+ if (metadata?.provider !== Providers.ANTHROPIC) {
205
+ return skipHandling;
206
+ }
207
+ if (
208
+ typeof content === 'string' ||
209
+ content == null ||
210
+ content.length === 0 ||
211
+ (content.length === 1 &&
212
+ (content[0] as t.ToolResultContent).tool_use_id == null)
213
+ ) {
214
+ return skipHandling;
215
+ }
216
+
217
+ for (const contentPart of content) {
218
+ const toolUseId = (contentPart as t.ToolResultContent).tool_use_id;
219
+ if (toolUseId == null || toolUseId === '') {
220
+ continue;
221
+ }
222
+ const stepId = graph.toolCallStepIds.get(toolUseId);
223
+ if (stepId == null || stepId === '') {
224
+ console.warn(
225
+ `Tool use ID ${toolUseId} not found in graph, cannot dispatch tool result.`
226
+ );
227
+ continue;
228
+ }
229
+ const runStep = graph.getRunStep(stepId);
230
+ if (!runStep) {
231
+ console.warn(
232
+ `Run step for ${stepId} does not exist, cannot dispatch tool result.`
233
+ );
234
+ continue;
235
+ } else if (runStep.type !== StepTypes.TOOL_CALLS) {
236
+ console.warn(
237
+ `Run step for ${stepId} is not a tool call step, cannot dispatch tool result.`
238
+ );
239
+ continue;
240
+ }
241
+
242
+ const toolCall =
243
+ runStep.stepDetails.type === StepTypes.TOOL_CALLS
244
+ ? (runStep.stepDetails.tool_calls?.find(
245
+ (toolCall) => toolCall.id === toolUseId
246
+ ) as ToolCall)
247
+ : undefined;
248
+
249
+ if (!toolCall) {
250
+ continue;
251
+ }
252
+
253
+ if (
254
+ contentPart.type === 'web_search_result' ||
255
+ contentPart.type === 'web_search_tool_result'
256
+ ) {
257
+ handleAnthropicSearchResults({
258
+ contentPart: contentPart as t.ToolResultContent,
259
+ toolCall,
260
+ metadata,
261
+ graph,
262
+ });
263
+ }
264
+
265
+ if (!skipHandling) {
266
+ skipHandling = true;
267
+ }
268
+ }
269
+
270
+ return skipHandling;
271
+ }
272
+
273
+ function handleAnthropicSearchResults({
274
+ contentPart,
275
+ toolCall,
276
+ metadata,
277
+ graph,
278
+ }: {
279
+ contentPart: t.ToolResultContent;
280
+ toolCall: ToolCall;
281
+ metadata?: Record<string, unknown>;
282
+ graph: Graph;
283
+ }): void {
284
+ if (!Array.isArray(contentPart.content)) {
285
+ console.warn(
286
+ `Expected content to be an array, got ${typeof contentPart.content}`
287
+ );
288
+ return;
289
+ }
290
+
291
+ if (!isAnthropicWebSearchResult(contentPart.content[0])) {
292
+ console.warn(
293
+ `Expected content to be an Anthropic web search result, got ${JSON.stringify(
294
+ contentPart.content
295
+ )}`
296
+ );
297
+ return;
298
+ }
299
+
300
+ const searchResultData = coerceAnthropicSearchResults({
301
+ results: contentPart.content as AnthropicWebSearchResultBlockParam[],
302
+ turn: graph.invokedToolIds?.size,
303
+ });
304
+
305
+ const name = toolCall.name;
306
+ const input = toolCall.args;
307
+ const artifact = {
308
+ [Constants.WEB_SEARCH]: searchResultData,
309
+ };
310
+ const output = new ToolMessage({
311
+ name,
312
+ artifact,
313
+ content: 'Anthropic web search results',
314
+ tool_call_id: toolCall.id!,
315
+ });
316
+ graph.handleToolCallCompleted(
317
+ {
318
+ input,
319
+ output,
320
+ },
321
+ metadata
322
+ );
323
+
324
+ if (graph.invokedToolIds == null) {
325
+ graph.invokedToolIds = new Set<string>();
326
+ }
327
+
328
+ graph.invokedToolIds.add(toolCall.id!);
329
+ }
@@ -0,0 +1,51 @@
1
+ import type {
2
+ AnthropicTextBlockParam,
3
+ AnthropicWebSearchResultBlockParam,
4
+ } from '@/llm/anthropic/types';
5
+ import type { SearchResultData, ProcessedOrganic } from './types';
6
+ import { getAttribution } from './utils';
7
+
8
+ /**
9
+ * Coerces Anthropic web search results to the SearchResultData format
10
+ * @param results - Array of Anthropic web search results
11
+ * @param turn - The turn number to associate with these results
12
+ * @returns SearchResultData with minimal ProcessedOrganic items
13
+ */
14
+ export function coerceAnthropicSearchResults({
15
+ results,
16
+ turn = 0,
17
+ }: {
18
+ results: (AnthropicTextBlockParam | AnthropicWebSearchResultBlockParam)[];
19
+ turn?: number;
20
+ }): SearchResultData {
21
+ const organic: ProcessedOrganic[] = results
22
+ .filter((result) => result.type === 'web_search_result')
23
+ .map((result, index) => ({
24
+ link: result.url,
25
+ position: index + 1,
26
+ title: result.title,
27
+ date: result.page_age ?? undefined,
28
+ attribution: getAttribution(result.url),
29
+ }));
30
+
31
+ return {
32
+ turn,
33
+ organic,
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Helper function to check if an object is an Anthropic web search result
39
+ */
40
+ export function isAnthropicWebSearchResult(
41
+ obj: unknown
42
+ ): obj is AnthropicWebSearchResultBlockParam {
43
+ return (
44
+ typeof obj === 'object' &&
45
+ obj !== null &&
46
+ 'type' in obj &&
47
+ obj.type === 'web_search_result' &&
48
+ 'url' in obj &&
49
+ typeof (obj as Record<string, unknown>).url === 'string'
50
+ );
51
+ }
@@ -233,6 +233,7 @@ const createSearXNGAPI = (
233
233
  const getSources = async ({
234
234
  query,
235
235
  numResults = 8,
236
+ safeSearch,
236
237
  type,
237
238
  }: t.GetSourcesParams): Promise<t.SearchResult> => {
238
239
  if (!query.trim()) {
@@ -267,7 +268,7 @@ const createSearXNGAPI = (
267
268
  pageno: 1,
268
269
  categories: category,
269
270
  language: 'all',
270
- safesearch: 0,
271
+ safesearch: safeSearch,
271
272
  engines: 'google,bing,duckduckgo',
272
273
  };
273
274
 
@@ -287,34 +288,111 @@ const createSearXNGAPI = (
287
288
 
288
289
  const data = response.data;
289
290
 
291
+ // Helper function to identify news results since SearXNG doesn't provide that classification by default
292
+ const isNewsResult = (result: t.SearXNGResult): boolean => {
293
+ const url = result.url?.toLowerCase() ?? '';
294
+ const title = result.title?.toLowerCase() ?? '';
295
+
296
+ // News-related keywords in title/content
297
+ const newsKeywords = [
298
+ 'breaking news',
299
+ 'latest news',
300
+ 'top stories',
301
+ 'news today',
302
+ 'developing story',
303
+ 'trending news',
304
+ 'news',
305
+ ];
306
+
307
+ // Check if title/content contains news keywords
308
+ const hasNewsKeywords = newsKeywords.some(
309
+ (keyword) => title.toLowerCase().includes(keyword) // just title probably fine, content parsing is overkill for what we need: || content.includes(keyword)
310
+ );
311
+
312
+ // Check if URL contains news-related paths
313
+ const hasNewsPath =
314
+ url.includes('/news/') ||
315
+ url.includes('/world/') ||
316
+ url.includes('/politics/') ||
317
+ url.includes('/breaking/');
318
+
319
+ return hasNewsKeywords || hasNewsPath;
320
+ };
321
+
290
322
  // Transform SearXNG results to match SerperAPI format
291
323
  const organicResults = (data.results ?? [])
292
324
  .slice(0, numResults)
293
- .map((result: t.SearXNGResult) => ({
294
- title: result.title ?? '',
295
- link: result.url ?? '',
296
- snippet: result.content ?? '',
297
- date: result.publishedDate ?? '',
298
- }));
325
+ .map((result: t.SearXNGResult, index: number) => {
326
+ let attribution = '';
327
+ try {
328
+ attribution = new URL(result.url ?? '').hostname;
329
+ } catch {
330
+ attribution = '';
331
+ }
332
+
333
+ return {
334
+ position: index + 1,
335
+ title: result.title ?? '',
336
+ link: result.url ?? '',
337
+ snippet: result.content ?? '',
338
+ date: result.publishedDate ?? '',
339
+ attribution,
340
+ };
341
+ });
299
342
 
300
- // Extract image results if available
301
343
  const imageResults = (data.results ?? [])
302
344
  .filter((result: t.SearXNGResult) => result.img_src)
303
345
  .slice(0, 6)
304
- .map((result: t.SearXNGResult) => ({
346
+ .map((result: t.SearXNGResult, index: number) => ({
305
347
  title: result.title ?? '',
306
348
  imageUrl: result.img_src ?? '',
349
+ position: index + 1,
350
+ source: new URL(result.url ?? '').hostname,
351
+ domain: new URL(result.url ?? '').hostname,
352
+ link: result.url ?? '',
307
353
  }));
308
354
 
309
- // Format results to match SerperAPI structure
355
+ // Extract news results from organic results
356
+ const newsResults = (data.results ?? [])
357
+ .filter(isNewsResult)
358
+ .map((result: t.SearXNGResult, index: number) => {
359
+ let attribution = '';
360
+ try {
361
+ attribution = new URL(result.url ?? '').hostname;
362
+ } catch {
363
+ attribution = '';
364
+ }
365
+
366
+ return {
367
+ title: result.title ?? '',
368
+ link: result.url ?? '',
369
+ snippet: result.content ?? '',
370
+ date: result.publishedDate ?? '',
371
+ source: attribution,
372
+ imageUrl: result.img_src ?? '',
373
+ position: index + 1,
374
+ };
375
+ });
376
+
377
+ const topStories = newsResults.slice(0, 5);
378
+
379
+ const relatedSearches = Array.isArray(data.suggestions)
380
+ ? data.suggestions.map((suggestion: string) => ({ query: suggestion }))
381
+ : [];
382
+
310
383
  const results: t.SearchResultData = {
311
384
  organic: organicResults,
312
385
  images: imageResults,
313
- topStories: [],
314
- // Use undefined instead of null for optional properties
315
- relatedSearches: data.suggestions ?? [],
386
+ topStories: topStories, // Use first 5 extracted news as top stories
387
+ relatedSearches,
316
388
  videos: [],
317
- news: [],
389
+ news: newsResults,
390
+ // Add empty arrays for other Serper fields to maintain parity
391
+ places: [],
392
+ shopping: [],
393
+ peopleAlsoAsk: [],
394
+ knowledgeGraph: undefined,
395
+ answerBox: undefined,
318
396
  };
319
397
 
320
398
  return { success: true, data: results };