diff --git a/docs/api_refs/blacklisted-entrypoints.json b/docs/api_refs/blacklisted-entrypoints.json index 05088f93bdc6..a2972288c02c 100644 --- a/docs/api_refs/blacklisted-entrypoints.json +++ b/docs/api_refs/blacklisted-entrypoints.json @@ -8,6 +8,7 @@ "../../langchain/src/tools/gmail.ts", "../../langchain/src/tools/google_places.ts", "../../langchain/src/tools/google_trends.ts", + "../../langchain/src/tools/hyperbrowser.ts", "../../langchain/src/embeddings/bedrock.ts", "../../langchain/src/embeddings/cloudflare_workersai.ts", "../../langchain/src/embeddings/ollama.ts", diff --git a/docs/core_docs/docs/integrations/document_loaders/web_loaders/hyperbrowser.mdx b/docs/core_docs/docs/integrations/document_loaders/web_loaders/hyperbrowser.mdx new file mode 100644 index 000000000000..d40cf1bf49dc --- /dev/null +++ b/docs/core_docs/docs/integrations/document_loaders/web_loaders/hyperbrowser.mdx @@ -0,0 +1,196 @@ +--- +sidebar_class_name: node-only +--- + +# Hyperbrowser + +## Overview + +The Hyperbrowser document loader provides a way to load web content into your LangChain application using Hyperbrowser's cloud browser infrastructure. It supports: + +- Single-page scraping +- Multi-page crawling +- Multiple output formats (markdown, HTML, links) +- Advanced browser automation features + +| Class | Package | Local | [PY support](https://python.langchain.com/docs/integrations/document_loaders/hyperbrowser/)| +| :---: | :---: | :---: | :---: | +| HyperbrowserLoader | langchain-js | ❌ | ✅ | + +## Setup + +To access the Hyperbrowser Document Loader, you'll first need to install the @langchain/community package. You will also need a hyperbrowser account, and the associated API key. + +### Credentials + +You'll need a Hyperbrowser API key. Get one from [Hyperbrowser](https://hyperbrowser.ai/) and set it as an environment variable: + +### Installation + +First, install the required packages: + +```bash npm2yarn +npm install @langchain/community @langchain/core @hyperbrowser/sdk +``` + + +```bash +export HYPERBROWSER_API_KEY="your-api-key" +``` + +Or pass it directly to the loader: + +```typescript +const loader = new HyperbrowserLoader({ + apiKey: "your-api-key", + // ... other options +}); +``` + +## Instantiation + +Load content from a single webpage: + +```typescript +import { HyperbrowserLoader } from "@langchain/community/document_loaders/web/hyperbrowser"; + +const loader = new HyperbrowserLoader({ + url: "https://example.com", + mode: "scrape", + outputFormat: ["markdown"] +}); + +const docs = await loader.load(); +``` + +## Advanced Usage + +### Multi-page Loading + +Load content from multiple linked pages: + +```typescript +import { HyperbrowserLoader } from "@langchain/community/document_loaders/web/hyperbrowser"; + +const loader = new HyperbrowserLoader({ + url: "https://example.com", + mode: "crawl", + maxPages: 10, + outputFormat: ["markdown"] +}); + +const docs = await loader.load(); +``` + +### Different Output Formats + +```typescript +// Markdown output +const markdownLoader = new HyperbrowserLoader({ + url: "https://example.com", + mode: "scrape", + outputFormat: ["markdown"] +}); + +// HTML output +const htmlLoader = new HyperbrowserLoader({ + url: "https://example.com", + mode: "scrape", + outputFormat: ["html"] +}); + +// Multiple formats +const multiFormatLoader = new HyperbrowserLoader({ + url: "https://example.com", + mode: "scrape", + outputFormat: ["markdown", "html", "links"] +}); +``` + +### With Session Options + +```typescript +const loader = new HyperbrowserLoader({ + url: "https://example.com", + mode: "scrape", + outputFormat: ["markdown"], + sessionOptions: { + useProxy: true, + solveCaptchas: true, + ... + } +}); +``` + +## Parameters + +### Required Parameters + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `url` | `string` | The URL to scrape or crawl | +| `mode` | `"scrape" \| "crawl"` | Whether to scrape a single page or crawl multiple pages | + +### Optional Parameters + +| Parameter | Type | Default | Description | +| --------- | ---- | ------- | ----------- | +| `outputFormat` | `Array<"markdown" \| "html" \| "links" \| "screenshot">` | `["markdown"]` | Desired output formats | +| `maxPages` | `number` | `10` | Maximum pages to crawl (crawl mode only) | +| `sessionOptions` | `object` | See below | A complete list of options can be found on our [docs](https://docs.hyperbrowser.ai/reference/api-reference/sessions#api-session)| + +## Document Schema + +Each document in the returned array has this structure: + +```typescript +interface Document { + pageContent: string; // The extracted content in specified format + metadata: { + source: string; // The URL of the page + timestamp?: string; // When the page was scraped + title?: string; // Page title if available + // ... other metadata from the page + }; +} +``` + +Example document: + +```typescript +{ + pageContent: "# Main Title\n\nArticle content here...", + metadata: { + source: "https://example.com/article", + timestamp: "2024-03-20T10:30:00Z", + title: "Example Article" + } +} +``` + +## Metadata Handling + +The loader automatically extracts and includes metadata: + +```typescript +const loader = new HyperbrowserLoader({ + url: "https://example.com", + mode: "scrape", + outputFormat: ["markdown"] +}); + +const docs = await loader.load(); +console.log(docs[0].metadata); +// { +// source: "https://example.com", +// timestamp: "2024-03-20T10:30:00Z", +// title: "Example Page Title" +// } +``` + +## Related + +- Tool [conceptual guide](docs/concepts/document_loaders) +- Tool [how-to guides](/docs/how_to/#document-loaders) +- [Hyperbrowser Docs](https://docs.hyperbrowser.ai) +- [Hyperbrowser Browser Agents and Automation Tools](/docs/integrations/tools/hyperbrowser) \ No newline at end of file diff --git a/docs/core_docs/docs/integrations/tools/hyperbrowser.mdx b/docs/core_docs/docs/integrations/tools/hyperbrowser.mdx new file mode 100644 index 000000000000..b51a2798524d --- /dev/null +++ b/docs/core_docs/docs/integrations/tools/hyperbrowser.mdx @@ -0,0 +1,261 @@ +--- +sidebar_class_name: node-only +--- + +# Hyperbrowser + +## Overview + +The Hyperbrowser tools provide a way to interact with web browsers programmatically, enabling web scraping, crawling, and browser automation capabilities along with powerful browser agents. + +| Tool Class | Package | [Python Support](https://python.langchain.com/docs/integrations/tools/hyperbrowser) | +| :---: | :---: | :---: | +| HyperbrowserScrapingTool | langchain-js | ✅ | +| HyperbrowserCrawlTool | langchain-js | ✅ | +| HyperbrowserExtractTool | langchain-js | ✅ | +| HyperbrowserBrowserUseTool | langchain-js | ✅ | +| HyperbrowserClaudeComputerUseTool | langchain-js | ✅ | +| HyperbrowserOpenAIComputerUseTool | langchain-js | ✅ | + +## Installation + + + +## Setup + +The hyperbrowser tools integration lives in the `@langchain/community` package. + +```bash npm2yarn +npm install @langchain/community @langchain/core @hyperbrowser/sdk +``` + +### Credentials + +You'll need a Hyperbrowser API key. Get one from [Hyperbrowser](https://app.hyperbrowser.ai/) and set it as an environment variable: + +```bash +export HYPERBROWSER_API_KEY="your-api-key" +``` + +It's also helpful (but not needed) to set up LangSmith for best-in-class observability: + +```bash +process.env.LANGSMITH_TRACING="true" +process.env.LANGSMITH_API_KEY="your-api-key" +``` + +## Instantiation + +You can import and instantiate an instance of one of the Hyperbrowser tool like this: + +``` typescript +import { HyperbrowserScrapingTool } from "@langchain/community/tools/hyperbrowser" + +const tool = new HyperbrowserScrapingTool({}) +``` + + +### Invocation +Invoke directly with args + +```typescript +const result = await tool.invoke({ + url: "https://example.com", + scrapeOptions: { + formats: ["markdown"], + }, + sessionOptions: { + useProxy: false, + solveCaptchas: false, + }, +}); +``` + +## Agents + +For guides on how to use LangChain tools in agents, see the LangGraph.js docs. + + +## Tools + +### Web Automation Tools + +These tools provide core web automation capabilities for scraping, crawling, and structured data extraction. + +#### HyperbrowserScrapingTool + +Tool for extracting content from a single webpage in various formats. + +```typescript +import { HyperbrowserScrapingTool } from "@langchain/community/tools/hyperbrowser"; + +const tool = new HyperbrowserScrapingTool(); + +const result = await tool.invoke({ + url: "https://example.com", + scrapeOptions: { + formats: ["markdown", "html", "links"] + }, + sessionOptions: { + useProxy: false, + solveCaptchas: false + } +}); +``` + +#### HyperbrowserCrawlTool + +Tool for systematically exploring and extracting content from multiple linked pages. + +```typescript +import { HyperbrowserCrawlTool } from "@langchain/community/tools/hyperbrowser"; + +const tool = new HyperbrowserCrawlTool(); + +const result = await tool.invoke({ + url: "https://example.com", + maxPages: 5, + scrapeOptions: { + formats: ["markdown"] + }, + sessionOptions: { + useProxy: false, + solveCaptchas: false + } +}); +``` + +#### HyperbrowserExtractTool + +Tool for extracting structured data from webpages using schemas. + +```typescript +import { HyperbrowserExtractTool } from "@langchain/community/tools/hyperbrowser"; +import { z } from "zod"; + +const tool = new HyperbrowserExtractTool(); + +const articleSchema = z.object({ + title: z.string().describe("The main title of the article"), + author: z.string().describe("The author of the article"), + publishDate: z.string().optional().describe("The publication date if available") +}); + +const result = await tool.invoke({ + url: "https://example.com", + extractOptions: { + prompt: "Extract the article information", + schema: articleSchema + } +}); +``` + +### Browser Agents + +These tools provide advanced browser automation capabilities using different AI models for complex interactions. + +#### HyperbrowserBrowserUseTool + +Fast, efficient browser automation tool optimized for explicit instructions. + +```typescript +import { HyperbrowserBrowserUseTool } from "@langchain/community/tools/hyperbrowser"; + +const tool = new HyperbrowserBrowserUseTool(); + +const result = await tool.invoke({ + task: "Navigate to example.com and click the first link in the navigation menu", + maxSteps: 3 +}); +``` + +#### HyperbrowserClaudeComputerUseTool + +Advanced browser automation using Claude's sophisticated reasoning for complex tasks. + +```typescript +import { HyperbrowserClaudeComputerUseTool } from "@langchain/community/tools/hyperbrowser"; + +const tool = new HyperbrowserClaudeComputerUseTool(); + +const result = await tool.invoke({ + task: "Go to hackernews, find all the latest articles about AI, and extract their titles", + maxSteps: 5 +}); +``` + +#### HyperbrowserOpenAIComputerUseTool + +Reliable browser automation using OpenAI's capabilities for general-purpose tasks. + +```typescript +import { HyperbrowserOpenAIComputerUseTool } from "@langchain/community/tools/hyperbrowser"; + +const tool = new HyperbrowserOpenAIComputerUseTool(); + +const result = await tool.invoke({ + task: "Visit example.com and fill out the contact form with test data", + maxSteps: 4 +}); +``` + +## Parameters + +### Common Parameters + +All tools accept these common parameters: + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `sessionOptions` | `object` | Browser session configuration | + +Session Options supports two parameters: + +```typescript +interface SessionOptions { + useProxy: boolean, + solveCaptchas: boolean, +} +``` + +Browser session configuration options including proxy settings, CAPTCHA solving, cookie handling, and stealth mode. Controls how the browser behaves during the session. + +### Tool-specific Parameters + +#### Scraping & Crawling Tools +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `url` | `string` | The URL to scrape/crawl | +| `formats` | `Array<"markdown" \| "html" \| "links" \| "screenshot">` | Output formats | +| `maxPages` | `number` | Maximum pages to crawl (crawl tool only) | + +#### Extract Tool +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `url` | `string` | The URL of the page to extract data from | +| `extractOptions` | `object` | Extraction configuration with either prompt or schema | +| `extractOptions.prompt` | `string` | Instructions on what to extract. Prefer providing the schema so that data is in a known structured format. Can be used to augment the schema. | +| `extractOptions.schema` | `object` | JSON Schema format for structured extraction. Prefer this to the schema. | + +#### Browser Automation Tools +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `task` | `string` | Description of the automation task | +| `maxSteps` | `number` | Maximum number of automation steps | + +#### Response + +Each tool returns a structured response: + +```typescript +interface ToolResponse { + data?: any; // The extracted/processed data + error?: string; // Error message if something went wrong +} +``` + +## Additional Resources + +- [Hyperbrowser Documentation](https://docs.hyperbrowser.ai) +- [Document Loader Integration](/docs/integrations/document_loaders/web_loaders/hyperbrowser) +- [Tool Concepts](/docs/concepts/tools) \ No newline at end of file diff --git a/libs/langchain-community/.gitignore b/libs/langchain-community/.gitignore index fc0cb520b693..d03677b646af 100644 --- a/libs/langchain-community/.gitignore +++ b/libs/langchain-community/.gitignore @@ -82,6 +82,10 @@ tools/google_scholar.cjs tools/google_scholar.js tools/google_scholar.d.ts tools/google_scholar.d.cts +tools/hyperbrowser.cjs +tools/hyperbrowser.js +tools/hyperbrowser.d.ts +tools/hyperbrowser.d.cts tools/ifttt.cjs tools/ifttt.js tools/ifttt.d.ts diff --git a/libs/langchain-community/langchain.config.js b/libs/langchain-community/langchain.config.js index 3a67261b225b..6c98dc076777 100644 --- a/libs/langchain-community/langchain.config.js +++ b/libs/langchain-community/langchain.config.js @@ -54,6 +54,7 @@ export const config = { "tools/google_trends": "tools/google_trends", "tools/google_routes": "tools/google_routes", "tools/google_scholar": "tools/google_scholar", + "tools/hyperbrowser": "tools/hyperbrowser", "tools/ifttt": "tools/ifttt", "tools/searchapi": "tools/searchapi", "tools/searxng_search": "tools/searxng_search", @@ -300,6 +301,7 @@ export const config = { "document_loaders/web/google_cloud_storage", "document_loaders/web/gitbook": "document_loaders/web/gitbook", "document_loaders/web/hn": "document_loaders/web/hn", + "document_loaders/web/hyperbrowser": "document_loaders/web/hyperbrowser", "document_loaders/web/imsdb": "document_loaders/web/imsdb", "document_loaders/web/jira": "document_loaders/web/jira", "document_loaders/web/figma": "document_loaders/web/figma", diff --git a/libs/langchain-community/package.json b/libs/langchain-community/package.json index 844b3b01332d..c29acdafacc6 100644 --- a/libs/langchain-community/package.json +++ b/libs/langchain-community/package.json @@ -80,6 +80,7 @@ "@gradientai/nodejs-sdk": "^1.2.0", "@huggingface/inference": "^2.6.4", "@huggingface/transformers": "^3.2.3", + "@hyperbrowser/sdk": "^0.42.0", "@ibm-cloud/watsonx-ai": "^1.6.4", "@jest/globals": "^29.5.0", "@lancedb/lancedb": "^0.13.0", @@ -255,6 +256,7 @@ "@gradientai/nodejs-sdk": "^1.2.0", "@huggingface/inference": "^2.6.4", "@huggingface/transformers": "^3.2.3", + "@hyperbrowser/sdk": "^0.42.0", "@ibm-cloud/watsonx-ai": "*", "@lancedb/lancedb": "^0.12.0", "@langchain/core": ">=0.2.21 <0.4.0", @@ -443,6 +445,9 @@ "@huggingface/transformers": { "optional": true }, + "@hyperbrowser/sdk": { + "optional": true + }, "@lancedb/lancedb": { "optional": true }, @@ -922,6 +927,15 @@ "import": "./tools/google_scholar.js", "require": "./tools/google_scholar.cjs" }, + "./tools/hyperbrowser": { + "types": { + "import": "./tools/hyperbrowser.d.ts", + "require": "./tools/hyperbrowser.d.cts", + "default": "./tools/hyperbrowser.d.ts" + }, + "import": "./tools/hyperbrowser.js", + "require": "./tools/hyperbrowser.cjs" + }, "./tools/ifttt": { "types": { "import": "./tools/ifttt.d.ts", @@ -2920,6 +2934,15 @@ "import": "./document_loaders/web/hn.js", "require": "./document_loaders/web/hn.cjs" }, + "./document_loaders/web/hyperbrowser": { + "types": { + "import": "./document_loaders/web/hyperbrowser.d.ts", + "require": "./document_loaders/web/hyperbrowser.d.cts", + "default": "./document_loaders/web/hyperbrowser.d.ts" + }, + "import": "./document_loaders/web/hyperbrowser.js", + "require": "./document_loaders/web/hyperbrowser.cjs" + }, "./document_loaders/web/imsdb": { "types": { "import": "./document_loaders/web/imsdb.d.ts", diff --git a/libs/langchain-community/src/document_loaders/tests/hyperbrowser_loader.int.test.ts b/libs/langchain-community/src/document_loaders/tests/hyperbrowser_loader.int.test.ts new file mode 100644 index 000000000000..dbb418fd91ca --- /dev/null +++ b/libs/langchain-community/src/document_loaders/tests/hyperbrowser_loader.int.test.ts @@ -0,0 +1,63 @@ +import { test } from "@jest/globals"; +import { HyperbrowserLoader } from "../web/hyperbrowser.js"; + +test.skip("HyperbrowserLoader scrape mode", async () => { + const loader = new HyperbrowserLoader({ + url: "https://example.com", + mode: "scrape", + outputFormat: ["markdown"], + sessionOptions: { + useProxy: false, + solveCaptchas: false, + acceptCookies: false, + useStealth: false, + }, + }); + + const docs = await loader.load(); + expect(docs).toBeTruthy(); + expect(docs.length).toBe(1); + expect(docs[0].pageContent).toBeTruthy(); + expect(docs[0].metadata.source).toBe("https://example.com"); +}); + +test.skip("HyperbrowserLoader crawl mode", async () => { + const loader = new HyperbrowserLoader({ + url: "https://example.com", + mode: "crawl", + maxPages: 2, + outputFormat: ["markdown"], + sessionOptions: { + useProxy: false, + solveCaptchas: false, + acceptCookies: false, + useStealth: false, + }, + }); + + const docs = await loader.load(); + expect(docs).toBeTruthy(); + expect(docs.length).toBeGreaterThan(0); + expect(docs[0].pageContent).toBeTruthy(); + expect(docs[0].metadata.source).toBeTruthy(); +}); + +test.skip("HyperbrowserLoader with different output formats", async () => { + const loader = new HyperbrowserLoader({ + url: "https://example.com", + mode: "scrape", + outputFormat: ["markdown", "html", "links"], + sessionOptions: { + useProxy: false, + solveCaptchas: false, + acceptCookies: false, + useStealth: false, + }, + }); + + const docs = await loader.load(); + expect(docs).toBeTruthy(); + expect(docs.length).toBe(1); + expect(docs[0].pageContent).toBeTruthy(); + expect(docs[0].metadata.source).toBe("https://example.com"); +}); diff --git a/libs/langchain-community/src/document_loaders/web/hyperbrowser.ts b/libs/langchain-community/src/document_loaders/web/hyperbrowser.ts new file mode 100644 index 000000000000..76cec14c9e28 --- /dev/null +++ b/libs/langchain-community/src/document_loaders/web/hyperbrowser.ts @@ -0,0 +1,190 @@ +import { Hyperbrowser } from "@hyperbrowser/sdk"; +import type { CreateSessionParams } from "@hyperbrowser/sdk/types"; +import { ScrapeJobData, CrawledPage } from "@hyperbrowser/sdk/types"; +import { Document, type DocumentInterface } from "@langchain/core/documents"; +import { getEnvironmentVariable } from "@langchain/core/utils/env"; +import { BaseDocumentLoader } from "@langchain/core/document_loaders/base"; + +/** + * Interface representing the parameters for the Hyperbrowser loader. + */ +interface HyperbrowserLoaderParameters { + /** + * URL to scrape or crawl + */ + url: string; + + /** + * API key for Hyperbrowser. If not provided, the default value is the value of the HYPERBROWSER_API_KEY environment variable. + */ + apiKey?: string; + + /** + * Mode of operation. Can be "crawl" or "scrape". + */ + mode?: "crawl" | "scrape"; + + /** + * Maximum number of pages to crawl (only applicable in crawl mode) + */ + maxPages?: number; + + /** + * Format of the output. Can be "markdown", "html", "links", or "screenshot" + */ + outputFormat?: Array<"markdown" | "html" | "links" | "screenshot">; + + /** + * Session options for the browser + */ + sessionOptions?: { + useProxy: boolean; + solveCaptchas: boolean; + acceptCookies: boolean; + useStealth: boolean; + }; +} + +/** + * Class representing a document loader for loading data from Hyperbrowser. + * It extends the BaseDocumentLoader class. + * @example + * ```typescript + * const loader = new HyperbrowserLoader({ + * url: "https://example.com", + * apiKey: "your-api-key", + * mode: "scrape", + * outputFormat: ["markdown"] + * }); + * const docs = await loader.load(); + * ``` + */ +export class HyperbrowserLoader extends BaseDocumentLoader { + private apiKey: string; + private url: string; + private mode: "crawl" | "scrape"; + private maxPages: number; + private outputFormat: Array<"markdown" | "html" | "links" | "screenshot">; + private sessionOptions?: CreateSessionParams; + + constructor(params: HyperbrowserLoaderParameters) { + super(); + + const { + apiKey = getEnvironmentVariable("HYPERBROWSER_API_KEY"), + url, + mode = "scrape", + maxPages = 10, + outputFormat = ["markdown"], + sessionOptions, + } = params; + + if (!apiKey) { + throw new Error( + "Hyperbrowser API key not set. You can set it as HYPERBROWSER_API_KEY in your .env file, or pass it to HyperbrowserLoader." + ); + } + + this.apiKey = apiKey; + this.url = url; + this.mode = mode; + this.maxPages = maxPages; + this.outputFormat = outputFormat; + this.sessionOptions = sessionOptions; + } + + /** + * Loads data from Hyperbrowser. + * @returns An array of Documents representing the retrieved data. + */ + public async load(): Promise { + const client = new Hyperbrowser({ apiKey: this.apiKey }); + + try { + if (this.mode === "scrape") { + const response = await client.scrape.startAndWait({ + url: this.url, + scrapeOptions: { + formats: this.outputFormat, + }, + sessionOptions: this.sessionOptions, + }); + + if (response.error) { + throw new Error( + `Hyperbrowser: Failed to scrape URL. Error: ${response.error}` + ); + } + + if (!response.data) { + return []; + } + + return [ + new Document({ + pageContent: this.extractContent(response.data), + metadata: { + source: this.url, + ...response.data?.metadata, + }, + }), + ]; + } else if (this.mode === "crawl") { + const response = await client.crawl.startAndWait({ + url: this.url, + maxPages: this.maxPages, + scrapeOptions: { + formats: this.outputFormat, + }, + sessionOptions: this.sessionOptions, + }); + + if (response.error) { + throw new Error( + `Hyperbrowser: Failed to crawl URL. Error: ${response.error}` + ); + } + + return (response.data || []).map( + (page) => + new Document({ + pageContent: this.extractContent(page), + metadata: { + source: page.url || this.url, + pageMeta: page.metadata, + }, + }) + ); + } + + throw new Error( + `Unrecognized mode '${this.mode}'. Expected one of 'crawl', 'scrape'.` + ); + } catch (error: any) { + throw new Error( + `Failed to load data from Hyperbrowser: ${ + error.message || JSON.stringify(error) + }` + ); + } + } + + private extractContent(data: ScrapeJobData | CrawledPage[]): string { + if (!data) return ""; + // If it's an array (like in crawl results), join the content + if (Array.isArray(data)) { + return data + .map((item) => this.extractContent(item)) + .filter(Boolean) + .join("\n\n"); + } + + // Prioritize markdown over HTML over raw content + if (data.markdown) return data.markdown; + if (data.html) return data.html; + if (typeof data === "string") return data; + + // If it's an object, stringify it + return JSON.stringify(data); + } +} diff --git a/libs/langchain-community/src/tools/hyperbrowser.ts b/libs/langchain-community/src/tools/hyperbrowser.ts new file mode 100644 index 000000000000..966276f23504 --- /dev/null +++ b/libs/langchain-community/src/tools/hyperbrowser.ts @@ -0,0 +1,274 @@ +import { StructuredTool } from "@langchain/core/tools"; +import { getEnvironmentVariable } from "@langchain/core/utils/env"; +import { Hyperbrowser } from "@hyperbrowser/sdk"; +import * as z from "zod"; + +const sessionOptions = z.object({ + useProxy: z + .boolean() + .default(false) + .describe( + "Whether to use a proxy. Proxy should be used only if there are issues accessing a page. Defaults to false." + ), + solveCaptchas: z + .boolean() + .default(false) + .describe( + "Whether to automatically solve captchas. Should be used only if there are captchas blocking a page. Defaults to false." + ), +}); + +const scrapeOptions = z.object({ + formats: z + .array( + z + .enum(["markdown", "html", "links", "screenshot"]) + .describe("The format to return the scraped content in") + ) + .optional() + .describe( + "The array of formats to scrape from the requested page. If undefined, then defaults to only returning markdown." + ), +}); + +const extractionOptions = z.object({ + prompt: z + .string() + .describe( + "Instruction on what to extract. Strongly prefer providing a schema instead." + ), + schema: z + .record(z.any()) + .describe( + "Extraction schema in JSON Schema format. Strongly prefer using this to the prompt." + ), +}); + +const browserAgentOptions = z.object({ + task: z.string().describe("The task to perform inside the browser"), + maxSteps: z + .number() + .optional() + .describe( + "The maximum number of steps to perform. If uncertain, default to empty" + ), + sessionOptions: sessionOptions.optional(), +}); + +abstract class HyperbrowserToolBase extends StructuredTool { + protected client: Hyperbrowser; + + constructor(apiKey?: string) { + super(); + let key = apiKey ?? getEnvironmentVariable("HYPERBROWSER_API_KEY"); + this.client = new Hyperbrowser({ apiKey: key }); + } + + protected async getHyperbrowser(): Promise { + if (this.client) return this.client; + + throw new Error("Hyperbrowser client not instantiated."); + } +} + +function isErrorWithMessage(error: unknown): error is { message: string } { + return ( + typeof error === "object" && + error !== null && + "message" in error && + typeof (error as { message: unknown }).message === "string" + ); +} + +export class HyperbrowserScrapingTool extends HyperbrowserToolBase { + name = "hyperbrowser_scrape_webpage"; + schema = z.object({ + url: z.string().describe("The URL of the webpage to scrape"), + scrapeOptions: scrapeOptions, + sessionOptions: sessionOptions, + }); + + description = + "Use this tool to scrape a specific page given a url. The format can be markdown, html, or all the links on the page."; + + async _call( + input: z.infer + ): Promise<{ data: any; error: any }> { + try { + const response = await this.client.scrape.startAndWait({ + url: input.url, + scrapeOptions: input.scrapeOptions, + sessionOptions: input.sessionOptions, + }); + return { data: response.data, error: response.error }; + } catch (error) { + const message = isErrorWithMessage(error) + ? error.message + : typeof error === "object" && error !== null && "toString" in error + ? error.toString() + : JSON.stringify(error); + return { data: undefined, error: message }; + } + } +} + +export class HyperbrowserExtractTool extends HyperbrowserToolBase { + name = "hyperbrowser_extract_webpage"; + schema = z.object({ + url: z.string().describe("The URL of the page to scrape from"), + extractOptions: extractionOptions, + sessionOptions: sessionOptions.optional(), + }); + + description = + "Use this tool to extract structured information from the current web page using Hyperbrowser. The input should include either an 'instruction' string and/or a 'schema' object representing the extraction schema in JSON Schema format."; + + async _call( + input: z.infer + ): Promise<{ data: any; error: any }> { + const { url, extractOptions, sessionOptions } = input; + + try { + const result = await this.client.extract.startAndWait({ + urls: [url], + schema: extractOptions.schema, + prompt: extractOptions.prompt, + sessionOptions: sessionOptions, + }); + return { data: result.data, error: result.error }; + } catch (error: unknown) { + const message = isErrorWithMessage(error) + ? error.message + : typeof error === "object" && error !== null && "toString" in error + ? error.toString() + : JSON.stringify(error); + return { data: undefined, error: message }; + } + } +} + +export class HyperbrowserCrawlTool extends HyperbrowserToolBase { + name = "hyperbrowser_crawl_tool"; + schema = z.object({ + url: z.string().describe("The URL of the webpage to scrape"), + maxPages: z + .number() + .optional() + .describe( + "The maximum number of pages to crawl. If uncertain, default to empty" + ), + scrapeOptions: scrapeOptions, + sessionOptions: sessionOptions, + }); + + description = + "Use this tool to observe the current web page and retrieve possible actions using Hyperbrowser. The input can be an optional instruction string."; + + async _call( + input: z.infer + ): Promise<{ data: any; error: any }> { + try { + const { url, maxPages, scrapeOptions, sessionOptions } = input; + const result = await this.client.crawl.startAndWait({ + url, + maxPages, + scrapeOptions, + sessionOptions, + }); + return { data: result.data, error: result.error }; + } catch (error: unknown) { + const message = isErrorWithMessage(error) + ? error.message + : typeof error === "object" && error !== null && "toString" in error + ? error.toString() + : JSON.stringify(error); + return { data: undefined, error: message }; + } + } +} + +export class HyperbrowserBrowserUseTool extends HyperbrowserToolBase { + name = "hyperbrowser_browser_use"; + schema = browserAgentOptions; + description = + "Use this tool to perform browser automation tasks using a fast, efficient agent optimized for explicit instructions. Best for straightforward, well-defined tasks that require precise browser interactions. Uses the Browser-Use agent."; + + async _call( + input: z.infer + ): Promise<{ data: any; error: any }> { + try { + const { task, maxSteps, sessionOptions } = input; + const result = await this.client.agents.browserUse.startAndWait({ + task, + maxSteps, + sessionOptions, + }); + return { data: result.data?.finalResult, error: result.error }; + } catch (error: unknown) { + const message = isErrorWithMessage(error) + ? error.message + : typeof error === "object" && error !== null && "toString" in error + ? error.toString() + : JSON.stringify(error); + return { data: undefined, error: message }; + } + } +} + +export class HyperbrowserClaudeComputerUseTool extends HyperbrowserToolBase { + name = "hyperbrowser_claude_computer_use"; + schema = browserAgentOptions; + + description = + "Use this tool to perform complex browser automation tasks using Claude's advanced reasoning capabilities. Best for tasks requiring sophisticated decision-making and context understanding. Uses the Claude Computer Use agent"; + + async _call( + input: z.infer + ): Promise<{ data: any; error: any }> { + try { + const { task, maxSteps, sessionOptions } = input; + const result = await this.client.agents.claudeComputerUse.startAndWait({ + task, + maxSteps, + sessionOptions, + }); + return { data: result.data?.finalResult, error: result.error }; + } catch (error: unknown) { + const message = isErrorWithMessage(error) + ? error.message + : typeof error === "object" && error !== null && "toString" in error + ? error.toString() + : JSON.stringify(error); + return { data: undefined, error: message }; + } + } +} + +export class HyperbrowserOpenAIComputerUseTool extends HyperbrowserToolBase { + name = "hyperbrowser_openai_computer_use"; + schema = browserAgentOptions; + + description = + "Use this tool to perform browser automation tasks using OpenAI's balanced capabilities. Best for general-purpose tasks requiring reliable execution and practical reasoning. Uses the OpenAI CUA agent."; + + async _call( + input: z.infer + ): Promise<{ data: any; error: any }> { + try { + const { task, maxSteps, sessionOptions } = input; + const result = await this.client.agents.cua.startAndWait({ + task, + maxSteps, + sessionOptions, + }); + return { data: result.data?.finalResult, error: result.error }; + } catch (error: unknown) { + const message = isErrorWithMessage(error) + ? error.message + : typeof error === "object" && error !== null && "toString" in error + ? error.toString() + : JSON.stringify(error); + return { data: undefined, error: message }; + } + } +} diff --git a/libs/langchain-community/src/tools/tests/hyperbrowser.int.test.ts b/libs/langchain-community/src/tools/tests/hyperbrowser.int.test.ts new file mode 100644 index 000000000000..f0d06f2f80f8 --- /dev/null +++ b/libs/langchain-community/src/tools/tests/hyperbrowser.int.test.ts @@ -0,0 +1,103 @@ +import { test } from "@jest/globals"; +import { + HyperbrowserScrapingTool, + HyperbrowserExtractTool, + HyperbrowserCrawlTool, + HyperbrowserBrowserUseTool, + HyperbrowserClaudeComputerUseTool, + HyperbrowserOpenAIComputerUseTool, +} from "../hyperbrowser.js"; + +test.skip("HyperbrowserScrapingTool", async () => { + const tool = new HyperbrowserScrapingTool(); + const result = await tool.invoke({ + url: "https://example.com", + scrapeOptions: { + formats: ["markdown"], + }, + sessionOptions: { + useProxy: false, + solveCaptchas: false, + }, + }); + expect(result.data).toBeTruthy(); + expect(result.error).toBeFalsy(); +}); + +test.skip("HyperbrowserExtractTool", async () => { + const tool = new HyperbrowserExtractTool(); + const result = await tool.invoke({ + url: "https://example.com", + extractOptions: { + prompt: "Extract the main heading and first paragraph", + schema: { + type: "object", + properties: { + heading: { type: "string" }, + firstParagraph: { type: "string" }, + }, + }, + }, + }); + expect(result.data).toBeTruthy(); + expect(result.error).toBeFalsy(); +}); + +test.skip("HyperbrowserCrawlTool", async () => { + const tool = new HyperbrowserCrawlTool(); + const result = await tool.invoke({ + url: "https://example.com", + maxPages: 1, + scrapeOptions: { + formats: ["markdown"], + }, + sessionOptions: { + useProxy: false, + solveCaptchas: false, + }, + }); + expect(result.data).toBeTruthy(); + expect(result.error).toBeFalsy(); +}); + +test.skip("HyperbrowserBrowserUseTool", async () => { + const tool = new HyperbrowserBrowserUseTool(); + const result = await tool.invoke({ + task: "Navigate to example.com and summarize the page. Do absolutely nothing else.", + maxSteps: 3, + sessionOptions: { + useProxy: false, + solveCaptchas: false, + }, + }); + expect(result.data).toBeTruthy(); + expect(result.error).toBeFalsy(); +}); + +test.skip("HyperbrowserClaudeComputerUseTool", async () => { + const tool = new HyperbrowserClaudeComputerUseTool(); + const result = await tool.invoke({ + task: "Navigate to example.com and summarize the page. Do absolutely nothing else.", + maxSteps: 3, + sessionOptions: { + useProxy: false, + solveCaptchas: false, + }, + }); + expect(result.data).toBeTruthy(); + expect(result.error).toBeFalsy(); +}); + +test.skip("HyperbrowserOpenAIComputerUseTool", async () => { + const tool = new HyperbrowserOpenAIComputerUseTool(); + const result = await tool.invoke({ + task: "Navigate to example.com and summarize the page. Do absolutely nothing else.", + maxSteps: 3, + sessionOptions: { + useProxy: false, + solveCaptchas: false, + }, + }); + expect(result.data).toBeTruthy(); + expect(result.error).toBeFalsy(); +}); diff --git a/yarn.lock b/yarn.lock index 5801b41a2945..cce6d2c46b56 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8540,6 +8540,18 @@ __metadata: languageName: node linkType: hard +"@hyperbrowser/sdk@npm:^0.42.0": + version: 0.42.0 + resolution: "@hyperbrowser/sdk@npm:0.42.0" + dependencies: + form-data: ^4.0.1 + node-fetch: 2.7.0 + zod: ^3.24.1 + zod-to-json-schema: ^3.24.1 + checksum: d2e5f69aa2a25645895b8501f71a7b773b03a8f7d8d82cc325929d8c9df70a6057d600d195b0a0b56ef923dba2d00f0f533cddf87b3c772e4e2f79883c763b35 + languageName: node + linkType: hard + "@iarna/toml@npm:2.2.5": version: 2.2.5 resolution: "@iarna/toml@npm:2.2.5" @@ -9779,6 +9791,7 @@ __metadata: "@gradientai/nodejs-sdk": ^1.2.0 "@huggingface/inference": ^2.6.4 "@huggingface/transformers": ^3.2.3 + "@hyperbrowser/sdk": ^0.42.0 "@ibm-cloud/watsonx-ai": ^1.6.4 "@jest/globals": ^29.5.0 "@lancedb/lancedb": ^0.13.0 @@ -9963,6 +9976,7 @@ __metadata: "@gradientai/nodejs-sdk": ^1.2.0 "@huggingface/inference": ^2.6.4 "@huggingface/transformers": ^3.2.3 + "@hyperbrowser/sdk": "*" "@ibm-cloud/watsonx-ai": "*" "@lancedb/lancedb": ^0.12.0 "@langchain/core": ">=0.2.21 <0.4.0" @@ -10121,6 +10135,8 @@ __metadata: optional: true "@huggingface/transformers": optional: true + "@hyperbrowser/sdk": + optional: true "@lancedb/lancedb": optional: true "@layerup/layerup-security": @@ -23705,6 +23721,18 @@ __metadata: languageName: node linkType: hard +"es-set-tostringtag@npm:^2.1.0": + version: 2.1.0 + resolution: "es-set-tostringtag@npm:2.1.0" + dependencies: + es-errors: ^1.3.0 + get-intrinsic: ^1.2.6 + has-tostringtag: ^1.0.2 + hasown: ^2.0.2 + checksum: 789f35de4be3dc8d11fdcb91bc26af4ae3e6d602caa93299a8c45cf05d36cc5081454ae2a6d3afa09cceca214b76c046e4f8151e092e6fc7feeb5efb9e794fc6 + languageName: node + linkType: hard + "es-shim-unscopables@npm:^1.0.0": version: 1.0.0 resolution: "es-shim-unscopables@npm:1.0.0" @@ -26125,6 +26153,18 @@ __metadata: languageName: node linkType: hard +"form-data@npm:^4.0.1": + version: 4.0.2 + resolution: "form-data@npm:4.0.2" + dependencies: + asynckit: ^0.4.0 + combined-stream: ^1.0.8 + es-set-tostringtag: ^2.1.0 + mime-types: ^2.1.12 + checksum: e887298b22c13c7c9c5a8ba3716f295a479a13ca78bfd855ef11cbce1bcf22bc0ae2062e94808e21d46e5c667664a1a1a8a7f57d7040193c1fefbfb11af58aab + languageName: node + linkType: hard + "formdata-node@npm:^4.3.2": version: 4.4.1 resolution: "formdata-node@npm:4.4.1" @@ -26520,7 +26560,7 @@ __metadata: languageName: node linkType: hard -"get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.3.0": +"get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.3.0": version: 1.3.0 resolution: "get-intrinsic@npm:1.3.0" dependencies: