mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
feat(blocks): Add Exa API Blocks (#8835)
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: <!-- Put your test plan here: --> - [ ] ... <details> <summary>Example test plan</summary> - [ ] 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 </details> #### 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**) <details> <summary>Examples of configuration changes</summary> - Changing ports - Adding new services that need to communicate with each other - Secrets or environment variable changes - New or infrastructure changes such as databases </details> --------- Co-authored-by: Nicholas Tindle <nicholas.tindle@agpt.co>
This commit is contained in:
35
autogpt_platform/backend/backend/blocks/exa/_auth.py
Normal file
35
autogpt_platform/backend/backend/blocks/exa/_auth.py
Normal file
@@ -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
autogpt_platform/backend/backend/blocks/exa/search.py
Normal file
157
autogpt_platform/backend/backend/blocks/exa/search.py
Normal file
@@ -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", []
|
||||
@@ -69,6 +69,7 @@ export const providerIcons: Record<
|
||||
fal: fallbackIcon,
|
||||
revid: fallbackIcon,
|
||||
unreal_speech: fallbackIcon,
|
||||
exa: fallbackIcon,
|
||||
hubspot: fallbackIcon,
|
||||
};
|
||||
// --8<-- [end:ProviderIconsEmbed]
|
||||
|
||||
@@ -43,6 +43,7 @@ const providerDisplayNames: Record<CredentialsProviderName, string> = {
|
||||
fal: "FAL",
|
||||
revid: "Rev.ID",
|
||||
unreal_speech: "Unreal Speech",
|
||||
exa: "Exa",
|
||||
hubspot: "Hubspot",
|
||||
} as const;
|
||||
// --8<-- [end:CredentialsProviderNames]
|
||||
|
||||
@@ -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<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;
|
||||
@@ -252,6 +337,19 @@ export const NodeGenericInputField: FC<{
|
||||
|
||||
switch (propSchema.type) {
|
||||
case "string":
|
||||
if ("format" in propSchema && propSchema.format === "date-time") {
|
||||
return (
|
||||
<NodeDateTimeInput
|
||||
selfKey={propKey}
|
||||
schema={propSchema}
|
||||
value={currentValue}
|
||||
error={errors[propKey]}
|
||||
className={className}
|
||||
displayName={displayName}
|
||||
handleInputChange={handleInputChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<NodeStringInput
|
||||
selfKey={propKey}
|
||||
|
||||
@@ -83,6 +83,7 @@ export type BlockIOStringSubSchema = BlockIOSubSchemaMeta & {
|
||||
enum?: string[];
|
||||
secret?: true;
|
||||
default?: string;
|
||||
format?: string;
|
||||
};
|
||||
|
||||
export type BlockIONumberSubSchema = BlockIOSubSchemaMeta & {
|
||||
@@ -121,6 +122,7 @@ export const PROVIDER_NAMES = {
|
||||
FAL: "fal",
|
||||
REVID: "revid",
|
||||
UNREAL_SPEECH: "unreal_speech",
|
||||
EXA: "exa",
|
||||
HUBSPOT: "hubspot",
|
||||
} as const;
|
||||
// --8<-- [end:BlockIOCredentialsSubSchema]
|
||||
|
||||
Reference in New Issue
Block a user