mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
I'm getting circular import issues because there is a lot of cross-importing between `backend.data`, `backend.blocks`, and other modules. This change reduces block-related cross-imports and thus risk of breaking circular imports. ### Changes 🏗️ - Strip down `backend.data.block` - Move `Block` base class and related class/enum defs to `backend.blocks._base` - Move `is_block_auth_configured` to `backend.blocks._utils` - Move `get_blocks()`, `get_io_block_ids()` etc. to `backend.blocks` (`__init__.py`) - Update imports everywhere - Remove unused and poorly typed `Block.create()` - Change usages from `block_cls.create()` to `block_cls()` - Improve typing of `load_all_blocks` and `get_blocks` - Move cross-import of `backend.api.features.library.model` from `backend/data/__init__.py` to `backend/data/integrations.py` - Remove deprecated attribute `NodeModel.webhook` - Re-generate OpenAPI spec and fix frontend usage - Eliminate module-level `backend.blocks` import from `blocks/agent.py` - Eliminate module-level `backend.data.execution` and `backend.executor.manager` imports from `blocks/helpers/review.py` - Replace `BlockInput` with `GraphInput` for graph inputs ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - CI static type-checking + tests should be sufficient for this
372 lines
11 KiB
Python
372 lines
11 KiB
Python
import logging
|
|
from typing import AsyncGenerator, Literal, Optional, overload
|
|
|
|
from prisma.models import AgentNode, AgentPreset, IntegrationWebhook
|
|
from prisma.types import (
|
|
IntegrationWebhookCreateInput,
|
|
IntegrationWebhookUpdateInput,
|
|
IntegrationWebhookWhereInput,
|
|
Serializable,
|
|
)
|
|
from pydantic import Field, computed_field
|
|
|
|
from backend.data.event_bus import AsyncRedisEventBus
|
|
from backend.data.includes import (
|
|
INTEGRATION_WEBHOOK_INCLUDE,
|
|
MAX_INTEGRATION_WEBHOOKS_FETCH,
|
|
)
|
|
from backend.integrations.creds_manager import IntegrationCredentialsManager
|
|
from backend.integrations.providers import ProviderName
|
|
from backend.integrations.webhooks import get_webhook_manager
|
|
from backend.integrations.webhooks.utils import webhook_ingress_url
|
|
from backend.util.exceptions import NotFoundError
|
|
from backend.util.json import SafeJson
|
|
|
|
from .db import BaseDbModel
|
|
from .graph import NodeModel
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class Webhook(BaseDbModel):
|
|
user_id: str
|
|
provider: ProviderName
|
|
credentials_id: str
|
|
webhook_type: str
|
|
resource: str
|
|
events: list[str]
|
|
config: dict = Field(default_factory=dict)
|
|
secret: str
|
|
|
|
provider_webhook_id: str
|
|
|
|
@computed_field
|
|
@property
|
|
def url(self) -> str:
|
|
return webhook_ingress_url(self.provider, self.id)
|
|
|
|
@staticmethod
|
|
def from_db(webhook: IntegrationWebhook):
|
|
return Webhook(
|
|
id=webhook.id,
|
|
user_id=webhook.userId,
|
|
provider=ProviderName(webhook.provider),
|
|
credentials_id=webhook.credentialsId,
|
|
webhook_type=webhook.webhookType,
|
|
resource=webhook.resource,
|
|
events=webhook.events,
|
|
config=dict(webhook.config),
|
|
secret=webhook.secret,
|
|
provider_webhook_id=webhook.providerWebhookId,
|
|
)
|
|
|
|
|
|
# LibraryAgentPreset import must be after Webhook definition to avoid
|
|
# broken circular import:
|
|
# integrations.py → library/model.py → integrations.py (for Webhook)
|
|
from backend.api.features.library.model import LibraryAgentPreset # noqa: E402
|
|
|
|
# Resolve forward refs
|
|
LibraryAgentPreset.model_rebuild()
|
|
|
|
|
|
class WebhookWithRelations(Webhook):
|
|
triggered_nodes: list[NodeModel]
|
|
triggered_presets: list[LibraryAgentPreset]
|
|
|
|
@staticmethod
|
|
def from_db(webhook: IntegrationWebhook):
|
|
if webhook.AgentNodes is None or webhook.AgentPresets is None:
|
|
raise ValueError(
|
|
"AgentNodes and AgentPresets must be included in "
|
|
"IntegrationWebhook query with relations"
|
|
)
|
|
|
|
return WebhookWithRelations(
|
|
**Webhook.from_db(webhook).model_dump(),
|
|
triggered_nodes=[NodeModel.from_db(node) for node in webhook.AgentNodes],
|
|
triggered_presets=[
|
|
LibraryAgentPreset.from_db(preset) for preset in webhook.AgentPresets
|
|
],
|
|
)
|
|
|
|
|
|
# --------------------- CRUD functions --------------------- #
|
|
|
|
|
|
async def create_webhook(webhook: Webhook) -> Webhook:
|
|
created_webhook = await IntegrationWebhook.prisma().create(
|
|
data=IntegrationWebhookCreateInput(
|
|
id=webhook.id,
|
|
userId=webhook.user_id,
|
|
provider=webhook.provider.value,
|
|
credentialsId=webhook.credentials_id,
|
|
webhookType=webhook.webhook_type,
|
|
resource=webhook.resource,
|
|
events=webhook.events,
|
|
config=SafeJson(webhook.config),
|
|
secret=webhook.secret,
|
|
providerWebhookId=webhook.provider_webhook_id,
|
|
)
|
|
)
|
|
return Webhook.from_db(created_webhook)
|
|
|
|
|
|
@overload
|
|
async def get_webhook(
|
|
webhook_id: str, *, include_relations: Literal[True]
|
|
) -> WebhookWithRelations: ...
|
|
@overload
|
|
async def get_webhook(
|
|
webhook_id: str, *, include_relations: Literal[False] = False
|
|
) -> Webhook: ...
|
|
|
|
|
|
async def get_webhook(
|
|
webhook_id: str, *, include_relations: bool = False
|
|
) -> Webhook | WebhookWithRelations:
|
|
"""
|
|
⚠️ No `user_id` check: DO NOT USE without check in user-facing endpoints.
|
|
|
|
Raises:
|
|
NotFoundError: if no record with the given ID exists
|
|
"""
|
|
webhook = await IntegrationWebhook.prisma().find_unique(
|
|
where={"id": webhook_id},
|
|
include=INTEGRATION_WEBHOOK_INCLUDE if include_relations else None,
|
|
)
|
|
if not webhook:
|
|
raise NotFoundError(f"Webhook #{webhook_id} not found")
|
|
return (WebhookWithRelations if include_relations else Webhook).from_db(webhook)
|
|
|
|
|
|
@overload
|
|
async def get_all_webhooks_by_creds(
|
|
user_id: str,
|
|
credentials_id: str,
|
|
*,
|
|
include_relations: Literal[True],
|
|
limit: int = MAX_INTEGRATION_WEBHOOKS_FETCH,
|
|
) -> list[WebhookWithRelations]: ...
|
|
@overload
|
|
async def get_all_webhooks_by_creds(
|
|
user_id: str,
|
|
credentials_id: str,
|
|
*,
|
|
include_relations: Literal[False] = False,
|
|
limit: int = MAX_INTEGRATION_WEBHOOKS_FETCH,
|
|
) -> list[Webhook]: ...
|
|
|
|
|
|
async def get_all_webhooks_by_creds(
|
|
user_id: str,
|
|
credentials_id: str,
|
|
*,
|
|
include_relations: bool = False,
|
|
limit: int = MAX_INTEGRATION_WEBHOOKS_FETCH,
|
|
) -> list[Webhook] | list[WebhookWithRelations]:
|
|
if not credentials_id:
|
|
raise ValueError("credentials_id must not be empty")
|
|
webhooks = await IntegrationWebhook.prisma().find_many(
|
|
where={"userId": user_id, "credentialsId": credentials_id},
|
|
include=INTEGRATION_WEBHOOK_INCLUDE if include_relations else None,
|
|
order={"createdAt": "desc"},
|
|
take=limit,
|
|
)
|
|
return [
|
|
(WebhookWithRelations if include_relations else Webhook).from_db(webhook)
|
|
for webhook in webhooks
|
|
]
|
|
|
|
|
|
async def find_webhook_by_credentials_and_props(
|
|
user_id: str,
|
|
credentials_id: str,
|
|
webhook_type: str,
|
|
resource: str,
|
|
events: list[str],
|
|
) -> Webhook | None:
|
|
webhook = await IntegrationWebhook.prisma().find_first(
|
|
where={
|
|
"userId": user_id,
|
|
"credentialsId": credentials_id,
|
|
"webhookType": webhook_type,
|
|
"resource": resource,
|
|
"events": {"has_every": events},
|
|
},
|
|
)
|
|
return Webhook.from_db(webhook) if webhook else None
|
|
|
|
|
|
async def find_webhook_by_graph_and_props(
|
|
user_id: str,
|
|
provider: str,
|
|
webhook_type: str,
|
|
graph_id: Optional[str] = None,
|
|
preset_id: Optional[str] = None,
|
|
) -> Webhook | None:
|
|
"""Either `graph_id` or `preset_id` must be provided."""
|
|
where_clause: IntegrationWebhookWhereInput = {
|
|
"userId": user_id,
|
|
"provider": provider,
|
|
"webhookType": webhook_type,
|
|
}
|
|
|
|
if preset_id:
|
|
where_clause["AgentPresets"] = {"some": {"id": preset_id}}
|
|
elif graph_id:
|
|
where_clause["AgentNodes"] = {"some": {"agentGraphId": graph_id}}
|
|
else:
|
|
raise ValueError("Either graph_id or preset_id must be provided")
|
|
|
|
webhook = await IntegrationWebhook.prisma().find_first(
|
|
where=where_clause,
|
|
)
|
|
return Webhook.from_db(webhook) if webhook else None
|
|
|
|
|
|
async def update_webhook(
|
|
webhook_id: str,
|
|
config: Optional[dict[str, Serializable]] = None,
|
|
events: Optional[list[str]] = None,
|
|
) -> Webhook:
|
|
"""⚠️ No `user_id` check: DO NOT USE without check in user-facing endpoints."""
|
|
data: IntegrationWebhookUpdateInput = {}
|
|
if config is not None:
|
|
data["config"] = SafeJson(config)
|
|
if events is not None:
|
|
data["events"] = events
|
|
if not data:
|
|
raise ValueError("Empty update query")
|
|
|
|
_updated_webhook = await IntegrationWebhook.prisma().update(
|
|
where={"id": webhook_id},
|
|
data=data,
|
|
)
|
|
if _updated_webhook is None:
|
|
raise NotFoundError(f"Webhook #{webhook_id} not found")
|
|
return Webhook.from_db(_updated_webhook)
|
|
|
|
|
|
async def find_webhooks_by_graph_id(graph_id: str, user_id: str) -> list[Webhook]:
|
|
"""
|
|
Find all webhooks that trigger nodes OR presets in a specific graph for a user.
|
|
|
|
Args:
|
|
graph_id: The ID of the graph
|
|
user_id: The ID of the user
|
|
|
|
Returns:
|
|
list[Webhook]: List of webhooks associated with the graph
|
|
"""
|
|
where_clause: IntegrationWebhookWhereInput = {
|
|
"userId": user_id,
|
|
"OR": [
|
|
# Webhooks that trigger nodes in this graph
|
|
{"AgentNodes": {"some": {"agentGraphId": graph_id}}},
|
|
# Webhooks that trigger presets for this graph
|
|
{"AgentPresets": {"some": {"agentGraphId": graph_id}}},
|
|
],
|
|
}
|
|
webhooks = await IntegrationWebhook.prisma().find_many(where=where_clause)
|
|
return [Webhook.from_db(webhook) for webhook in webhooks]
|
|
|
|
|
|
async def unlink_webhook_from_graph(
|
|
webhook_id: str, graph_id: str, user_id: str
|
|
) -> None:
|
|
"""
|
|
Unlink a webhook from all nodes and presets in a specific graph.
|
|
If the webhook has no remaining triggers, it will be automatically deleted
|
|
and deregistered with the provider.
|
|
|
|
Args:
|
|
webhook_id: The ID of the webhook
|
|
graph_id: The ID of the graph to unlink from
|
|
user_id: The ID of the user (for authorization)
|
|
"""
|
|
# Avoid circular imports
|
|
from backend.api.features.library.db import set_preset_webhook
|
|
from backend.data.graph import set_node_webhook
|
|
|
|
# Find all nodes in this graph that use this webhook
|
|
nodes = await AgentNode.prisma().find_many(
|
|
where={"agentGraphId": graph_id, "webhookId": webhook_id}
|
|
)
|
|
|
|
# Unlink webhook from each node
|
|
for node in nodes:
|
|
await set_node_webhook(node.id, None)
|
|
|
|
# Find all presets for this graph that use this webhook
|
|
presets = await AgentPreset.prisma().find_many(
|
|
where={"agentGraphId": graph_id, "webhookId": webhook_id, "userId": user_id}
|
|
)
|
|
|
|
# Unlink webhook from each preset
|
|
for preset in presets:
|
|
await set_preset_webhook(user_id, preset.id, None)
|
|
|
|
# Check if webhook needs cleanup (prune_webhook_if_dangling handles the trigger check)
|
|
webhook = await get_webhook(webhook_id, include_relations=False)
|
|
webhook_manager = get_webhook_manager(webhook.provider)
|
|
creds_manager = IntegrationCredentialsManager()
|
|
credentials = (
|
|
await creds_manager.get(user_id, webhook.credentials_id)
|
|
if webhook.credentials_id
|
|
else None
|
|
)
|
|
await webhook_manager.prune_webhook_if_dangling(user_id, webhook.id, credentials)
|
|
|
|
|
|
async def delete_webhook(user_id: str, webhook_id: str) -> None:
|
|
deleted = await IntegrationWebhook.prisma().delete_many(
|
|
where={"id": webhook_id, "userId": user_id}
|
|
)
|
|
if deleted < 1:
|
|
raise NotFoundError(f"Webhook #{webhook_id} not found")
|
|
|
|
|
|
# --------------------- WEBHOOK EVENTS --------------------- #
|
|
|
|
|
|
class WebhookEvent(BaseDbModel):
|
|
provider: str
|
|
webhook_id: str
|
|
event_type: str
|
|
payload: dict
|
|
|
|
|
|
class WebhookEventBus(AsyncRedisEventBus[WebhookEvent]):
|
|
Model = WebhookEvent
|
|
|
|
@property
|
|
def event_bus_name(self) -> str:
|
|
return "webhooks"
|
|
|
|
|
|
_webhook_event_bus = WebhookEventBus()
|
|
|
|
|
|
async def publish_webhook_event(event: WebhookEvent):
|
|
await _webhook_event_bus.publish_event(
|
|
event, f"{event.webhook_id}/{event.event_type}"
|
|
)
|
|
|
|
|
|
async def listen_for_webhook_events(
|
|
webhook_id: str, event_type: Optional[str] = None
|
|
) -> AsyncGenerator[WebhookEvent, None]:
|
|
async for event in _webhook_event_bus.listen_events(
|
|
f"{webhook_id}/{event_type or '*'}"
|
|
):
|
|
yield event
|
|
|
|
|
|
async def wait_for_webhook_event(
|
|
webhook_id: str, event_type: Optional[str] = None, timeout: Optional[float] = None
|
|
) -> WebhookEvent | None:
|
|
return await _webhook_event_bus.wait_for_event(
|
|
f"{webhook_id}/{event_type or '*'}", timeout
|
|
)
|