Skip to content

Commit 3c8c3bf

Browse files
refactor websearch into a service
1 parent f37275e commit 3c8c3bf

File tree

6 files changed

+287
-214
lines changed

6 files changed

+287
-214
lines changed

packages/core/src/generation.ts

+17-13
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,10 @@ import {
4949
//VerifiableInferenceProvider,
5050
TelemetrySettings,
5151
TokenizerType,
52+
IWebSearchService,
53+
SearchOptions,
5254
} from "./types.ts";
5355
import { fal } from "@fal-ai/client";
54-
import { tavily } from "@tavily/core";
5556

5657
type Tool = CoreTool<any, any>;
5758
type StepResult = AIStepResult<any>;
@@ -1725,23 +1726,26 @@ export const generateCaption = async (
17251726
};
17261727

17271728
export const generateWebSearch = async (
1728-
query: string,
1729+
data: {
1730+
query: string
1731+
options?: SearchOptions
1732+
},
17291733
runtime: IAgentRuntime
17301734
): Promise<SearchResponse> => {
17311735
try {
1732-
const apiKey = runtime.getSetting("TAVILY_API_KEY") as string;
1733-
if (!apiKey) {
1734-
throw new Error("TAVILY_API_KEY is not set");
1736+
const { query, options } = data;
1737+
const webSearchService =
1738+
runtime.getService<IWebSearchService>(
1739+
ServiceType.WEB_SEARCH
1740+
);
1741+
1742+
if (!webSearchService) {
1743+
throw new Error("Web search service not found");
17351744
}
1736-
const tvly = tavily({ apiKey });
1737-
const response = await tvly.search(query, {
1738-
includeAnswer: true,
1739-
maxResults: 3, // 5 (default)
1740-
topic: "general", // "general"(default) "news"
1741-
searchDepth: "basic", // "basic"(default) "advanced"
1742-
includeImages: false, // false (default) true
1743-
});
1745+
1746+
const response = await webSearchService.search(query, runtime, options);
17441747
return response;
1748+
17451749
} catch (error) {
17461750
elizaLogger.error("Error:", error);
17471751
}

packages/core/src/types.ts

+18
Original file line numberDiff line numberDiff line change
@@ -1328,6 +1328,23 @@ export interface IPdfService extends Service {
13281328
convertPdfToText(pdfBuffer: Buffer): Promise<string>;
13291329
}
13301330

1331+
export interface IWebSearchService extends Service {
1332+
search(
1333+
query: string,
1334+
runtime: IAgentRuntime,
1335+
options?: SearchOptions,
1336+
): Promise<SearchResponse>;
1337+
}
1338+
1339+
export interface SearchOptions {
1340+
limit?: number;
1341+
type?: "news" | "general";
1342+
includeAnswer?: boolean;
1343+
searchDepth?: "basic" | "advanced";
1344+
includeImages?: boolean;
1345+
days?: number; // 1 means current day, 2 means last 2 days
1346+
}
1347+
13311348
export interface IAwsS3Service extends Service {
13321349
uploadFile(
13331350
imagePath: string,
@@ -1431,6 +1448,7 @@ export enum ServiceType {
14311448
IRYS = "irys",
14321449
TEE_LOG = "tee_log",
14331450
GOPLUS_SECURITY = "goplus_security",
1451+
WEB_SEARCH = "web_search",
14341452
}
14351453

14361454
export enum LoggingLevel {

packages/plugin-node/src/services/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { PdfService } from "./pdf.ts";
66
import { SpeechService } from "./speech.ts";
77
import { TranscriptionService } from "./transcription.ts";
88
import { VideoService } from "./video.ts";
9+
import { WebSearchService } from "./webSearch.ts";
910

1011
export {
1112
AwsS3Service,
@@ -16,4 +17,5 @@ export {
1617
SpeechService,
1718
TranscriptionService,
1819
VideoService,
20+
WebSearchService
1921
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {
2+
Service,
3+
IWebSearchService,
4+
ServiceType,
5+
IAgentRuntime,
6+
SearchResponse,
7+
SearchOptions,
8+
} from "@elizaos/core";
9+
import { tavily } from "@tavily/core";
10+
11+
export class WebSearchService extends Service implements IWebSearchService {
12+
static serviceType: ServiceType = ServiceType.WEB_SEARCH;
13+
14+
async initialize(_runtime: IAgentRuntime): Promise<void> {}
15+
16+
getInstance(): IWebSearchService {
17+
return WebSearchService.getInstance();
18+
}
19+
20+
async search(
21+
query: string,
22+
runtime: IAgentRuntime,
23+
options?: SearchOptions,
24+
): Promise<SearchResponse> {
25+
try {
26+
const apiKey = runtime.getSetting("TAVILY_API_KEY") as string;
27+
if (!apiKey) {
28+
throw new Error("TAVILY_API_KEY is not set");
29+
}
30+
31+
const tvly = tavily({ apiKey });
32+
const response = await tvly.search(query, {
33+
includeAnswer: options?.includeAnswer || true,
34+
maxResults: options?.limit || 3,
35+
topic: options?.type || "general",
36+
searchDepth: options?.searchDepth || "basic",
37+
includeImages: options?.includeImages || false,
38+
days: options?.days || 3,
39+
});
40+
41+
return response;
42+
} catch (error) {
43+
console.error("Web search error:", error);
44+
throw error;
45+
}
46+
}
47+
}
+201
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import { elizaLogger } from "@elizaos/core";
2+
import {
3+
Action,
4+
HandlerCallback,
5+
IAgentRuntime,
6+
Memory,
7+
State,
8+
} from "@elizaos/core";
9+
import { generateWebSearch } from "@elizaos/core";
10+
import { SearchResult } from "@elizaos/core";
11+
import { encodingForModel, TiktokenModel } from "js-tiktoken";
12+
13+
const DEFAULT_MAX_WEB_SEARCH_TOKENS = 4000;
14+
const DEFAULT_MODEL_ENCODING = "gpt-3.5-turbo";
15+
16+
function getTotalTokensFromString(
17+
str: string,
18+
encodingName: TiktokenModel = DEFAULT_MODEL_ENCODING
19+
) {
20+
const encoding = encodingForModel(encodingName);
21+
return encoding.encode(str).length;
22+
}
23+
24+
function MaxTokens(
25+
data: string,
26+
maxTokens: number = DEFAULT_MAX_WEB_SEARCH_TOKENS
27+
): string {
28+
if (getTotalTokensFromString(data) >= maxTokens) {
29+
return data.slice(0, maxTokens);
30+
}
31+
return data;
32+
}
33+
34+
export const webSearch: Action = {
35+
name: "WEB_SEARCH",
36+
similes: [
37+
"SEARCH_WEB",
38+
"INTERNET_SEARCH",
39+
"LOOKUP",
40+
"QUERY_WEB",
41+
"FIND_ONLINE",
42+
"SEARCH_ENGINE",
43+
"WEB_LOOKUP",
44+
"ONLINE_SEARCH",
45+
"FIND_INFORMATION",
46+
],
47+
suppressInitialMessage: true,
48+
description:
49+
"Perform a web search to find information related to the message.",
50+
validate: async (runtime: IAgentRuntime, message: Memory) => {
51+
const tavilyApiKeyOk = !!runtime.getSetting("TAVILY_API_KEY");
52+
53+
return tavilyApiKeyOk;
54+
},
55+
handler: async (
56+
runtime: IAgentRuntime,
57+
message: Memory,
58+
state: State,
59+
options: any,
60+
callback: HandlerCallback
61+
) => {
62+
elizaLogger.log("Composing state for message:", message);
63+
state = (await runtime.composeState(message)) as State;
64+
const userId = runtime.agentId;
65+
elizaLogger.log("User ID:", userId);
66+
67+
const webSearchPrompt = message.content.text;
68+
elizaLogger.log("web search prompt received:", webSearchPrompt);
69+
70+
elizaLogger.log("Generating image with prompt:", webSearchPrompt);
71+
const searchResponse = await generateWebSearch(
72+
webSearchPrompt,
73+
runtime
74+
);
75+
76+
if (searchResponse && searchResponse.results.length) {
77+
const responseList = searchResponse.answer
78+
? `${searchResponse.answer}${
79+
Array.isArray(searchResponse.results) &&
80+
searchResponse.results.length > 0
81+
? `\n\nFor more details, you can check out these resources:\n${searchResponse.results
82+
.map(
83+
(result: SearchResult, index: number) =>
84+
`${index + 1}. [${result.title}](${result.url})`
85+
)
86+
.join("\n")}`
87+
: ""
88+
}`
89+
: "";
90+
91+
callback({
92+
text: MaxTokens(responseList, DEFAULT_MAX_WEB_SEARCH_TOKENS),
93+
});
94+
} else {
95+
elizaLogger.error("search failed or returned no data.");
96+
}
97+
},
98+
examples: [
99+
[
100+
{
101+
user: "{{user1}}",
102+
content: {
103+
text: "Find the latest news about SpaceX launches.",
104+
},
105+
},
106+
{
107+
user: "{{agentName}}",
108+
content: {
109+
text: "Here is the latest news about SpaceX launches:",
110+
action: "WEB_SEARCH",
111+
},
112+
},
113+
],
114+
[
115+
{
116+
user: "{{user1}}",
117+
content: {
118+
text: "Can you find details about the iPhone 16 release?",
119+
},
120+
},
121+
{
122+
user: "{{agentName}}",
123+
content: {
124+
text: "Here are the details I found about the iPhone 16 release:",
125+
action: "WEB_SEARCH",
126+
},
127+
},
128+
],
129+
[
130+
{
131+
user: "{{user1}}",
132+
content: {
133+
text: "What is the schedule for the next FIFA World Cup?",
134+
},
135+
},
136+
{
137+
user: "{{agentName}}",
138+
content: {
139+
text: "Here is the schedule for the next FIFA World Cup:",
140+
action: "WEB_SEARCH",
141+
},
142+
},
143+
],
144+
[
145+
{
146+
user: "{{user1}}",
147+
content: { text: "Check the latest stock price of Tesla." },
148+
},
149+
{
150+
user: "{{agentName}}",
151+
content: {
152+
text: "Here is the latest stock price of Tesla I found:",
153+
action: "WEB_SEARCH",
154+
},
155+
},
156+
],
157+
[
158+
{
159+
user: "{{user1}}",
160+
content: {
161+
text: "What are the current trending movies in the US?",
162+
},
163+
},
164+
{
165+
user: "{{agentName}}",
166+
content: {
167+
text: "Here are the current trending movies in the US:",
168+
action: "WEB_SEARCH",
169+
},
170+
},
171+
],
172+
[
173+
{
174+
user: "{{user1}}",
175+
content: {
176+
text: "What is the latest score in the NBA finals?",
177+
},
178+
},
179+
{
180+
user: "{{agentName}}",
181+
content: {
182+
text: "Here is the latest score from the NBA finals:",
183+
action: "WEB_SEARCH",
184+
},
185+
},
186+
],
187+
[
188+
{
189+
user: "{{user1}}",
190+
content: { text: "When is the next Apple keynote event?" },
191+
},
192+
{
193+
user: "{{agentName}}",
194+
content: {
195+
text: "Here is the information about the next Apple keynote event:",
196+
action: "WEB_SEARCH",
197+
},
198+
},
199+
],
200+
],
201+
} as Action;

0 commit comments

Comments
 (0)