@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.
- package/dist/cjs/graphs/Graph.cjs +7 -6
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/index.cjs +8 -8
- package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/types.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +15 -0
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/main.cjs +2 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/stream.cjs +8 -0
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +8 -2
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/handlers.cjs +101 -1
- package/dist/cjs/tools/handlers.cjs.map +1 -1
- package/dist/cjs/tools/search/anthropic.cjs +40 -0
- package/dist/cjs/tools/search/anthropic.cjs.map +1 -0
- package/dist/cjs/tools/search/search.cjs +82 -15
- package/dist/cjs/tools/search/search.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +7 -6
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/llm/anthropic/index.mjs +8 -8
- package/dist/esm/llm/anthropic/index.mjs.map +1 -1
- package/dist/esm/llm/anthropic/types.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +15 -0
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/main.mjs +1 -1
- package/dist/esm/stream.mjs +9 -1
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +8 -2
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/handlers.mjs +101 -3
- package/dist/esm/tools/handlers.mjs.map +1 -1
- package/dist/esm/tools/search/anthropic.mjs +37 -0
- package/dist/esm/tools/search/anthropic.mjs.map +1 -0
- package/dist/esm/tools/search/search.mjs +82 -15
- package/dist/esm/tools/search/search.mjs.map +1 -1
- package/dist/types/graphs/Graph.d.ts +2 -0
- package/dist/types/llm/anthropic/types.d.ts +2 -0
- package/dist/types/llm/anthropic/utils/message_inputs.d.ts +1 -1
- package/dist/types/llm/anthropic/utils/output_parsers.d.ts +3 -3
- package/dist/types/scripts/ant_web_search.d.ts +1 -0
- package/dist/types/tools/CodeExecutor.d.ts +2 -2
- package/dist/types/tools/ToolNode.d.ts +1 -1
- package/dist/types/tools/handlers.d.ts +11 -0
- package/dist/types/tools/search/anthropic.d.ts +16 -0
- package/dist/types/tools/search/types.d.ts +9 -0
- package/dist/types/types/llm.d.ts +3 -2
- package/dist/types/types/stream.d.ts +9 -1
- package/package.json +5 -3
- package/src/graphs/Graph.ts +8 -6
- package/src/llm/anthropic/Jacob_Lee_Resume_2023.pdf +0 -0
- package/src/llm/anthropic/index.ts +7 -12
- package/src/llm/anthropic/llm.spec.ts +447 -115
- package/src/llm/anthropic/types.ts +16 -0
- package/src/llm/anthropic/utils/message_inputs.ts +17 -2
- package/src/llm/anthropic/utils/output_parsers.ts +4 -4
- package/src/scripts/ant_web_search.ts +158 -0
- package/src/stream.ts +16 -5
- package/src/tools/ToolNode.ts +17 -3
- package/src/tools/handlers.ts +163 -1
- package/src/tools/search/anthropic.ts +51 -0
- package/src/tools/search/search.ts +92 -14
- package/src/tools/search/types.ts +9 -0
- package/src/types/llm.ts +4 -2
- 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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
8
|
+
ToolCallTypes,
|
|
9
9
|
ContentTypes,
|
|
10
10
|
GraphEvents,
|
|
11
|
-
|
|
11
|
+
StepTypes,
|
|
12
12
|
Providers,
|
|
13
13
|
} from '@/common';
|
|
14
|
-
import {
|
|
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 &&
|
package/src/tools/ToolNode.ts
CHANGED
|
@@ -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
|
-
(
|
|
163
|
+
(message.tool_calls?.length ?? 0) > 0 &&
|
|
164
|
+
!areToolCallsInvoked(message, invokedToolIds)
|
|
151
165
|
) {
|
|
152
166
|
return GraphNodeKeys.TOOLS;
|
|
153
167
|
} else {
|
package/src/tools/handlers.ts
CHANGED
|
@@ -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 {
|
|
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:
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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 };
|