added exawebsets and answers moved it over to new credentails system

This commit is contained in:
SwiftyOS
2025-06-06 14:16:33 +02:00
parent 31b31e00d9
commit 7f10fe9d70
9 changed files with 971 additions and 129 deletions

View File

@@ -1,32 +0,0 @@
from typing import Literal
from pydantic import SecretStr
from backend.data.model import APIKeyCredentials, CredentialsField, CredentialsMetaInput
from backend.integrations.providers import ProviderName
ExaCredentials = APIKeyCredentials
ExaCredentialsInput = CredentialsMetaInput[
Literal[ProviderName.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(description="The Exa integration requires an API Key.")

View File

@@ -0,0 +1,131 @@
from backend.sdk import (
APIKeyCredentials,
Block,
BlockCategory,
BlockOutput,
BlockSchema,
Boolean,
CredentialsField,
CredentialsMetaInput,
Dict,
List,
SchemaField,
SecretStr,
Settings,
String,
default_credentials,
provider,
requests,
)
settings = Settings()
@provider("exa")
@default_credentials(
APIKeyCredentials(
id="01234567-89ab-cdef-0123-456789abcdef",
provider="exa",
api_key=SecretStr(settings.secrets.exa_api_key),
title="Use Credits for Exa",
expires_at=None,
)
)
class ExaAnswerBlock(Block):
class Input(BlockSchema):
credentials: CredentialsMetaInput = CredentialsField(
provider="exa",
supported_credential_types={"api_key"},
description="The Exa integration requires an API Key.",
)
query: String = SchemaField(
description="The question or query to answer",
placeholder="What is the latest valuation of SpaceX?",
)
stream: Boolean = SchemaField(
default=False,
description="If true, the response is returned as a server-sent events (SSE) stream",
advanced=True,
)
text: Boolean = SchemaField(
default=False,
description="If true, the response includes full text content in the search results",
advanced=True,
)
model: String = SchemaField(
default="exa",
description="The search model to use (exa or exa-pro)",
placeholder="exa",
advanced=True,
)
class Output(BlockSchema):
answer: String = SchemaField(
description="The generated answer based on search results",
)
citations: List[Dict] = SchemaField(
description="Search results used to generate the answer",
default_factory=list,
)
cost_dollars: Dict = SchemaField(
description="Cost breakdown of the request",
default_factory=dict,
)
error: String = SchemaField(
description="Error message if the request failed",
default="",
)
def __init__(self):
super().__init__(
id="f8e7d6c5-b4a3-5c2d-9e1f-3a7b8c9d4e6f",
description="Get an LLM answer to a question informed by Exa search results",
categories={BlockCategory.SEARCH, BlockCategory.AI},
input_schema=ExaAnswerBlock.Input,
output_schema=ExaAnswerBlock.Output,
test_input={
"query": "What is the capital of France?",
"text": False,
"stream": False,
"model": "exa",
},
test_output=[
("answer", "Paris"),
("citations", []),
("cost_dollars", {}),
],
)
def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
url = "https://api.exa.ai/answer"
headers = {
"Content-Type": "application/json",
"x-api-key": credentials.api_key.get_secret_value(),
}
# Build the payload
payload = {
"query": input_data.query,
"stream": input_data.stream,
"text": input_data.text,
"model": input_data.model,
}
try:
# Note: This endpoint doesn't support streaming in our block implementation
# If stream=True is requested, we still make a regular request
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
data = response.json()
yield "answer", data.get("answer", "")
yield "citations", data.get("citations", [])
yield "cost_dollars", data.get("costDollars", {})
except Exception as e:
yield "error", str(e)
yield "answer", ""
yield "citations", []
yield "cost_dollars", {}

View File

@@ -1,57 +1,46 @@
from typing import List
from pydantic import BaseModel
from backend.blocks.exa._auth import (
ExaCredentials,
ExaCredentialsField,
ExaCredentialsInput,
from backend.sdk import (
APIKeyCredentials,
Block,
BlockCategory,
BlockOutput,
BlockSchema,
CredentialsField,
CredentialsMetaInput,
List,
SchemaField,
String,
provider,
requests,
)
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.util.request import requests
class ContentRetrievalSettings(BaseModel):
text: dict = SchemaField(
description="Text content settings",
default={"maxCharacters": 1000, "includeHtmlTags": False},
advanced=True,
)
highlights: dict = SchemaField(
description="Highlight settings",
default={
"numSentences": 3,
"highlightsPerUrl": 3,
"query": "",
},
advanced=True,
)
summary: dict = SchemaField(
description="Summary settings",
default={"query": ""},
advanced=True,
)
from .helpers import ContentSettings
@provider("exa")
class ExaContentsBlock(Block):
class Input(BlockSchema):
credentials: ExaCredentialsInput = ExaCredentialsField()
ids: List[str] = SchemaField(
credentials: CredentialsMetaInput = CredentialsField(
provider="exa",
supported_credential_types={"api_key"},
description="The Exa integration requires an API Key.",
)
ids: List[String] = SchemaField(
description="Array of document IDs obtained from searches",
)
contents: ContentRetrievalSettings = SchemaField(
contents: ContentSettings = SchemaField(
description="Content retrieval settings",
default=ContentRetrievalSettings(),
default=ContentSettings(),
advanced=True,
)
class Output(BlockSchema):
results: list = SchemaField(
results: List = SchemaField(
description="List of document contents",
default_factory=list,
)
error: str = SchemaField(description="Error message if the request failed")
error: String = SchemaField(
description="Error message if the request failed", default=""
)
def __init__(self):
super().__init__(
@@ -63,7 +52,7 @@ class ExaContentsBlock(Block):
)
def run(
self, input_data: Input, *, credentials: ExaCredentials, **kwargs
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
url = "https://api.exa.ai/contents"
headers = {
@@ -71,11 +60,24 @@ class ExaContentsBlock(Block):
"x-api-key": credentials.api_key.get_secret_value(),
}
# Convert ContentSettings to API format
contents_dict = input_data.contents.model_dump()
payload = {
"ids": input_data.ids,
"text": input_data.contents.text,
"highlights": input_data.contents.highlights,
"summary": input_data.contents.summary,
"text": {
"maxCharacters": contents_dict["text"]["max_characters"],
"includeHtmlTags": contents_dict["text"]["include_html_tags"],
},
"highlights": {
"numSentences": contents_dict["highlights"]["num_sentences"],
"highlightsPerUrl": contents_dict["highlights"]["highlights_per_url"],
"query": contents_dict["summary"][
"query"
], # Note: query comes from summary
},
"summary": {
"query": contents_dict["summary"]["query"],
},
}
try:

View File

@@ -1,17 +1,13 @@
from typing import Optional
from pydantic import BaseModel
from backend.data.model import SchemaField
from backend.sdk import BaseModel, Boolean, Integer, List, Optional, SchemaField, String
class TextSettings(BaseModel):
max_characters: int = SchemaField(
max_characters: Integer = SchemaField(
default=1000,
description="Maximum number of characters to return",
placeholder="1000",
)
include_html_tags: bool = SchemaField(
include_html_tags: Boolean = SchemaField(
default=False,
description="Whether to include HTML tags in the text",
placeholder="False",
@@ -19,12 +15,12 @@ class TextSettings(BaseModel):
class HighlightSettings(BaseModel):
num_sentences: int = SchemaField(
num_sentences: Integer = SchemaField(
default=3,
description="Number of sentences per highlight",
placeholder="3",
)
highlights_per_url: int = SchemaField(
highlights_per_url: Integer = SchemaField(
default=3,
description="Number of highlights per URL",
placeholder="3",
@@ -32,7 +28,7 @@ class HighlightSettings(BaseModel):
class SummarySettings(BaseModel):
query: Optional[str] = SchemaField(
query: Optional[String] = SchemaField(
default="",
description="Query string for summarization",
placeholder="Enter query",
@@ -52,3 +48,83 @@ class ContentSettings(BaseModel):
default=SummarySettings(),
description="Summary settings",
)
# Websets Models
class WebsetEntitySettings(BaseModel):
type: Optional[String] = SchemaField(
default=None,
description="Entity type (e.g., 'company', 'person')",
placeholder="company",
)
class WebsetCriterion(BaseModel):
description: String = SchemaField(
description="Description of the criterion",
placeholder="Must be based in the US",
)
success_rate: Optional[Integer] = SchemaField(
default=None,
description="Success rate percentage",
ge=0,
le=100,
)
class WebsetSearchConfig(BaseModel):
query: String = SchemaField(
description="Search query",
placeholder="Marketing agencies based in the US",
)
count: Integer = SchemaField(
default=10,
description="Number of results to return",
ge=1,
le=100,
)
entity: Optional[WebsetEntitySettings] = SchemaField(
default=None,
description="Entity settings for the search",
)
criteria: Optional[List[WebsetCriterion]] = SchemaField(
default=None,
description="Search criteria",
)
behavior: Optional[String] = SchemaField(
default="override",
description="Behavior when updating results ('override' or 'append')",
placeholder="override",
)
class EnrichmentOption(BaseModel):
label: String = SchemaField(
description="Label for the enrichment option",
placeholder="Option 1",
)
class WebsetEnrichmentConfig(BaseModel):
title: String = SchemaField(
description="Title of the enrichment",
placeholder="Company Details",
)
description: String = SchemaField(
description="Description of what this enrichment does",
placeholder="Extract company information",
)
format: String = SchemaField(
default="text",
description="Format of the enrichment result",
placeholder="text",
)
instructions: Optional[String] = SchemaField(
default=None,
description="Instructions for the enrichment",
placeholder="Extract key company metrics",
)
options: Optional[List[EnrichmentOption]] = SchemaField(
default=None,
description="Options for the enrichment",
)

View File

@@ -1,46 +1,59 @@
from datetime import datetime
from typing import List
from backend.blocks.exa._auth import (
ExaCredentials,
ExaCredentialsField,
ExaCredentialsInput,
from backend.sdk import (
APIKeyCredentials,
Block,
BlockCategory,
BlockOutput,
BlockSchema,
Boolean,
CredentialsField,
CredentialsMetaInput,
Integer,
List,
SchemaField,
String,
provider,
requests,
)
from backend.blocks.exa.helpers import ContentSettings
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.util.request import requests
from .helpers import ContentSettings
@provider("exa")
class ExaSearchBlock(Block):
class Input(BlockSchema):
credentials: ExaCredentialsInput = ExaCredentialsField()
query: str = SchemaField(description="The search query")
use_auto_prompt: bool = SchemaField(
credentials: CredentialsMetaInput = CredentialsField(
provider="exa",
supported_credential_types={"api_key"},
description="The Exa integration requires an API Key.",
)
query: String = SchemaField(description="The search query")
use_auto_prompt: Boolean = SchemaField(
description="Whether to use autoprompt",
default=True,
advanced=True,
)
type: str = SchemaField(
type: String = SchemaField(
description="Type of search",
default="",
advanced=True,
)
category: str = SchemaField(
category: String = SchemaField(
description="Category to search within",
default="",
advanced=True,
)
number_of_results: int = SchemaField(
number_of_results: Integer = SchemaField(
description="Number of results to return",
default=10,
advanced=True,
)
include_domains: List[str] = SchemaField(
include_domains: List[String] = SchemaField(
description="Domains to include in search",
default_factory=list,
)
exclude_domains: List[str] = SchemaField(
exclude_domains: List[String] = SchemaField(
description="Domains to exclude from search",
default_factory=list,
advanced=True,
@@ -57,12 +70,12 @@ class ExaSearchBlock(Block):
end_published_date: datetime = SchemaField(
description="End date for published content",
)
include_text: List[str] = SchemaField(
include_text: List[String] = SchemaField(
description="Text patterns to include",
default_factory=list,
advanced=True,
)
exclude_text: List[str] = SchemaField(
exclude_text: List[String] = SchemaField(
description="Text patterns to exclude",
default_factory=list,
advanced=True,
@@ -74,12 +87,13 @@ class ExaSearchBlock(Block):
)
class Output(BlockSchema):
results: list = SchemaField(
results: List = SchemaField(
description="List of search results",
default_factory=list,
)
error: str = SchemaField(
error: String = SchemaField(
description="Error message if the request failed",
default="",
)
def __init__(self):
@@ -92,7 +106,7 @@ class ExaSearchBlock(Block):
)
def run(
self, input_data: Input, *, credentials: ExaCredentials, **kwargs
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
url = "https://api.exa.ai/search"
headers = {
@@ -104,7 +118,7 @@ class ExaSearchBlock(Block):
"query": input_data.query,
"useAutoprompt": input_data.use_auto_prompt,
"numResults": input_data.number_of_results,
"contents": input_data.contents.dict(),
"contents": input_data.contents.model_dump(),
}
date_field_mapping = {

View File

@@ -1,35 +1,47 @@
from datetime import datetime
from typing import Any, List
from backend.blocks.exa._auth import (
ExaCredentials,
ExaCredentialsField,
ExaCredentialsInput,
from backend.sdk import (
Any,
APIKeyCredentials,
Block,
BlockCategory,
BlockOutput,
BlockSchema,
CredentialsField,
CredentialsMetaInput,
Integer,
List,
SchemaField,
String,
provider,
requests,
)
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.util.request import requests
from .helpers import ContentSettings
@provider("exa")
class ExaFindSimilarBlock(Block):
class Input(BlockSchema):
credentials: ExaCredentialsInput = ExaCredentialsField()
url: str = SchemaField(
credentials: CredentialsMetaInput = CredentialsField(
provider="exa",
supported_credential_types={"api_key"},
description="The Exa integration requires an API Key.",
)
url: String = SchemaField(
description="The url for which you would like to find similar links"
)
number_of_results: int = SchemaField(
number_of_results: Integer = SchemaField(
description="Number of results to return",
default=10,
advanced=True,
)
include_domains: List[str] = SchemaField(
include_domains: List[String] = SchemaField(
description="Domains to include in search",
default_factory=list,
advanced=True,
)
exclude_domains: List[str] = SchemaField(
exclude_domains: List[String] = SchemaField(
description="Domains to exclude from search",
default_factory=list,
advanced=True,
@@ -46,12 +58,12 @@ class ExaFindSimilarBlock(Block):
end_published_date: datetime = SchemaField(
description="End date for published content",
)
include_text: List[str] = SchemaField(
include_text: List[String] = SchemaField(
description="Text patterns to include (max 1 string, up to 5 words)",
default_factory=list,
advanced=True,
)
exclude_text: List[str] = SchemaField(
exclude_text: List[String] = SchemaField(
description="Text patterns to exclude (max 1 string, up to 5 words)",
default_factory=list,
advanced=True,
@@ -67,7 +79,9 @@ class ExaFindSimilarBlock(Block):
description="List of similar documents with title, URL, published date, author, and score",
default_factory=list,
)
error: str = SchemaField(description="Error message if the request failed")
error: String = SchemaField(
description="Error message if the request failed", default=""
)
def __init__(self):
super().__init__(
@@ -79,7 +93,7 @@ class ExaFindSimilarBlock(Block):
)
def run(
self, input_data: Input, *, credentials: ExaCredentials, **kwargs
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
url = "https://api.exa.ai/findSimilar"
headers = {
@@ -90,7 +104,7 @@ class ExaFindSimilarBlock(Block):
payload = {
"url": input_data.url,
"numResults": input_data.number_of_results,
"contents": input_data.contents.dict(),
"contents": input_data.contents.model_dump(),
}
optional_field_mapping = {

View File

@@ -0,0 +1,523 @@
from backend.sdk import (
Any,
APIKeyCredentials,
Block,
BlockCategory,
BlockOutput,
BlockSchema,
Boolean,
CredentialsField,
CredentialsMetaInput,
Dict,
Integer,
List,
Optional,
SchemaField,
String,
provider,
requests,
)
from .helpers import WebsetEnrichmentConfig, WebsetSearchConfig
@provider("exa")
class ExaCreateWebsetBlock(Block):
class Input(BlockSchema):
credentials: CredentialsMetaInput = CredentialsField(
provider="exa",
supported_credential_types={"api_key"},
description="The Exa integration requires an API Key.",
)
search: WebsetSearchConfig = SchemaField(
description="Initial search configuration for the Webset",
)
enrichments: Optional[List[WebsetEnrichmentConfig]] = SchemaField(
default=None,
description="Enrichments to apply to Webset items",
advanced=True,
)
external_id: Optional[String] = SchemaField(
default=None,
description="External identifier for the webset",
placeholder="my-webset-123",
advanced=True,
)
metadata: Optional[Dict] = SchemaField(
default=None,
description="Key-value pairs to associate with this webset",
advanced=True,
)
class Output(BlockSchema):
webset_id: String = SchemaField(
description="The unique identifier for the created webset",
)
status: String = SchemaField(
description="The status of the webset",
)
external_id: Optional[String] = SchemaField(
description="The external identifier for the webset",
default=None,
)
created_at: String = SchemaField(
description="The date and time the webset was created",
)
error: String = SchemaField(
description="Error message if the request failed",
default="",
)
def __init__(self):
super().__init__(
id="a7c3b1d4-9e2f-4c5a-8f1b-3e6d7a9c2b5e",
description="Create a new Exa Webset for persistent web search collections",
categories={BlockCategory.SEARCH},
input_schema=ExaCreateWebsetBlock.Input,
output_schema=ExaCreateWebsetBlock.Output,
)
def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
url = "https://api.exa.ai/websets/v0/websets"
headers = {
"Content-Type": "application/json",
"x-api-key": credentials.api_key.get_secret_value(),
}
# Build the payload
payload: dict[str, Any] = {
"search": input_data.search.model_dump(exclude_none=True),
}
# Convert enrichments to API format
if input_data.enrichments:
enrichments_data = []
for enrichment in input_data.enrichments:
enrichments_data.append(enrichment.model_dump(exclude_none=True))
payload["enrichments"] = enrichments_data
if input_data.external_id:
payload["externalId"] = input_data.external_id
if input_data.metadata:
payload["metadata"] = input_data.metadata
try:
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
data = response.json()
yield "webset_id", data.get("id", "")
yield "status", data.get("status", "")
yield "external_id", data.get("externalId")
yield "created_at", data.get("createdAt", "")
except Exception as e:
yield "error", str(e)
yield "webset_id", ""
yield "status", ""
yield "created_at", ""
@provider("exa")
class ExaUpdateWebsetBlock(Block):
class Input(BlockSchema):
credentials: CredentialsMetaInput = CredentialsField(
provider="exa",
supported_credential_types={"api_key"},
description="The Exa integration requires an API Key.",
)
webset_id: String = SchemaField(
description="The ID or external ID of the Webset to update",
placeholder="webset-id-or-external-id",
)
metadata: Optional[Dict] = SchemaField(
default=None,
description="Key-value pairs to associate with this webset (set to null to clear)",
)
class Output(BlockSchema):
webset_id: String = SchemaField(
description="The unique identifier for the webset",
)
status: String = SchemaField(
description="The status of the webset",
)
external_id: Optional[String] = SchemaField(
description="The external identifier for the webset",
default=None,
)
metadata: Dict = SchemaField(
description="Updated metadata for the webset",
default_factory=dict,
)
updated_at: String = SchemaField(
description="The date and time the webset was updated",
)
error: String = SchemaField(
description="Error message if the request failed",
default="",
)
def __init__(self):
super().__init__(
id="c9e5d3f6-2a4b-6e7c-1f3d-5a8b9c4e7d2f",
description="Update metadata for an existing Webset",
categories={BlockCategory.SEARCH},
input_schema=ExaUpdateWebsetBlock.Input,
output_schema=ExaUpdateWebsetBlock.Output,
)
def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
url = f"https://api.exa.ai/websets/v0/websets/{input_data.webset_id}"
headers = {
"Content-Type": "application/json",
"x-api-key": credentials.api_key.get_secret_value(),
}
# Build the payload
payload = {}
if input_data.metadata is not None:
payload["metadata"] = input_data.metadata
try:
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
data = response.json()
yield "webset_id", data.get("id", "")
yield "status", data.get("status", "")
yield "external_id", data.get("externalId")
yield "metadata", data.get("metadata", {})
yield "updated_at", data.get("updatedAt", "")
except Exception as e:
yield "error", str(e)
yield "webset_id", ""
yield "status", ""
yield "metadata", {}
yield "updated_at", ""
@provider("exa")
class ExaListWebsetsBlock(Block):
class Input(BlockSchema):
credentials: CredentialsMetaInput = CredentialsField(
provider="exa",
supported_credential_types={"api_key"},
description="The Exa integration requires an API Key.",
)
cursor: Optional[String] = SchemaField(
default=None,
description="Cursor for pagination through results",
advanced=True,
)
limit: Integer = SchemaField(
default=25,
description="Number of websets to return (1-100)",
ge=1,
le=100,
advanced=True,
)
class Output(BlockSchema):
websets: List = SchemaField(
description="List of websets",
default_factory=list,
)
has_more: Boolean = SchemaField(
description="Whether there are more results to paginate through",
default=False,
)
next_cursor: Optional[String] = SchemaField(
description="Cursor for the next page of results",
default=None,
)
error: String = SchemaField(
description="Error message if the request failed",
default="",
)
def __init__(self):
super().__init__(
id="f3h8g6i9-5d7e-9b1f-4c6g-8d2f3h7i1a5c",
description="List all Websets with pagination support",
categories={BlockCategory.SEARCH},
input_schema=ExaListWebsetsBlock.Input,
output_schema=ExaListWebsetsBlock.Output,
)
def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
url = "https://api.exa.ai/websets/v0/websets"
headers = {
"x-api-key": credentials.api_key.get_secret_value(),
}
params: dict[str, Any] = {
"limit": input_data.limit,
}
if input_data.cursor:
params["cursor"] = input_data.cursor
try:
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
data = response.json()
yield "websets", data.get("data", [])
yield "has_more", data.get("hasMore", False)
yield "next_cursor", data.get("nextCursor")
except Exception as e:
yield "error", str(e)
yield "websets", []
yield "has_more", False
@provider("exa")
class ExaGetWebsetBlock(Block):
class Input(BlockSchema):
credentials: CredentialsMetaInput = CredentialsField(
provider="exa",
supported_credential_types={"api_key"},
description="The Exa integration requires an API Key.",
)
webset_id: String = SchemaField(
description="The ID or external ID of the Webset to retrieve",
placeholder="webset-id-or-external-id",
)
expand_items: Boolean = SchemaField(
default=False,
description="Include items in the response",
advanced=True,
)
class Output(BlockSchema):
webset_id: String = SchemaField(
description="The unique identifier for the webset",
)
status: String = SchemaField(
description="The status of the webset",
)
external_id: Optional[String] = SchemaField(
description="The external identifier for the webset",
default=None,
)
searches: List[Dict] = SchemaField(
description="The searches performed on the webset",
default_factory=list,
)
enrichments: List[Dict] = SchemaField(
description="The enrichments applied to the webset",
default_factory=list,
)
monitors: List[Dict] = SchemaField(
description="The monitors for the webset",
default_factory=list,
)
items: Optional[List[Dict]] = SchemaField(
description="The items in the webset (if expand_items is true)",
default=None,
)
metadata: Dict = SchemaField(
description="Key-value pairs associated with the webset",
default_factory=dict,
)
created_at: String = SchemaField(
description="The date and time the webset was created",
)
updated_at: String = SchemaField(
description="The date and time the webset was last updated",
)
error: String = SchemaField(
description="Error message if the request failed",
default="",
)
def __init__(self):
super().__init__(
id="b8d4c2e5-1f3a-5d6b-9e2c-4f7a8b3d6c9f",
description="Retrieve a Webset by ID or external ID",
categories={BlockCategory.SEARCH},
input_schema=ExaGetWebsetBlock.Input,
output_schema=ExaGetWebsetBlock.Output,
)
def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
url = f"https://api.exa.ai/websets/v0/websets/{input_data.webset_id}"
headers = {
"x-api-key": credentials.api_key.get_secret_value(),
}
params = {}
if input_data.expand_items:
params["expand[]"] = "items"
try:
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
data = response.json()
yield "webset_id", data.get("id", "")
yield "status", data.get("status", "")
yield "external_id", data.get("externalId")
yield "searches", data.get("searches", [])
yield "enrichments", data.get("enrichments", [])
yield "monitors", data.get("monitors", [])
yield "items", data.get("items")
yield "metadata", data.get("metadata", {})
yield "created_at", data.get("createdAt", "")
yield "updated_at", data.get("updatedAt", "")
except Exception as e:
yield "error", str(e)
yield "webset_id", ""
yield "status", ""
yield "searches", []
yield "enrichments", []
yield "monitors", []
yield "metadata", {}
yield "created_at", ""
yield "updated_at", ""
@provider("exa")
class ExaDeleteWebsetBlock(Block):
class Input(BlockSchema):
credentials: CredentialsMetaInput = CredentialsField(
provider="exa",
supported_credential_types={"api_key"},
description="The Exa integration requires an API Key.",
)
webset_id: String = SchemaField(
description="The ID or external ID of the Webset to delete",
placeholder="webset-id-or-external-id",
)
class Output(BlockSchema):
webset_id: String = SchemaField(
description="The unique identifier for the deleted webset",
)
external_id: Optional[String] = SchemaField(
description="The external identifier for the deleted webset",
default=None,
)
status: String = SchemaField(
description="The status of the deleted webset",
)
success: String = SchemaField(
description="Whether the deletion was successful",
default="true",
)
error: String = SchemaField(
description="Error message if the request failed",
default="",
)
def __init__(self):
super().__init__(
id="d1f6e4g7-3b5c-7f8d-2a4e-6b9c1d5f8e3a",
description="Delete a Webset and all its items",
categories={BlockCategory.SEARCH},
input_schema=ExaDeleteWebsetBlock.Input,
output_schema=ExaDeleteWebsetBlock.Output,
)
def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
url = f"https://api.exa.ai/websets/v0/websets/{input_data.webset_id}"
headers = {
"x-api-key": credentials.api_key.get_secret_value(),
}
try:
response = requests.delete(url, headers=headers)
response.raise_for_status()
data = response.json()
yield "webset_id", data.get("id", "")
yield "external_id", data.get("externalId")
yield "status", data.get("status", "")
yield "success", "true"
except Exception as e:
yield "error", str(e)
yield "webset_id", ""
yield "status", ""
yield "success", "false"
@provider("exa")
class ExaCancelWebsetBlock(Block):
class Input(BlockSchema):
credentials: CredentialsMetaInput = CredentialsField(
provider="exa",
supported_credential_types={"api_key"},
description="The Exa integration requires an API Key.",
)
webset_id: String = SchemaField(
description="The ID or external ID of the Webset to cancel",
placeholder="webset-id-or-external-id",
)
class Output(BlockSchema):
webset_id: String = SchemaField(
description="The unique identifier for the webset",
)
status: String = SchemaField(
description="The status of the webset after cancellation",
)
external_id: Optional[String] = SchemaField(
description="The external identifier for the webset",
default=None,
)
success: String = SchemaField(
description="Whether the cancellation was successful",
default="true",
)
error: String = SchemaField(
description="Error message if the request failed",
default="",
)
def __init__(self):
super().__init__(
id="e2g7f5h8-4c6d-8a9e-3b5f-7c1d2e6g9f4b",
description="Cancel all operations being performed on a Webset",
categories={BlockCategory.SEARCH},
input_schema=ExaCancelWebsetBlock.Input,
output_schema=ExaCancelWebsetBlock.Output,
)
def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
url = f"https://api.exa.ai/websets/v0/websets/{input_data.webset_id}/cancel"
headers = {
"x-api-key": credentials.api_key.get_secret_value(),
}
try:
response = requests.post(url, headers=headers)
response.raise_for_status()
data = response.json()
yield "webset_id", data.get("id", "")
yield "status", data.get("status", "")
yield "external_id", data.get("externalId")
yield "success", "true"
except Exception as e:
yield "error", str(e)
yield "webset_id", ""
yield "status", ""
yield "success", "false"

View File

@@ -1,6 +1,6 @@
import asyncio
import logging
from typing import TYPE_CHECKING, Annotated, Awaitable, Literal
from typing import TYPE_CHECKING, Annotated, Awaitable, Dict, List, Literal
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query, Request
from pydantic import BaseModel, Field
@@ -20,6 +20,7 @@ from backend.integrations.creds_manager import IntegrationCredentialsManager
from backend.integrations.oauth import HANDLERS_BY_NAME
from backend.integrations.providers import ProviderName
from backend.integrations.webhooks import get_webhook_manager
from backend.sdk.auto_registry import get_registry
from backend.util.exceptions import NeedConfirmation, NotFoundError
from backend.util.settings import Settings
@@ -416,3 +417,89 @@ def _get_provider_oauth_handler(
client_secret=client_secret,
redirect_uri=f"{frontend_base_url}/auth/integrations/oauth_callback",
)
# === PROVIDER DISCOVERY ENDPOINTS ===
@router.get("/providers", response_model=List[str])
async def list_providers() -> List[str]:
"""
Get a list of all available provider names.
Returns both statically defined providers (from ProviderName enum)
and dynamically registered providers (from SDK decorators).
"""
# Get static providers from enum
static_providers = [member.value for member in ProviderName]
# Get dynamic providers from registry
registry = get_registry()
dynamic_providers = list(registry.providers)
# Combine and deduplicate
all_providers = list(set(static_providers + dynamic_providers))
all_providers.sort()
logger.info(f"Returning {len(all_providers)} providers")
return all_providers
class ProviderDetails(BaseModel):
name: str
source: Literal["static", "dynamic", "both"]
has_oauth: bool
has_webhooks: bool
supported_credential_types: List[CredentialsType] = Field(default_factory=list)
@router.get("/providers/details", response_model=Dict[str, ProviderDetails])
async def get_providers_details() -> Dict[str, ProviderDetails]:
"""
Get detailed information about all providers.
Returns a dictionary mapping provider names to their details,
including supported credential types and other metadata.
"""
registry = get_registry()
# Build provider details
provider_details: Dict[str, ProviderDetails] = {}
# Add static providers
for member in ProviderName:
provider_details[member.value] = ProviderDetails(
name=member.value,
source="static",
has_oauth=member.value in registry.oauth_handlers,
has_webhooks=member.value in registry.webhook_managers,
)
# Add/update with dynamic providers
for provider in registry.providers:
if provider not in provider_details:
provider_details[provider] = ProviderDetails(
name=provider,
source="dynamic",
has_oauth=provider in registry.oauth_handlers,
has_webhooks=provider in registry.webhook_managers,
)
else:
provider_details[provider].source = "both"
provider_details[provider].has_oauth = provider in registry.oauth_handlers
provider_details[provider].has_webhooks = (
provider in registry.webhook_managers
)
# Determine supported credential types for each provider
# This is a simplified version - in reality, you might want to inspect
# the blocks or credentials to determine this more accurately
for provider_name, details in provider_details.items():
credential_types = []
if details.has_oauth:
credential_types.append("oauth2")
# Most providers support API keys
credential_types.append("api_key")
details.supported_credential_types = credential_types
return provider_details

View File

@@ -62,9 +62,36 @@ async def lifespan_context(app: fastapi.FastAPI):
try:
from backend.sdk.auto_registry import setup_auto_registration
setup_auto_registration()
logger.info("Starting SDK auto-registration system...")
registry = setup_auto_registration()
# Log successful registration
logger.info("Auto-registration completed successfully:")
logger.info(f" - {len(registry.block_costs)} block costs registered")
logger.info(
f" - {len(registry.default_credentials)} default credentials registered"
)
logger.info(f" - {len(registry.oauth_handlers)} OAuth handlers registered")
logger.info(f" - {len(registry.webhook_managers)} webhook managers registered")
logger.info(f" - {len(registry.providers)} providers registered")
# Log specific credential providers for debugging
credential_providers = [
getattr(cred, "provider", "unknown")
for cred in registry.default_credentials
]
if credential_providers:
logger.info(
f" - Default credential providers: {', '.join(credential_providers)}"
)
except Exception as e:
logger.warning(f"Auto-registration setup failed: {e}")
logger.error(f"Auto-registration setup failed: {e}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
# Don't let this failure prevent startup, but make it very visible
raise
await backend.data.user.migrate_and_encrypt_user_integrations()
await backend.data.graph.fix_llm_provider_credentials()