diff --git a/SERPEX_INTEGRATION.md b/SERPEX_INTEGRATION.md new file mode 100644 index 000000000000..97731b7b19a1 --- /dev/null +++ b/SERPEX_INTEGRATION.md @@ -0,0 +1,153 @@ +# LangChain Serpex Integration + +Official Serpex search integration for LangChain.js + +## Installation + +```bash +npm install @langchain/community +``` + +## Setup + +Get your API key from [Serpex](https://serpex.dev) and set it as an environment variable: + +```bash +export SERPEX_API_KEY="your-api-key-here" +``` + +## Usage + +### Basic Example + +```typescript +import { Serpex } from "@langchain/community/tools/serpex"; + +const tool = new Serpex(process.env.SERPEX_API_KEY); + +const result = await tool.invoke("latest AI developments"); +console.log(result); +``` + +### With Custom Parameters + +```typescript +import { Serpex } from "@langchain/community/tools/serpex"; + +const tool = new Serpex(process.env.SERPEX_API_KEY, { + engine: "google", // auto, google, bing, duckduckgo, brave, yahoo, yandex + category: "web", // currently only "web" supported + time_range: "day" // all, day, week, month, year (not supported by Brave) +}); + +const result = await tool.invoke("coffee shops near me"); +console.log(result); +``` + +### With LangChain Agent + +```typescript +import { Serpex } from "@langchain/community/tools/serpex"; +import { ChatOpenAI } from "@langchain/openai"; +import { AgentExecutor, createReactAgent } from "langchain/agents"; +import { pull } from "langchain/hub"; + +const searchTool = new Serpex(process.env.SERPEX_API_KEY, { + engine: "auto", + time_range: "week" +}); + +const model = new ChatOpenAI({ + model: "gpt-4", + temperature: 0, +}); + +const prompt = await pull("hwchase17/react"); + +const agent = await createReactAgent({ + llm: model, + tools: [searchTool], + prompt, +}); + +const agentExecutor = new AgentExecutor({ + agent, + tools: [searchTool], +}); + +const result = await agentExecutor.invoke({ + input: "What are the latest developments in AI?" +}); + +console.log(result.output); +``` + +## API Parameters + +### Supported Parameters + +- **`engine`** (optional): Search engine to use + - Options: `"auto"`, `"google"`, `"bing"`, `"duckduckgo"`, `"brave"`, `"yahoo"`, `"yandex"` + - Default: `"auto"` (automatically routes with retry logic) + +- **`category`** (optional): Search category + - Options: `"web"` (more categories coming soon) + - Default: `"web"` + +- **`time_range`** (optional): Filter results by time + - Options: `"all"`, `"day"`, `"week"`, `"month"`, `"year"` + - Note: Not supported by Brave engine + +### Response Format + +The tool returns formatted search results as a string containing: + +1. **Instant Answers**: Direct answers from knowledge panels +2. **Infoboxes**: Knowledge panel descriptions +3. **Organic Results**: Web search results with titles, URLs, and snippets +4. **Suggestions**: Related search queries +5. **Corrections**: Suggested query corrections + +Example response: +``` +Found 10 results: + +[1] Starbucks - Coffee Shop +URL: https://www.starbucks.com/store-locator/store/1234 +Premium coffee, perfect lattes, and great atmosphere in the heart of downtown... + +[2] Local Coffee Roasters +URL: https://localcoffee.com +Artisanal coffee beans, locally sourced and expertly roasted daily... +``` + +## Features + +- **Multi-Engine Support**: Automatically routes requests across multiple search engines +- **Smart Retry Logic**: Built-in retry mechanism for failed requests +- **Real-Time Results**: Get fresh search results from the web +- **Simple Integration**: Easy to use with LangChain agents and chains +- **Structured Output**: Clean, formatted search results ready for LLM consumption + +## Cost + +All search engines cost 1 credit per successful request. Credits never expire. + +## Rate Limits + +- 300 requests per second +- No daily limits + +## Documentation + +For detailed API documentation, visit: [https://serpex.dev/docs](https://serpex.dev/docs) + +## Support + +- Email: support@serpex.dev +- Documentation: https://serpex.dev/docs +- Dashboard: https://serpex.dev/dashboard + +## License + +MIT diff --git a/libs/langchain-community/src/load/import_map.ts b/libs/langchain-community/src/load/import_map.ts index 1886f66c420d..52652c3a88bc 100644 --- a/libs/langchain-community/src/load/import_map.ts +++ b/libs/langchain-community/src/load/import_map.ts @@ -25,6 +25,7 @@ export * as tools__ifttt from "../tools/ifttt.js"; export * as tools__searchapi from "../tools/searchapi.js"; export * as tools__searxng_search from "../tools/searxng_search.js"; export * as tools__serpapi from "../tools/serpapi.js"; +export * as tools__serpex from "../tools/serpex.js"; export * as tools__serper from "../tools/serper.js"; export * as tools__stackexchange from "../tools/stackexchange.js"; export * as tools__wikipedia_query_run from "../tools/wikipedia_query_run.js"; diff --git a/libs/langchain-community/src/tools/serpex.ts b/libs/langchain-community/src/tools/serpex.ts new file mode 100644 index 000000000000..7caff6e98fed --- /dev/null +++ b/libs/langchain-community/src/tools/serpex.ts @@ -0,0 +1,237 @@ +import { getEnvironmentVariable } from "@langchain/core/utils/env"; +import { Tool } from "@langchain/core/tools"; + +/** + * SERPEX API Parameters + * + * SERPEX provides multi-engine web search results in JSON format. + * Supports Google, Bing, DuckDuckGo, Brave, Yahoo, and Yandex search engines. + * + * For detailed documentation, visit: https://serpex.dev/docs + */ +export interface SerpexParameters { + /** + * Search query string (required) + */ + q: string; + + /** + * Search engine to use + * Options: "auto", "google", "bing", "duckduckgo", "brave", "yahoo", "yandex" + * Default: "auto" (automatically routes to best available engine) + */ + engine?: string; + + /** + * Search category + * Currently only "web" is supported + * More categories (images, videos, news) coming soon + * Default: "web" + */ + category?: string; + + /** + * Time range filter for results + * Options: "all", "day", "week", "month", "year" + * Note: Not supported by Brave engine + */ + time_range?: string; +} + +/** + * Serpex Class + * + * A tool for searching the web using the SERPEX API, which provides + * multi-engine search results from Google, Bing, DuckDuckGo, Brave, Yahoo, and Yandex. + * + * Requires SERPEX_API_KEY environment variable or passed as parameter. + * Get your API key at: https://serpex.dev + * + * @example + * ```typescript + * const serpex = new Serpex("your-api-key", { + * engine: "auto", + * category: "web", + * time_range: "day" + * }); + * + * const agent = RunnableSequence.from([ + * ChatPromptTemplate.fromMessages([ + * ["ai", "Answer the following questions using concise bullet points."], + * ["human", "{input}"], + * ]), + * new ChatOpenAI({ model: "gpt-4", temperature: 0 }), + * (input: BaseMessageChunk) => ({ + * log: "Processed search results", + * returnValues: { + * output: input, + * }, + * }), + * ]); + * + * const executor = AgentExecutor.fromAgentAndTools({ + * agent, + * tools: [serpex], + * }); + * + * const result = await executor.invoke({ + * input: "What are the latest AI developments?" + * }); + * console.log(result); + * ``` + */ +export class Serpex extends Tool { + static lc_name() { + return "Serpex"; + } + + name = "serpex_search"; + + description = + "A powerful multi-engine web search tool. Useful for answering questions about current events, finding information from the web, and getting real-time data. Input should be a search query string. Supports automatic routing with retry logic and multiple search engines (Google, Bing, DuckDuckGo, Brave, Yahoo, Yandex)."; + + protected apiKey: string; + + protected params: Partial; + + protected baseURL: string; + + /** + * @param apiKey - SERPEX API key (optional if SERPEX_API_KEY env var is set) + * @param params - Default parameters for all searches + * @param baseURL - Base URL for Serpex API (defaults to production API) + */ + constructor( + apiKey: string | undefined = getEnvironmentVariable("SERPEX_API_KEY"), + params: Partial = {}, + baseURL: string = getEnvironmentVariable("SERPEX_BASE_URL") || "https://api.serpex.dev" + ) { + super(); + + if (!apiKey) { + throw new Error( + "SERPEX API key is required. Set it as SERPEX_API_KEY in your environment variables, or pass it to the Serpex constructor." + ); + } + + this.apiKey = apiKey; + this.params = params; + this.baseURL = baseURL; + } + + /** + * Converts the Serpex instance to JSON + * @returns Throws an error (not implemented) + */ + toJSON() { + return this.toJSONNotImplemented(); + } + + /** + * Builds the API request URL with query parameters + * @param searchQuery - The search query string + * @returns Complete API URL with parameters + */ + protected buildUrl(searchQuery: string): string { + const preparedParams: [string, string][] = Object.entries({ + engine: "auto", + category: "web", + ...this.params, + q: searchQuery, + }) + .filter(([key, value]) => value !== undefined && value !== null) + .map(([key, value]) => [key, `${value}`]); + + const searchParams = new URLSearchParams(preparedParams); + return `${this.baseURL}/api/search?${searchParams}`; + } + + /** + * Executes the search and processes results + * @param input - Search query string + * @returns Formatted search results + */ + async _call(input: string): Promise { + try { + const url = this.buildUrl(input); + + const response = await fetch(url, { + method: "GET", + headers: { + "Authorization": `Bearer ${this.apiKey}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `SERPEX API request failed with status ${response.status}: ${errorText}` + ); + } + + const json = await response.json(); + + if (json.error) { + throw new Error( + `SERPEX API returned an error: ${json.error}` + ); + } + + // Process response based on actual Serpex API format + // Response structure: { metadata, id, query, engines, results, answers, corrections, infoboxes, suggestions } + + // Instant answers (from knowledge panels/answer boxes) + if (json.answers && Array.isArray(json.answers) && json.answers.length > 0) { + const answer = json.answers[0]; + if (answer.answer || answer.snippet) { + return answer.answer || answer.snippet; + } + } + + // Infoboxes (knowledge panels) + if (json.infoboxes && Array.isArray(json.infoboxes) && json.infoboxes.length > 0) { + const infobox = json.infoboxes[0]; + if (infobox.description) { + return infobox.description; + } + } + + // Organic search results + if (json.results && Array.isArray(json.results) && json.results.length > 0) { + const snippets = json.results + .filter((result: any) => result.snippet || result.title) + .slice(0, 10) // Limit to top 10 results + .map((result: any, index: number) => { + const title = result.title || ""; + const snippet = result.snippet || ""; + const url = result.url || ""; + const published = result.published_date ? `\nPublished: ${result.published_date}` : ""; + return `[${index + 1}] ${title}\nURL: ${url}\n${snippet}${published}`; + }); + + if (snippets.length > 0) { + const header = `Found ${json.metadata?.number_of_results || json.results.length} results:\n\n`; + return header + snippets.join("\n\n"); + } + } + + // Search suggestions + if (json.suggestions && Array.isArray(json.suggestions) && json.suggestions.length > 0) { + return `No direct results found. Related searches:\n${json.suggestions.join("\n")}`; + } + + // Query corrections + if (json.corrections && Array.isArray(json.corrections) && json.corrections.length > 0) { + return `Did you mean: ${json.corrections.join(", ")}?`; + } + + return "No search results found."; + } catch (error) { + if (error instanceof Error) { + return `Error searching with SERPEX: ${error.message}`; + } + return `Unknown error occurred while searching with SERPEX`; + } + } +} diff --git a/libs/langchain-community/src/tools/tests/serpex.test.ts b/libs/langchain-community/src/tools/tests/serpex.test.ts new file mode 100644 index 000000000000..1ea50ca94c75 --- /dev/null +++ b/libs/langchain-community/src/tools/tests/serpex.test.ts @@ -0,0 +1,78 @@ +import { test, expect } from "@jest/globals"; +import { Serpex } from "../serpex.js"; + +// Mock API key for testing +const MOCK_API_KEY = "sk_a002b8bf71992a04cb58c0896b906808ffcdea5b939269dec74b718e846259a9"; + +test("Serpex can be instantiated with API key", () => { + const serpex = new Serpex(MOCK_API_KEY); + expect(serpex).toBeDefined(); + expect(serpex.name).toBe("serpex_search"); +}); + +test("Serpex throws error without API key", () => { + expect(() => new Serpex(undefined)).toThrow( + "SERPEX API key is required" + ); +}); + +test("Serpex can be instantiated with custom parameters", () => { + const serpex = new Serpex(MOCK_API_KEY, { + engine: "google", + category: "web", + time_range: "day", + }); + expect(serpex).toBeDefined(); +}); + +test("Serpex buildUrl creates correct URL with default params", () => { + const serpex = new Serpex(MOCK_API_KEY); + const url = (serpex as any).buildUrl("test query"); + + expect(url).toContain("api.serpex.com/api/search"); + expect(url).toContain("q=test+query"); + expect(url).toContain("engine=auto"); + expect(url).toContain("category=web"); +}); + +test("Serpex buildUrl creates correct URL with custom params", () => { + const serpex = new Serpex(MOCK_API_KEY, { + engine: "google", + time_range: "week", + }); + const url = (serpex as any).buildUrl("AI news"); + + expect(url).toContain("q=AI+news"); + expect(url).toContain("engine=google"); + expect(url).toContain("time_range=week"); +}); + +test("Serpex can use custom base URL", () => { + const customBaseURL = "https://api.serpex.dev"; + const serpex = new Serpex(MOCK_API_KEY, {}, customBaseURL); + const url = (serpex as any).buildUrl("test"); + + expect(url).toContain(customBaseURL); +}); + +// Integration test (commented out - requires real API key) +/* +test("Serpex performs real search", async () => { + const apiKey = process.env.SERPEX_API_KEY; + if (!apiKey) { + console.log("Skipping integration test: SERPEX_API_KEY not set"); + return; + } + + const serpex = new Serpex(apiKey, { + engine: "auto", + category: "web", + }); + + const result = await serpex._call("weather in San Francisco"); + + expect(result).toBeTruthy(); + expect(result).not.toContain("Error"); + expect(result.length).toBeGreaterThan(0); +}, 30000); +*/