From dcfad263cb5d103aa31f6bc06d240c5600cdb6c6 Mon Sep 17 00:00:00 2001 From: Aarushi <50577581+aarushik93@users.noreply.github.com> Date: Fri, 6 Dec 2024 10:45:40 +0000 Subject: [PATCH] feat(blocks): Add Exa API Blocks (#8835) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adding Exa API blocks because it does very cool search and web scrapping ### Changes 🏗️ Adding Exa API blocks: Search Added a new calendar and time input Added _auth.py for Exa API too. ### Checklist 📋 #### For code changes: - [ ] I have clearly listed my changes in the PR description - [ ] I have made a test plan - [ ] I have tested my changes according to the test plan: - [ ] ...
Example test plan - [ ] Create from scratch and execute an agent with at least 3 blocks - [ ] Import an agent from file upload, and confirm it executes correctly - [ ] Upload agent to marketplace - [ ] Import an agent from marketplace and confirm it executes correctly - [ ] Edit an agent from monitor, and confirm it executes correctly
#### For configuration changes: - [ ] `.env.example` is updated or already compatible with my changes - [ ] `docker-compose.yml` is updated or already compatible with my changes - [ ] I have included a list of my configuration changes in the PR description (under **Changes**)
Examples of configuration changes - Changing ports - Adding new services that need to communicate with each other - Secrets or environment variable changes - New or infrastructure changes such as databases
--------- Co-authored-by: Nicholas Tindle --- .../backend/backend/blocks/exa/_auth.py | 35 ++++ .../backend/backend/blocks/exa/search.py | 157 ++++++++++++++++++ .../integrations/credentials-input.tsx | 1 + .../integrations/credentials-provider.tsx | 1 + .../src/components/node-input-components.tsx | 98 +++++++++++ .../src/lib/autogpt-server-api/types.ts | 2 + 6 files changed, 294 insertions(+) create mode 100644 autogpt_platform/backend/backend/blocks/exa/_auth.py create mode 100644 autogpt_platform/backend/backend/blocks/exa/search.py diff --git a/autogpt_platform/backend/backend/blocks/exa/_auth.py b/autogpt_platform/backend/backend/blocks/exa/_auth.py new file mode 100644 index 000000000000..412143a5c24c --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/exa/_auth.py @@ -0,0 +1,35 @@ +from typing import Literal + +from pydantic import SecretStr + +from backend.data.model import APIKeyCredentials, CredentialsField, CredentialsMetaInput + +ExaCredentials = APIKeyCredentials +ExaCredentialsInput = CredentialsMetaInput[ + Literal["exa"], + Literal["api_key"], +] + +TEST_CREDENTIALS = APIKeyCredentials( + id="01234567-89ab-cdef-0123-456789abcdef", + provider="exa", + api_key=SecretStr("mock-exa-api-key"), + title="Mock Exa API key", + expires_at=None, +) + +TEST_CREDENTIALS_INPUT = { + "provider": TEST_CREDENTIALS.provider, + "id": TEST_CREDENTIALS.id, + "type": TEST_CREDENTIALS.type, + "title": TEST_CREDENTIALS.title, +} + + +def ExaCredentialsField() -> ExaCredentialsInput: + """Creates an Exa credentials input on a block.""" + return CredentialsField( + provider="exa", + supported_credential_types={"api_key"}, + description="The Exa integration requires an API Key.", + ) diff --git a/autogpt_platform/backend/backend/blocks/exa/search.py b/autogpt_platform/backend/backend/blocks/exa/search.py new file mode 100644 index 000000000000..ed3270f46a81 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/exa/search.py @@ -0,0 +1,157 @@ +from datetime import datetime +from typing import List + +from pydantic import BaseModel + +from backend.blocks.exa._auth import ( + ExaCredentials, + ExaCredentialsField, + ExaCredentialsInput, +) +from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema +from backend.data.model import SchemaField +from backend.util.request import requests + + +class ContentSettings(BaseModel): + text: dict = SchemaField( + description="Text content settings", + default={"maxCharacters": 1000, "includeHtmlTags": False}, + ) + highlights: dict = SchemaField( + description="Highlight settings", + default={"numSentences": 3, "highlightsPerUrl": 3}, + ) + summary: dict = SchemaField( + description="Summary settings", + default={"query": ""}, + ) + + +class ExaSearchBlock(Block): + class Input(BlockSchema): + credentials: ExaCredentialsInput = ExaCredentialsField() + query: str = SchemaField(description="The search query") + useAutoprompt: bool = SchemaField( + description="Whether to use autoprompt", + default=True, + ) + type: str = SchemaField( + description="Type of search", + default="", + ) + category: str = SchemaField( + description="Category to search within", + default="", + ) + numResults: int = SchemaField( + description="Number of results to return", + default=10, + ) + includeDomains: List[str] = SchemaField( + description="Domains to include in search", + default=[], + ) + excludeDomains: List[str] = SchemaField( + description="Domains to exclude from search", + default=[], + ) + startCrawlDate: datetime = SchemaField( + description="Start date for crawled content", + ) + endCrawlDate: datetime = SchemaField( + description="End date for crawled content", + ) + startPublishedDate: datetime = SchemaField( + description="Start date for published content", + ) + endPublishedDate: datetime = SchemaField( + description="End date for published content", + ) + includeText: List[str] = SchemaField( + description="Text patterns to include", + default=[], + ) + excludeText: List[str] = SchemaField( + description="Text patterns to exclude", + default=[], + ) + contents: ContentSettings = SchemaField( + description="Content retrieval settings", + default=ContentSettings(), + ) + + class Output(BlockSchema): + results: list = SchemaField( + description="List of search results", + default=[], + ) + + def __init__(self): + super().__init__( + id="996cec64-ac40-4dde-982f-b0dc60a5824d", + description="Searches the web using Exa's advanced search API", + categories={BlockCategory.SEARCH}, + input_schema=ExaSearchBlock.Input, + output_schema=ExaSearchBlock.Output, + ) + + def run( + self, input_data: Input, *, credentials: ExaCredentials, **kwargs + ) -> BlockOutput: + url = "https://api.exa.ai/search" + headers = { + "Content-Type": "application/json", + "x-api-key": credentials.api_key.get_secret_value(), + } + + payload = { + "query": input_data.query, + "useAutoprompt": input_data.useAutoprompt, + "numResults": input_data.numResults, + "contents": { + "text": {"maxCharacters": 1000, "includeHtmlTags": False}, + "highlights": { + "numSentences": 3, + "highlightsPerUrl": 3, + }, + "summary": {"query": ""}, + }, + } + + # Add dates if they exist + date_fields = [ + "startCrawlDate", + "endCrawlDate", + "startPublishedDate", + "endPublishedDate", + ] + for field in date_fields: + value = getattr(input_data, field, None) + if value: + payload[field] = value.strftime("%Y-%m-%dT%H:%M:%S.000Z") + + # Add other fields + optional_fields = [ + "type", + "category", + "includeDomains", + "excludeDomains", + "includeText", + "excludeText", + ] + + for field in optional_fields: + value = getattr(input_data, field) + if value: # Only add non-empty values + payload[field] = value + + try: + response = requests.post(url, headers=headers, json=payload) + response.raise_for_status() + data = response.json() + # Extract just the results array from the response + yield "results", data.get("results", []) + except Exception as e: + yield "error", str(e) + yield "results", [] diff --git a/autogpt_platform/frontend/src/components/integrations/credentials-input.tsx b/autogpt_platform/frontend/src/components/integrations/credentials-input.tsx index c0823b92ce63..879f70aff203 100644 --- a/autogpt_platform/frontend/src/components/integrations/credentials-input.tsx +++ b/autogpt_platform/frontend/src/components/integrations/credentials-input.tsx @@ -69,6 +69,7 @@ export const providerIcons: Record< fal: fallbackIcon, revid: fallbackIcon, unreal_speech: fallbackIcon, + exa: fallbackIcon, hubspot: fallbackIcon, }; // --8<-- [end:ProviderIconsEmbed] diff --git a/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx b/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx index 5afee0c68bec..0b965aa2ed65 100644 --- a/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx +++ b/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx @@ -43,6 +43,7 @@ const providerDisplayNames: Record = { fal: "FAL", revid: "Rev.ID", unreal_speech: "Unreal Speech", + exa: "Exa", hubspot: "Hubspot", } as const; // --8<-- [end:CredentialsProviderNames] diff --git a/autogpt_platform/frontend/src/components/node-input-components.tsx b/autogpt_platform/frontend/src/components/node-input-components.tsx index b93a894b9e92..da84699d0656 100644 --- a/autogpt_platform/frontend/src/components/node-input-components.tsx +++ b/autogpt_platform/frontend/src/components/node-input-components.tsx @@ -1,3 +1,11 @@ +import { Calendar } from "@/components/ui/calendar"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { format } from "date-fns"; +import { CalendarIcon, Clock } from "lucide-react"; import { Cross2Icon, Pencil2Icon, PlusIcon } from "@radix-ui/react-icons"; import { beautifyString, cn } from "@/lib/utils"; import { @@ -93,6 +101,83 @@ const NodeObjectInputTree: FC = ({ export default NodeObjectInputTree; +const NodeDateTimeInput: FC<{ + selfKey: string; + schema: BlockIOStringSubSchema; + value?: string; + error?: string; + handleInputChange: NodeObjectInputTreeProps["handleInputChange"]; + className?: string; + displayName: string; +}> = ({ + selfKey, + schema, + value = "", + error, + handleInputChange, + className, + displayName, +}) => { + const date = value ? new Date(value) : new Date(); + const [timeInput, setTimeInput] = useState( + value ? format(date, "HH:mm") : "00:00", + ); + + const handleDateSelect = (newDate: Date | undefined) => { + if (!newDate) return; + + const [hours, minutes] = timeInput.split(":").map(Number); + newDate.setHours(hours, minutes); + handleInputChange(selfKey, newDate.toISOString()); + }; + + const handleTimeChange = (e: React.ChangeEvent) => { + const newTime = e.target.value; + setTimeInput(newTime); + + if (value) { + const [hours, minutes] = newTime.split(":").map(Number); + const newDate = new Date(value); + newDate.setHours(hours, minutes); + handleInputChange(selfKey, newDate.toISOString()); + } + }; + + return ( +
+ + + + + + + + + + {error && {error}} +
+ ); +}; + export const NodeGenericInputField: FC<{ nodeId: string; propKey: string; @@ -252,6 +337,19 @@ export const NodeGenericInputField: FC<{ switch (propSchema.type) { case "string": + if ("format" in propSchema && propSchema.format === "date-time") { + return ( + + ); + } return (