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 (