Skip to content

Commit

Permalink
feat(blocks): Add Exa API Blocks (#8835)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
aarushik93 and ntindle authored Dec 6, 2024
1 parent 9ad9dd9 commit dcfad26
Show file tree
Hide file tree
Showing 6 changed files with 294 additions and 0 deletions.
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 @@ -69,6 +69,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 @@ -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]
Expand Down
98 changes: 98 additions & 0 deletions autogpt_platform/frontend/src/components/node-input-components.tsx
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") {
return (
<NodeDateTimeInput
selfKey={propKey}
schema={propSchema}
value={currentValue}
error={errors[propKey]}
className={className}
displayName={displayName}
handleInputChange={handleInputChange}
/>
);
}
return (
<NodeStringInput
selfKey={propKey}
Expand Down
2 changes: 2 additions & 0 deletions autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts
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 @@ -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]
Expand Down

0 comments on commit dcfad26

Please sign in to comment.