Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(blocks): Add Exa API Blocks #8835

Open
wants to merge 10 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions autogpt_platform/backend/backend/blocks/exa/_auth.py
Original file line number Diff line number Diff line change
@@ -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.",
)
157 changes: 157 additions & 0 deletions autogpt_platform/backend/backend/blocks/exa/search.py
Original file line number Diff line number Diff line change
@@ -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", []
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export const providerIcons: Record<
fal: fallbackIcon,
revid: fallbackIcon,
unreal_speech: fallbackIcon,
exa: fallbackIcon,
hubspot: fallbackIcon,
};
// --8<-- [end:ProviderIconsEmbed]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const providerDisplayNames: Record<CredentialsProviderName, string> = {
fal: "FAL",
revid: "Rev.ID",
unreal_speech: "Unreal Speech",
exa: "Exa",
hubspot: "Hubspot",
} as const;
// --8<-- [end:CredentialsProviderNames]
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -93,6 +101,83 @@ const NodeObjectInputTree: FC<NodeObjectInputTreeProps> = ({

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<HTMLInputElement>) => {
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 (
<div className={cn("flex flex-col gap-2", className)}>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn(
"w-full justify-start text-left font-normal",
!value && "text-muted-foreground",
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{value ? format(date, "PPP") : <span>Pick a date</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={date}
onSelect={handleDateSelect}
initialFocus
/>
</PopoverContent>
</Popover>
<LocalValuedInput
type="time"
value={timeInput}
onChange={handleTimeChange}
className="w-full"
/>
{error && <span className="error-message">{error}</span>}
</div>
);
};

export const NodeGenericInputField: FC<{
nodeId: string;
propKey: string;
Expand Down Expand Up @@ -252,6 +337,19 @@ export const NodeGenericInputField: FC<{

switch (propSchema.type) {
case "string":
if ("format" in propSchema && propSchema.format === "date-time") {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jsonschema converts data time to type string with format date-time

return (
<NodeDateTimeInput
selfKey={propKey}
schema={propSchema}
value={currentValue}
error={errors[propKey]}
className={className}
displayName={displayName}
handleInputChange={handleInputChange}
/>
);
}
return (
<NodeStringInput
selfKey={propKey}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export type BlockIOStringSubSchema = BlockIOSubSchemaMeta & {
enum?: string[];
secret?: true;
default?: string;
format?: string;
};

export type BlockIONumberSubSchema = BlockIOSubSchemaMeta & {
Expand Down Expand Up @@ -119,6 +120,7 @@ export const PROVIDER_NAMES = {
FAL: "fal",
REVID: "revid",
UNREAL_SPEECH: "unreal_speech",
EXA: "exa",
HUBSPOT: "hubspot",
} as const;
// --8<-- [end:BlockIOCredentialsSubSchema]
Expand Down
Loading
Loading